mod cache;
mod decl;
mod generic;
mod guard;
mod infer;
mod member;
mod overload_resolve;
mod reference;
mod semantic_info;
mod type_check;
mod visibility;

use std::collections::HashMap;
use std::sync::{Arc, Mutex, MutexGuard};

pub use cache::{CacheEntry, CacheOptions, LuaAnalysisPhase, LuaInferCache, PendingStrTplTypeDecl};
pub use decl::{enum_variable_is_param, parse_require_module_info};
use glua_parser::{
    LuaAstNode, LuaCallExpr, LuaChunk, LuaDocType, LuaExpr, LuaIndexExpr, LuaIndexKey,
    LuaIndexMemberExpr, LuaNameExpr, LuaParseError, LuaSyntaxKind, LuaSyntaxNode, LuaSyntaxToken,
    LuaTableExpr,
};
pub(crate) use infer::check_iter_var_range;
pub use infer::infer_index_expr;
pub(crate) use infer::resolve_decl_backed_global_path_member_type;
use infer::{infer_bind_value_type, infer_call_arg_expr_list_types, infer_expr_list_types};
pub use infer::{infer_table_field_value_should_be, infer_table_should_be};
use lsp_types::Uri;
pub use member::LuaMemberInfo;
pub use member::find_index_operations;
use member::find_member_origin_owner;
pub(crate) use member::find_members;
pub use member::get_member_map;
pub(crate) use member::infer_owner_raw_member_type_with_realm;
pub(crate) use member::member_key_matches_type;
pub(crate) use member::merge_open_table_types;
use reference::is_reference_to;
use rowan::{NodeOrToken, TextRange};
pub use semantic_info::SemanticInfo;
pub(crate) use semantic_info::{
    SemanticDeclGuard, infer_expr_semantic_decl, infer_node_semantic_decl, resolve_global_decl_id,
};
use semantic_info::{
    infer_node_semantic_info, infer_token_semantic_decl, infer_token_semantic_info,
};
pub(crate) use type_check::check_type_compact;
use type_check::is_sub_type_of;
pub use visibility::check_export_visibility;
use visibility::check_visibility;

pub use crate::semantic::member::{
    find_member_origin_owner_at_offset, find_members_with_key,
    find_members_with_key_in_workspace_for_file,
    find_members_with_key_in_workspace_for_file_at_offset,
};
use crate::semantic::type_check::check_type_compact_detail;
use crate::{Emmyrc, LuaDocument, LuaSemanticDeclId, ModuleInfo, db_index::LuaTypeDeclId};
use crate::{
    FileId,
    db_index::{DbIndex, LuaType},
};
use crate::{LuaFunctionType, LuaMemberId, LuaMemberKey, LuaTypeOwner};
pub use generic::*;
pub use guard::{InferGuard, InferGuardRef};
pub use infer::InferFailReason;
pub use infer::infer_call_expr_func;
pub(crate) use infer::infer_enclosing_self_type;
pub(crate) use infer::infer_expr;
pub(crate) use infer::remove_false_or_nil;
pub use infer::{SelfRefId, VarRefId, VarRefRootId};
pub(crate) use infer::{contains_gmod_null_type, get_var_expr_var_ref_id};
pub use infer::{infer_param, infer_param_with_cache};
use overload_resolve::resolve_signature;
pub use semantic_info::SemanticDeclLevel;
pub use type_check::{TypeCheckFailReason, TypeCheckResult};

pub use generic::get_keyof_members;
pub use infer::{DocTypeInferContext, infer_doc_type};

/// Recursively unwrap parenthesised expressions to extract the underlying
/// `LuaNameExpr`, if any. Returns `None` if the expression (after fully
/// unwrapping parens) is not a name expression.
pub(crate) fn unwrap_paren_to_name_expr(expr: &LuaExpr) -> Option<LuaNameExpr> {
    match expr {
        LuaExpr::NameExpr(name_expr) => Some(name_expr.clone()),
        LuaExpr::ParenExpr(paren_expr) => unwrap_paren_to_name_expr(&paren_expr.get_expr()?),
        _ => None,
    }
}

pub(crate) fn is_doc_tag_table_const(root: &LuaSyntaxNode, range: TextRange) -> bool {
    let mut node = match root.covering_element(range) {
        NodeOrToken::Node(node) => Some(node),
        NodeOrToken::Token(token) => token.parent(),
    };

    while let Some(current) = node {
        if !current.text_range().contains_range(range) {
            return false;
        }
        if current.text_range() == range && LuaDocType::can_cast(current.kind().into()) {
            return current.parent().is_some_and(|parent| {
                matches!(
                    parent.kind().into(),
                    LuaSyntaxKind::DocTagAs | LuaSyntaxKind::DocTagType
                )
            });
        }
        node = current.parent();
    }

    false
}

