//! Constant evaluation details

use std::{
    collections::HashMap,
    fmt::{Display, Write},
};

use chalk_ir::{BoundVar, DebruijnIndex, GenericArgData, IntTy, Scalar};
use hir_def::{
    builtin_type::BuiltinInt,
    expr::{ArithOp, BinaryOp, Expr, ExprId, Literal, Pat, PatId},
    path::ModPath,
    resolver::{resolver_for_expr, ResolveValueResult, Resolver, ValueNs},
    src::HasChildSource,
    type_ref::ConstScalar,
    ConstId, DefWithBodyId, EnumVariantId, Lookup,
};
use la_arena::{Arena, Idx, RawIdx};
use stdx::never;
use syntax::ast::HasName;

use crate::{
    db::HirDatabase, infer::InferenceContext, lower::ParamLoweringMode, to_placeholder_idx,
    utils::Generics, Const, ConstData, ConstValue, GenericArg, InferenceResult, Interner, Ty,
    TyBuilder, TyKind,
};

/// Extension trait for [`Const`]
pub trait ConstExt {
    /// Is a [`Const`] unknown?
    fn is_unknown(&self) -> bool;
}

impl ConstExt for Const {
    fn is_unknown(&self) -> bool {
        match self.data(Interner).value {
            // interned Unknown
            chalk_ir::ConstValue::Concrete(chalk_ir::ConcreteConst {
                interned: ConstScalar::Unknown,
            }) => true,

            // interned concrete anything else
            chalk_ir::ConstValue::Concrete(..) => false,

            _ => {
                tracing::error!(
                    "is_unknown was called on a non-concrete constant value! {:?}",
                    self
                );
                true
            }
        }
    }
}

pub struct ConstEvalCtx<'a> {
    pub db: &'a dyn HirDatabase,
    pub owner: DefWithBodyId,
    pub exprs: &'a Arena<Expr>,
    pub pats: &'a Arena<Pat>,
    pub local_data: HashMap<PatId, ComputedExpr>,
    infer: &'a InferenceResult,
}

