mod condition_flow;
mod get_type_at_cast_flow;
mod get_type_at_flow;
mod narrow_type;
mod var_ref_id;

use crate::{
    CacheEntry, DbIndex, FlowAntecedent, FlowId, FlowNode, FlowNodeKind, FlowTree, InferFailReason,
    LuaInferCache, LuaType, TypeOps,
    db_index::LuaTypeDeclId,
    get_real_type, infer_param_with_cache,
    semantic::infer::{
        InferResult,
        infer_name::{find_decl_member_type, infer_global_type},
    },
};
pub use get_type_at_cast_flow::get_type_at_call_expr_inline_cast;
use glua_parser::{LuaAstNode, LuaChunk, LuaExpr};
pub use narrow_type::{narrow_down_type, narrow_false_or_nil, remove_false_or_nil};
pub use var_ref_id::{SelfRefId, VarRefId, VarRefRootId, get_var_expr_var_ref_id};

const GMOD_NULL_TYPE_NAME: &str = "NULL";

fn gmod_null_decl_id() -> LuaTypeDeclId {
    LuaTypeDeclId::global(GMOD_NULL_TYPE_NAME)
}

pub(crate) fn gmod_null_type() -> LuaType {
    LuaType::Ref(gmod_null_decl_id())
}

pub(crate) fn gmod_null_type_is_defined(db: &DbIndex) -> bool {
    if !db.get_emmyrc().gmod.enabled {
        return false;
    }

    db.get_type_index()
        .get_type_decl(&gmod_null_decl_id())
        .is_some()
}

pub(crate) fn contains_gmod_null_type(db: &DbIndex, typ: &LuaType) -> bool {
    if !db.get_emmyrc().gmod.enabled {
        return false;
    }

    let real_type = get_real_type(db, typ).unwrap_or(typ);
    match real_type {
        LuaType::Ref(type_id) | LuaType::Def(type_id) => type_id == &gmod_null_decl_id(),
        LuaType::Union(union_type) => union_type
            .into_vec()
            .iter()
            .any(|member| contains_gmod_null_type(db, member)),
        LuaType::MultiLineUnion(multi_union) => multi_union
            .get_unions()
            .iter()
            .any(|(member, _)| contains_gmod_null_type(db, member)),
        LuaType::Instance(instance_type) => contains_gmod_null_type(db, instance_type.get_base()),
        _ => false,
    }
}

pub(crate) fn remove_gmod_null_type(db: &DbIndex, typ: LuaType) -> LuaType {
    if !db.get_emmyrc().gmod.enabled {
        return typ;
    }

    if is_gmod_null_type(db, &typ) {
        return LuaType::Never;
    }

    let removed_ref = TypeOps::Remove.apply(db, &typ, &gmod_null_type());
    let null_def = LuaType::Def(gmod_null_decl_id());
    TypeOps::Remove.apply(db, &removed_ref, &null_def)
}

fn is_gmod_null_type(db: &DbIndex, typ: &LuaType) -> bool {
    if !db.get_emmyrc().gmod.enabled {
        return false;
    }

    let real_type = get_real_type(db, typ).unwrap_or(typ);
    match real_type {
        LuaType::Ref(type_id) | LuaType::Def(type_id) => type_id == &gmod_null_decl_id(),
        LuaType::Instance(instance_type) => is_gmod_null_type(db, instance_type.get_base()),
        _ => false,
    }
}

pub fn infer_expr_narrow_type(
    db: &DbIndex,
    cache: &mut LuaInferCache,
    expr: LuaExpr,
    var_ref_id: VarRefId,
) -> InferResult {
    let file_id = cache.get_file_id();
    let Some(flow_tree) = db.get_flow_index().get_flow_tree(&file_id) else {
        return get_var_ref_type(db, cache, &var_ref_id);
    };

    let Some(flow_id) = flow_tree.get_flow_id(expr.get_syntax_id()) else {
        return get_var_ref_type(db, cache, &var_ref_id);
    };

    let root = LuaChunk::cast(expr.get_root()).ok_or(InferFailReason::None)?;
    let query_realm = db
        .get_gmod_infer_index()
        .get_realm_at_offset(&file_id, expr.get_position());
    let previous_query_realm = cache.flow_query_realm.replace(query_realm);
    let result =
        get_type_at_flow::get_type_at_flow(db, flow_tree, cache, &root, &var_ref_id, flow_id);
    cache.flow_query_realm = previous_query_realm;
    result
}

/// Like [`infer_expr_narrow_type`], but seeds the flow origin with a
/// region-aware base type for an implicit `self` reference.
///
/// `self_base` is the receiver type inferred at the colon-method prefix
/// position (region-aware). When the flow walk reaches the origin for this
/// exact `var_ref_id`, the seed is used as the base instead of the
/// position-insensitive receiver decl/member cache. This keeps `self` flowing
/// through the normal narrowing pipeline (so guards still narrow) while fixing
/// reused-local region collapse.
///
/// When `self_base` is `None`, this is identical to [`infer_expr_narrow_type`].
pub fn infer_expr_narrow_type_with_self_base(
    db: &DbIndex,
    cache: &mut LuaInferCache,
    expr: LuaExpr,
    var_ref_id: VarRefId,
    self_base: Option<LuaType>,
) -> InferResult {
    let Some(self_base) = self_base else {
        return infer_expr_narrow_type(db, cache, expr, var_ref_id);
    };

    let previous_seed = cache
        .self_base_seed
        .replace((var_ref_id.clone(), self_base));
    let result = infer_expr_narrow_type(db, cache, expr, var_ref_id);
    cache.self_base_seed = previous_seed;
    result
}

