use std::{cmp::PartialEq, convert::TryFrom, fmt};

use crate::app::AppState;
use crate::lisp::{
    error::{EvalError, LispError},
    eval::lookup,
    number::LispNumber,
    EnvList,
};

#[derive(Debug, Copy, PartialEq, Clone)]
pub enum Arity {
    Exact(usize),
    Atleast(usize),
    Atmost(usize),
    Range(usize, usize),
    None,
}

impl Arity {
    pub fn check<T>(self, args: &[T]) -> bool {
        match self {
            Arity::Exact(a) => args.len() == a,
            Arity::Atleast(a) => args.len() >= a,
            Arity::Atmost(a) => args.len() <= a,
            Arity::Range(low, high) => args.len() >= low && args.len() <= high,
            Arity::None => true,
        }
    }
    pub fn to_error(self) -> LispError {
        LispError::Eval(EvalError::ArgumentCount(self))
    }
}

#[derive(Clone)]
pub struct PrimitiveFunc {
    pub arity: Arity, // minimim arity
    pub closure: fn(&[LispExpr], &mut AppState) -> Result<LispExpr, LispError>,
    pub name: &'static str,
}

impl PrimitiveFunc {
    pub fn call(&self, args: &[LispExpr], app: &mut AppState) -> Result<LispExpr, LispError> {
        if !self.arity.check(args) {
            return Err(EvalError::ArgumentCount(self.arity).into());
        }
        (self.closure)(args, app)
    }
}

pub type Ident = String;
pub type BoolLit = bool;

#[derive(Clone)]
pub struct LispFunction {
    pub params: Vec<String>,
    pub body: Vec<LispExpr>,
}

#[derive(Clone)]
pub enum LispExpr {
    Unit,
    Number(LispNumber),
    List(Vec<LispExpr>),
    StringLit(String),
    BoolLit(bool),
    Ident(Ident),
    PrimitiveFunc(PrimitiveFunc),
    Function(LispFunction),
    Char(char),

    // none of these depths should be zero
    Quasiquote(Box<LispExpr>, u32),
    Comma(Box<LispExpr>, u32),
    CommaAt(Box<LispExpr>, u32),
    Quote(Box<LispExpr>, u32),
}

impl LispExpr {
    pub fn comma(self, n: u32) -> LispExpr {
        match self {
            LispExpr::Comma(v, i) => LispExpr::Comma(v, i.checked_add(n).expect("comma overflow")),
            LispExpr::CommaAt(v, i) => LispExpr::CommaAt(v, i + n),
            v => LispExpr::Comma(Box::new(v), n),
        }
    }

    pub fn comma_at(self, n: u32) -> LispExpr {
        match self {
            LispExpr::CommaAt(v, i) => {
                LispExpr::CommaAt(v, i.checked_add(n).expect("comma_at overflow"))
            }
            v => LispExpr::CommaAt(Box::new(v), n),
        }
    }

    pub fn quote(self, n: u32) -> LispExpr {
        match self {
            LispExpr::Quote(v, i) => LispExpr::Quote(v, i.checked_add(n).expect("quote overflow")),
            v => LispExpr::Quote(Box::new(v), n),
        }
    }

    pub fn quasiquote(self, n: u32) -> LispExpr {
        match self {
            LispExpr::Quasiquote(v, i) => {
                LispExpr::Quasiquote(v, i.checked_add(n).expect("quasiquote overflow"))
            }
            v => LispExpr::Quasiquote(Box::new(v), n),
        }
    }

