use ergotree_ir::mir::bin_op::ArithOp;
use ergotree_ir::mir::bin_op::BinOp;
use ergotree_ir::mir::bin_op::BinOpKind;
use ergotree_ir::mir::constant::Constant;
use ergotree_ir::mir::expr::Expr;
use ergotree_ir::mir::global_vars::GlobalVars;
use hir::BinaryOp;
use rowan::TextRange;

use crate::error::pretty_error_desc;
use crate::hir;

#[derive(Debug, PartialEq, Eq)]
pub struct MirLoweringError {
    msg: String,
    span: TextRange,
}

impl MirLoweringError {
    pub fn new(msg: String, span: TextRange) -> Self {
        Self { msg, span }
    }

    pub fn pretty_desc(&self, source: &str) -> String {
        pretty_error_desc(source, self.span, &self.msg)
    }
}

pub fn lower(hir_expr: hir::Expr) -> Result<Expr, MirLoweringError> {
    let mir: Expr = match &hir_expr.kind {
        hir::ExprKind::GlobalVars(hir) => match hir {
            hir::GlobalVars::Height => GlobalVars::Height.into(),
        },
        hir::ExprKind::Ident(_) => {
            return Err(MirLoweringError::new(
                format!("MIR error: Unresolved Ident {0:?}", hir_expr),
                hir_expr.span,
            ))
        }
        hir::ExprKind::Binary(hir) => {
            let l = lower(*hir.lhs.clone())?;
            let r = lower(*hir.rhs.clone())?;
            BinOp {
                kind: hir.op.node.clone().into(),
                left: l.into(),
                right: r.into(),
            }
            .into()
        }
        hir::ExprKind::Literal(hir) => {
            let constant: Constant = match *hir {
                hir::Literal::Int(v) => v.into(),
                hir::Literal::Long(v) => v.into(),
            };
            constant.into()
        }
    };
    let hir_tpe = hir_expr.tpe.clone().ok_or_else(|| {
        MirLoweringError::new(
            format!("MIR error: missing tpe for HIR: {0:?}", hir_expr),
            hir_expr.span,
        )
    })?;
    if mir.tpe() == hir_tpe {
        Ok(mir)
    } else {
        Err(MirLoweringError::new(
            format!(
                "MIR error: lowered MIR type != HIR type ({0:?} != {1:?})",
                mir.tpe(),
                hir_expr.tpe
            ),
            hir_expr.span,
        ))
    }
}

impl From<hir::BinaryOp> for BinOpKind {
    fn from(op: hir::BinaryOp) -> Self {
        match op {
            BinaryOp::Plus => ArithOp::Plus.into(),
            BinaryOp::Minus => ArithOp::Minus.into(),
            BinaryOp::Multiply => ArithOp::Multiply.into(),
            BinaryOp::Divide => ArithOp::Divide.into(),
        }
    }
}

#[cfg(test)]
pub fn check(input: &str, expected_tree: expect_test::Expect) {
    let parse = crate::parser::parse(input);
    let syntax = parse.syntax();
    let root = crate::ast::Root::cast(syntax).unwrap();
    let hir = hir::lower(root).unwrap();
    let binder = crate::binder::Binder::new(crate::script_env::ScriptEnv::new());
    let bind = binder.bind(hir).unwrap();
    let typed = crate::type_infer::assign_type(bind).unwrap();
    let res = lower(typed).unwrap();
    expected_tree.assert_eq(&res.debug_tree());
}

#[cfg(test)]
mod tests {
    use expect_test::expect;

    use super::*;

    #[test]
    fn bin_smoke() {
        check(
            "HEIGHT + HEIGHT",
            expect![[r#"
            BinOp(
                BinOp {
                    kind: Arith(
                        Plus,
                    ),
                    left: GlobalVars(
                        Height,
                    ),
                    right: GlobalVars(
                        Height,
                    ),
                },
            )"#]],
        )
    }

    #[test]
    fn literal_int() {
        check(
            "42",
            expect![[r#"
                Const(
                    "42: SInt",
                )"#]],
        );
    }

    #[test]
    fn literal_long() {
        check(
            "42L",
            expect![[r#"
                Const(
                    "42: SLong",
                )"#]],
        );
    }

    #[test]
    fn bin_numeric_int() {
        check(
            "4+2",
            expect![[r#"
                BinOp(
                    BinOp {
                        kind: Arith(
                            Plus,
                        ),
                        left: Const(
                            "4: SInt",
                        ),
                        right: Const(
                            "2: SInt",
                        ),
                    },
                )"#]],
        );
    }

    #[test]
    fn bin_numeric_long() {
        check(
            "4L+2L",
            expect![[r#"
            BinOp(
                BinOp {
                    kind: Arith(
                        Plus,
                    ),
                    left: Const(
                        "4: SLong",
                    ),
                    right: Const(
                        "2: SLong",
                    ),
                },
            )"#]],
        );
    }
}
