use itertools::Itertools as _;

use super::ast::{BinOp, Expr, UnOp};

pub fn ast_to_js(expr: &Expr<'_>) -> String {
    inner(expr)
}

// TODO: #[recursive]
fn inner(expr: &Expr<'_>) -> String {
    match expr {
        Expr::Ident(ident) => ident.0.to_string(),
        Expr::Bool(value) => value.to_string(),
        Expr::Integer(value) => value.to_string(),
        Expr::Decimal(value) => format!("{value:?}"),
        Expr::String(value) => format!("{value:?}"),
        Expr::Regex(value) => format!("/{value:?}/"),
        Expr::Function(expr, params) => {
            let mut expr = inner(&expr.inner);

            if expr == "if" {
                expr = "__if_expr__".to_string();
            }

            let params = params
                .inner
                .iter()
                .map(|param| inner(&param.inner))
                .join(",");

            format!("({expr})({params})")
        }
        Expr::Method(expr, name, params) => {
            let expr = inner(&expr.inner);
            let params = params
                .inner
                .iter()
                .map(|param| inner(&param.inner))
                .join(",");

            format!("({expr}).{}({params})", name.0)
        }
        Expr::Member(expr, name) => {
            let expr = inner(&expr.inner);

            format!("({expr}).{}", name.0)
        }
        Expr::Index(expr, index) => {
            let expr = inner(&expr.inner);
            let index = inner(&index.inner);

            format!("({expr})[{index}]")
        }
        Expr::BinOp(op, lhs, rhs) => {
            let lhs = inner(&lhs.inner);
            let rhs = inner(&rhs.inner);

            let sym = match op.inner {
                BinOp::Add => "+",
                BinOp::Sub => "-",
                BinOp::Mul => "*",
                BinOp::Div => "/",
                BinOp::Mod => "%",
                BinOp::Equal => "==",
                BinOp::NotEqual => "!=",
                BinOp::GreaterThan => ">",
                BinOp::LessThan => "<",
                BinOp::GreaterEqual => ">=",
                BinOp::LessEqual => "<=",
                BinOp::And => "&&",
                BinOp::Or => "||",
            };

            format!("({lhs} {sym} {rhs})")
        }
        Expr::UnOp(op, expr) => {
            let expr = inner(&expr.inner);

            let sym = match op.inner {
                UnOp::Not => "!",
                UnOp::Plus => "+",
                UnOp::Neg => "-",
            };

            format!("({sym}{expr})")
        }
        Expr::Group(expr) => format!("({})", inner(&expr.inner)),
    }
}

#[allow(
    clippy::too_many_lines,
    clippy::cognitive_complexity,
    reason = "theses are tests..."
)]
#[cfg(test)]
mod tests {
    use chumsky::span::{SimpleSpan, Spanned};

    use super::super::ast::{BinOp, Expr, Ident, UnOp};

    use super::ast_to_js;

    fn s<T>(t: T) -> Spanned<T> {
        Spanned {
            span: SimpleSpan::from(0..1),
            inner: t,
        }
    }