    pub fn compare(&self, other: &Self, envs: &EnvList) -> Result<BoolLit, LispError> {
        match (self, other) {
            (LispExpr::Unit, LispExpr::Unit) => Ok(true),
            (LispExpr::Number(s), LispExpr::Number(o)) => Ok(s == o),
            (LispExpr::List(s), LispExpr::List(o)) => Ok(s
                .iter()
                .zip(o)
                .all(|(a, b)| matches!(a.compare(b, envs), Ok(true)))),
            (LispExpr::List(s), LispExpr::Unit) => Ok(s.len() == 0),
            (LispExpr::Unit, LispExpr::List(s)) => Ok(s.len() == 0),
            (LispExpr::StringLit(s), LispExpr::StringLit(o)) => Ok(s == o),
            (LispExpr::Char(s), LispExpr::Char(o)) => Ok(s == o),
            (LispExpr::BoolLit(s), LispExpr::BoolLit(o)) => Ok(s == o),
            (LispExpr::Ident(s), LispExpr::Ident(o)) => {
                let s = lookup(envs, s)?;
                let o = lookup(envs, o)?;
                s.compare(&o, envs)
            }
            (LispExpr::PrimitiveFunc(s), LispExpr::PrimitiveFunc(o)) => {
                let PrimitiveFunc { closure: s, .. } = s;
                let PrimitiveFunc { closure: o, .. } = o;
                Ok(*s as usize == *o as usize)
            }
            (LispExpr::Function(_), LispExpr::Function(_)) => Err(EvalError::BadForm.into()),
            (LispExpr::Quasiquote(s, sd), LispExpr::Quasiquote(o, od)) => {
                Ok(s.compare(o, envs)? && sd == od)
            }
            (LispExpr::Comma(s, sd), LispExpr::Comma(o, od)) => Ok(s.compare(o, envs)? && sd == od),
            (LispExpr::CommaAt(s, sd), LispExpr::CommaAt(o, od)) => {
                Ok(s.compare(o, envs)? && sd == od)
            }
            (LispExpr::Quote(s, sd), LispExpr::Quote(o, od)) => Ok(s.compare(o, envs)? && sd == od),
            _ => Err(EvalError::TypeMismatch.into()),
        }
    }

    // have these be code gen'd somehow
    pub fn unwrap_number(&self) -> LispNumber {
        match &self {
            LispExpr::Number(p) => p.clone(),
            _ => panic!("attempt to call `unwrap_number` on invalid type"),
        }
    }

    pub fn unwrap_list(&self) -> Vec<LispExpr> {
        match &self {
            LispExpr::List(p) => p.clone(),
            _ => panic!("attempt to call `unwrap_number` on invalid type"),
        }
    }

    pub fn unwrap_stringlit(&self) -> String {
        match &self {
            LispExpr::StringLit(p) => p.clone(),
            _ => panic!("attempt to call `unwrap_stringlit` on invalid type"),
        }
    }

    pub fn unwrap_ident(&self) -> String {
        match &self {
            LispExpr::Ident(p) => p.clone(),
            _ => panic!("attempt to call `unwrap_ident` on invalid type"),
        }
    }

    pub fn unwrap_primitive_func(&self) -> PrimitiveFunc {
        match &self {
            LispExpr::PrimitiveFunc(p) => p.clone(),
            _ => panic!("attempt to call `unwrap_primitive_func` on invalid type"),
        }
    }

    pub fn unwrap_function(&self) -> LispFunction {
        match &self {
            LispExpr::Function(p) => p.clone(),
            _ => panic!("attempt to call `unwrap_function` on invalid type"),
        }
    }

    pub fn cast_bool(&self) -> bool {
        match &self {
            LispExpr::BoolLit(false) => false,
            _ => true,
        }
    }
}

impl fmt::Display for LispExpr {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            LispExpr::Unit => write!(f, "()")?,
            LispExpr::Number(n) => write!(f, "{}", n)?,
            LispExpr::List(l) => {
                write!(
                    f,
                    "({})",
                    &l.iter().map(|expr| format!("{}", expr)).collect::<Vec<_>>()[..].join(" ")
                )?;
            }
            LispExpr::StringLit(s) => write!(f, "{:?}", s)?,
            LispExpr::Char(c) => write!(f, "{}", c)?,
            LispExpr::BoolLit(b) => {
                if *b {
                    write!(f, "#t")?
                } else {
                    write!(f, "#f")?
                }
            }
            LispExpr::Ident(s) => write!(f, "{}", s)?,
            LispExpr::PrimitiveFunc(_) => write!(f, "<#primitive>")?,
            LispExpr::Function(func) => write!(f, "<#lambda {}>", func.params.join(" "))?,
            LispExpr::Quasiquote(val, depth) => {
                write!(f, "{}{}", "`".repeat(*depth as usize), val)?
            }
            LispExpr::Comma(val, depth) => write!(f, "{}{}", ",".repeat(*depth as usize), val)?,
            LispExpr::CommaAt(val, depth) => write!(f, "{}@{}", ",".repeat(*depth as usize), val)?,
            LispExpr::Quote(val, depth) => write!(f, "{}{}", "'".repeat(*depth as usize - 1), val)?,
        };
        Ok(())
    }
}

