use std::convert::TryInto;
use std::ops::Index;

use bitcoin_hashes::hex::ToHex;
use bitcoincash::blockdata::script::{read_scriptint, read_uint};
use bitcoincash::Network;
use bitcoincash::{blockdata::opcodes, Script};
use std::fmt::Display;

use crate::utilscript::read_push_from_script;
use anyhow::Result;

use super::cashaddr;

pub const PARENT_GROUP_ID_LENGTH: usize = 32;
const OPRETURN_GROUP_PREFIXES: [usize; 4] = [88888888, 88888890, 88888891, 88888892];
// Limits on minimum number of chars in group tickers for 88888890 and 88888891
pub const MIN_TICKER_LENGTH: usize = 2;
// Limits on maximum number of chars in group tickers
pub const MAX_TICKER_LENGTH: usize = 8;
// Limits on number of chars in group names for 88888890 and 88888891
pub const MIN_NAME_LENGTH: usize = 2;
pub const MAX_NAME_LENGTH: usize = 25;
// Maximum number of allowed decimal places
pub const MAX_DECIMAL_PLACES: i64 = 18;

#[derive(PartialEq, Eq, Clone, Hash)]
pub struct TokenID {
    parent_id: [u8; 32],
    subgroup: Vec<u8>,
}

impl TokenID {
    pub fn from_vec(data: Vec<u8>) -> Result<Self> {
        if data.len() < PARENT_GROUP_ID_LENGTH {
            bail!(
                "Token ID is too short ({} < {}",
                data.len(),
                PARENT_GROUP_ID_LENGTH
            )
        }
        // TODO: Use split_array when out of nightly.
        let (parent, subgroup) = data.split_at(PARENT_GROUP_ID_LENGTH);

        Ok(Self {
            parent_id: parent.try_into().unwrap(),
            subgroup: subgroup.to_vec(),
        })
    }

    pub fn from_slice(slice: &[u8]) -> Result<Self> {
        Self::from_vec(slice.to_vec())
    }

    pub fn from_hex(hex: &str) -> Result<Self> {
        let decoded = hex::decode(hex)?;
        Self::from_vec(decoded)
    }

    pub fn from_inner(parent_id: [u8; 32]) -> Self {
        Self {
            parent_id,
            subgroup: vec![],
        }
    }

    pub fn into_vec(self) -> Vec<u8> {
        self.parent_id
            .iter()
            .copied()
            .chain(self.subgroup)
            .collect::<Vec<u8>>()
    }

    pub fn as_vec(&self) -> Vec<u8> {
        self.parent_id
            .iter()
            .copied()
            .chain(self.subgroup.clone())
            .collect::<Vec<u8>>()
    }

    pub fn to_hex(&self) -> String {
        hex::encode(self.as_vec())
    }

    /**
     * Convert TokenID into parent token ID + subgroup data.
     *
     * If token is not a subgroup, then subgroup data is an empty vector.
     */
    pub fn into_parent_and_subgroup(self) -> ([u8; 32], Vec<u8>) {
        (self.parent_id, self.subgroup)
    }

    pub fn from_parent_and_subgroup(
        parent_id: [u8; PARENT_GROUP_ID_LENGTH],
        subgroup: Vec<u8>,
    ) -> TokenID {
        Self {
            parent_id,
            subgroup,
        }
    }
    /**
     * If this ID belongs to a subtoken.
     */
    pub fn is_subtoken(&self) -> bool {
        !self.subgroup.is_empty()
    }

    pub fn parent_id(&self) -> [u8; 32] {
        self.parent_id
    }

    /**
     * A hash value of 0.
     */
    pub fn all_zeros() -> TokenID {
        TokenID {
            parent_id: [0; 32],
            subgroup: vec![],
        }
    }

    /**
     * Encode token ID as a cashaddr
     */
    pub fn as_cashaddr(&self, network: Network) -> String {
        cashaddr::encode(
            &self.as_vec(),
            cashaddr::version_byte_flags::TYPE_GROUP,
            network,
        )
        .expect("failed to encode token id")
    }
}

impl Display for TokenID {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.to_hex())
    }
}