    #[test]
    fn simple() {
        assert_eq!("test", ast_to_js(&Expr::Ident(Ident("test".into()))));

        assert_eq!("true", ast_to_js(&Expr::Bool(true)));
        assert_eq!("false", ast_to_js(&Expr::Bool(false)));

        assert_eq!("0", ast_to_js(&Expr::Integer(0)));
        assert_eq!("1", ast_to_js(&Expr::Integer(1)));

        assert_eq!("0.0", ast_to_js(&Expr::Decimal(0.0)));
        assert_eq!("1.0", ast_to_js(&Expr::Decimal(1.0)));
        assert_eq!(
            "1.000000000000001",
            ast_to_js(&Expr::Decimal(1.000_000_000_000_001))
        );

        assert_eq!("\"test\\\"s\"", ast_to_js(&Expr::String("test\"s".into())));

        assert_eq!(
            "(test)()",
            ast_to_js(&Expr::Function(
                Box::new(s(Expr::Ident(Ident("test".into())))),
                s(vec![]),
            ))
        );
        assert_eq!(
            "(test)(a)",
            ast_to_js(&Expr::Function(
                Box::new(s(Expr::Ident(Ident("test".into())))),
                s(vec![s(Expr::Ident(Ident("a".into())))]),
            ))
        );
        assert_eq!(
            "(test)(a,b)",
            ast_to_js(&Expr::Function(
                Box::new(s(Expr::Ident(Ident("test".into())))),
                s(vec![
                    s(Expr::Ident(Ident("a".into()))),
                    s(Expr::Ident(Ident("b".into()))),
                ]),
            ))
        );

        assert_eq!(
            "(test).test()",
            ast_to_js(&Expr::Method(
                Box::new(s(Expr::Ident(Ident("test".into())))),
                s(Ident("test".into())),
                s(vec![]),
            ))
        );
        assert_eq!(
            "(test).test(a)",
            ast_to_js(&Expr::Method(
                Box::new(s(Expr::Ident(Ident("test".into())))),
                s(Ident("test".into())),
                s(vec![s(Expr::Ident(Ident("a".into())))]),
            ))
        );
        assert_eq!(
            "(test).test(a,b)",
            ast_to_js(&Expr::Method(
                Box::new(s(Expr::Ident(Ident("test".into())))),
                s(Ident("test".into())),
                s(vec![
                    s(Expr::Ident(Ident("a".into()))),
                    s(Expr::Ident(Ident("b".into()))),
                ]),
            ))
        );

        assert_eq!(
            "(test).test",
            ast_to_js(&Expr::Member(
                Box::new(s(Expr::Ident(Ident("test".into())))),
                s(Ident("test".into())),
            ))
        );

        assert_eq!(
            "(test)[1]",
            ast_to_js(&Expr::Index(
                Box::new(s(Expr::Ident(Ident("test".into())))),
                Box::new(s(Expr::Integer(1))),
            ))
        );

        assert_eq!(
            "(a + b)",
            ast_to_js(&Expr::BinOp(
                s(BinOp::Add),
                Box::new(s(Expr::Ident(Ident("a".into())))),
                Box::new(s(Expr::Ident(Ident("b".into())))),
            ))
        );
        assert_eq!(
            "(a - b)",
            ast_to_js(&Expr::BinOp(
                s(BinOp::Sub),
                Box::new(s(Expr::Ident(Ident("a".into())))),
                Box::new(s(Expr::Ident(Ident("b".into())))),
            ))
        );
        assert_eq!(
            "(a * b)",
            ast_to_js(&Expr::BinOp(
                s(BinOp::Mul),
                Box::new(s(Expr::Ident(Ident("a".into())))),
                Box::new(s(Expr::Ident(Ident("b".into())))),
            ))
        );
        assert_eq!(
            "(a / b)",
            ast_to_js(&Expr::BinOp(
                s(BinOp::Div),
                Box::new(s(Expr::Ident(Ident("a".into())))),
                Box::new(s(Expr::Ident(Ident("b".into())))),
            ))
        );
        assert_eq!(
            "(a % b)",
            ast_to_js(&Expr::BinOp(
                s(BinOp::Mod),
                Box::new(s(Expr::Ident(Ident("a".into())))),
                Box::new(s(Expr::Ident(Ident("b".into())))),
            ))
        );
        assert_eq!(
            "(a == b)",
            ast_to_js(&Expr::BinOp(
                s(BinOp::Equal),
                Box::new(s(Expr::Ident(Ident("a".into())))),
                Box::new(s(Expr::Ident(Ident("b".into())))),
            ))
        );
        assert_eq!(
            "(a != b)",
            ast_to_js(&Expr::BinOp(
                s(BinOp::NotEqual),
                Box::new(s(Expr::Ident(Ident("a".into())))),
                Box::new(s(Expr::Ident(Ident("b".into())))),
            ))
        );
        assert_eq!(
            "(a > b)",
            ast_to_js(&Expr::BinOp(
                s(BinOp::GreaterThan),
                Box::new(s(Expr::Ident(Ident("a".into())))),
                Box::new(s(Expr::Ident(Ident("b".into())))),
            ))
        );
        assert_eq!(
            "(a < b)",
            ast_to_js(&Expr::BinOp(
                s(BinOp::LessThan),
                Box::new(s(Expr::Ident(Ident("a".into())))),
                Box::new(s(Expr::Ident(Ident("b".into())))),
            ))
        );
        assert_eq!(
            "(a >= b)",
            ast_to_js(&Expr::BinOp(
                s(BinOp::GreaterEqual),
                Box::new(s(Expr::Ident(Ident("a".into())))),
                Box::new(s(Expr::Ident(Ident("b".into())))),
            ))
        );
        assert_eq!(
            "(a <= b)",
            ast_to_js(&Expr::BinOp(
                s(BinOp::LessEqual),
                Box::new(s(Expr::Ident(Ident("a".into())))),
                Box::new(s(Expr::Ident(Ident("b".into())))),
            ))
        );
        assert_eq!(
            "(a && b)",
            ast_to_js(&Expr::BinOp(
                s(BinOp::And),
                Box::new(s(Expr::Ident(Ident("a".into())))),
                Box::new(s(Expr::Ident(Ident("b".into())))),
            ))
        );
        assert_eq!(
            "(a || b)",
            ast_to_js(&Expr::BinOp(
                s(BinOp::Or),
                Box::new(s(Expr::Ident(Ident("a".into())))),
                Box::new(s(Expr::Ident(Ident("b".into())))),
            ))
        );

        assert_eq!(
            "(!a)",
            ast_to_js(&Expr::UnOp(
                s(UnOp::Not),
                Box::new(s(Expr::Ident(Ident("a".into())))),
            ))
        );
        assert_eq!(
            "(+a)",
            ast_to_js(&Expr::UnOp(
                s(UnOp::Plus),
                Box::new(s(Expr::Ident(Ident("a".into())))),
            ))
        );
        assert_eq!(
            "(-a)",
            ast_to_js(&Expr::UnOp(
                s(UnOp::Neg),
                Box::new(s(Expr::Ident(Ident("a".into())))),
            ))
        );

        assert_eq!(
            "(a)",
            ast_to_js(&Expr::Group(Box::new(s(Expr::Ident(Ident("a".into())))),))
        );
    }
}
