//! File symbol extraction.

use base_db::FxIndexSet;
use either::Either;
use hir_def::{
    AdtId, AssocItemId, Complete, DefWithBodyId, ExternCrateId, HasModule, ImplId, Lookup, MacroId,
    ModuleDefId, ModuleId, TraitId,
    db::DefDatabase,
    item_scope::{ImportId, ImportOrExternCrate, ImportOrGlob},
    per_ns::Item,
    src::{HasChildSource, HasSource},
    visibility::{Visibility, VisibilityExplicitness},
};
use hir_expand::{HirFileId, name::Name};
use hir_ty::{
    db::HirDatabase,
    display::{HirDisplay, hir_display_with_store},
};
use intern::Symbol;
use rustc_hash::FxHashMap;
use syntax::{AstNode, AstPtr, SmolStr, SyntaxNode, SyntaxNodePtr, ToSmolStr, ast::HasName};

use crate::{HasCrate, Module, ModuleDef, Semantics};

/// The actual data that is stored in the index. It should be as compact as
/// possible.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FileSymbol {
    pub name: Symbol,
    pub def: ModuleDef,
    pub loc: DeclarationLocation,
    pub container_name: Option<SmolStr>,
    /// Whether this symbol is a doc alias for the original symbol.
    pub is_alias: bool,
    pub is_assoc: bool,
    pub is_import: bool,
    pub do_not_complete: Complete,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DeclarationLocation {
    /// The file id for both the `ptr` and `name_ptr`.
    pub hir_file_id: HirFileId,
    /// This points to the whole syntax node of the declaration.
    pub ptr: SyntaxNodePtr,
    /// This points to the [`syntax::ast::Name`] identifier of the declaration.
    pub name_ptr: AstPtr<Either<syntax::ast::Name, syntax::ast::NameRef>>,
}

impl DeclarationLocation {
    pub fn syntax<DB: HirDatabase>(&self, sema: &Semantics<'_, DB>) -> SyntaxNode {
        let root = sema.parse_or_expand(self.hir_file_id);
        self.ptr.to_node(&root)
    }
}

/// Represents an outstanding module that the symbol collector must collect symbols from.
#[derive(Debug)]
struct SymbolCollectorWork {
    module_id: ModuleId,
    parent: Option<Name>,
}

pub struct SymbolCollector<'a> {
    db: &'a dyn HirDatabase,
    symbols: FxIndexSet<FileSymbol>,
    work: Vec<SymbolCollectorWork>,
    current_container_name: Option<SmolStr>,
}

/// Given a [`ModuleId`] and a [`HirDatabase`], use the DefMap for the module's crate to collect
/// all symbols that should be indexed for the given module.
impl<'a> SymbolCollector<'a> {
    pub fn new(db: &'a dyn HirDatabase) -> Self {
        SymbolCollector {
            db,
            symbols: Default::default(),
            work: Default::default(),
            current_container_name: None,
        }
    }

    pub fn new_module(db: &dyn HirDatabase, module: Module) -> Box<[FileSymbol]> {
        let mut symbol_collector = SymbolCollector::new(db);
        symbol_collector.collect(module);
        symbol_collector.finish()
    }

    pub fn collect(&mut self, module: Module) {
        let _p = tracing::info_span!("SymbolCollector::collect", ?module).entered();
        tracing::info!(?module, "SymbolCollector::collect");

        // The initial work is the root module we're collecting, additional work will
        // be populated as we traverse the module's definitions.
        self.work.push(SymbolCollectorWork { module_id: module.into(), parent: None });

        while let Some(work) = self.work.pop() {
            self.do_work(work);
        }
    }

    pub fn finish(self) -> Box<[FileSymbol]> {
        self.symbols.into_iter().collect()
    }

    fn do_work(&mut self, work: SymbolCollectorWork) {
        let _p = tracing::info_span!("SymbolCollector::do_work", ?work).entered();
        tracing::info!(?work, "SymbolCollector::do_work");
        self.db.unwind_if_revision_cancelled();

        let parent_name = work.parent.map(|name| name.as_str().to_smolstr());
        self.with_container_name(parent_name, |s| s.collect_from_module(work.module_id));
    }