impl std::fmt::Debug for TokenID {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("TokenID")
            .field("parent_id", &self.parent_id.to_hex())
            .field("subgroup", &self.subgroup.to_hex())
            .finish()
    }
}

impl serde::Serialize for TokenID {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&self.to_hex())
    }
}

#[derive(Default)]
pub struct Token {
    pub hash: Option<TokenID>,
    pub ticker: Option<String>,
    pub name: Option<String>,
    pub document_url: Option<String>,
    pub document_hash: Option<[u8; 32]>,
    pub decimal_places: Option<u8>,
    pub op_return_id: Option<u32>,
}

/**
 * Serialize amount as encoded in Nexa
 */
pub fn serialize_amount(amount: i64) -> Vec<u8> {
    if amount < 0 {
        return bitcoincash::consensus::serialize::<u64>(&(amount as u64));
    }
    if amount <= u16::MAX as i64 {
        return bitcoincash::consensus::serialize::<u16>(&(amount as u16));
    }
    if amount <= u32::MAX as i64 {
        return bitcoincash::consensus::serialize::<u32>(&(amount as u32));
    }
    bitcoincash::consensus::serialize::<u64>(&(amount as u64))
}

/**
 * Deserialize amount as encoded in Nexa
 */
pub fn deserialize_amount(data: &[u8]) -> Result<i64> {
    let decoded = match data.len() {
        2 => bitcoincash::consensus::deserialize::<u16>(data).map(|x| x as u64),
        4 => bitcoincash::consensus::deserialize::<u32>(data).map(|x| x as u64),
        8 => bitcoincash::consensus::deserialize::<u64>(data),
        _ => bail!("Invalid length {}", data.len()),
    };
    match decoded {
        Err(e) => Err(anyhow::Error::from(e)),
        Ok(n) => Ok(n as i64),
    }
}

/**
 * Parse group transfer in a output. (This includes new token generation).
 *
 * Returns hash + amount (negative value if operation)
 */
pub fn parse_token_from_scriptpubkey(s: &Script) -> Option<(TokenID, i64)> {
    // At minimum, push op + 20 bytes + push op + 2 bytes amount
    if s.len() < 1 + 20 + 1 + 2 {
        return None;
    }

    if s.index(0) == &opcodes::all::OP_PUSHBYTES_0.to_u8() {
        // Not a group transaction
        return None;
    }

    let iter = s.as_bytes().iter();
    let (iter, tokenhash) = read_push_from_script(iter).ok()?;
    let tokenhash = tokenhash?;

    if tokenhash.len() < PARENT_GROUP_ID_LENGTH {
        // Minimum 32 bytes. Subgroups can be larger (up to consensus stack size).
        return None;
    }

    let (_, amount_serialized) = read_push_from_script(iter).ok()?;
    let amount_serialized = amount_serialized?;
    let amount = deserialize_amount(&amount_serialized).ok()?;

    match TokenID::from_vec(tokenhash) {
        Ok(hash) => Some((hash, amount)),
        _ => None,
    }
}

/**
 * Parse group token details from a OP_RETURN
 */