impl fmt::Debug for LispExpr {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            LispExpr::Unit => f.debug_tuple("Unit").finish(),
            LispExpr::Number(n) => write!(f, "Number({:?})", n),
            LispExpr::List(l) => f.debug_list().entries(l.iter()).finish(),
            LispExpr::StringLit(s) => write!(f, "String({:?})", s),
            LispExpr::Char(c) => write!(f, "Char({:?})", c),
            LispExpr::BoolLit(b) => write!(f, "Bool({:?})", b),
            LispExpr::Ident(s) => write!(f, "Ident({})", s),
            LispExpr::PrimitiveFunc(_) => write!(f, "Primitive"),
            LispExpr::Function(func) => write!(f, "<#lambda> {}", func.params.join(" ")),
            LispExpr::Quasiquote(val, depth) => write!(f, "{}{}", "`".repeat(*depth as usize), val),
            LispExpr::Comma(val, depth) => write!(f, "{}{}", ",".repeat(*depth as usize), val),
            LispExpr::CommaAt(val, depth) => write!(f, "{}@{}", ",".repeat(*depth as usize), val),
            LispExpr::Quote(val, depth) => write!(f, "{}{}", "'".repeat(*depth as usize), val),
        }
    }
}

impl PartialEq for LispExpr {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (LispExpr::Unit, LispExpr::Unit) => true,
            (LispExpr::Number(s), LispExpr::Number(o)) => s == o,
            (LispExpr::List(s), LispExpr::List(o)) => s.iter().zip(o).all(|(a, b)| a == b),
            (LispExpr::StringLit(s), LispExpr::StringLit(o)) => s == o,
            (LispExpr::Char(s), LispExpr::Char(o)) => s == o,
            (LispExpr::BoolLit(s), LispExpr::BoolLit(o)) => s == o,
            (LispExpr::Ident(s), LispExpr::Ident(o)) => s == o,
            (LispExpr::PrimitiveFunc(_), LispExpr::PrimitiveFunc(_)) => false,
            (LispExpr::Function(_), LispExpr::Function(_)) => false,
            (LispExpr::Quasiquote(s, sd), LispExpr::Quasiquote(o, od)) => (s, sd) == (o, od),
            (LispExpr::Comma(s, sd), LispExpr::Comma(o, od)) => (s, sd) == (o, od),
            (LispExpr::CommaAt(s, sd), LispExpr::CommaAt(o, od)) => (s, sd) == (o, od),
            (LispExpr::Quote(s, sd), LispExpr::Quote(o, od)) => (s, sd) == (o, od),
            _ => false,
        }
    }
}

impl TryFrom<LispExpr> for LispNumber {
    type Error = LispError;
    fn try_from(value: LispExpr) -> Result<Self, Self::Error> {
        match value {
            LispExpr::Number(i) => Ok(i),
            _ => Err(LispError::Eval(EvalError::TypeMismatch)),
        }
    }
}

impl<'a> TryFrom<&'a LispExpr> for &'a LispNumber {
    type Error = LispError;
    fn try_from(value: &'a LispExpr) -> Result<Self, Self::Error> {
        match value {
            LispExpr::Number(i) => Ok(i),
            _ => Err(LispError::Eval(EvalError::TypeMismatch)),
        }
    }
}

impl TryFrom<LispExpr> for String {
    type Error = LispError;
    fn try_from(value: LispExpr) -> Result<Self, Self::Error> {
        match value {
            LispExpr::StringLit(i) => Ok(i),
            LispExpr::Ident(i) => Ok(i),
            _ => Err(LispError::Eval(EvalError::TypeMismatch)),
        }
    }
}

impl AsRef<str> for LispExpr {
    fn as_ref(&self) -> &str {
        match self {
            LispExpr::StringLit(i) => &i,
            LispExpr::Ident(i) => &i,
            _ => panic!("invalid downcast!"),
        }
    }
}

impl TryFrom<LispExpr> for BoolLit {
    type Error = LispError;
    fn try_from(value: LispExpr) -> Result<Self, Self::Error> {
        match value {
            LispExpr::BoolLit(i) => Ok(i),
            _ => Err(LispError::Eval(EvalError::TypeMismatch)),
        }
    }
}