use std::io::Write;
use std::ops::Index;

use crate::chaindef::OutPointHash;
use crate::encode::{decode_vector, encode_vector};
use crate::impl_nexa_consensus_encoding;
use crate::nexa::hash_types::TxIdem;
use crate::utilscript::read_push_from_script;
use anyhow::{Context, Result};
use bitcoin_hashes::hex::ToHex;
use bitcoin_hashes::Hash;
use bitcoincash::blockdata::opcodes;
use bitcoincash::consensus::Decodable;
use bitcoincash::consensus::Encodable;
use bitcoincash::VarInt;
use bitcoincash::{Script, Txid};
use byteorder::WriteBytesExt;

use super::token::{deserialize_amount, TokenID};

#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct OutPoint {
    pub hash: OutPointHash,
}

impl OutPoint {
    pub fn new(tx_idem: TxIdem, out_index: u32) -> OutPoint {
        let mut e = OutPointHash::engine();
        tx_idem
            .consensus_encode(&mut e)
            .expect("failed to encode txidem");
        out_index
            .consensus_encode(&mut e)
            .expect("failed to encode output_index");

        OutPoint {
            hash: OutPointHash::from_engine(e),
        }
    }
    pub fn is_null(&self) -> bool {
        self.hash.eq(&OutPointHash::all_zeros())
    }
}
impl_nexa_consensus_encoding!(OutPoint, hash);

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub struct TxIn {
    pub txin_type: u8,
    pub previous_output: OutPoint,
    pub script_sig: bitcoincash::Script,
    pub sequence: u32,
    pub amount: u64,
}

impl_nexa_consensus_encoding!(
    TxIn,
    txin_type,
    previous_output,
    script_sig,
    sequence,
    amount
);

/**
 * Output type
 */
pub enum TxOutType {
    // Bitcoin-style output
    SATOSHI = 0,
    // Nexa template style output. May contain tokens.
    TEMPLATE = 1,
}

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub struct TxOut {
    pub txout_type: u8,
    pub value: u64,
    pub script_pubkey: bitcoincash::Script,
}
impl_nexa_consensus_encoding!(TxOut, txout_type, value, script_pubkey);

impl TxOut {
    /**
     * Removes the token and token amount from script_pubkey. Replaces it with a OP_0
     * (as if there was no token in transaction).
     *
     * Returns modified script or None if no modification is needed (to avoid a copy).
     */
    pub fn scriptpubkey_without_token(&self) -> Result<Option<Script>> {
        if self.txout_type != (TxOutType::TEMPLATE as u8) {
            return Ok(None);
        }

        if self.script_pubkey.index(0) == &opcodes::all::OP_PUSHBYTES_0.to_u8() {
            return Ok(None);
        }

        let iter = self.script_pubkey.as_bytes().iter();
        let (iter, _) = read_push_from_script(iter).context(anyhow!("failed to read tokenhash"))?;
        let (iter, _) = read_push_from_script(iter).context("invalid token amount")?;

        let normalized_script = Script::from(
            std::iter::once(&opcodes::all::OP_PUSHBYTES_0.to_u8())
                .chain(iter)
                .cloned()
                .collect::<Vec<u8>>(),
        );
        Ok(Some(normalized_script))
    }

    /**
     * If this output has a token transfer/action.
     */
    pub fn has_token(&self) -> bool {
        if self.script_pubkey.is_empty() || self.txout_type != (TxOutType::TEMPLATE as u8) {
            return false;
        }
        self.script_pubkey.index(0) != &opcodes::all::OP_PUSHBYTES_0.to_u8()
    }
}

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub struct Transaction {
    pub version: u8,
    pub lock_time: u32,
    pub input: Vec<TxIn>,
    pub output: Vec<TxOut>,
}

impl Transaction {
    pub fn txidem(&self) -> TxIdem {
        let mut s = TxIdem::engine();
        self.version.consensus_encode(&mut s).unwrap();

        // We need to encode inputs without scriptsig, so not using `encode_vector`.
        VarInt(self.input.len() as u64)
            .consensus_encode(&mut s)
            .unwrap();
        for i in &self.input {
            i.txin_type.consensus_encode(&mut s).unwrap();
            i.previous_output.consensus_encode(&mut s).unwrap();
            i.sequence.consensus_encode(&mut s).unwrap();
            i.amount.consensus_encode(&mut s).unwrap();
        }
        encode_vector(&mut s, &self.output).unwrap();
        self.lock_time.consensus_encode(&mut s).unwrap();

        TxIdem::from_engine(s)
    }

    pub fn txid(&self) -> Txid {
        use bitcoin_hashes::sha256d;
        let mut satisfier_script_hash = sha256d::Hash::engine();
        i32::consensus_encode(&(self.input.len() as i32), &mut satisfier_script_hash).unwrap();

        for i in &self.input {
            satisfier_script_hash
                .write_all(i.script_sig.as_bytes())
                .unwrap();
            satisfier_script_hash.write_u8(0xff).unwrap();
        }

        let mut txid = Txid::engine();
        let txidem = self.txidem();
        let satisfier = sha256d::Hash::from_engine(satisfier_script_hash);
        txidem.consensus_encode(&mut txid).unwrap();
        satisfier.consensus_encode(&mut txid).unwrap();

        Txid::from_engine(txid)
    }