#[derive(Debug)]
pub struct LuaInferCacheCell {
    inner: Mutex<LuaInferCache>,
}

impl LuaInferCacheCell {
    fn new(cache: LuaInferCache) -> Self {
        Self {
            inner: Mutex::new(cache),
        }
    }

    pub fn borrow_mut(&self) -> MutexGuard<'_, LuaInferCache> {
        // Keep analysis available even if a previous holder panicked while mutating the cache.
        self.inner
            .lock()
            .unwrap_or_else(|poisoned| poisoned.into_inner())
    }
}

#[derive(Debug)]
pub struct SemanticModel<'a> {
    file_id: FileId,
    db: &'a DbIndex,
    infer_cache: LuaInferCacheCell,
    emmyrc: Arc<Emmyrc>,
    root: LuaChunk,
}

impl<'a> SemanticModel<'a> {
    pub fn new(
        file_id: FileId,
        db: &'a DbIndex,
        infer_config: LuaInferCache,
        emmyrc: Arc<Emmyrc>,
        root: LuaChunk,
    ) -> Self {
        Self {
            file_id,
            db,
            infer_cache: LuaInferCacheCell::new(infer_config),
            emmyrc,
            root,
        }
    }

    pub fn set_analysis_phase(&self, phase: LuaAnalysisPhase) {
        self.infer_cache.borrow_mut().set_phase(phase);
    }

