use emmylua_parser::{
    LuaAstNode, LuaExpr, LuaForStat, LuaIndexKey, LuaIndexMemberExpr, LuaNameExpr, LuaUnaryExpr,
    UnaryOperator,
};

use crate::{
    DbIndex, InferFailReason, LuaArrayLen, LuaArrayType, LuaInferCache, LuaMemberKey, LuaType,
    TypeOps, check_type_compact,
    semantic::infer::{infer_index::infer_expr_for_index, narrow::get_var_expr_var_ref_id},
};

pub(super) fn infer_array_member_by_key(
    db: &DbIndex,
    cache: &mut LuaInferCache,
    array_type: &LuaArrayType,
    index_expr: LuaIndexMemberExpr,
    key_type: &LuaType,
    key: &LuaMemberKey,
) -> Result<LuaType, InferFailReason> {
    let base = array_type.get_base();

    let index_prefix_expr = match index_expr {
        LuaIndexMemberExpr::TableField(_) => return Ok(base.clone()),
        _ => index_expr.get_prefix_expr().ok_or(InferFailReason::None)?,
    };

    if let LuaMemberKey::Integer(index_value) = key {
        if !db.get_emmyrc().strict.array_index {
            return Ok(base.clone());
        }

        if let LuaArrayLen::Max(max_len) = array_type.get_len()
            && *index_value > 0
            && *index_value <= *max_len
        {
            return Ok(base.clone());
        }

        return Ok(array_member_fallback(db, base));
    }

    let is_integer_key = key_type.is_integer() || key_type_matches(db, &LuaType::Integer, key_type);
    if !is_integer_key {
        if key_type.is_number() || key_type_matches(db, &LuaType::Number, key_type) {
            return Ok(array_member_fallback(db, base));
        }

        return Err(InferFailReason::FieldNotFound);
    }

    if let LuaArrayLen::Max(max_len) = array_type.get_len()
        && let LuaType::IntegerConst(index_value) | LuaType::DocIntegerConst(index_value) = key_type
        && *index_value > 0
        && *index_value <= *max_len
    {
        return Ok(base.clone());
    }

    if let Some(LuaIndexKey::Expr(expr)) = index_expr.get_index_key()
        && check_iter_var_range(db, cache, &expr, index_prefix_expr).unwrap_or(false)
    {
        return Ok(base.clone());
    }

    Ok(array_member_fallback(db, base))
}

fn key_type_matches(db: &DbIndex, expected: &LuaType, actual: &LuaType) -> bool {
    !matches!(
        actual,
        LuaType::Any | LuaType::Unknown | LuaType::TplRef(_) | LuaType::StrTplRef(_)
    ) && check_type_compact(db, expected, actual).is_ok()
}

pub(super) fn array_member_fallback(db: &DbIndex, base: &LuaType) -> LuaType {
    match base {
        LuaType::Any | LuaType::Unknown => base.clone(),
        _ if db.get_emmyrc().strict.array_index => TypeOps::Union.apply(db, base, &LuaType::Nil),
        _ => base.clone(),
    }
}

pub(super) fn check_iter_var_range(
    db: &DbIndex,
    cache: &mut LuaInferCache,
    may_iter_var: &LuaExpr,
    prefix_expr: LuaExpr,
) -> Option<bool> {
    match may_iter_var {
        LuaExpr::NameExpr(name_expr) => check_index_var_in_range(db, cache, name_expr, prefix_expr),
        LuaExpr::UnaryExpr(unary_expr) => check_is_len(db, cache, unary_expr, prefix_expr),
        _ => None,
    }
}

fn check_index_var_in_range(
    db: &DbIndex,
    cache: &mut LuaInferCache,
    iter_var: &LuaNameExpr,
    prefix_expr: LuaExpr,
) -> Option<bool> {
    let decl_id = db
        .get_reference_index()
        .get_var_reference_decl(&cache.get_file_id(), iter_var.get_range())?;

    let decl = db.get_decl_index().get_decl(&decl_id)?;
    let decl_syntax_id = decl.get_syntax_id();
    if !decl_syntax_id.is_token() {
        return None;
    }

    let root = prefix_expr.get_root();
    let token = decl_syntax_id.to_token_from_root(&root)?;
    let parent_node = token.parent()?;
    let for_stat = LuaForStat::cast(parent_node)?;
    let iter_exprs = for_stat.get_iter_expr().collect::<Vec<_>>();
    let test_len_expr = match iter_exprs.len() {
        2 => {
            let LuaExpr::UnaryExpr(unary_expr) = iter_exprs[1].clone() else {
                return None;
            };
            unary_expr
        }
        3 => {
            let step_type = infer_expr_for_index(db, cache, iter_exprs[2].clone()).ok()?;
            let LuaType::IntegerConst(step_value) = step_type else {
                return None;
            };
            if step_value > 0 {
                let LuaExpr::UnaryExpr(unary_expr) = iter_exprs[1].clone() else {
                    return None;
                };
                unary_expr
            } else if step_value < 0 {
                let LuaExpr::UnaryExpr(unary_expr) = iter_exprs[0].clone() else {
                    return None;
                };
                unary_expr
            } else {
                return None;
            }
        }
        _ => return None,
    };

    let op = test_len_expr.get_op_token()?;
    if op.get_op() != UnaryOperator::OpLen {
        return None;
    }

    let len_expr = test_len_expr.get_expr()?;
    let len_expr_var_ref_id = get_var_expr_var_ref_id(db, cache, len_expr)?;
    let prefix_expr_var_ref_id = get_var_expr_var_ref_id(db, cache, prefix_expr)?;

    Some(len_expr_var_ref_id == prefix_expr_var_ref_id)
}

fn check_is_len(
    db: &DbIndex,
    cache: &mut LuaInferCache,
    unary_expr: &LuaUnaryExpr,
    prefix_expr: LuaExpr,
) -> Option<bool> {
    let op = unary_expr.get_op_token()?;
    if op.get_op() != UnaryOperator::OpLen {
        return None;
    }

    let inner_var_expr = unary_expr.get_expr()?;
    let len_expr_var_ref_id = get_var_expr_var_ref_id(db, cache, inner_var_expr)?;
    let prefix_expr_var_ref_id = get_var_expr_var_ref_id(db, cache, prefix_expr)?;

    Some(len_expr_var_ref_id == prefix_expr_var_ref_id)
}