pub fn get_var_ref_type(
    db: &DbIndex,
    cache: &mut LuaInferCache,
    var_ref_id: &VarRefId,
) -> InferResult {
    // Region-aware `self` base: if a seed was set for this exact `self`
    // reference, use it as the flow origin type instead of resolving the
    // (position-insensitive) receiver decl/member cache. See
    // `infer_expr_narrow_type_with_self_base`.
    if var_ref_id.is_self_ref()
        && let Some((seed_ref, seed_type)) = cache.self_base_seed.as_ref()
        && seed_ref == var_ref_id
    {
        return Ok(seed_type.clone());
    }

    if let Some(decl_id) = var_ref_id.get_decl_id_ref() {
        let decl = db
            .get_decl_index()
            .get_decl(&decl_id)
            .ok_or(InferFailReason::None)?;

        // Parameter declarations carry their canonical type in signature metadata.
        // Flow/assignment analysis may also create a decl type cache entry for params,
        // but that inferred cache must not replace the declared parameter type.
        if decl.is_param() {
            if let Ok(param_type) = infer_param_with_cache(db, cache, decl) {
                return Ok(param_type);
            }

            if let Some(type_cache) = db.get_type_index().get_type_cache(&decl.get_id().into()) {
                return Ok(type_cache.as_type().clone());
            }

            return Err(InferFailReason::UnResolveDeclType(decl.get_id()));
        }

        if decl.is_global() {
            if decl.get_name() == GMOD_NULL_TYPE_NAME && gmod_null_type_is_defined(db) {
                return Ok(gmod_null_type());
            }

            if let Some(type_cache) = db.get_type_index().get_type_cache(&decl.get_id().into()) {
                let typ = type_cache.as_type();
                return if typ.contain_tpl() {
                    Ok(LuaType::Unknown)
                } else {
                    Ok(typ.clone())
                };
            }

            let name = decl.get_name();
            return infer_global_type(db, Some(cache.get_file_id()), None, name);
        }

        if let Some(type_cache) = db.get_type_index().get_type_cache(&decl.get_id().into()) {
            let result = type_cache.as_type().clone();
            // 不要在此阶段展开泛型别名, 必须让后续的泛型匹配阶段基于声明形态完成推断
            return Ok(result);
        }

        // Collect unique UnResolveDeclType decl names for profiling
        cache.prof_unresolve_decl_sample_count += 1;
        if cache.prof_unresolve_decl_names.len() < 30 {
            let name = decl.get_name().to_string();
            let key = format!("{}:{}", name, u32::from(decl.get_id().position));
            if !cache.prof_unresolve_decl_names.contains(&key) {
                cache.prof_unresolve_decl_names.push(key);
            }
        }
        // Track per-decl-id counts
        *cache
            .prof_unresolve_decl_ids
            .entry(u32::from(decl.get_id().position))
            .or_insert(0) += 1;

        Err(InferFailReason::UnResolveDeclType(decl.get_id()))
    } else if let Some(member_id) = var_ref_id.get_member_id_ref() {
        find_decl_member_type(db, member_id)
    } else if let VarRefId::GlobalName(name, position) = var_ref_id {
        Ok(infer_global_type(
            db,
            Some(cache.get_file_id()),
            Some(*position),
            name.as_str(),
        )
        .unwrap_or(LuaType::Unknown))
    } else {
        if let Some(type_cache) = cache.get_index_ref_origin_type_cache(var_ref_id)
            && let CacheEntry::Cache(ty) = type_cache
        {
            return Ok(ty.clone());
        }

        Err(InferFailReason::None)
    }
}

fn get_single_antecedent(tree: &FlowTree, flow: &FlowNode) -> Result<FlowId, InferFailReason> {
    match &flow.antecedent {
        Some(antecedent) => match antecedent {
            FlowAntecedent::Single(id) => Ok(*id),
            FlowAntecedent::Multiple(multi_id) => {
                let multi_flow = tree
                    .get_multi_antecedents(*multi_id)
                    .ok_or(InferFailReason::None)?;
                if !multi_flow.is_empty() {
                    if let Some(preferred) = multi_flow.iter().copied().find(|flow_id| {
                        tree.get_flow_node(*flow_id).is_some_and(|flow_node| {
                            !matches!(
                                flow_node.kind,
                                FlowNodeKind::Unreachable
                                    | FlowNodeKind::Return
                                    | FlowNodeKind::Break
                            )
                        })
                    }) {
                        Ok(preferred)
                    } else {
                        Ok(multi_flow[0])
                    }
                } else {
                    Err(InferFailReason::None)
                }
            }
        },
        None => Err(InferFailReason::None),
    }
}

#[derive(Debug)]
pub enum ResultTypeOrContinue {
    Result(LuaType),
    Continue,
}