pub fn parse_token_description(script: &Script) -> Result<Token> {
    let mut iter = script.as_ref().iter();
    if let Some(o) = iter.next() {
        if *o != 0x6a
        /* OP_RETURN */
        {
            bail!("Not a OP_RETURN script")
        }
    } else {
        bail!("Empty script")
    }

    let (mut iter, prefix) = read_push_from_script(iter)?;
    if let Some(ref p) = prefix {
        let prefix = match read_uint(p, 4).ok() {
            Some(p) => p,
            None => return Ok(Token::default()),
        };

        if !OPRETURN_GROUP_PREFIXES
            .iter()
            .any(|valid_prefix| valid_prefix == &prefix)
        {
            return Ok(Token::default());
        }
    } else {
        return Ok(Token::default());
    };

    // this should be safe because we check that it decoded earlier already
    let prefix: [u8; 4] = match prefix.unwrap().try_into() {
        Ok(p) => p,
        Err(_) => {
            // Prefix should be exactly 4 bytes
            return Ok(Token::default());
        }
    };
    // pushdata opcode pushes in LE
    let prefix: u32 = u32::from_ne_bytes(prefix);
    let op_return_id = Some(prefix);

    let mut ticker = None;
    // 88888892 does not have a ticker field
    if prefix != 88888892 {
        let tmp;
        (iter, tmp) = read_push_from_script(iter)?;
        ticker = if let Some(t) = tmp {
            // 88888890 and 88888891 have only between 2 and 8 tickers
            if prefix == 88888890 || prefix == 88888891 {
                if t.len() < MIN_TICKER_LENGTH || t.len() > MAX_TICKER_LENGTH {
                    None
                } else {
                    String::from_utf8(t).ok()
                }
            } else if t.len() <= MAX_TICKER_LENGTH {
                String::from_utf8(t).ok()
            } else {
                None
            }
        } else {
            None
        };
    }

    let mut name = None;
    // 88888892 does not have a name field
    if prefix != 88888892 {
        let tmp;
        (iter, tmp) = read_push_from_script(iter)?;
        name = if let Some(n) = tmp {
            // 88888890 and 88888891 have only between 2 and 25 length names
            if prefix == 88888890 || prefix == 88888891 {
                if n.len() < MIN_NAME_LENGTH || n.len() > MAX_NAME_LENGTH {
                    None
                } else {
                    String::from_utf8(n).ok()
                }
            } else if prefix == 88888888 {
                String::from_utf8(n).ok()
            } else {
                None
            }
        } else {
            None
        };
    }

    let (iter, document_url) = read_push_from_script(iter)?;
    let document_url = if let Some(url) = document_url {
        String::from_utf8(url).ok()
    } else {
        None
    };

    let (iter, document_hash) = read_push_from_script(iter)?;
    let document_hash = document_hash.unwrap_or_default();
    let document_hash = if document_hash.len() == 32 {
        let mut d: [u8; 32] = document_hash.try_into().unwrap();
        d.reverse();
        Some(d)
    } else {
        None
    };

    let (_iter, decimal_places) = read_push_from_script(iter)?;
    let decimal_places = if let Some(d) = decimal_places {
        // 88888892 does not have decimal field
        if prefix == 88888892 {
            None
        }
        // 88888891 has a decimal field for legacy reasons and it must be 0
        else if prefix == 88888891 {
            Some(0_u8)
        } else {
            match read_scriptint(&d) {
                Ok(d) => {
                    if (0..=MAX_DECIMAL_PLACES).contains(&d) {
                        Some(d as u8)
                    } else {
                        // Valid number encoded, but it exceed decimals allowed by the protocol.
                        None
                    }
                }
                Err(_) => {
                    // Value encoded is not a valid number
                    None
                }
            }
        }
    } else {
        // 88888892 has no decimal field
        if prefix == 88888892 {
            None
        } else {
            // If the field does not exist, 0 is implied for standards with a decimal field.
            Some(0)
        }
    };

    Ok(Token {
        hash: None,
        name,
        ticker,
        document_url,
        document_hash,
        decimal_places,
        op_return_id,
    })
}

#[cfg(test)]
mod test {
    use std::collections::HashMap;

    use super::*;

    #[test]
    fn test_serialize_amount() {
        assert_eq!(-42, deserialize_amount(&serialize_amount(-42)).unwrap());

        assert_eq!(
            u16::MAX as i64,
            deserialize_amount(&serialize_amount(u16::MAX as i64)).unwrap()
        );

        assert_eq!(
            u32::MAX as i64,
            deserialize_amount(&serialize_amount(u32::MAX as i64)).unwrap()
        );

        assert_eq!(
            i64::MAX as i64,
            deserialize_amount(&serialize_amount(i64::MAX as i64)).unwrap()
        );
    }

