use crate::{
    app::AppState,
    lisp::{
        error::{EvalError, LispError},
        expr::{Arity, Ident, LispExpr, LispFunction},
        Environment,
    },
    type_match,
};

use std::convert::TryInto;

use log::{error, info};

pub type Context = Vec<String>;

pub struct Evaluator<'ctx, 'global> {
    pub app: &'global mut AppState<'ctx>,
    pub context: Context,
}

impl<'ctx, 'global> Evaluator<'ctx, 'global>
where
    'ctx: 'global,
{
    pub fn eval(&mut self, expr: &LispExpr) -> Result<LispExpr, LispError> {
        match expr {
            LispExpr::Unit => Ok(expr.clone()),
            LispExpr::StringLit(_) => Ok(expr.clone()),
            LispExpr::Char(_) => Ok(expr.clone()),
            LispExpr::Number(_) => Ok(expr.clone()),
            LispExpr::BoolLit(_) => Ok(expr.clone()),
            LispExpr::Ident(ref id) => lookup(&self.app.lisp_env, id),
            LispExpr::Quote(item, _) => Ok(apply_quote(&item.as_ref())),
            LispExpr::List(li) => {
                let func_expr = &li[0];
                match func_expr {
                    LispExpr::Ident(s) => match s.as_ref() {
                        "define" => self.define_var(&li[1..]),
                        "set!" => self.set_var(&li[1..]),
                        "lambda" => create_lambda(&li[1..]),
                        "if" => self.eval_if(&li[1..]),
                        "cond" => self.eval_cond(&li[1..]),
                        "quote" => Ok(apply_quote(&li[1])),
                        "let" => self.eval_let(&li[1..]),
                        _ => {
                            let mut new_ls = vec![self.eval(&func_expr)?];
                            new_ls.extend(li[1..].to_vec());
                            self.eval(&(LispExpr::List(new_ls)))
                        }
                    },
                    LispExpr::PrimitiveFunc(f) => {
                        let mut args = Vec::new();
                        // context.push(f.name.to_string());
                        for item in li[1..].iter() {
                            args.push(self.eval(item)?);
                        }
                        f.call(&args, &mut self.app)
                    }
                    LispExpr::Function(f) => {
                        let mut args = Vec::new();
                        for item in li[1..].iter() {
                            let i = self.eval(item)?;
                            args.push(i);
                        }
                        if f.params.len() != args.len() {
                            info!("too many or too little number of args");
                            Err(EvalError::ArgumentCount(Arity::Exact(f.params.len())).into())
                        } else {
                            let nested_env: Environment =
                                f.params.clone().into_iter().zip(args).collect();
                            self.app.lisp_env.push(nested_env);
                            let result = if f.body.is_empty() {
                                Ok(LispExpr::Unit)
                            } else {
                                self.eval(&LispExpr::List(f.body.clone()))
                            };
                            self.app.lisp_env.pop();
                            return result;
                        }
                    }
                    LispExpr::List(_) => {
                        info!("list as funciton");
                        let func_expr = self.eval(&func_expr)?;
                        let mut new_ls = vec![func_expr];
                        new_ls.extend(li[1..].to_vec());
                        self.eval(&(LispExpr::List(new_ls)))
                    }
                    _ => Err(EvalError::BadForm.into()),
                }
            }
            _ => Err(EvalError::BadForm.into()),
        }
    }

    pub fn define_var(&mut self, args: &[LispExpr]) -> Result<LispExpr, LispError> {
        let arity = Arity::Exact(2);
        if !arity.check(args) {
            return Err(arity.to_error());
        }
        match args {
            [LispExpr::Ident(id), expr] => {
                let value = self.eval(&expr)?;
                let local_env = &mut self.app.lisp_env.last_mut();
                if let Some(env) = local_env {
                    env.insert(id.into(), value);
                } else {
                    error!("Unable to create global definition");
                    return Err(EvalError::BadForm.into());
                }
                return Ok(LispExpr::Unit);
            }
            [LispExpr::List(shorthand), LispExpr::List(body)] => {
                // (define (func arg) <body>) shorthand

                let id = shorthand[0].unwrap_ident();
                let params = if shorthand.len() > 1 {
                    &shorthand[1..]
                } else {
                    &[]
                }
                .to_vec()
                .into_iter()
                .map(|arg| arg.try_into())
                .collect::<Result<Vec<Ident>, LispError>>()?;
                let value = LispExpr::Function(LispFunction {
                    params,
                    body: body.to_vec(),
                });

                let local_env = &mut self.app.lisp_env.last_mut();
                if let Some(env) = local_env {
                    env.insert(id.into(), value);
                } else {
                    error!("Unable to create global definition");
                    return Err(EvalError::BadForm.into());
                }
                return Ok(LispExpr::Unit);
            }
            _ => {
                error!("Invalid usage of `define`");
                Err(EvalError::BadForm.into())
            }
        }
    }

    pub fn set_var(&mut self, args: &[LispExpr]) -> Result<LispExpr, LispError> {
        let arity = Arity::Exact(2);
        if !arity.check(args) {
            return Err(arity.to_error());
        }
        match args {
            [LispExpr::Ident(id), expr] => {
                let value = self.eval(&expr)?;
                let local_env = self.app.lisp_env.last_mut();
                if let Some(env) = local_env {
                    return env
                        .insert(id.into(), value)
                        .ok_or(EvalError::UnboundVariable(id.into()).into());
                } else {
                    error!("Unable to set in global env!");
                    return Err(EvalError::BadForm.into());
                }
            }
            _ => {
                error!("Invalid usage of `set!`");
                return Err(EvalError::BadForm.into());
            }
        }
    }

    pub fn eval_if(&mut self, args: &[LispExpr]) -> Result<LispExpr, LispError> {
        let arity = Arity::Exact(3);
        if !arity.check(args) {
            return Err(arity.to_error());
        } else {
            match args {
                [predicate, then, else_] => {
                    let predicate = self.eval(&predicate)?;
                    if matches!(predicate, LispExpr::BoolLit(false)) {
                        return self.eval(&else_);
                    } else {
                        return self.eval(&then);
                    }
                }
                _ => {
                    panic!("panicked at `if` expression")
                }
            }
        }
    }

    pub fn eval_cond(&mut self, args: &[LispExpr]) -> Result<LispExpr, LispError> {
        let arity = Arity::Atleast(1);
        let valid_cond_stmt = |expr: &LispExpr| matches!(expr, LispExpr::List(v) if v.len() == 2);
        if !arity.check(args) {
            return Err(arity.to_error());
        } else {
            for cond_stmt in args {
                if valid_cond_stmt(cond_stmt) {
                    match &cond_stmt.unwrap_list()[..] {
                        [predicate, then] => {
                            if self.eval(&predicate)?.cast_bool() {
                                return self.eval(&then);
                            }
                        }
                        _ => return Err(EvalError::BadForm.into()),
                    }
                } else {
                    error!("bad `cond` form");
                    return Err(EvalError::BadForm.into());
                }
            }
            return Ok(LispExpr::Unit);
        }
    }

    pub fn eval_let(&mut self, args: &[LispExpr]) -> Result<LispExpr, LispError> {
        let arity = Arity::Exact(2);
        let valid_binding_stmt =
            |expr: &LispExpr| matches!(expr, LispExpr::List(v) if v.len() == 2);
        if !arity.check(args) {
            return Err(arity.to_error());
        } else {
            let nested_env = Environment::new();
            self.app.lisp_env.push(nested_env);
            match args {
                [LispExpr::List(bindings), body] => {
                    for binding_stmt in bindings {
                        if valid_binding_stmt(binding_stmt) {
                            match &binding_stmt.unwrap_list()[..] {
                                [LispExpr::Ident(id), bind_val] => {
                                    let value = self.eval(&bind_val)?;
                                    if let Some(env) = self.app.lisp_env.last_mut() {
                                        env.insert(id.into(), value);
                                    }
                                }
                                _ => {
                                    error!("bad let binding form");
                                    return Err(EvalError::BadForm.into());
                                }
                            }
                        } else {
                            error!("bad `let` form");
                            return Err(EvalError::BadForm.into());
                        }
                    }
                    let result = self.eval(&body);
                    self.app.lisp_env.pop();
                    return result;
                }
                _ => {
                    error!("bad `let` form");
                    return Err(EvalError::BadForm.into());
                }
            }
        }
    }
}

