use crate::{
    arena::Arena,
    binder::Eval,
    constant::{integer_from, Constant, Integer},
    flat::Ctx,
    machine::MachineError,
};

#[derive(Debug, PartialEq)]
pub enum PlutusData<'a> {
    Constr {
        tag: u64,
        fields: &'a [&'a PlutusData<'a>],
    },
    Map(&'a [(&'a PlutusData<'a>, &'a PlutusData<'a>)]),
    Integer(&'a Integer),
    ByteString(&'a [u8]),
    List(&'a [&'a PlutusData<'a>]),
}

impl<'a> PlutusData<'a> {
    pub fn constr(
        arena: &'a Arena,
        tag: u64,
        fields: &'a [&'a PlutusData<'a>],
    ) -> &'a PlutusData<'a> {
        arena.alloc(PlutusData::Constr { tag, fields })
    }

    pub fn list(arena: &'a Arena, items: &'a [&'a PlutusData<'a>]) -> &'a PlutusData<'a> {
        arena.alloc(PlutusData::List(items))
    }

    pub fn map(
        arena: &'a Arena,
        items: &'a [(&'a PlutusData<'a>, &'a PlutusData<'a>)],
    ) -> &'a PlutusData<'a> {
        arena.alloc(PlutusData::Map(items))
    }

    pub fn integer(arena: &'a Arena, i: &'a Integer) -> &'a PlutusData<'a> {
        arena.alloc(PlutusData::Integer(i))
    }

    pub fn integer_from(arena: &'a Arena, i: i128) -> &'a PlutusData<'a> {
        arena.alloc(PlutusData::Integer(integer_from(arena, i)))
    }

    pub fn byte_string(arena: &'a Arena, bytes: &'a [u8]) -> &'a PlutusData<'a> {
        arena.alloc(PlutusData::ByteString(bytes))
    }

    pub fn from_cbor(
        arena: &'a Arena,
        cbor: &'_ [u8],
    ) -> Result<&'a PlutusData<'a>, minicbor::decode::Error> {
        minicbor::decode_with(
            cbor,
            &mut Ctx {
                arena,
                version: None,
                plutus_version: None,
                protocol_version: None,
            },
        )
    }

    pub fn unwrap_constr<V>(
        &'a self,
    ) -> Result<(&'a u64, &'a [&'a PlutusData<'a>]), MachineError<'a, V>>
    where
        V: Eval<'a>,
    {
        match self {
            PlutusData::Constr { tag, fields } => Ok((tag, fields)),
            _ => Err(MachineError::malformed_data(self)),
        }
    }

    pub fn unwrap_map<V>(
        &'a self,
    ) -> Result<&'a [(&'a PlutusData<'a>, &'a PlutusData<'a>)], MachineError<'a, V>>
    where
        V: Eval<'a>,
    {
        match self {
            PlutusData::Map(fields) => Ok(fields),
            _ => Err(MachineError::malformed_data(self)),
        }
    }

    pub fn unwrap_integer<V>(&'a self) -> Result<&'a Integer, MachineError<'a, V>>
    where
        V: Eval<'a>,
    {
        match self {
            PlutusData::Integer(i) => Ok(i),
            _ => Err(MachineError::malformed_data(self)),
        }
    }

    pub fn unwrap_byte_string<V>(&'a self) -> Result<&'a [u8], MachineError<'a, V>>
    where
        V: Eval<'a>,
    {
        match self {
            PlutusData::ByteString(bytes) => Ok(bytes),
            _ => Err(MachineError::malformed_data(self)),
        }
    }

    pub fn unwrap_list<V>(&'a self) -> Result<&'a [&'a PlutusData<'a>], MachineError<'a, V>>
    where
        V: Eval<'a>,
    {
        match self {
            PlutusData::List(items) => Ok(items),
            _ => Err(MachineError::malformed_data(self)),
        }
    }

    pub fn constant(&'a self, arena: &'a Arena) -> &'a Constant<'a> {
        Constant::data(arena, self)
    }

    pub fn to_bytes<V>(&'a self, arena: &'a Arena) -> Result<&'a [u8], MachineError<'a, V>>
    where
        V: Eval<'a>,
    {
        minicbor::to_vec(self)
            .map(|vec| arena.alloc(vec) as &'a [u8])
            .map_err(|_| MachineError::serialization_error(self))
    }
}