    fn collect_from_module(&mut self, module_id: ModuleId) {
        let push_decl = |this: &mut Self, def, name| {
            match def {
                ModuleDefId::ModuleId(id) => this.push_module(id, name),
                ModuleDefId::FunctionId(id) => {
                    this.push_decl(id, name, false, None);
                    this.collect_from_body(id, Some(name.clone()));
                }
                ModuleDefId::AdtId(AdtId::StructId(id)) => {
                    this.push_decl(id, name, false, None);
                }
                ModuleDefId::AdtId(AdtId::EnumId(id)) => {
                    this.push_decl(id, name, false, None);
                    let enum_name = this.db.enum_signature(id).name.as_str().to_smolstr();
                    this.with_container_name(Some(enum_name), |this| {
                        let variants = id.enum_variants(this.db);
                        for (variant_id, variant_name, _) in &variants.variants {
                            this.push_decl(*variant_id, variant_name, true, None);
                        }
                    });
                }
                ModuleDefId::AdtId(AdtId::UnionId(id)) => {
                    this.push_decl(id, name, false, None);
                }
                ModuleDefId::ConstId(id) => {
                    this.push_decl(id, name, false, None);
                    this.collect_from_body(id, Some(name.clone()));
                }
                ModuleDefId::StaticId(id) => {
                    this.push_decl(id, name, false, None);
                    this.collect_from_body(id, Some(name.clone()));
                }
                ModuleDefId::TraitId(id) => {
                    let trait_do_not_complete = this.push_decl(id, name, false, None);
                    this.collect_from_trait(id, trait_do_not_complete);
                }
                ModuleDefId::TypeAliasId(id) => {
                    this.push_decl(id, name, false, None);
                }
                ModuleDefId::MacroId(id) => {
                    match id {
                        MacroId::Macro2Id(id) => this.push_decl(id, name, false, None),
                        MacroId::MacroRulesId(id) => this.push_decl(id, name, false, None),
                        MacroId::ProcMacroId(id) => this.push_decl(id, name, false, None),
                    };
                }
                // Don't index these.
                ModuleDefId::BuiltinType(_) => {}
                ModuleDefId::EnumVariantId(_) => {}
            }
        };

        // Nested trees are very common, so a cache here will hit a lot.
        let import_child_source_cache = &mut FxHashMap::default();

        let is_explicit_import = |vis| match vis {
            Visibility::Public => true,
            Visibility::PubCrate(_) => true,
            Visibility::Module(_, VisibilityExplicitness::Explicit) => true,
            Visibility::Module(_, VisibilityExplicitness::Implicit) => false,
        };

        let mut push_import = |this: &mut Self, i: ImportId, name: &Name, def: ModuleDefId, vis| {
            let source = import_child_source_cache
                .entry(i.use_)
                .or_insert_with(|| i.use_.child_source(this.db));
            let Some(use_tree_src) = source.value.get(i.idx) else { return };
            let rename = use_tree_src.rename().and_then(|rename| rename.name());
            let name_syntax = match rename {
                Some(name) => Some(Either::Left(name)),
                None if is_explicit_import(vis) => {
                    (|| use_tree_src.path()?.segment()?.name_ref().map(Either::Right))()
                }
                None => None,
            };
            let Some(name_syntax) = name_syntax else {
                return;
            };
            let dec_loc = DeclarationLocation {
                hir_file_id: source.file_id,
                ptr: SyntaxNodePtr::new(use_tree_src.syntax()),
                name_ptr: AstPtr::new(&name_syntax),
            };
            this.symbols.insert(FileSymbol {
                name: name.symbol().clone(),
                def: def.into(),
                container_name: this.current_container_name.clone(),
                loc: dec_loc,
                is_alias: false,
                is_assoc: false,
                is_import: true,
                do_not_complete: Complete::Yes,
            });
        };

        let push_extern_crate =
            |this: &mut Self, i: ExternCrateId, name: &Name, def: ModuleDefId, vis| {
                let loc = i.lookup(this.db);
                let source = loc.source(this.db);
                let rename = source.value.rename().and_then(|rename| rename.name());

                let name_syntax = match rename {
                    Some(name) => Some(Either::Left(name)),
                    None if is_explicit_import(vis) => None,
                    None => source.value.name_ref().map(Either::Right),
                };
                let Some(name_syntax) = name_syntax else {
                    return;
                };
                let dec_loc = DeclarationLocation {
                    hir_file_id: source.file_id,
                    ptr: SyntaxNodePtr::new(source.value.syntax()),
                    name_ptr: AstPtr::new(&name_syntax),
                };
                this.symbols.insert(FileSymbol {
                    name: name.symbol().clone(),
                    def: def.into(),
                    container_name: this.current_container_name.clone(),
                    loc: dec_loc,
                    is_alias: false,
                    is_assoc: false,
                    is_import: false,
                    do_not_complete: Complete::Yes,
                });
            };

        let def_map = module_id.def_map(self.db);
        let scope = &def_map[module_id.local_id].scope;

        for impl_id in scope.impls() {
            self.collect_from_impl(impl_id);
        }

        for (name, Item { def, vis, import }) in scope.types() {
            if let Some(i) = import {
                match i {
                    ImportOrExternCrate::Import(i) => push_import(self, i, name, def, vis),
                    ImportOrExternCrate::Glob(_) => (),
                    ImportOrExternCrate::ExternCrate(i) => {
                        push_extern_crate(self, i, name, def, vis)
                    }
                }

                continue;
            }
            // self is a declaration
            push_decl(self, def, name)
        }

        for (name, Item { def, vis, import }) in scope.macros() {
            if let Some(i) = import {
                match i {
                    ImportOrExternCrate::Import(i) => push_import(self, i, name, def.into(), vis),
                    ImportOrExternCrate::Glob(_) => (),
                    ImportOrExternCrate::ExternCrate(_) => (),
                }
                continue;
            }
            // self is a declaration
            push_decl(self, def.into(), name)
        }

        for (name, Item { def, vis, import }) in scope.values() {
            if let Some(i) = import {
                match i {
                    ImportOrGlob::Import(i) => push_import(self, i, name, def, vis),
                    ImportOrGlob::Glob(_) => (),
                }
                continue;
            }
            // self is a declaration
            push_decl(self, def, name)
        }

        for const_id in scope.unnamed_consts() {
            self.collect_from_body(const_id, None);
        }

        for (name, id) in scope.legacy_macros() {
            for &id in id {
                if id.module(self.db) == module_id {
                    match id {
                        MacroId::Macro2Id(id) => self.push_decl(id, name, false, None),
                        MacroId::MacroRulesId(id) => self.push_decl(id, name, false, None),
                        MacroId::ProcMacroId(id) => self.push_decl(id, name, false, None),
                    };
                }
            }
        }
    }

