use std::fmt::Debug;

use mck::refin::{Boolean, RArray, RBitvector, RefinementDomain, RefinementValue};

use indexmap::IndexMap;
use serde::{Deserialize, Serialize};

use crate::iir::{
    context::IContext,
    func::{IFn, IFnDeclaration},
    path::IIdent,
    ty::IElementaryType,
};

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct IMachine {
    pub machine: IStructId,
    pub init: IFnId,
    pub next: IFnId,

    pub input: IStructId,
    pub param: IStructId,
    pub state: IStructId,

    pub description: IDescription,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct IDescription {
    pub structs: IndexMap<IIdent, IStruct>,
}

#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct IStructId(pub usize);

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct IStructDeclaration {
    pub fields: IndexMap<IIdent, IElementaryType>,
    pub fns: IndexMap<(ITrait, IIdent), IFnDeclaration>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct IStruct {
    pub fields: IndexMap<IIdent, IElementaryType>,
    pub fns: IndexMap<(ITrait, IIdent), IFn>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ITrait {
    Inherent,
    Machine,
}

#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct IImpl {
    fns: IndexMap<IIdent, IFn>,
}

impl IMachine {
    pub fn machine(&self) -> &IStruct {
        self.description.struct_with_id(self.machine)
    }

    pub fn input(&self) -> &IStruct {
        self.description.struct_with_id(self.input)
    }

    pub fn param(&self) -> &IStruct {
        self.description.struct_with_id(self.param)
    }

    pub fn state(&self) -> &IStruct {
        self.description.struct_with_id(self.state)
    }

    pub fn init(&self) -> &IFn {
        self.description.fn_with_id(self.init)
    }
    pub fn next(&self) -> &IFn {
        self.description.fn_with_id(self.next)
    }
}

impl IDescription {
    pub fn context(&self) -> IContext {
        IContext {
            structs: Some(&self.structs),
        }
    }

    pub fn struct_with_id(&self, id: IStructId) -> &IStruct {
        self.structs
            .get_index(id.0)
            .expect("Struct with given id should exist")
            .1
    }

    pub fn fn_with_id(&self, id: IFnId) -> &IFn {
        self.struct_with_id(id.struct_id)
            .fns
            .get_index(id.fn_index)
            .expect("Call with given id should exist")
            .1
    }
}

impl IStruct {
    pub fn clean_refin(&self, description: &IDescription) -> RefinementValue {
        let mut result = Vec::new();
        for field_ty in self.fields.values() {
            let field_result = match field_ty {
                IElementaryType::Bitvector(width) => {
                    RefinementValue::Bitvector(RBitvector::new_unmarked(*width))
                }
                IElementaryType::Array(array) => RefinementValue::Array(RArray::new_unmarked(
                    array.index_width,
                    array.element_width,
                )),
                IElementaryType::Boolean => RefinementValue::Boolean(Boolean::new_unmarked()),
                IElementaryType::Struct(struct_id) => {
                    let iir_struct = description.struct_with_id(*struct_id);
                    iir_struct.clean_refin(description)
                }
            };
            result.push(field_result)
        }

        RefinementValue::Struct(result)
    }

    pub fn dirty_refin(&self, description: &IDescription) -> RefinementValue {
        let mut result = Vec::new();
        for field_ty in self.fields.values() {
            let field_result = match field_ty {
                IElementaryType::Bitvector(width) => {
                    RefinementValue::Bitvector(RBitvector::new_marked_unimportant(*width))
                }
                IElementaryType::Array(array) => RefinementValue::Array(
                    RArray::new_marked_unimportant(array.index_width, array.element_width),
                ),
                IElementaryType::Boolean => {
                    RefinementValue::Boolean(Boolean::new_marked_unimportant())
                }
                IElementaryType::Struct(struct_id) => {
                    let iir_struct = description.struct_with_id(*struct_id);
                    iir_struct.dirty_refin(description)
                }
            };
            result.push(field_result)
        }

        RefinementValue::Struct(result)
    }

    pub fn into_declaration(self) -> IStructDeclaration {
        let fns = self
            .fns
            .into_iter()
            .map(|func| (func.0, func.1.into_declaration()))
            .collect();

        IStructDeclaration {
            fields: self.fields,
            fns,
        }
    }
}

impl Debug for IStructId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "${}", self.0)
    }
}

#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct IFnId {
    pub struct_id: IStructId,
    pub fn_index: usize,
}

impl Debug for IFnId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:?}::{}", self.struct_id, self.fn_index)
    }
}
