use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;

use std::fmt::Display;

use crate::SyntaxShape;

#[derive(Clone, Debug, Default, EnumIter, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub enum Type {
    Any,
    Binary,
    Block,
    Bool,
    CellPath,
    Closure,
    Custom(String),
    Date,
    Duration,
    Error,
    Filesize,
    Float,
    Int,
    List(Box<Type>),
    ListStream,
    MatchPattern,
    #[default]
    Nothing,
    Number,
    Range,
    Record(Vec<(String, Type)>),
    Signature,
    String,
    Table(Vec<(String, Type)>),
}

impl Type {
    pub fn is_subtype(&self, other: &Type) -> bool {
        match (self, other) {
            (t, u) if t == u => true,
            (Type::Float, Type::Number) => true,
            (Type::Int, Type::Number) => true,
            (_, Type::Any) => true,
            (Type::List(t), Type::List(u)) if t.is_subtype(u) => true, // List is covariant

            // TODO: Currently Record types specify their field types. If we are
            // going to continue to do that, then it might make sense to define
            // a "structural subtyping" whereby r1 is a subtype of r2 is the
            // fields of r1 are a "subset" of the fields of r2 (names are a
            // subset and agree on types). However, if we do that, then we need
            // a way to specify the supertype of all Records. For now, we define
            // any Record to be a subtype of any other Record. This allows
            // Record(vec![]) to be used as an ad-hoc supertype of all Records
            // in command signatures. This comment applies to Tables also, with
            // "columns" in place of "fields".
            (Type::Record(_), Type::Record(_)) => true,
            (Type::Table(_), Type::Table(_)) => true,
            _ => false,
        }
    }

    pub fn is_numeric(&self) -> bool {
        matches!(self, Type::Int | Type::Float | Type::Number)
    }

    pub fn is_list(&self) -> bool {
        matches!(self, Type::List(_))
    }

    /// Does this type represent a data structure containing values that can be addressed using 'cell paths'?
    pub fn accepts_cell_paths(&self) -> bool {
        matches!(self, Type::List(_) | Type::Record(_) | Type::Table(_))
    }

    pub fn to_shape(&self) -> SyntaxShape {
        match self {
            Type::Int => SyntaxShape::Int,
            Type::Float => SyntaxShape::Number,
            Type::Range => SyntaxShape::Range,
            Type::Bool => SyntaxShape::Boolean,
            Type::String => SyntaxShape::String,
            Type::Block => SyntaxShape::Block, // FIXME needs more accuracy
            Type::Closure => SyntaxShape::Closure(None), // FIXME needs more accuracy
            Type::CellPath => SyntaxShape::CellPath,
            Type::Duration => SyntaxShape::Duration,
            Type::Date => SyntaxShape::DateTime,
            Type::Filesize => SyntaxShape::Filesize,
            Type::List(x) => SyntaxShape::List(Box::new(x.to_shape())),
            Type::Number => SyntaxShape::Number,
            Type::Nothing => SyntaxShape::Nothing,
            Type::Record(_) => SyntaxShape::Record,
            Type::Table(_) => SyntaxShape::Table,
            Type::ListStream => SyntaxShape::List(Box::new(SyntaxShape::Any)),
            Type::Any => SyntaxShape::Any,
            Type::Error => SyntaxShape::Any,
            Type::Binary => SyntaxShape::Binary,
            Type::Custom(_) => SyntaxShape::Any,
            Type::Signature => SyntaxShape::Signature,
            Type::MatchPattern => SyntaxShape::MatchPattern,
        }
    }

    /// Get a string representation, without inner type specification of lists,
    /// tables and records (get `list` instead of `list<any>`
    pub fn get_non_specified_string(&self) -> String {
        match self {
            Type::Block => String::from("block"),
            Type::Closure => String::from("closure"),
            Type::Bool => String::from("bool"),
            Type::CellPath => String::from("cell path"),
            Type::Date => String::from("date"),
            Type::Duration => String::from("duration"),
            Type::Filesize => String::from("filesize"),
            Type::Float => String::from("float"),
            Type::Int => String::from("int"),
            Type::Range => String::from("range"),
            Type::Record(_) => String::from("record"),
            Type::Table(_) => String::from("table"),
            Type::List(_) => String::from("list"),
            Type::MatchPattern => String::from("match pattern"),
            Type::Nothing => String::from("nothing"),
            Type::Number => String::from("number"),
            Type::String => String::from("string"),
            Type::ListStream => String::from("list stream"),
            Type::Any => String::from("any"),
            Type::Error => String::from("error"),
            Type::Binary => String::from("binary"),
            Type::Custom(_) => String::from("custom"),
            Type::Signature => String::from("signature"),
        }
    }
}

impl Display for Type {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Type::Block => write!(f, "block"),
            Type::Closure => write!(f, "closure"),
            Type::Bool => write!(f, "bool"),
            Type::CellPath => write!(f, "cell path"),
            Type::Date => write!(f, "date"),
            Type::Duration => write!(f, "duration"),
            Type::Filesize => write!(f, "filesize"),
            Type::Float => write!(f, "float"),
            Type::Int => write!(f, "int"),
            Type::Range => write!(f, "range"),
            Type::Record(fields) => {
                if fields.is_empty() {
                    write!(f, "record")
                } else {
                    write!(
                        f,
                        "record<{}>",
                        fields
                            .iter()
                            .map(|(x, y)| format!("{x}: {y}"))
                            .collect::<Vec<String>>()
                            .join(", "),
                    )
                }
            }
            Type::Table(columns) => {
                if columns.is_empty() {
                    write!(f, "table")
                } else {
                    write!(
                        f,
                        "table<{}>",
                        columns
                            .iter()
                            .map(|(x, y)| format!("{x}: {y}"))
                            .collect::<Vec<String>>()
                            .join(", ")
                    )
                }
            }
            Type::List(l) => write!(f, "list<{l}>"),
            Type::Nothing => write!(f, "nothing"),
            Type::Number => write!(f, "number"),
            Type::String => write!(f, "string"),
            Type::ListStream => write!(f, "list stream"),
            Type::Any => write!(f, "any"),
            Type::Error => write!(f, "error"),
            Type::Binary => write!(f, "binary"),
            Type::Custom(custom) => write!(f, "{custom}"),
            Type::Signature => write!(f, "signature"),
            Type::MatchPattern => write!(f, "match pattern"),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::Type;
    use strum::IntoEnumIterator;

    mod subtype_relation {
        use super::*;

        #[test]
        fn test_reflexivity() {
            for ty in Type::iter() {
                assert!(ty.is_subtype(&ty));
            }
        }

        #[test]
        fn test_any_is_top_type() {
            for ty in Type::iter() {
                assert!(ty.is_subtype(&Type::Any));
            }
        }

        #[test]
        fn test_number_supertype() {
            assert!(Type::Int.is_subtype(&Type::Number));
            assert!(Type::Float.is_subtype(&Type::Number));
        }

        #[test]
        fn test_list_covariance() {
            for ty1 in Type::iter() {
                for ty2 in Type::iter() {
                    let list_ty1 = Type::List(Box::new(ty1.clone()));
                    let list_ty2 = Type::List(Box::new(ty2.clone()));
                    assert_eq!(list_ty1.is_subtype(&list_ty2), ty1.is_subtype(&ty2));
                }
            }
        }
    }
}