    fn collect_from_body(&mut self, body_id: impl Into<DefWithBodyId>, name: Option<Name>) {
        let body_id = body_id.into();
        let body = self.db.body(body_id);

        // Descend into the blocks and enqueue collection of all modules within.
        for (_, def_map) in body.blocks(self.db) {
            for (id, _) in def_map.modules() {
                self.work.push(SymbolCollectorWork {
                    module_id: def_map.module_id(id),
                    parent: name.clone(),
                });
            }
        }
    }

    fn collect_from_impl(&mut self, impl_id: ImplId) {
        let impl_data = self.db.impl_signature(impl_id);
        let impl_name = Some(
            hir_display_with_store(impl_data.self_ty, &impl_data.store)
                .display(
                    self.db,
                    crate::Impl::from(impl_id).krate(self.db).to_display_target(self.db),
                )
                .to_smolstr(),
        );
        self.with_container_name(impl_name, |s| {
            for &(ref name, assoc_item_id) in &impl_id.impl_items(self.db).items {
                s.push_assoc_item(assoc_item_id, name, None)
            }
        })
    }

    fn collect_from_trait(&mut self, trait_id: TraitId, trait_do_not_complete: Complete) {
        let trait_data = self.db.trait_signature(trait_id);
        self.with_container_name(Some(trait_data.name.as_str().into()), |s| {
            for &(ref name, assoc_item_id) in &trait_id.trait_items(self.db).items {
                s.push_assoc_item(assoc_item_id, name, Some(trait_do_not_complete));
            }
        });
    }