pub fn apply_quote(arg: &LispExpr) -> LispExpr {
    match arg {
        i @ LispExpr::Unit
        | i @ LispExpr::StringLit(_)
        | i @ LispExpr::Char(_)
        | i @ LispExpr::Number(_)
        | i @ LispExpr::BoolLit(_) => i.clone(),
        LispExpr::Quote(expr, depth) => LispExpr::Quote(Box::clone(expr), depth + 1),
        LispExpr::List(ls) => LispExpr::List(ls.iter().map(|a| apply_quote(a)).collect::<Vec<_>>()),
        _ => arg.clone(),
    }
}

pub fn create_lambda(cdr: &[LispExpr]) -> Result<LispExpr, LispError> {
    let arity: Arity = Arity::Exact(2);
    if !arity.check(cdr) {
        return Err(arity.to_error());
    }
    match cdr {
        [LispExpr::List(params), LispExpr::List(body)] if type_match!(params, (..) => LispExpr::Ident(_)) =>
        {
            return Ok(LispExpr::Function(LispFunction {
                params: params
                    .into_iter()
                    .map(|p| p.unwrap_ident())
                    .collect::<Vec<_>>(),
                body: body.clone(),
            }));
        }
        _ => {
            error!("Invalid usage of `lambda`");
            return Err(EvalError::BadForm.into());
        }
    }
}

