use zeroize::Zeroize;

use crate::ejsonpath::{DotArg, IndexArg, Selector};

/// A mac-able type.
///
/// Types implementing `Mac` are able to be `mac`ed with an instance of [`Macca`].
///
/// `Mac` & `Macca` are synonymous with [`std::hash::Hash`] & [`std::hash::Hasher`] respectively.
pub(crate) trait Mac {
    fn mac<M>(&self, state: &mut M)
    where
        M: Macca;
}

/// A trait for mac'ing a type.
///
/// Used in conjunction with [`Mac`].
pub(crate) trait Macca: Zeroize {
    fn update(&mut self, bytes: &[u8]);
    fn finalize_reset<const N: usize>(&mut self) -> [u8; N];
}

impl Mac for Selector {
    fn mac<M>(&self, state: &mut M)
    where
        M: Macca,
    {
        // Note that the info strings are padded to all be the same length in order to prevent ambiguous coding errors.
        const SELECTOR_ROOT: &[u8] = "SEL:ROO".as_bytes();
        const SELECTOR_DOT: &[u8] = "SEL:DOT".as_bytes();
        const SELECTOR_INDEX: &[u8] = "SEL:IDX".as_bytes();

        match self {
            Selector::Root => state.update(SELECTOR_ROOT),
            Selector::Dot(selector, dot_arg) => {
                state.update(SELECTOR_DOT);
                selector.mac(state);
                dot_arg.mac(state);
            }
            Selector::Index(selector, index_arg) => {
                state.update(SELECTOR_INDEX);
                selector.mac(state);
                index_arg.mac(state);
            }
        }
    }
}

impl Mac for DotArg {
    fn mac<M>(&self, state: &mut M)
    where
        M: Macca,
    {
        const DOTARG_FIELD: &[u8] = "DOT:FLD".as_bytes();

        match self {
            DotArg::Field(f) => {
                state.update(DOTARG_FIELD);
                state.update(f.as_bytes());
            }
        }
    }
}

impl Mac for IndexArg {
    fn mac<M>(&self, state: &mut M)
    where
        M: Macca,
    {
        // Note that the info strings are padded to all be the same length in order to prevent ambiguous coding errors.
        const INDEXARG_NUMBER: &[u8] = "IDX:NUM".as_bytes();
        const INDEXARG_FIELD: &[u8] = "IDX:FLD".as_bytes();
        const INDEXARG_WILDCARD: &[u8] = "IDX:WLD".as_bytes();
        const INDEXARG_ITEM: &[u8] = "IDX:ITM".as_bytes();

        match self {
            IndexArg::Number(n) => {
                state.update(INDEXARG_NUMBER);
                state.update(&n.to_be_bytes()[..]);
            }
            IndexArg::Field(f) => {
                state.update(INDEXARG_FIELD);
                state.update(f.as_bytes());
            }
            IndexArg::Wildcard => {
                state.update(INDEXARG_WILDCARD);
            }
            IndexArg::Item => {
                state.update(INDEXARG_ITEM);
            }
        }
    }
}

#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
pub(crate) enum JsonbMacTerm {
    Null,
    Object,
    Array,
}

impl JsonbMacTerm {
    // Note that the info strings are padded to all be the same length in order to prevent ambiguous coding errors.
    const INFO_NULL: &'static str = "NULL00";
    const INFO_OBJECT: &'static str = "OBJECT";
    const INFO_ARRAY: &'static str = "ARRY[]";
}

impl Mac for JsonbMacTerm {
    fn mac<M>(&self, state: &mut M)
    where
        M: Macca,
    {
        match self {
            JsonbMacTerm::Null => state.update(Self::INFO_NULL.as_bytes()),
            JsonbMacTerm::Object => state.update(Self::INFO_OBJECT.as_bytes()),
            JsonbMacTerm::Array => state.update(Self::INFO_ARRAY.as_bytes()),
        }
    }
}
