use crate::chaindef::OutPointHash;
use crate::chaindef::TokenID;
use crate::store::db_decode;
use crate::store::db_encode;
use crate::store::Row;
use bitcoincash::hashes::Hash;

use super::DBRow;

const OUTPUTTOKENINDEX_CF: &str = "outputtoken";

#[derive(serde::Serialize, serde::Deserialize, Eq, PartialEq, Debug)]
pub struct OutputTokenIndexKey {
    pub outpoint_hash: [u8; 32],
    pub parent_token_id: [u8; 32],
    pub subgroup_blob: Vec<u8>,
}

#[derive(serde::Serialize, serde::Deserialize, Eq, PartialEq, Debug)]
pub struct OutputTokenIndexRow {
    key: OutputTokenIndexKey,
    token_amount: u64,
}

/**
 * Indexes `outpointhash` -> `tokenid`
 */
impl OutputTokenIndexRow {
    #[cfg(nexa)]
    pub fn new(outpoint_hash: &OutPointHash, token_id: TokenID, token_amount: i64) -> Self {
        let (parent, sub) = token_id.into_parent_and_subgroup();
        Self {
            key: OutputTokenIndexKey {
                outpoint_hash: outpoint_hash.into_inner(),
                parent_token_id: parent,
                subgroup_blob: sub,
            },
            token_amount: token_amount as u64,
        }
    }

    #[cfg(bch)]
    pub fn new(
        outpoint_hash: &OutPointHash,
        token_id: TokenID,
        commitment: Vec<u8>,
        token_amount: i64,
    ) -> Self {
        Self {
            key: OutputTokenIndexKey {
                outpoint_hash: outpoint_hash.into_inner(),
                parent_token_id: token_id.into_inner(),
                subgroup_blob: commitment,
            },
            token_amount: token_amount as u64,
        }
    }

    pub fn filter_by_outpointhash(hash: &OutPointHash) -> Vec<u8> {
        hash.to_vec()
    }

    #[cfg(nexa)]
    pub fn filter_by_outpointhash_and_token(hash: &OutPointHash, token_id: TokenID) -> Vec<u8> {
        let (parent, sub) = token_id.into_parent_and_subgroup();
        db_encode(&OutputTokenIndexKey {
            outpoint_hash: hash.into_inner(),
            parent_token_id: parent,
            subgroup_blob: sub,
        })
        .unwrap()
    }

    pub fn outpoint_hash(&self) -> OutPointHash {
        OutPointHash::from_inner(self.key.outpoint_hash)
    }

    #[cfg(nexa)]
    pub fn token_id(&self) -> TokenID {
        TokenID::from_parent_and_subgroup(self.key.parent_token_id, self.key.subgroup_blob.clone())
    }

    #[cfg(bch)]
    pub fn token_id(&self) -> TokenID {
        TokenID::from_inner(self.key.parent_token_id)
    }

    #[cfg(nexa)]
    pub fn into_token_id(self) -> TokenID {
        TokenID::from_parent_and_subgroup(self.key.parent_token_id, self.key.subgroup_blob)
    }

    #[cfg(bch)]
    pub fn into_token_id(self) -> TokenID {
        TokenID::from_inner(self.key.parent_token_id)
    }

    #[cfg(bch)]
    pub fn into_token_id_and_commitment(self) -> (TokenID, Vec<u8>) {
        (
            TokenID::from_inner(self.key.parent_token_id),
            self.key.subgroup_blob,
        )
    }

    pub fn token_amount(&self) -> i64 {
        self.token_amount as i64
    }

    pub fn into_parent_and_subgroup(self) -> ([u8; 32], Vec<u8>) {
        (self.key.parent_token_id, self.key.subgroup_blob)
    }
}

impl DBRow for OutputTokenIndexRow {
    const CF: &'static str = OUTPUTTOKENINDEX_CF;
    fn to_row(&self) -> Row {
        Row {
            key: db_encode(&self.key).unwrap().into_boxed_slice(),
            value: db_encode(&self.token_amount).unwrap().into_boxed_slice(),
        }
    }

    fn from_row(row: &Row) -> Self {
        Self {
            key: db_decode(&row.key).unwrap(),
            token_amount: db_decode(&row.value).unwrap(),
        }
    }
}

// TODO: Enable tests for bch
#[cfg(test)]
#[cfg(nexa)]
mod tests {
    use bitcoin_hashes::hex::ToHex;

    use super::*;
    #[test]
    fn test_to_from_row() {
        let row = OutputTokenIndexRow::new(
            &OutPointHash::hash(&[42]),
            TokenID::from_inner([0xAA; 32]),
            42,
        );
        let decoded_row = OutputTokenIndexRow::from_row(&row.to_row());
        assert_eq!(row, decoded_row);
    }

    #[test]
    fn test_filter_by_outpointhash_and_token() {
        let token_a = TokenID::from_inner([0xAA; 32]);
        let token_b = TokenID::from_inner([0xBB; 32]);
        let outpointhash = OutPointHash::hash(&[42]);
        let row_with_token_a =
            OutputTokenIndexRow::new(&outpointhash, token_a.clone(), 42).to_row();
        let row_with_token_b =
            OutputTokenIndexRow::new(&outpointhash, token_b.clone(), 24).to_row();

        let filter_token_a =
            OutputTokenIndexRow::filter_by_outpointhash_and_token(&outpointhash, token_a);
        let all = OutputTokenIndexRow::filter_by_outpointhash(&outpointhash);

        let starts_with = |a: &Row, b: &Vec<u8>| -> bool {
            a.key.to_vec().iter().zip(b.iter()).all(|(x, y)| x == y)
        };
        assert!(starts_with(&row_with_token_a, &filter_token_a));
        assert!(!starts_with(&row_with_token_b, &filter_token_a));

        assert!(starts_with(&row_with_token_a, &all));
        assert!(starts_with(&row_with_token_b, &all));
    }

    #[test]
    fn test_filter_matches_key() {
        let token = TokenID::from_inner([0xAA; 32]);
        let outpointhash = OutPointHash::hash(&[42]);
        let row = OutputTokenIndexRow::new(&outpointhash, token.clone(), 42).to_row();
        let filter = OutputTokenIndexRow::filter_by_outpointhash_and_token(&outpointhash, token);

        assert_eq!(row.key.to_vec().to_hex(), filter.to_hex());
    }
}