    fn with_container_name(&mut self, container_name: Option<SmolStr>, f: impl FnOnce(&mut Self)) {
        if let Some(container_name) = container_name {
            let prev = self.current_container_name.replace(container_name);
            f(self);
            self.current_container_name = prev;
        } else {
            f(self);
        }
    }

    fn push_assoc_item(
        &mut self,
        assoc_item_id: AssocItemId,
        name: &Name,
        trait_do_not_complete: Option<Complete>,
    ) {
        match assoc_item_id {
            AssocItemId::FunctionId(id) => self.push_decl(id, name, true, trait_do_not_complete),
            AssocItemId::ConstId(id) => self.push_decl(id, name, true, trait_do_not_complete),
            AssocItemId::TypeAliasId(id) => self.push_decl(id, name, true, trait_do_not_complete),
        };
    }

    fn push_decl<L>(
        &mut self,
        id: L,
        name: &Name,
        is_assoc: bool,
        trait_do_not_complete: Option<Complete>,
    ) -> Complete
    where
        L: Lookup<Database = dyn DefDatabase> + Into<ModuleDefId>,
        <L as Lookup>::Data: HasSource,
        <<L as Lookup>::Data as HasSource>::Value: HasName,
    {
        let loc = id.lookup(self.db);
        let source = loc.source(self.db);
        let Some(name_node) = source.value.name() else { return Complete::Yes };
        let def = ModuleDef::from(id.into());
        let dec_loc = DeclarationLocation {
            hir_file_id: source.file_id,
            ptr: SyntaxNodePtr::new(source.value.syntax()),
            name_ptr: AstPtr::new(&name_node).wrap_left(),
        };

        let mut do_not_complete = Complete::Yes;

        if let Some(attrs) = def.attrs(self.db) {
            do_not_complete = Complete::extract(matches!(def, ModuleDef::Trait(_)), &attrs);
            if let Some(trait_do_not_complete) = trait_do_not_complete {
                do_not_complete = Complete::for_trait_item(trait_do_not_complete, do_not_complete);
            }

            for alias in attrs.doc_aliases() {
                self.symbols.insert(FileSymbol {
                    name: alias.clone(),
                    def,
                    loc: dec_loc.clone(),
                    container_name: self.current_container_name.clone(),
                    is_alias: true,
                    is_assoc,
                    is_import: false,
                    do_not_complete,
                });
            }
        }

        self.symbols.insert(FileSymbol {
            name: name.symbol().clone(),
            def,
            container_name: self.current_container_name.clone(),
            loc: dec_loc,
            is_alias: false,
            is_assoc,
            is_import: false,
            do_not_complete,
        });

        do_not_complete
    }

    fn push_module(&mut self, module_id: ModuleId, name: &Name) {
        let def_map = module_id.def_map(self.db);
        let module_data = &def_map[module_id.local_id];
        let Some(declaration) = module_data.origin.declaration() else { return };
        let module = declaration.to_node(self.db);
        let Some(name_node) = module.name() else { return };
        let dec_loc = DeclarationLocation {
            hir_file_id: declaration.file_id,
            ptr: SyntaxNodePtr::new(module.syntax()),
            name_ptr: AstPtr::new(&name_node).wrap_left(),
        };

        let def = ModuleDef::Module(module_id.into());

        let mut do_not_complete = Complete::Yes;
        if let Some(attrs) = def.attrs(self.db) {
            do_not_complete = Complete::extract(matches!(def, ModuleDef::Trait(_)), &attrs);

            for alias in attrs.doc_aliases() {
                self.symbols.insert(FileSymbol {
                    name: alias.clone(),
                    def,
                    loc: dec_loc.clone(),
                    container_name: self.current_container_name.clone(),
                    is_alias: true,
                    is_assoc: false,
                    is_import: false,
                    do_not_complete,
                });
            }
        }

        self.symbols.insert(FileSymbol {
            name: name.symbol().clone(),
            def: ModuleDef::Module(module_id.into()),
            container_name: self.current_container_name.clone(),
            loc: dec_loc,
            is_alias: false,
            is_assoc: false,
            is_import: false,
            do_not_complete,
        });
    }
}
