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

use super::DBRow;

const TOKENOUTPUTINDEX_CF: &str = "tokenoutput";

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

#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq)]
pub struct TokenOutputIndexRow {
    key: TokenOutputIndexKey,
}

#[cfg(nexa)]
fn get_parent_id(token_id: &TokenID) -> [u8; 32] {
    token_id.parent_id()
}

#[cfg(bch)]
fn get_parent_id(token_id: &TokenID) -> [u8; 32] {
    token_id.into_inner()
}

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

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

    /**
     * Filter for finding outputs for specific token
     */
    #[cfg(nexa)]
    pub fn filter_by_parent_and_subgroup(token_id: &TokenID) -> Vec<u8> {
        use crate::store::db_encode;

        let (parent, subgroup) = &token_id.clone().into_parent_and_subgroup();

        db_encode(&parent)
            .into_iter()
            .chain(db_encode(subgroup))
            .flatten()
            .collect()
    }

    /**
     * Filter for finding outputs for specific token
     */
    #[cfg(bch)]
    pub fn filter_by_parent_and_subgroup(token_id: &TokenID, commitment: &[u8]) -> Vec<u8> {
        let parent: [u8; 32] = get_parent_id(token_id);

        db_encode(&parent)
            .into_iter()
            .chain(db_encode(commitment))
            .flatten()
            .collect()
    }

    /**
     * Filter for finding outputs for token, including all subtokens of it.
     */
    pub fn filter_by_parent_including_subroup(parent: &TokenID) -> Vec<u8> {
        let parent: [u8; 32] = get_parent_id(parent);
        db_encode(&parent).into_iter().flatten().collect()
    }

    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(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)
    }

    #[inline]
    pub fn is_subtoken(&self) -> bool {
        !self.key.subgroup_blob.is_empty()
    }

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

impl DBRow for TokenOutputIndexRow {
    const CF: &'static str = TOKENOUTPUTINDEX_CF;
    fn to_row(&self) -> Row {
        Row {
            key: db_encode(&self.key).unwrap().into_boxed_slice(),
            value: vec![].into_boxed_slice(),
        }
    }

    fn from_row(row: &Row) -> Self {
        Self {
            key: db_decode(&row.key).expect("failed to parse TokenOutputIndexKey"),
        }
    }
}

// TODO: Enable tests for BCH
#[cfg(test)]
mod tests {
    #[cfg(nexa)]
    use super::*;
    #[cfg(nexa)]
    use crate::nexa::token::PARENT_GROUP_ID_LENGTH;
    #[cfg(nexa)]
    #[test]
    fn test_parent_to_from_row() {
        let row =
            TokenOutputIndexRow::new(TokenID::from_inner([0xaa; 32]), &OutPointHash::hash(&[11]));
        let decoded_row = TokenOutputIndexRow::from_row(&row.to_row());
        assert_eq!(row, decoded_row);
    }

    #[cfg(nexa)]
    #[test]
    fn test_subgroup_to_from_row() {
        let row = TokenOutputIndexRow::new(
            TokenID::from_vec(vec![0xbb; 256]).unwrap(),
            &OutPointHash::hash(&[11]),
        );
        let decoded_row = TokenOutputIndexRow::from_row(&row.to_row());
        assert_eq!(row, decoded_row);
    }

    #[cfg(nexa)]
    #[test]
    fn test_filter_by_tokenid() {
        let parent = [0xaa; PARENT_GROUP_ID_LENGTH];
        let sub = vec![0xbb; 42];

        let token_id_parent = TokenID::from_inner(parent);
        let token_id_subgroup = TokenID::from_parent_and_subgroup(parent, sub);

        let parent_row =
            TokenOutputIndexRow::new(token_id_parent.clone(), &OutPointHash::hash(&[11])).to_row();
        let sub_row =
            TokenOutputIndexRow::new(token_id_subgroup.clone(), &OutPointHash::hash(&[22]))
                .to_row();

        let filter_parent: Vec<u8> =
            TokenOutputIndexRow::filter_by_parent_and_subgroup(&token_id_parent);
        let filter_parent_prefix: Vec<u8> =
            TokenOutputIndexRow::filter_by_parent_including_subroup(&token_id_parent);
        let filter_parent_and_subgroup: Vec<u8> =
            TokenOutputIndexRow::filter_by_parent_and_subgroup(&token_id_subgroup);

        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(&parent_row, &filter_parent));
        assert!(starts_with(&parent_row, &filter_parent_prefix));

        assert!(!starts_with(&sub_row, &filter_parent));
        assert!(starts_with(&sub_row, &filter_parent_prefix));
        assert!(starts_with(&sub_row, &filter_parent_and_subgroup))
    }
}