impl ConstEvalCtx<'_> {
    fn expr_ty(&mut self, expr: ExprId) -> Ty {
        self.infer[expr].clone()
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConstEvalError {
    NotSupported(&'static str),
    SemanticError(&'static str),
    Loop,
    IncompleteExpr,
    Panic(String),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ComputedExpr {
    Literal(Literal),
    Enum(String, EnumVariantId, Literal),
    Tuple(Box<[ComputedExpr]>),
}

impl Display for ComputedExpr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ComputedExpr::Literal(l) => match l {
                Literal::Int(x, _) => {
                    if *x >= 10 {
                        write!(f, "{x} ({x:#X})")
                    } else {
                        x.fmt(f)
                    }
                }
                Literal::Uint(x, _) => {
                    if *x >= 10 {
                        write!(f, "{x} ({x:#X})")
                    } else {
                        x.fmt(f)
                    }
                }
                Literal::Float(x, _) => x.fmt(f),
                Literal::Bool(x) => x.fmt(f),
                Literal::Char(x) => std::fmt::Debug::fmt(x, f),
                Literal::String(x) => std::fmt::Debug::fmt(x, f),
                Literal::ByteString(x) => std::fmt::Debug::fmt(x, f),
            },
            ComputedExpr::Enum(name, _, _) => name.fmt(f),
            ComputedExpr::Tuple(t) => {
                f.write_char('(')?;
                for x in &**t {
                    x.fmt(f)?;
                    f.write_str(", ")?;
                }
                f.write_char(')')
            }
        }
    }
}

fn scalar_max(scalar: &Scalar) -> i128 {
    match scalar {
        Scalar::Bool => 1,
        Scalar::Char => u32::MAX as i128,
        Scalar::Int(x) => match x {
            IntTy::Isize => isize::MAX as i128,
            IntTy::I8 => i8::MAX as i128,
            IntTy::I16 => i16::MAX as i128,
            IntTy::I32 => i32::MAX as i128,
            IntTy::I64 => i64::MAX as i128,
            IntTy::I128 => i128::MAX,
        },
        Scalar::Uint(x) => match x {
            chalk_ir::UintTy::Usize => usize::MAX as i128,
            chalk_ir::UintTy::U8 => u8::MAX as i128,
            chalk_ir::UintTy::U16 => u16::MAX as i128,
            chalk_ir::UintTy::U32 => u32::MAX as i128,
            chalk_ir::UintTy::U64 => u64::MAX as i128,
            chalk_ir::UintTy::U128 => i128::MAX, // ignore too big u128 for now
        },
        Scalar::Float(_) => 0,
    }
}

fn is_valid(scalar: &Scalar, value: i128) -> bool {
    if value < 0 {
        !matches!(scalar, Scalar::Uint(_)) && -scalar_max(scalar) - 1 <= value
    } else {
        value <= scalar_max(scalar)
    }
}

fn get_name(ctx: &mut ConstEvalCtx<'_>, variant: EnumVariantId) -> String {
    let loc = variant.parent.lookup(ctx.db.upcast());
    let children = variant.parent.child_source(ctx.db.upcast());
    let item_tree = loc.id.item_tree(ctx.db.upcast());

    let variant_name = children.value[variant.local_id].name();
    let enum_name = item_tree[loc.id.value].name.to_string();
    enum_name + "::" + &variant_name.unwrap().to_string()
}

pub fn eval_const(
    expr_id: ExprId,
    ctx: &mut ConstEvalCtx<'_>,
) -> Result<ComputedExpr, ConstEvalError> {
    let u128_to_i128 = |it: u128| -> Result<i128, ConstEvalError> {
        it.try_into().map_err(|_| ConstEvalError::NotSupported("u128 is too big"))
    };

    let expr = &ctx.exprs[expr_id];
    match expr {
        Expr::Missing => match ctx.owner {
            // evaluate the implicit variant index of an enum variant without expression
            // FIXME: This should return the type of the enum representation
            DefWithBodyId::VariantId(variant) => {
                let prev_idx: u32 = variant.local_id.into_raw().into();
                let prev_idx = prev_idx.checked_sub(1).map(RawIdx::from).map(Idx::from_raw);
                let value = match prev_idx {
                    Some(local_id) => {
                        let prev_variant = EnumVariantId { local_id, parent: variant.parent };
                        1 + match ctx.db.const_eval_variant(prev_variant)? {
                            ComputedExpr::Literal(Literal::Int(v, _)) => v,
                            ComputedExpr::Literal(Literal::Uint(v, _)) => u128_to_i128(v)?,
                            _ => {
                                return Err(ConstEvalError::NotSupported(
                                    "Enum can't contain this kind of value",
                                ))
                            }
                        }
                    }
                    _ => 0,
                };
                Ok(ComputedExpr::Literal(Literal::Int(value, Some(BuiltinInt::I128))))
            }
            _ => Err(ConstEvalError::IncompleteExpr),
        },
        Expr::Literal(l) => Ok(ComputedExpr::Literal(l.clone())),
        &Expr::UnaryOp { expr, op } => {
            let ty = &ctx.expr_ty(expr);
            let ev = eval_const(expr, ctx)?;
            match op {
                hir_def::expr::UnaryOp::Deref => Err(ConstEvalError::NotSupported("deref")),
                hir_def::expr::UnaryOp::Not => {
                    let v = match ev {
                        ComputedExpr::Literal(Literal::Bool(b)) => {
                            return Ok(ComputedExpr::Literal(Literal::Bool(!b)))
                        }
                        ComputedExpr::Literal(Literal::Int(v, _)) => v,
                        ComputedExpr::Literal(Literal::Uint(v, _)) => u128_to_i128(v)?,
                        _ => return Err(ConstEvalError::NotSupported("this kind of operator")),
                    };
                    let r = match ty.kind(Interner) {
                        TyKind::Scalar(Scalar::Uint(x)) => match x {
                            chalk_ir::UintTy::U8 => !(v as u8) as i128,
                            chalk_ir::UintTy::U16 => !(v as u16) as i128,
                            chalk_ir::UintTy::U32 => !(v as u32) as i128,
                            chalk_ir::UintTy::U64 => !(v as u64) as i128,
                            chalk_ir::UintTy::U128 => {
                                return Err(ConstEvalError::NotSupported("negation of u128"))
                            }
                            chalk_ir::UintTy::Usize => !(v as usize) as i128,
                        },
                        TyKind::Scalar(Scalar::Int(x)) => match x {
                            chalk_ir::IntTy::I8 => !(v as i8) as i128,
                            chalk_ir::IntTy::I16 => !(v as i16) as i128,
                            chalk_ir::IntTy::I32 => !(v as i32) as i128,
                            chalk_ir::IntTy::I64 => !(v as i64) as i128,
                            chalk_ir::IntTy::I128 => !v,
                            chalk_ir::IntTy::Isize => !(v as isize) as i128,
                        },
                        _ => return Err(ConstEvalError::NotSupported("unreachable?")),
                    };
                    Ok(ComputedExpr::Literal(Literal::Int(r, None)))
                }
                hir_def::expr::UnaryOp::Neg => {
                    let v = match ev {
                        ComputedExpr::Literal(Literal::Int(v, _)) => v,
                        ComputedExpr::Literal(Literal::Uint(v, _)) => u128_to_i128(v)?,
                        _ => return Err(ConstEvalError::NotSupported("this kind of operator")),
                    };
                    Ok(ComputedExpr::Literal(Literal::Int(
                        v.checked_neg().ok_or_else(|| {
                            ConstEvalError::Panic("overflow in negation".to_string())
                        })?,
                        None,
                    )))
                }
            }
        }
        &Expr::BinaryOp { lhs, rhs, op } => {
            let ty = &ctx.expr_ty(lhs);
            let lhs = eval_const(lhs, ctx)?;
            let rhs = eval_const(rhs, ctx)?;
            let op = op.ok_or(ConstEvalError::IncompleteExpr)?;
            let v1 = match lhs {
                ComputedExpr::Literal(Literal::Int(v, _)) => v,
                ComputedExpr::Literal(Literal::Uint(v, _)) => u128_to_i128(v)?,
                _ => return Err(ConstEvalError::NotSupported("this kind of operator")),
            };
            let v2 = match rhs {
                ComputedExpr::Literal(Literal::Int(v, _)) => v,
                ComputedExpr::Literal(Literal::Uint(v, _)) => u128_to_i128(v)?,
                _ => return Err(ConstEvalError::NotSupported("this kind of operator")),
            };
            match op {
                BinaryOp::ArithOp(b) => {
                    let panic_arith = ConstEvalError::Panic(
                        "attempt to run invalid arithmetic operation".to_string(),
                    );
                    let r = match b {
                        ArithOp::Add => v1.checked_add(v2).ok_or_else(|| panic_arith.clone())?,
                        ArithOp::Mul => v1.checked_mul(v2).ok_or_else(|| panic_arith.clone())?,
                        ArithOp::Sub => v1.checked_sub(v2).ok_or_else(|| panic_arith.clone())?,
                        ArithOp::Div => v1.checked_div(v2).ok_or_else(|| panic_arith.clone())?,
                        ArithOp::Rem => v1.checked_rem(v2).ok_or_else(|| panic_arith.clone())?,
                        ArithOp::Shl => v1
                            .checked_shl(v2.try_into().map_err(|_| panic_arith.clone())?)
                            .ok_or_else(|| panic_arith.clone())?,
                        ArithOp::Shr => v1
                            .checked_shr(v2.try_into().map_err(|_| panic_arith.clone())?)
                            .ok_or_else(|| panic_arith.clone())?,
                        ArithOp::BitXor => v1 ^ v2,
                        ArithOp::BitOr => v1 | v2,
                        ArithOp::BitAnd => v1 & v2,
                    };
                    if let TyKind::Scalar(s) = ty.kind(Interner) {
                        if !is_valid(s, r) {
                            return Err(panic_arith);
                        }
                    }
                    Ok(ComputedExpr::Literal(Literal::Int(r, None)))
                }
                BinaryOp::LogicOp(_) => Err(ConstEvalError::SemanticError("logic op on numbers")),
                _ => Err(ConstEvalError::NotSupported("bin op on this operators")),
            }
        }
        Expr::Block { statements, tail, .. } => {
            let mut prev_values = HashMap::<PatId, Option<ComputedExpr>>::default();
            for statement in &**statements {
                match *statement {
                    hir_def::expr::Statement::Let { pat: pat_id, initializer, .. } => {
                        let pat = &ctx.pats[pat_id];
                        match pat {
                            Pat::Bind { subpat, .. } if subpat.is_none() => (),
                            _ => {
                                return Err(ConstEvalError::NotSupported("complex patterns in let"))
                            }
                        };
                        let value = match initializer {
                            Some(x) => eval_const(x, ctx)?,
                            None => continue,
                        };
                        if !prev_values.contains_key(&pat_id) {
                            let prev = ctx.local_data.insert(pat_id, value);
                            prev_values.insert(pat_id, prev);
                        } else {
                            ctx.local_data.insert(pat_id, value);
                        }
                    }
                    hir_def::expr::Statement::Expr { .. } => {
                        return Err(ConstEvalError::NotSupported("this kind of statement"))
                    }
                }
            }
            let r = match tail {
                &Some(x) => eval_const(x, ctx),
                None => Ok(ComputedExpr::Tuple(Box::new([]))),
            };
            // clean up local data, so caller will receive the exact map that passed to us
            for (name, val) in prev_values {
                match val {
                    Some(x) => ctx.local_data.insert(name, x),
                    None => ctx.local_data.remove(&name),
                };
            }
            r
        }
        Expr::Path(p) => {
            let resolver = resolver_for_expr(ctx.db.upcast(), ctx.owner, expr_id);
            let pr = resolver
                .resolve_path_in_value_ns(ctx.db.upcast(), p.mod_path())
                .ok_or(ConstEvalError::SemanticError("unresolved path"))?;
            let pr = match pr {
                ResolveValueResult::ValueNs(v) => v,
                ResolveValueResult::Partial(..) => {
                    return match ctx
                        .infer
                        .assoc_resolutions_for_expr(expr_id)
                        .ok_or(ConstEvalError::SemanticError("unresolved assoc item"))?
                        .0
                    {
                        hir_def::AssocItemId::FunctionId(_) => {
                            Err(ConstEvalError::NotSupported("assoc function"))
                        }
                        // FIXME use actual impl for trait assoc const
                        hir_def::AssocItemId::ConstId(c) => ctx.db.const_eval(c),
                        hir_def::AssocItemId::TypeAliasId(_) => {
                            Err(ConstEvalError::NotSupported("assoc type alias"))
                        }
                    };
                }
            };
            match pr {
                ValueNs::LocalBinding(pat_id) => {
                    let r = ctx
                        .local_data
                        .get(&pat_id)
                        .ok_or(ConstEvalError::NotSupported("Unexpected missing local"))?;
                    Ok(r.clone())
                }
                ValueNs::ConstId(id) => ctx.db.const_eval(id),
                ValueNs::GenericParam(_) => {
                    Err(ConstEvalError::NotSupported("const generic without substitution"))
                }
                ValueNs::EnumVariantId(id) => match ctx.db.const_eval_variant(id)? {
                    ComputedExpr::Literal(lit) => {
                        Ok(ComputedExpr::Enum(get_name(ctx, id), id, lit))
                    }
                    _ => Err(ConstEvalError::NotSupported(
                        "Enums can't evalute to anything but numbers",
                    )),
                },
                _ => Err(ConstEvalError::NotSupported("path that are not const or local")),
            }
        }
        // FIXME: Handle the cast target
        &Expr::Cast { expr, .. } => match eval_const(expr, ctx)? {
            ComputedExpr::Enum(_, _, lit) => Ok(ComputedExpr::Literal(lit)),
            _ => Err(ConstEvalError::NotSupported("Can't cast these types")),
        },
        _ => Err(ConstEvalError::NotSupported("This kind of expression")),
    }
}

pub(crate) fn path_to_const(
    db: &dyn HirDatabase,
    resolver: &Resolver,
    path: &ModPath,
    mode: ParamLoweringMode,
    args_lazy: impl FnOnce() -> Generics,
    debruijn: DebruijnIndex,
) -> Option<Const> {
    match resolver.resolve_path_in_value_ns_fully(db.upcast(), path) {
        Some(ValueNs::GenericParam(p)) => {
            let ty = db.const_param_ty(p);
            let args = args_lazy();
            let value = match mode {
                ParamLoweringMode::Placeholder => {
                    ConstValue::Placeholder(to_placeholder_idx(db, p.into()))
                }
                ParamLoweringMode::Variable => match args.param_idx(p.into()) {
                    Some(x) => ConstValue::BoundVar(BoundVar::new(debruijn, x)),
                    None => {
                        never!(
                            "Generic list doesn't contain this param: {:?}, {}, {:?}",
                            args,
                            path,
                            p
                        );
                        return None;
                    }
                },
            };
            Some(ConstData { ty, value }.intern(Interner))
        }
        _ => None,
    }
}

pub fn unknown_const(ty: Ty) -> Const {
    ConstData {
        ty,
        value: ConstValue::Concrete(chalk_ir::ConcreteConst { interned: ConstScalar::Unknown }),
    }
    .intern(Interner)
}

pub fn unknown_const_as_generic(ty: Ty) -> GenericArg {
    GenericArgData::Const(unknown_const(ty)).intern(Interner)
}

/// Interns a constant scalar with the given type
pub fn intern_const_scalar(value: ConstScalar, ty: Ty) -> Const {
    ConstData { ty, value: ConstValue::Concrete(chalk_ir::ConcreteConst { interned: value }) }
        .intern(Interner)
}

/// Interns a possibly-unknown target usize
pub fn usize_const(value: Option<u128>) -> Const {
    intern_const_scalar(value.map_or(ConstScalar::Unknown, ConstScalar::UInt), TyBuilder::usize())
}

pub(crate) fn const_eval_recover(
    _: &dyn HirDatabase,
    _: &[String],
    _: &ConstId,
) -> Result<ComputedExpr, ConstEvalError> {
    Err(ConstEvalError::Loop)
}

pub(crate) fn const_eval_variant_recover(
    _: &dyn HirDatabase,
    _: &[String],
    _: &EnumVariantId,
) -> Result<ComputedExpr, ConstEvalError> {
    Err(ConstEvalError::Loop)
}

pub(crate) fn const_eval_variant_query(
    db: &dyn HirDatabase,
    const_id: ConstId,
) -> Result<ComputedExpr, ConstEvalError> {
    let def = const_id.into();
    let body = db.body(def);
    let infer = &db.infer(def);
    let result = eval_const(
        body.body_expr,
        &mut ConstEvalCtx {
            db,
            owner: const_id.into(),
            exprs: &body.exprs,
            pats: &body.pats,
            local_data: HashMap::default(),
            infer,
        },
    );
    result
}

pub(crate) fn const_eval_query_variant(
    db: &dyn HirDatabase,
    variant_id: EnumVariantId,
) -> Result<ComputedExpr, ConstEvalError> {
    let def = variant_id.into();
    let body = db.body(def);
    let infer = &db.infer(def);
    eval_const(
        body.body_expr,
        &mut ConstEvalCtx {
            db,
            owner: def,
            exprs: &body.exprs,
            pats: &body.pats,
            local_data: HashMap::default(),
            infer,
        },
    )
}

pub(crate) fn eval_to_const(
    expr: Idx<Expr>,
    mode: ParamLoweringMode,
    ctx: &mut InferenceContext<'_>,
    args: impl FnOnce() -> Generics,
    debruijn: DebruijnIndex,
) -> Const {
    if let Expr::Path(p) = &ctx.body.exprs[expr] {
        let db = ctx.db;
        let resolver = &ctx.resolver;
        if let Some(c) = path_to_const(db, resolver, p.mod_path(), mode, args, debruijn) {
            return c;
        }
    }
    let body = ctx.body.clone();
    let mut ctx = ConstEvalCtx {
        db: ctx.db,
        owner: ctx.owner,
        exprs: &body.exprs,
        pats: &body.pats,
        local_data: HashMap::default(),
        infer: &ctx.result,
    };
    let computed_expr = eval_const(expr, &mut ctx);
    let const_scalar = match computed_expr {
        Ok(ComputedExpr::Literal(literal)) => literal.into(),
        _ => ConstScalar::Unknown,
    };
    intern_const_scalar(const_scalar, TyBuilder::usize())
}

#[cfg(test)]
mod tests;