    /// Returns the regular byte-wise consensus-serialized size of this transaction.
    pub fn size(&self) -> usize {
        let mut input_size = 0;

        for input in &self.input {
            input_size += 32 + 4 + 4 + /* outpoint (32+4) + nSequence */ VarInt(input.script_sig.len() as u64).len() + input.script_sig.len();
        }
        let mut output_size = 0;
        for output in &self.output {
            output_size += 8 + /* value */ VarInt(output.script_pubkey.len() as u64).len() + output.script_pubkey.len();
        }
        let non_input_size =
        // version:
        4 +
        // count varints:
        VarInt(self.input.len() as u64).len() +
        VarInt(self.output.len() as u64).len() +
        output_size +
        // lock_time
        4;

        non_input_size + input_size
    }

    pub fn is_coin_base(&self) -> bool {
        self.input.is_empty()
    }
}

impl Encodable for Transaction {
    fn consensus_encode<W: std::io::Write + ?Sized>(
        &self,
        w: &mut W,
    ) -> Result<usize, std::io::Error> {
        let mut len = 0;
        len += self.version.consensus_encode(w)?;

        // Can't implement Encodable trait for Vec<TxIn> or TxOut because of Rust orphan rules.
        // (We don't own Encodeable and we don't own Vec).
        len += encode_vector(w, &self.input)?;
        len += encode_vector(w, &self.output)?;

        len += self.lock_time.consensus_encode(w)?;
        Ok(len)
    }
}

impl Decodable for Transaction {
    fn consensus_decode<R: std::io::Read + ?Sized>(
        r: &mut R,
    ) -> Result<Self, bitcoincash::consensus::encode::Error> {
        let version = u8::consensus_decode(r).unwrap();

        let input: Vec<TxIn> = decode_vector(r).unwrap();
        let output: Vec<TxOut> = decode_vector(r).unwrap();

        Ok(Transaction {
            version,
            input,
            output,
            lock_time: Decodable::consensus_decode(r).unwrap(),
        })
    }
}

pub const WELL_KNOWN_TEMPLATE_P2PKT: [u8; 1] = [1];

#[derive(Debug)]
pub struct ScriptTemplate {
    pub token: Option<TokenID>,
    pub quantity: Option<i64>,
    pub scripthash: Vec<u8>,
    pub argumenthash: Vec<u8>,
}

impl ScriptTemplate {
    /**
     * Human friendly representation of scripthash
     */
    pub fn scripthash_human(&self) -> String {
        if self.scripthash.as_slice() == WELL_KNOWN_TEMPLATE_P2PKT {
            "pay2pubkeytemplate".to_string()
        } else {
            self.scripthash.to_hex()
        }
    }

    /**
     * Get group authority flags
     */
    pub fn group_authority(&self) -> Option<u64> {
        self.quantity.filter(|x| *x < 0).map(|x| x as u64)
    }
}

/**
 * Parses a Nexa script template from scriptPubKey.
 *
 * Does not validate the values (except for that `TokenID` can be constructed).
 */
pub fn parse_script_template(s: &Script) -> Option<ScriptTemplate> {
    if s.is_empty() {
        return None;
    }

    let mut iter = s.as_bytes().iter();
    let (iter, token) = if s.index(0) == &opcodes::all::OP_PUSHBYTES_0.to_u8() {
        // Not a group transaction
        iter.next(); // Skip OP_0
        (iter, None)
    } else {
        let (iter, tokenhash) = read_push_from_script(iter).ok()?;
        let tokenhash = tokenhash?;
        let (iter, amount_serialized) = read_push_from_script(iter).ok()?;
        let amount_serialized = amount_serialized?;
        let amount = deserialize_amount(&amount_serialized).ok()?;
        (
            iter,
            match TokenID::from_vec(tokenhash) {
                Ok(hash) => Some((hash, amount)),
                _ => None,
            },
        )
    };

    let (iter, scripthash) = read_push_from_script(iter).ok()?;
    let (_, argumenthash) = read_push_from_script(iter).ok()?;

    Some(ScriptTemplate {
        quantity: token.as_ref().map(|(_, q)| *q),
        token: token.map(|(t, _)| t),
        scripthash: scripthash?,
        argumenthash: argumenthash?,
    })
}

#[cfg(test)]
mod test {
    use crate::{nexa::token::parse_token_from_scriptpubkey, nexa::token::TokenID};

    use super::*;
    use bitcoincash::{
        consensus::encode::deserialize, consensus::encode::deserialize_partial,
        consensus::encode::serialize,
    };