pub fn lookup(env_list: &[Environment], key: &str) -> Result<LispExpr, LispError> {
    if env_list.is_empty() {
        return Err(EvalError::UnboundVariable(key.into()).into());
    } else {
        let local_env = env_list.last().unwrap();
        if let Some(val) = local_env.get(key) {
            return Ok(val.clone());
        } else {
            return lookup(&env_list[..env_list.len() - 1], key);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use crate::lisp::{expr::LispExpr, lex::Lexer, number::LispNumber, parse::Parser};

    fn run(code: &str, app: &mut AppState) -> LispExpr {
        let mut parser = Parser::new(Lexer::new(code, 0));
        let mut evaluator = Evaluator {
            app,
            context: Vec::new(),
        };
        evaluator
            .eval(&parser.parse_single_expr().unwrap())
            .unwrap()
    }

    #[test]
    fn eval_all() {
        let sdl_context = sdl2::init().unwrap();
        let ttf_context = sdl2::ttf::init().unwrap();
        let mut app = AppState::init(100, 100, &sdl_context, &ttf_context, None, None).unwrap();
        eval_arithmetic(&mut app);
        eval_logical(&mut app);
        eval_quote(&mut app);
    }

    fn eval_arithmetic(app: &mut AppState) {
        assert_eq!(
            run("(+ 1 2 3)", app),
            LispExpr::Number(LispNumber::Integer(6))
        );
        assert_eq!(
            run("(+ 1.1 2.2 3.3)", app),
            LispExpr::Number(LispNumber::Float(6.6))
        );
        assert_eq!(
            run("(* 1 2 3 4 5)", app),
            LispExpr::Number(LispNumber::Integer(120))
        );
        assert_eq!(run("(< 1 2)", app), LispExpr::BoolLit(true));
        assert_eq!(run("(> 6 5 4 3 2 1)", app), LispExpr::BoolLit(true));
        assert_eq!(run("(< 1 2 3 4 5 6)", app), LispExpr::BoolLit(true));
        assert_eq!(run("(>= 5 5 4 3 2 1)", app), LispExpr::BoolLit(true));
        assert_eq!(run("(<= 2 2 3 4 5 6)", app), LispExpr::BoolLit(true));
    }

    fn eval_quote(app: &mut AppState) {
        assert!(run("(quote a)", app).cast_bool());
        assert!(run("(eq? 'a 'a)", app).cast_bool());
        assert!(run("(eq? '(1 2 3) '(1 2 3))", app).cast_bool());
        assert!(run("(eq? '(1 '(1 2 3)) '(1 '(1 2 3)))", app).cast_bool(),);
        assert!(run("(eq? '#t '#t)", app).cast_bool());

        assert!(run("(eq? 1 '1)", app).cast_bool());
        assert_eq!(
            run("'(1 2 3)", app),
            LispExpr::List(
                vec![1, 2, 3]
                    .into_iter()
                    .map(LispNumber::Integer)
                    .map(LispExpr::Number)
                    .collect()
            ),
        );
        assert_eq!(
            run("(caar (cdr '(1 (4 5))))", app),
            LispExpr::Number(LispNumber::Integer(4))
        );
        assert_eq!(run("''1", app), LispExpr::Number(LispNumber::Integer(1)));
    }

    fn eval_logical(app: &mut AppState) {
        assert!(run("(and #t #t)", app).cast_bool());
        assert!(run("(or #f #t)", app).cast_bool());
        assert!(!run("(not #t)", app).cast_bool());
        assert_eq!(run("(not #f)", app), run("(not (not #t))", app));
    }
}