use std::cmp::Ordering;

use crate::types::EuType;

impl EuType<'_> {
    #[must_use]
    pub fn eqv_ord(&self, other: &Self) -> Ordering {
        self.enum_index().cmp(&other.enum_index())
    }

    fn enum_index(&self) -> u8 {
        match self {
            Self::Bool(_) => 0,
            Self::Char(_) => 1,
            Self::I32(_) => 2,
            Self::I64(_) => 3,
            Self::IBig(_) => 4,
            Self::F64(_) => 5,
            Self::Word(_) => 6,
            Self::Str(_) => 7,
            Self::Opt(_) => 8,
            Self::Res(_) => 9,
            Self::Expr(_) => 10,
            Self::Vec(_) => 11,
            Self::Map(_) => 12,
            Self::Set(_) => 13,
            Self::Seq(_) => 14,
        }
    }

    #[must_use]
    pub fn loose_eq(&self, other: &Self) -> bool {
        if let Some((a, b)) = self.clone().num_tower(other.clone()) {
            a == b
        } else {
            self == other
        }
    }

    #[must_use]
    pub fn loose_cmp(&self, other: &Self) -> Ordering {
        if let Some((a, b)) = self.clone().num_tower(other.clone()) {
            a.cmp(&b)
        } else {
            self.cmp(other)
        }
    }
}

#[crabtime::function]
fn gen_partial_eq() {
    let types = [
        "Bool", "I32", "I64", "IBig", "F64", "Char", "Str", "Word", "Opt", "Res", "Vec", "Map",
        "Set", "Expr",
    ];
    let arms = types
        .map(|t| {
            crabtime::quote! {
                (Self::{{t}}(l0), Self::{{t}}(r0)) => l0 == r0,
            }
        })
        .join("");

    crabtime::output! {
        impl PartialEq for EuType<'_> {
            fn eq(&self, other: &Self) -> bool {
                match (self, other) {
                    {{arms}}
                    (Self::Seq(l0), Self::Seq(r0)) => l0.clone().eq(r0.clone()),
                    _ => false,
                }
            }
        }
    }
}

gen_partial_eq!();

impl Eq for EuType<'_> {}

impl PartialOrd for EuType<'_> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

#[crabtime::function]
fn gen_ord() {
    let types = [
        "Bool", "I32", "I64", "IBig", "F64", "Char", "Str", "Word", "Opt", "Res", "Vec", "Map",
        "Set", "Expr",
    ];
    let arms = types
        .map(|t| {
            crabtime::quote! {
                (Self::{{t}}(l0), Self::{{t}}(r0)) => l0.cmp(r0),
            }
        })
        .join("");

    crabtime::output! {
        impl Ord for EuType<'_> {
            fn cmp(&self, other: &Self) -> Ordering {
                match (self, other) {
                    {{arms}}
                    (Self::Seq(l0), Self::Seq(r0)) => l0.clone().cmp(r0.clone()),
                    (Self::Bool(l0), _) => l0.cmp(&!l0),
                    (_, Self::Bool(r0)) => r0.cmp(&!r0).reverse(),
                    (Self::Word(_), _) => Ordering::Greater,
                    (l0, r0 @ Self::Word(_)) => r0.cmp(l0).reverse(),
                    (a, b) if a.is_int() && b.is_int() => {
                        a.to_ibig().unwrap().cmp(&b.to_ibig().unwrap()).then_with(|| a.eqv_ord(b))
                    }
                    (a, b) if a.is_vecz() || a.is_str() || a.is_expr() || b.is_vecz() || b.is_str() || b.is_expr() => {
                        a.clone().to_seq().cmp(b.clone().to_seq()).then_with(|| a.eqv_ord(b))
                    }
                    (a, b) => a.eqv_ord(b),
                }
            }
        }
    }
}

gen_ord!();

#[cfg(test)]
mod tests {
    use std::{
        collections::hash_map::DefaultHasher,
        hash::{
            Hash,
            Hasher,
        },
    };

    use dashu_int::IBig;

    use super::*;

    #[test]
    fn ord_eq_consistent() {
        let vs = sample_values();
        for a in &vs {
            for b in &vs {
                assert_consistent(a, b);
            }
        }
    }

    #[test]
    fn ord_antisymmetric() {
        let vs = sample_values();
        for a in &vs {
            for b in &vs {
                assert_eq!(a.cmp(b).reverse(), b.cmp(a));
            }
        }
    }

    #[test]
    fn ord_transitive() {
        let vs = sample_values();
        for a in &vs {
            for b in &vs {
                for c in &vs {
                    if a <= b && b <= c {
                        assert!(a <= c);
                    }
                }
            }
        }
    }

    #[test]
    fn loose_eq() {
        assert!(EuType::i32(1).loose_eq(&EuType::i64(1)));
        assert!(EuType::i32(1).loose_eq(&EuType::ibig(1)));
        assert!(EuType::i32(1).loose_eq(&EuType::f64(1.0)));
        assert!(EuType::ibig(1).loose_eq(&EuType::f64(1.0)));
        assert!(EuType::Bool(true).loose_eq(&EuType::i32(1)));
        assert!(EuType::Bool(false).loose_eq(&EuType::i32(0)));
        assert!(EuType::Bool(true).loose_eq(&EuType::char(1)));
        assert!(EuType::Bool(false).loose_eq(&EuType::char(0)));
        assert!(!EuType::i32(1).loose_eq(&EuType::str("1")));
    }

    #[test]
    fn loose_cmp() {
        assert_eq!(EuType::i32(1).loose_cmp(&EuType::f64(2.0)), Ordering::Less);
        assert_eq!(
            EuType::f64(2.0).loose_cmp(&EuType::i32(1)),
            Ordering::Greater
        );
        assert_eq!(EuType::i32(1).loose_cmp(&EuType::f64(1.0)), Ordering::Equal);
    }

    fn assert_consistent<'a>(a: &EuType<'a>, b: &EuType<'a>) {
        let cmp = a.cmp(b);
        let eq = a == b;
        assert_eq!(cmp == Ordering::Equal, eq);
        if eq {
            assert_eq!(hash(a), hash(b));
        }
    }

    fn hash(t: &EuType<'_>) -> u64 {
        let mut h = DefaultHasher::new();
        t.hash(&mut h);
        h.finish()
    }

    fn sample_values() -> Vec<EuType<'static>> {
        vec![
            EuType::Bool(false),
            EuType::Bool(true),
            EuType::char(0),
            EuType::char(1),
            EuType::i32(-1),
            EuType::i32(0),
            EuType::i32(1),
            EuType::i64(-1),
            EuType::i64(0),
            EuType::i64(1),
            EuType::ibig(-1),
            EuType::ibig(0),
            EuType::ibig(1),
            EuType::ibig(IBig::from(2).pow(1024)),
            EuType::f64(-1.0),
            EuType::f64(0.0),
            EuType::f64(1.0),
            EuType::f64(f64::NAN),
            EuType::f64(f64::INFINITY),
            EuType::char('a'),
            EuType::str("asdf"),
            EuType::word("asdf"),
            EuType::vec([EuType::i32(1)]),
        ]
    }
}