    #[test]
    /** Coinbase transaction `8b81d5de14b7b0951a2981c2d58919e9878847af1fad7c4e0fa17957ed8216b6` */
    fn deserialize_coinbase_transaction() {
        let tx_hex = "0000020100ca9a3b0000000017005114a64ba51750cadde003c7e8cfc400959d7ec7e0420000000000000000000f6a03ba8e000008000000000000000000000000";
        let serialized = hex::decode(&tx_hex).unwrap();
        let tx: (Transaction, usize) = deserialize_partial(&serialized).unwrap();
        assert_eq!(tx.1, serialized.len());
        let tx = tx.0;
        assert!(tx.is_coin_base());
        assert_eq!(tx.output[0].value, 1000000000);
        assert_eq!(tx.output[1].value, 0);
    }

    #[test]
    /** Coinbase transaction `8b81d5de14b7b0951a2981c2d58919e9878847af1fad7c4e0fa17957ed8216b6` */
    fn serialize_coinbase_transaction() {
        let expected_tx_hex = "0000020100ca9a3b0000000017005114a64ba51750cadde003c7e8cfc400959d7ec7e0420000000000000000000f6a03ba8e000008000000000000000000000000";
        let tx: Transaction = deserialize(&hex::decode(&expected_tx_hex).unwrap()).unwrap();
        assert_eq!(hex::encode(&serialize(&tx)), expected_tx_hex)
    }

    #[test]
    fn txid_coinbase_transaction() {
        let tx_hex = "0000020100ca9a3b0000000017005114a64ba51750cadde003c7e8cfc400959d7ec7e0420000000000000000000f6a03ba8e000008000000000000000000000000";
        let tx: Transaction = deserialize(&hex::decode(&tx_hex).unwrap()).unwrap();
        assert_eq!(
            tx.txidem().to_string(),
            "79bd936e54853c39bd00e6922a6c050f123ceb9cf3d51cabf806f6a5a13afc9a"
        );
        assert_eq!(
            tx.txid().to_string(),
            "8b81d5de14b7b0951a2981c2d58919e9878847af1fad7c4e0fa17957ed8216b6"
        );
    }

    #[test]
    fn test_scriptpubkey_without_token() {
        let tx_hex = "00020094cac789cea15c56cd65dacb1c0c82c9f2d66347c6c2c049a6e908f5d1ce867d6422210385e6ec5324d707f22dcdf1237601cdbc0b0af30b468b34fc4fafa76ca6a6a2d5403705bea828e293b9c43df45d24eb04e909a2dce4d2d6295626904bee7b451f0907dcdba479095eddd7a49d00c230ba93f9690f0a2675bec3a49732809447367efeffffff220200000000000000ea8e221bb64936acac2a897002cfb6dbaf4f37d564366061b47bea257a6a1a9364222102eb89ce1df77ef589750c922a7e9d4d149355870c740bf29f57a0891f98d5ccd940dae8ba46b1c820d04837b4e03a4f663faa8060c8a49ac7e860ae1aec534ae80601d7692b63610caed05728d701dc8a19f6493443100d2be6fb67917785174bebfeffffffcac59a3b00000000030122020000000000003a201dc245acd1d1023f1c3e4a3143b41f0183ee3efa651581c053d2726ebeeb0000022a005114679fe206162bcfa16f20318a5004b2c67026ba8501220200000000000040201dc245acd1d1023f1c3e4a3143b41f0183ee3efa651581c053d2726ebeeb00000800000000000000fc5114d1cd4eb36aa94dcddba059611ec10833d1a7f8a101a3c19a3b0000000017005114468c14dac6c28b2d4be1f591aefbabceb3a8ad7700000000";
        let tx: Transaction = deserialize(&hex::decode(tx_hex).unwrap()).unwrap();
        let token_output = tx.output[0].clone();
        let (token, amount) = parse_token_from_scriptpubkey(&token_output.script_pubkey).unwrap();
        assert_eq!(
            token,
            TokenID::from_hex("1dc245acd1d1023f1c3e4a3143b41f0183ee3efa651581c053d2726ebeeb0000")
                .unwrap()
        );
        assert_eq!(amount, 42);
        let script_pubkey_no_token = token_output.scriptpubkey_without_token().unwrap().unwrap();
        assert_eq!(None, parse_token_from_scriptpubkey(&script_pubkey_no_token));
        assert_eq!(
            script_pubkey_no_token,
            Script::from(hex::decode("005114679fe206162bcfa16f20318a5004b2c67026ba85").unwrap())
        );
    }

    #[test]
    fn test_parse_script_template() {
        let hex = "209d5e7d33920610f39b50d0f8fe13eb67a0419747a2547cf0697d5fc22909000008ed0c0100000000fc5114ea6d19eea0c1dbb1a0be94946dac2c0d5a28287a";
        let script = Script::from(hex::decode(hex).unwrap());
        let template = parse_script_template(&script).unwrap();
        assert_eq!("pay2pubkeytemplate", template.scripthash_human());
        assert_eq!(
            "ea6d19eea0c1dbb1a0be94946dac2c0d5a28287a",
            template.argumenthash.to_hex()
        );
        assert_eq!(-288230376151642899, template.quantity.unwrap());
        assert_eq!(18158513697557908717, template.group_authority().unwrap());
    }
}