    #[test]
    fn test_parse_scriptpubkey() {
        let hex = "202ab2bcb6b2fb547edcdde5011e947c84b051f9767dc0758157b304ec452000000872790000000000fc511490793b86d7bdf8bd36ec795d0082ee41fbeff7e3";
        let script = Script::from(hex::decode(hex).unwrap());
        let (hash, amount) = parse_token_from_scriptpubkey(&script).unwrap();
        assert_eq!(
            hash.to_string(),
            "2ab2bcb6b2fb547edcdde5011e947c84b051f9767dc0758157b304ec45200000"
        );
        assert_eq!(amount, -288230376151680654);
    }

    #[test]
    fn test_parse_opreturn_legacy_token() {
        let hex = "6a0438564c0504444f4745084d75636820576f771368747470733a2f2f6578616d706c652e6f726720efbead0b00000000000000000000000000000000000000000000000000000000";
        let script = Script::from(hex::decode(hex).unwrap());
        let token = parse_token_description(&script).unwrap();
        assert_eq!("DOGE", token.ticker.unwrap());
        assert_eq!("Much Wow", token.name.unwrap());
        assert_eq!("https://example.org", token.document_url.unwrap());
        assert_eq!(
            hex::decode("000000000000000000000000000000000000000000000000000000000badbeef")
                .unwrap(),
            token.document_hash.unwrap()
        );
        assert_eq!(88888888, token.op_return_id.unwrap());
    }

    #[test]
    fn test_parse_opreturn_nrc1_token() {
        let hex = "6a043a564c05044e5553440a4e6174697665205553443268747470733a2f2f6e6174697665732e636173682f6170692f737461626c65732f6e7573642f676574546f6b656e496e666f20940106cb0d5a4ef94577ef444f765674008210e2e8f7679bdb4df8fe42cbe61456";
        let script = Script::from(hex::decode(hex).unwrap());
        let token = parse_token_description(&script).unwrap();
        assert_eq!("NUSD", token.ticker.unwrap());
        assert_eq!("Native USD", token.name.unwrap());
        assert_eq!(
            "https://natives.cash/api/stables/nusd/getTokenInfo",
            token.document_url.unwrap()
        );
        assert_eq!(
            hex::decode("14e6cb42fef84ddb9b67f7e8e21082007456764f44ef7745f94e5a0dcb060194")
                .unwrap(),
            token.document_hash.unwrap()
        );
        assert_eq!(6, token.decimal_places.unwrap());
        assert_eq!(88888890, token.op_return_id.unwrap());
    }

    #[test]
    fn test_parse_opreturn_nrc2_nft_collection() {
        let hex = "6a043b564c05034c4443174c6567656e64617279204475656c6973742043617264734c5068747470733a2f2f697066732e696f2f697066732f6261666b766d69626c6d326464346d6535797236763566327977787665786d78667667726c63367971357365646d6764746e7535786e6e666663652011a5b4763b6d73183688ec107bb1a2a9e5b24beab558975e7dc49d303e86662b00";
        let script = Script::from(hex::decode(hex).unwrap());
        let token = parse_token_description(&script).unwrap();
        assert_eq!("LDC", token.ticker.unwrap());
        assert_eq!("Legendary Duelist Cards", token.name.unwrap());
        assert_eq!(
            "https://ipfs.io/ipfs/bafkvmiblm2dd4me5yr6v5f2ywxvexmxfvgrlc6yq5sedmgdtnu5xnnffce",
            token.document_url.unwrap()
        );
        assert_eq!(
            hex::decode("2b66863e309dc47d5e9758b5ea4bb2e5a9a2b17b10ec883618736d3b76b4a511")
                .unwrap(),
            token.document_hash.unwrap()
        );
        assert_eq!(0, token.decimal_places.unwrap());
        assert_eq!(88888891, token.op_return_id.unwrap());
    }

