use std::{collections::BTreeMap, fmt::Display};

use itertools::{Itertools, Position};

use crate::{ContextWorker, Contextualizer, Escaper};

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
/// The type of a cookie cutter [`crate::Value`]. Since this is used in earlier stages than the
/// value more information is still available. For example the text type or whether it is a tuple
/// or an array.
pub enum Type {
    /// A text with a text type (see [`crate::Contextualizer`] for more information). Mirrors [`crate::Value::Text`].
    Text(String),
    /// A 64 bit integer. Mirrors [`crate::Value::Int`].
    Int,
    /// A 64 bit floating point number. Mirrors [`crate::Value::Float`].
    Float,
    /// A boolean. Mirrors [`crate::Value::Bool`].
    Bool,
    /// An array of values. The inner type may or may not be known (for example due to an empty
    /// array literal). Mirrors [`crate::Value::ArrayOrTuple`].
    Array(Option<Box<Type>>),
    /// A tuple of values. Mirrors [`crate::Value::ArrayOrTuple`].
    Tuple(Vec<Type>),
    /// A struct (key value pair) of values. Mirrors [`crate::Value::Struct`].
    Struct(BTreeMap<String, Type>),
}

impl Display for Type {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Text(s) => f.write_str(s),
            Self::Int => write!(f, "int"),
            Self::Float => write!(f, "float"),
            Self::Bool => write!(f, "bool"),
            Self::Array(None) => write!(f, "[?]"),
            Self::Array(Some(inner)) => write!(f, "[{inner}]"),
            Self::Tuple(inner) => {
                f.write_str("(")?;

                for (pos, ty) in inner.iter().with_position() {
                    write!(f, "{ty}")?;

                    if matches!(pos, Position::First | Position::Middle) {
                        f.write_str(", ")?;
                    }
                }

                f.write_str(")")
            }
            Self::Struct(inner) => {
                f.write_str("{")?;

                for (pos, (key, ty)) in inner.iter().with_position() {
                    write!(f, "{key}: {ty}")?;

                    if matches!(pos, Position::First | Position::Middle) {
                        f.write_str(", ")?;
                    }
                }

                f.write_str("}")
            }
        }
    }
}

impl Type {
    /// Tries to unify this `Type` with another (returning `Ok` if successfull) and mutates this
    /// `Type` to be more specific based on the source type.
    /// Does not mutate `self` if `Err` is returned.
    pub(crate) fn unify<Ctx: Contextualizer<Esc, Wrk>, Esc: Escaper, Wrk: ContextWorker<Esc>>(
        &mut self,
        source: &Type,
        ctxer: &Ctx,
    ) -> Result<Option<Esc>, ()> {
        match (self, source) {
            (Type::Text(a), Type::Text(b)) => {
                if a == b {
                    return Ok(None);
                }

                ctxer.escaper(a, b).map(Some).map_err(|_| ())
            }
            (Type::Float, Type::Float)
            | (Type::Bool, Type::Bool)
            | (Type::Int, Type::Int)
            | (Type::Array(None | Some(_)), Type::Array(None)) => Ok(None),
            (Type::Array(Some(ref mut self_inner)), Type::Array(Some(ref source_inner))) => {
                self_inner.unify(source_inner, ctxer)
            }
            (Type::Array(ref mut self_inner @ None), Type::Array(Some(ref source_inner))) => {
                *self_inner = Some(source_inner.clone());
                Ok(None)
            }
            (Type::Struct(ref mut self_inner), Type::Struct(source_inner)) => {
                let mut unified = self_inner.clone();
                if unified
                    .iter_mut()
                    .zip(source_inner.iter())
                    .all(|(self_key, source_key)| {
                        self_key.0 == source_key.0
                                // we only permit escaping at the top level, if there is escaping
                                // required at a deeper level we abort
                            && matches!(self_key
                                .1
                                .unify(source_key.1, ctxer), Ok(None))
                    })
                {
                    *self_inner = unified;
                    Ok(None)
                } else {
                    Err(())
                }
            }
            (Type::Tuple(ref mut self_inner), Type::Tuple(ref source_inner)) => {
                let mut unified = self_inner.clone();
                if unified
                    .iter_mut()
                    .zip(source_inner.iter())
                    .all(|(ty_self, ty_source)| matches!(ty_self.unify(ty_source, ctxer), Ok(None)))
                {
                    *self_inner = unified;
                    Ok(None)
                } else {
                    Err(())
                }
            }
            _ => Err(()),
        }
    }

    /// checks whether self is compatible with target; different from a simple equality check since
    /// `Type::Array(None)` is compatible with `Type::Array(Some(_))`
    pub(crate) fn is_compatible(&self, target: &Type) -> bool {
        match (self, target) {
            (Type::Text(a), Type::Text(b)) => a == b,
            (Type::Float, Type::Float)
            | (Type::Bool, Type::Bool)
            | (Type::Int, Type::Int)
            | (Type::Array(None | Some(_)), Type::Array(None))
            | (Type::Array(None), Type::Array(Some(_))) => true,
            (Type::Array(Some(ref self_inner)), Type::Array(Some(ref target_inner))) => {
                self_inner.is_compatible(target_inner)
            }
            (Type::Struct(ref self_inner), Type::Struct(target_inner)) => self_inner
                .iter()
                .zip(target_inner.iter())
                .all(|(key_inner, key_source)| {
                    key_inner.0 == key_source.0 && key_inner.1.is_compatible(key_source.1)
                }),
            (Type::Tuple(ref self_inner), Type::Tuple(ref target_inner)) => self_inner
                .iter()
                .zip(target_inner.iter())
                .all(|(ty_self, ty_source)| ty_self.is_compatible(ty_source)),
            _ => false,
        }
    }

    /// checks whether the type is printable and if so which text type it is if that information is
    /// available
    pub fn to_printable(&self) -> Option<Option<String>> {
        Some(match self {
            Type::Text(inner) => Some(inner.clone()),
            Type::Bool | Type::Float | Type::Int => None,
            Type::Array(inner) => return inner.as_deref().and_then(Type::to_printable),
            _ => return None,
        })
    }
}