    pub fn get_document(&'_ self) -> LuaDocument<'_> {
        self.db
            .get_vfs()
            .get_document(&self.file_id)
            .expect("always exists")
    }

    pub fn get_module(&self) -> Option<&ModuleInfo> {
        self.db.get_module_index().get_module(self.file_id)
    }

    pub fn get_document_by_file_id(&'_ self, file_id: FileId) -> Option<LuaDocument<'_>> {
        self.db.get_vfs().get_document(&file_id)
    }

    pub fn get_document_by_uri(&'_ self, uri: &Uri) -> Option<LuaDocument<'_>> {
        let file_id = self.db.get_vfs().get_file_id(uri)?;
        self.db.get_vfs().get_document(&file_id)
    }

    pub fn get_root_by_file_id(&self, file_id: FileId) -> Option<LuaChunk> {
        Some(
            self.db
                .get_vfs()
                .get_syntax_tree(&file_id)?
                .get_chunk_node(),
        )
    }

    pub fn get_file_parse_error(&self) -> Option<Vec<LuaParseError>> {
        self.db.get_vfs().get_file_parse_error(&self.file_id)
    }

    pub fn infer_expr(&self, expr: LuaExpr) -> Result<LuaType, InferFailReason> {
        infer_expr(self.db, &mut self.infer_cache.borrow_mut(), expr)
    }

    pub fn infer_table_should_be(&self, table: LuaTableExpr) -> Option<LuaType> {
        infer_table_should_be(self.db, &mut self.infer_cache.borrow_mut(), table).ok()
    }

    pub fn get_member_infos(&self, prefix_type: &LuaType) -> Option<Vec<LuaMemberInfo>> {
        let module_index = self.db.get_module_index();
        if let Some(workspace_id) = module_index.get_workspace_id(self.file_id) {
            return member::find_members_in_workspace_for_file(
                self.db,
                prefix_type,
                workspace_id,
                self.file_id,
            );
        }

        find_members(self.db, prefix_type)
    }

    pub fn get_member_infos_at_offset(
        &self,
        prefix_type: &LuaType,
        position_offset: rowan::TextSize,
    ) -> Option<Vec<LuaMemberInfo>> {
        let module_index = self.db.get_module_index();
        if let Some(workspace_id) = module_index.get_workspace_id(self.file_id) {
            return member::find_members_in_workspace_for_file_at_offset(
                self.db,
                prefix_type,
                workspace_id,
                self.file_id,
                position_offset,
            );
        }

        find_members(self.db, prefix_type)
    }

    pub fn get_member_info_with_key(
        &self,
        prefix_type: &LuaType,
        member_key: LuaMemberKey,
        find_all: bool,
    ) -> Option<Vec<LuaMemberInfo>> {
        let module_index = self.db.get_module_index();
        if let Some(workspace_id) = module_index.get_workspace_id(self.file_id) {
            return member::find_members_with_key_in_workspace_for_file(
                self.db,
                prefix_type,
                member_key,
                find_all,
                workspace_id,
                self.file_id,
            );
        }

        find_members_with_key(self.db, prefix_type, member_key, find_all)
    }

    pub fn get_member_info_with_key_at_offset(
        &self,
        prefix_type: &LuaType,
        member_key: LuaMemberKey,
        find_all: bool,
        position_offset: rowan::TextSize,
    ) -> Option<Vec<LuaMemberInfo>> {
        let module_index = self.db.get_module_index();
        if let Some(workspace_id) = module_index.get_workspace_id(self.file_id) {
            return member::find_members_with_key_in_workspace_for_file_at_offset(
                self.db,
                prefix_type,
                member_key,
                find_all,
                workspace_id,
                self.file_id,
                position_offset,
            );
        }

        find_members_with_key(self.db, prefix_type, member_key, find_all)
    }

    pub fn get_member_info_map(
        &self,
        prefix_type: &LuaType,
    ) -> Option<HashMap<LuaMemberKey, Vec<LuaMemberInfo>>> {
        let module_index = self.db.get_module_index();
        if let Some(workspace_id) = module_index.get_workspace_id(self.file_id) {
            return member::get_member_map_in_workspace_for_file(
                self.db,
                prefix_type,
                workspace_id,
                self.file_id,
            );
        }

        get_member_map(self.db, prefix_type)
    }

    pub fn get_member_info_map_at_offset(
        &self,
        prefix_type: &LuaType,
        position_offset: rowan::TextSize,
    ) -> Option<HashMap<LuaMemberKey, Vec<LuaMemberInfo>>> {
        let module_index = self.db.get_module_index();
        if let Some(workspace_id) = module_index.get_workspace_id(self.file_id) {
            return member::get_member_map_in_workspace_for_file_at_offset(
                self.db,
                prefix_type,
                workspace_id,
                self.file_id,
                position_offset,
            );
        }

        get_member_map(self.db, prefix_type)
    }

    pub fn type_check(&self, source: &LuaType, compact_type: &LuaType) -> TypeCheckResult {
        check_type_compact(self.db, source, compact_type)
    }

    pub fn type_check_detail(&self, source: &LuaType, compact_type: &LuaType) -> TypeCheckResult {
        check_type_compact_detail(self.db, source, compact_type)
    }

    pub fn infer_call_expr_func(
        &self,
        call_expr: LuaCallExpr,
        arg_count: Option<usize>,
    ) -> Option<Arc<LuaFunctionType>> {
        let prefix_expr = call_expr.get_prefix_expr()?;
        let mut cache = self.infer_cache.borrow_mut();
        let call_expr_type = infer_expr(self.db, &mut cache, prefix_expr).ok()?;
        infer_call_expr_func(
            self.db,
            &mut cache,
            call_expr,
            call_expr_type,
            &InferGuard::new(),
            arg_count,
        )
        .ok()
    }

    /// 推断表达式列表类型, 位于最后的表达式会触发多值推断
    pub fn infer_expr_list_types(
        &self,
        exprs: &[LuaExpr],
        var_count: Option<usize>,
    ) -> Vec<(LuaType, TextRange)> {
        let cache = &mut self.infer_cache.borrow_mut();
        infer_expr_list_types(self.db, cache, exprs, var_count, |db, cache, expr| {
            Ok(infer_expr(db, cache, expr).unwrap_or(LuaType::Unknown))
        })
        .unwrap_or_default()
    }

    pub fn infer_call_arg_expr_list_types(
        &self,
        call_expr: LuaCallExpr,
        var_count: Option<usize>,
    ) -> Vec<(LuaType, TextRange)> {
        infer_call_arg_expr_list_types(
            self.db,
            &mut self.infer_cache.borrow_mut(),
            call_expr,
            var_count,
        )
    }

    /// 推断值已经绑定的类型(不是推断值的类型). 例如从右值推断左值类型, 从调用参数推断函数参数类型
    pub fn infer_bind_value_type(&self, expr: LuaExpr) -> Option<LuaType> {
        infer_bind_value_type(self.db, &mut self.infer_cache.borrow_mut(), expr)
    }

    pub fn get_semantic_info(
        &self,
        node_or_token: NodeOrToken<LuaSyntaxNode, LuaSyntaxToken>,
    ) -> Option<SemanticInfo> {
        match node_or_token {
            NodeOrToken::Node(node) => {
                infer_node_semantic_info(self.db, &mut self.infer_cache.borrow_mut(), node)
            }
            NodeOrToken::Token(token) => {
                infer_token_semantic_info(self.db, &mut self.infer_cache.borrow_mut(), token)
            }
        }
    }

    pub fn find_decl(
        &self,
        node_or_token: NodeOrToken<LuaSyntaxNode, LuaSyntaxToken>,
        level: SemanticDeclLevel,
    ) -> Option<LuaSemanticDeclId> {
        match node_or_token {
            NodeOrToken::Node(node) => {
                infer_node_semantic_decl(self.db, &mut self.infer_cache.borrow_mut(), node, level)
            }
            NodeOrToken::Token(token) => {
                infer_token_semantic_decl(self.db, &mut self.infer_cache.borrow_mut(), token, level)
            }
        }
    }

    pub fn is_reference_to(
        &self,
        node: LuaSyntaxNode,
        semantic_decl_id: LuaSemanticDeclId,
        level: SemanticDeclLevel,
    ) -> bool {
        is_reference_to(
            self.db,
            &mut self.infer_cache.borrow_mut(),
            node,
            semantic_decl_id,
            level,
        )
        .unwrap_or(false)
    }

    pub fn is_semantic_visible(
        &self,
        token: LuaSyntaxToken,
        property_owner: LuaSemanticDeclId,
    ) -> bool {
        check_visibility(
            self.db,
            self.file_id,
            &self.emmyrc,
            &mut self.infer_cache.borrow_mut(),
            token,
            property_owner,
        )
        .unwrap_or(true)
    }

    pub fn is_sub_type_of(
        &self,
        sub_type_ref_id: &LuaTypeDeclId,
        super_type_ref_id: &LuaTypeDeclId,
    ) -> bool {
        is_sub_type_of(self.db, sub_type_ref_id, super_type_ref_id)
    }

    pub fn get_emmyrc(&self) -> &Emmyrc {
        &self.emmyrc
    }

    pub fn get_emmyrc_arc(&self) -> Arc<Emmyrc> {
        self.emmyrc.clone()
    }

    pub fn get_root(&self) -> &LuaChunk {
        &self.root
    }

    pub fn get_db(&self) -> &DbIndex {
        self.db
    }

    pub fn get_file_id(&self) -> FileId {
        self.file_id
    }

    pub fn get_cache(&self) -> &LuaInferCacheCell {
        &self.infer_cache
    }

    pub fn get_type(&self, type_owner: LuaTypeOwner) -> LuaType {
        self.db
            .get_type_index()
            .get_type_cache(&type_owner)
            .map(|cache| cache.as_type())
            .unwrap_or(&LuaType::Unknown)
            .clone()
    }

    pub fn get_member_key(&self, index_key: &LuaIndexKey) -> Option<LuaMemberKey> {
        LuaMemberKey::from_index_key(self.db, &mut self.infer_cache.borrow_mut(), index_key).ok()
    }

    pub fn infer_member_type(
        &self,
        prefix_type: &LuaType,
        member_key: &LuaMemberKey,
    ) -> Result<LuaType, InferFailReason> {
        member::infer_raw_member_type(self.db, prefix_type, member_key)
    }

    pub(crate) fn infer_index_member_type(
        &self,
        prefix_type: &LuaType,
        index_expr: &LuaIndexExpr,
    ) -> Result<LuaType, InferFailReason> {
        infer::infer_member_by_member_key(
            self.db,
            &mut self.infer_cache.borrow_mut(),
            prefix_type,
            LuaIndexMemberExpr::IndexExpr(index_expr.clone()),
            &InferGuard::new(),
        )
    }

    pub fn get_member_origin_owner(&self, member_id: LuaMemberId) -> Option<LuaSemanticDeclId> {
        find_member_origin_owner(self.db, &mut self.infer_cache.borrow_mut(), member_id)
    }

    pub fn get_member_origin_owner_at_offset(
        &self,
        member_id: LuaMemberId,
        position_offset: rowan::TextSize,
    ) -> Option<LuaSemanticDeclId> {
        find_member_origin_owner_at_offset(
            self.db,
            &mut self.infer_cache.borrow_mut(),
            member_id,
            position_offset,
        )
    }

    pub fn get_index_decl_type(&self, index_expr: LuaIndexExpr) -> Option<LuaType> {
        let cache = &mut self.infer_cache.borrow_mut();
        infer_index_expr(self.db, cache, index_expr, false).ok()
    }
}