    #[test]
    fn test_parse_opreturn_nrc3_nft() {
        let hex = "6a043c564c054c5068747470733a2f2f697066732e696f2f697066732f62616679666d69656f696b6e6336776e66636e36767664616e643633366264706f6d666f72657970713561667671626272676a6c3368726c63747120677705dab1ebbd58295991141085bc48eac52601bc43cb9cdf6710e7a4b9ac3c";
        let script = Script::from(hex::decode(hex).unwrap());
        let token = parse_token_description(&script).unwrap();
        assert_eq!(None, token.ticker);
        assert_eq!(None, token.name);
        assert_eq!(
            "https://ipfs.io/ipfs/bafyfmieoiknc6wnfcn6vvdand636bdpomforeypq5afvqbbrgjl3hrlctq",
            token.document_url.unwrap()
        );
        assert_eq!(
            hex::decode("3cacb9a4e71067df9ccb43bc0126c5ea48bc85101491592958bdebb1da057767")
                .unwrap(),
            token.document_hash.unwrap()
        );
        assert_eq!(None, token.decimal_places);
        assert_eq!(88888892, token.op_return_id.unwrap());
    }

    #[test]
    fn test_parse_subgroup_sciptpubkey() {
        let hex = "28442dbb752c2fca49d5f6901a14d953e12dd4a84862f556005de47f5d03110000c800000000000000022a0051144d8f8df0ed540551c38cdf8443dcb549a2f93c2b";
        let script = Script::from(hex::decode(hex).unwrap());
        let (token, amount) = parse_token_from_scriptpubkey(&script).unwrap();
        assert_eq!(
            "442dbb752c2fca49d5f6901a14d953e12dd4a84862f556005de47f5d03110000c800000000000000",
            token.to_hex()
        );
        assert_eq!(42, amount);
    }

    #[test]
    fn test_parse_opreturn_longname() {
        let hex = "6a0438564c050568656c6c6f2261206e616d65206f66206120746f6b656e20776974682061206c6f6e67206e616d650000";
        let script = Script::from(hex::decode(hex).unwrap());
        let token = parse_token_description(&script).unwrap();
        assert_eq!("a name of a token with a long name", token.name.unwrap());
    }

    #[test]
    fn test_tokenid_serialize() {
        let dummy_id = TokenID::from_inner([0xAA; PARENT_GROUP_ID_LENGTH]);
        assert_eq!(
            format!("\"{}\"", dummy_id.to_hex()),
            json!(dummy_id).to_string()
        )
    }

    #[test]
    fn test_tokenid_serialize_in_map() {
        let mut map: HashMap<TokenID, i64> = HashMap::new();
        let dummy_id = TokenID::from_inner([0xAA; PARENT_GROUP_ID_LENGTH]);
        map.insert(dummy_id.clone(), 42);
        assert_eq!(
            format!("{{\"{}\":42}}", dummy_id.to_hex()),
            json!(map).to_string()
        );
    }

    #[test]
    fn test_into_parent_and_subgroup() {
        let id = TokenID::from_inner([0xaa; PARENT_GROUP_ID_LENGTH]);
        let (parent, sub) = id.into_parent_and_subgroup();
        assert_eq!([0xaa; PARENT_GROUP_ID_LENGTH], parent);
        assert_eq!(0, sub.len());

        let id = TokenID::from_vec(vec![0xaa; 512]).unwrap();
        let (parent, sub) = id.into_parent_and_subgroup();
        assert_eq!([0xaa; PARENT_GROUP_ID_LENGTH], parent);
        assert_eq!(vec![0xaa; 512 - PARENT_GROUP_ID_LENGTH], sub);
    }

    #[test]
    fn test_from_parent_and_subgroup() {
        let parent = [0xaa; PARENT_GROUP_ID_LENGTH];
        let sub = vec![0xbb; 5];
        let id = TokenID::from_parent_and_subgroup(parent, sub.clone());

        let expected = parent
            .iter()
            .chain(sub.iter())
            .cloned()
            .collect::<Vec<u8>>();

        assert_eq!(expected, id.into_vec())
    }

    #[test]
    fn subtoken_tohex() {
        let parent = [0xaa; PARENT_GROUP_ID_LENGTH];
        let sub = vec![0xbb; 5];
        let id = TokenID::from_parent_and_subgroup(parent, sub.clone());

        let expected = parent
            .iter()
            .chain(sub.iter())
            .cloned()
            .collect::<Vec<u8>>();

        assert_eq!(PARENT_GROUP_ID_LENGTH + 5, expected.len());

        assert_eq!(hex::encode(expected), id.to_hex())
    }
}
