use std::borrow::Cow;

use log::debug;
use rue_ast::{AstExpr, AstNode, AstPrefixExpr};
use rue_diagnostic::DiagnosticKind;
use rue_hir::{Hir, UnaryOp, Value};
use rue_lir::{atom_bigint, bigint_atom};
use rue_parser::T;
use rue_types::{Atom, AtomRestriction, AtomSemantic, Atoms, Type};

use crate::{Compiler, compile_expr};

pub fn compile_prefix_expr(ctx: &mut Compiler, prefix: &AstPrefixExpr) -> Value {
    let Some(expr) = prefix.expr() else {
        debug!("Unresolved prefix expr");
        return ctx.builtins().unresolved.clone();
    };

    let value = compile_expr(ctx, &expr, None);

    if ctx.is_unresolved(value.ty) {
        debug!("Unresolved prefix expr");
        return ctx.builtins().unresolved.clone();
    }

    let Some(op) = prefix.op() else {
        return value;
    };

    match op.kind() {
        T![!] => {
            if ctx.is_assignable(value.ty, ctx.builtins().types.bool) {
                let is_true = ctx.is_assignable(value.ty, ctx.builtins().types.bool_true);
                let is_false = ctx.is_assignable(value.ty, ctx.builtins().types.bool_false);

                let hir = ctx.alloc_hir(Hir::Unary(UnaryOp::Not, value.hir));

                return value.flip_mappings().with_hir(hir).with_type(if is_true {
                    ctx.builtins().types.bool_false
                } else if is_false {
                    ctx.builtins().types.bool_true
                } else {
                    ctx.builtins().types.bool
                });
            }
        }
        T![+] => {
            if ctx.is_assignable(value.ty, ctx.builtins().types.int) {
                ctx.diagnostic(prefix.syntax(), DiagnosticKind::UnnecessaryPlus);
                return Value::new(value.hir, ctx.builtins().types.int);
            }
        }
        T![-] => {
            if let AstExpr::PrefixExpr(inner) = &expr
                && let Some(op) = inner.op()
                && op.kind() == T![-]
            {
                ctx.diagnostic(prefix.syntax(), DiagnosticKind::DoubleNegationWarning);
            }

            if ctx.is_assignable(value.ty, ctx.builtins().types.int) {
                let ty = if let Some(Atoms::Restricted(restrictions)) =
                    rue_types::extract_atoms(ctx.types_mut(), value.ty, true)
                    && restrictions.len() == 1
                    && let Some(AtomRestriction::Value(value)) = restrictions.iter().next()
                {
                    ctx.alloc_type(Type::Atom(Atom::new(
                        AtomSemantic::Int,
                        Some(AtomRestriction::Value(Cow::Owned(bigint_atom(
                            -atom_bigint(value),
                        )))),
                    )))
                } else {
                    ctx.builtins().types.int
                };

                let hir = ctx.alloc_hir(Hir::Unary(UnaryOp::Neg, value.hir));
                return Value::new(hir, ty);
            }

            if ctx.is_assignable(value.ty, ctx.builtins().types.public_key) {
                let hir = ctx.alloc_hir(Hir::Unary(UnaryOp::G1Negate, value.hir));
                return Value::new(hir, ctx.builtins().types.public_key);
            }

            if ctx.is_assignable(value.ty, ctx.builtins().types.signature) {
                let hir = ctx.alloc_hir(Hir::Unary(UnaryOp::G2Negate, value.hir));
                return Value::new(hir, ctx.builtins().types.signature);
            }
        }
        T![~] => {
            if ctx.is_assignable(value.ty, ctx.builtins().types.int) {
                let hir = ctx.alloc_hir(Hir::Unary(UnaryOp::BitwiseNot, value.hir));
                return Value::new(hir, ctx.builtins().types.int);
            }
        }
        _ => {}
    }

    debug!("Unresolved prefix expr due to incompatible op");

    let type_name = ctx.type_name(value.ty);
    ctx.diagnostic(
        prefix.syntax(),
        DiagnosticKind::IncompatibleUnaryOp(op.text().to_string(), type_name),
    );
    ctx.builtins().unresolved.clone()
}
