use std::cmp::Ordering;
use std::fmt::Debug;
use std::hash::Hash;
use std::num::ParseIntError;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
use amplify::confinement::{LargeOrdMap, LargeOrdSet, SmallVec, TinyOrdMap};
use amplify::hex;
use bp::seals::txout::TxoSeal;
use bp::{Outpoint, Txid};
use strict_encoding::{StrictDecode, StrictDumb, StrictEncode};
use crate::{
    Assign, AssignmentType, Assignments, AssignmentsRef, ContractId, ExposedSeal, ExposedState,
    Extension, Genesis, GlobalStateType, OpId, Operation, RevealedAttach, RevealedData,
    RevealedValue, SchemaId, SealWitness, SubSchema, Transition, TypedAssigns, VoidState,
    LIB_NAME_RGB,
};
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display)]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = LIB_NAME_RGB)]
#[cfg_attr(
    feature = "serde",
    derive(Serialize, Deserialize),
    serde(crate = "serde_crate", rename_all = "camelCase")
)]
#[display("{op}/{ty}/{no}")]
pub struct Opout {
    pub op: OpId,
    pub ty: AssignmentType,
    pub no: u16,
}
impl Opout {
    pub fn new(op: OpId, ty: u16, no: u16) -> Opout { Opout { op, ty, no } }
}
#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)]
#[display(inner)]
pub enum OpoutParseError {
    #[from]
    InvalidNodeId(hex::Error),
    InvalidType(ParseIntError),
    InvalidOutputNo(ParseIntError),
    #[display(doc_comments)]
    WrongFormat(String),
}
impl FromStr for Opout {
    type Err = OpoutParseError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut split = s.split('/');
        match (split.next(), split.next(), split.next(), split.next()) {
            (Some(op), Some(ty), Some(no), None) => Ok(Opout {
                op: op.parse()?,
                ty: ty.parse().map_err(OpoutParseError::InvalidType)?,
                no: no.parse().map_err(OpoutParseError::InvalidOutputNo)?,
            }),
            _ => Err(OpoutParseError::WrongFormat(s.to_owned())),
        }
    }
}
#[derive(Clone, Eq, Debug)]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = LIB_NAME_RGB)]
#[cfg_attr(
    feature = "serde",
    derive(Serialize, Deserialize),
    serde(crate = "serde_crate", rename_all = "camelCase")
)]
pub struct OutputAssignment<State: ExposedState> {
    pub opout: Opout,
    pub seal: Outpoint,
    pub state: State,
    pub witness: SealWitness,
}
impl<State: ExposedState> PartialEq for OutputAssignment<State> {
    fn eq(&self, other: &Self) -> bool {
        if self.opout == other.opout {
            debug_assert_eq!(self.seal, other.seal);
            debug_assert_eq!(self.state, other.state);
        }
        self.opout == other.opout
    }
}
impl<State: ExposedState> PartialOrd for OutputAssignment<State> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
impl<State: ExposedState> Ord for OutputAssignment<State> {
    fn cmp(&self, other: &Self) -> Ordering {
        if self == other {
            return Ordering::Equal;
        }
        self.opout.cmp(&other.opout)
    }
}
impl<State: ExposedState> OutputAssignment<State> {
    pub fn with_witness<Seal: TxoSeal>(
        seal: Seal,
        witness_txid: Txid,
        state: State,
        opid: OpId,
        ty: AssignmentType,
        no: u16,
    ) -> Self {
        OutputAssignment {
            opout: Opout::new(opid, ty, no),
            seal: seal.outpoint_or(witness_txid),
            state,
            witness: SealWitness::Present(witness_txid),
        }
    }
    pub fn with_genesis<Seal: TxoSeal>(
        seal: Seal,
        state: State,
        opid: OpId,
        ty: AssignmentType,
        no: u16,
    ) -> Self {
        OutputAssignment {
            opout: Opout::new(opid, ty, no),
            seal: seal
                .outpoint()
                .expect("seal must have txid information and come from genesis"),
            state,
            witness: SealWitness::Genesis,
        }
    }
    pub fn with_extension<Seal: TxoSeal>(
        seal: Seal,
        state: State,
        opid: OpId,
        ty: AssignmentType,
        no: u16,
    ) -> Self {
        OutputAssignment {
            opout: Opout::new(opid, ty, no),
            seal: seal
                .outpoint()
                .expect("seal must have txid information and come from state extension"),
            state,
            witness: SealWitness::Extension,
        }
    }
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Display)]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = LIB_NAME_RGB)]
#[display("{height}/{txid}")]
#[cfg_attr(
    feature = "serde",
    derive(Serialize, Deserialize),
    serde(crate = "serde_crate", rename_all = "camelCase")
)]
pub struct OrderedTxid {
    pub height: u32,
    pub txid: Txid,
}
impl PartialOrd for OrderedTxid {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
impl Ord for OrderedTxid {
    fn cmp(&self, other: &Self) -> Ordering {
        if self == other {
            return Ordering::Equal;
        }
        if self.height != other.height {
            return self.height.cmp(&other.height);
        }
        self.txid.cmp(&other.txid)
    }
}
impl OrderedTxid {
    pub fn new(height: u32, txid: Txid) -> Self { OrderedTxid { height, txid } }
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = LIB_NAME_RGB)]
#[cfg_attr(
    feature = "serde",
    derive(Serialize, Deserialize),
    serde(crate = "serde_crate", rename_all = "camelCase")
)]
pub struct GlobalOrd {
    pub ord_txid: Option<OrderedTxid>,
    pub idx: u16,
}
impl PartialOrd for GlobalOrd {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
impl Ord for GlobalOrd {
    fn cmp(&self, other: &Self) -> Ordering {
        if self == other {
            return Ordering::Equal;
        }
        match (self.ord_txid, &other.ord_txid) {
            (None, None) => self.idx.cmp(&other.idx),
            (None, Some(_)) => Ordering::Less,
            (Some(_), None) => Ordering::Greater,
            (Some(ord1), Some(ord2)) if ord1 == *ord2 => self.idx.cmp(&other.idx),
            (Some(ord1), Some(ord2)) => ord1.cmp(ord2),
        }
    }
}
impl GlobalOrd {
    pub fn new(height: u32, txid: Txid, idx: u16) -> Self {
        GlobalOrd {
            ord_txid: Some(OrderedTxid::new(height, txid)),
            idx,
        }
    }
    pub fn with(ord_txid: OrderedTxid, idx: u16) -> Self {
        GlobalOrd {
            ord_txid: Some(ord_txid),
            idx,
        }
    }
    pub fn genesis(idx: u16) -> Self {
        GlobalOrd {
            ord_txid: None,
            idx,
        }
    }
}
pub type RightsOutput = OutputAssignment<VoidState>;
pub type FungibleOutput = OutputAssignment<RevealedValue>;
pub type DataOutput = OutputAssignment<RevealedData>;
pub type AttachOutput = OutputAssignment<RevealedAttach>;
#[derive(Getters, Clone, Eq, PartialEq, Debug)]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = LIB_NAME_RGB)]
#[cfg_attr(
    feature = "serde",
    derive(Serialize, Deserialize),
    serde(crate = "serde_crate", rename_all = "camelCase")
)]
pub struct ContractHistory {
    #[getter(as_copy)]
    schema_id: SchemaId,
    #[getter(as_copy)]
    root_schema_id: Option<SchemaId>,
    #[getter(as_copy)]
    contract_id: ContractId,
    #[getter(skip)]
    global: TinyOrdMap<GlobalStateType, LargeOrdMap<GlobalOrd, RevealedData>>,
    rights: LargeOrdSet<RightsOutput>,
    fungibles: LargeOrdSet<FungibleOutput>,
    data: LargeOrdSet<DataOutput>,
    attach: LargeOrdSet<AttachOutput>,
}
impl ContractHistory {
    pub fn with(
        schema_id: SchemaId,
        root_schema_id: Option<SchemaId>,
        contract_id: ContractId,
        genesis: &Genesis,
    ) -> Self {
        let mut state = ContractHistory {
            schema_id,
            root_schema_id,
            contract_id,
            global: empty!(),
            rights: empty!(),
            fungibles: empty!(),
            data: empty!(),
            attach: empty!(),
        };
        state.update_genesis(genesis);
        state
    }
    pub fn update_genesis(&mut self, genesis: &Genesis) {
        self.add_operation(SealWitness::Genesis, genesis, None);
    }
    pub fn add_transition(&mut self, transition: &Transition, ord_txid: OrderedTxid) {
        self.add_operation(SealWitness::Present(ord_txid.txid), transition, Some(ord_txid));
    }
    pub fn add_extension(&mut self, extension: &Extension, ord_txid: OrderedTxid) {
        self.add_operation(SealWitness::Extension, extension, Some(ord_txid));
    }
    fn add_operation(
        &mut self,
        witness: SealWitness,
        op: &impl Operation,
        ord_txid: Option<OrderedTxid>,
    ) {
        let opid = op.id();
        for (ty, state) in op.globals() {
            let map = match self.global.get_mut(ty) {
                Some(map) => map,
                None => {
                    self.global.insert(*ty, empty!()).expect(
                        "consensus rules violation: do not add to the state consignments without \
                         validation against the schema",
                    );
                    self.global.get_mut(ty).expect("just inserted")
                }
            };
            for (idx, s) in state.iter().enumerate() {
                let idx = idx as u16;
                let glob_idx = GlobalOrd { ord_txid, idx };
                map.insert(glob_idx, s.clone())
                    .expect("contract global state exceeded 2^32 items, which is unrealistic");
            }
        }
        for input in op.inputs() {
            if let Some(o) = self.rights.iter().find(|r| r.opout == input.prev_out) {
                let o = o.clone(); self.rights
                    .remove(&o)
                    .expect("collection allows zero elements");
            }
            if let Some(o) = self.fungibles.iter().find(|r| r.opout == input.prev_out) {
                let o = o.clone();
                self.fungibles
                    .remove(&o)
                    .expect("collection allows zero elements");
            }
            if let Some(o) = self.data.iter().find(|r| r.opout == input.prev_out) {
                let o = o.clone();
                self.data
                    .remove(&o)
                    .expect("collection allows zero elements");
            }
            if let Some(o) = self.attach.iter().find(|r| r.opout == input.prev_out) {
                let o = o.clone();
                self.attach
                    .remove(&o)
                    .expect("collection allows zero elements");
            }
        }
        match op.assignments() {
            AssignmentsRef::Genesis(assignments) => {
                self.add_assignments(witness, opid, assignments)
            }
            AssignmentsRef::Graph(assignments) => self.add_assignments(witness, opid, assignments),
        }
    }
    fn add_assignments<Seal: ExposedSeal>(
        &mut self,
        witness: SealWitness,
        opid: OpId,
        assignments: &Assignments<Seal>,
    ) {
        fn process<State: ExposedState, Seal: ExposedSeal>(
            contract_state: &mut LargeOrdSet<OutputAssignment<State>>,
            assignments: &[Assign<State, Seal>],
            opid: OpId,
            ty: AssignmentType,
            witness: SealWitness,
        ) {
            for (no, seal, state) in assignments
                .iter()
                .enumerate()
                .filter_map(|(n, a)| a.to_revealed().map(|(seal, state)| (n, seal, state)))
            {
                let assigned_state = match witness {
                    SealWitness::Present(txid) => OutputAssignment::with_witness(
                        seal,
                        txid,
                        state.into(),
                        opid,
                        ty,
                        no as u16,
                    ),
                    SealWitness::Genesis => {
                        OutputAssignment::with_genesis(seal, state.into(), opid, ty, no as u16)
                    }
                    SealWitness::Extension => {
                        OutputAssignment::with_extension(seal, state.into(), opid, ty, no as u16)
                    }
                };
                contract_state
                    .push(assigned_state)
                    .expect("contract state exceeded 2^32 items, which is unrealistic");
            }
        }
        for (ty, assignments) in assignments.iter() {
            match assignments {
                TypedAssigns::Declarative(assignments) => {
                    process(&mut self.rights, &assignments, opid, *ty, witness)
                }
                TypedAssigns::Fungible(assignments) => {
                    process(&mut self.fungibles, &assignments, opid, *ty, witness)
                }
                TypedAssigns::Structured(assignments) => {
                    process(&mut self.data, &assignments, opid, *ty, witness)
                }
                TypedAssigns::Attachment(assignments) => {
                    process(&mut self.attach, &assignments, opid, *ty, witness)
                }
            }
        }
    }
}
#[derive(Clone, Eq, PartialEq, Debug)]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = LIB_NAME_RGB)]
#[cfg_attr(
    feature = "serde",
    derive(Serialize, Deserialize),
    serde(crate = "serde_crate", rename_all = "camelCase")
)]
pub struct ContractState {
    pub schema: SubSchema,
    pub history: ContractHistory,
}
impl Deref for ContractState {
    type Target = ContractHistory;
    fn deref(&self) -> &Self::Target { &self.history }
}
impl DerefMut for ContractState {
    fn deref_mut(&mut self) -> &mut Self::Target { &mut self.history }
}
impl ContractState {
    pub unsafe fn global_unchecked(&self, state_type: GlobalStateType) -> SmallVec<&RevealedData> {
        let schema = self
            .schema
            .global_types
            .get(&state_type)
            .expect("global type is not in the schema");
        let Some(state) = self.global.get(&state_type) else {
            return SmallVec::new()
        };
        let iter = state.values().take(schema.max_items as usize);
        SmallVec::try_from_iter(iter).expect("same size as previous confined collection")
    }
}