// Copyright © 2024-2025 The µcad authors <info@ucad.xyz>
// SPDX-License-Identifier: AGPL-3.0-or-later

use derive_more::{Deref, DerefMut};
use indexmap::IndexSet;

use crate::{resolve::*, syntax::*};

/// *Symbol table* holding global symbols.
#[derive(Default, Deref, DerefMut)]
pub struct SymbolTable(SymbolMap);

impl SymbolTable {
    /// Add a new symbol to the table
    pub fn add_symbol(&mut self, symbol: Symbol) -> ResolveResult<()> {
        self.insert_symbol(symbol.id(), symbol.clone())
    }

    /// Add a new symbol to the table
    pub fn insert_symbol(&mut self, id: Identifier, symbol: Symbol) -> ResolveResult<()> {
        log::trace!("insert symbol: {id}");
        if let Some(symbol) = self.insert(id, symbol.clone()) {
            Err(ResolveError::SymbolAlreadyDefined(symbol.full_name()))
        } else {
            Ok(())
        }
    }

    pub(super) fn symbols(&self) -> Symbols {
        self.values().cloned().collect()
    }

    /// Return a list of symbols which could not or have not been checked.
    pub fn unchecked(&self) -> Symbols {
        self.recursive_collect(|symbol| symbol.is_checked())
    }

    /// Return a list of unused private symbols
    ///
    /// Use this after eval for any useful result.
    pub fn unused_private(&self) -> Symbols {
        let used_in_module = &mut IndexSet::new();
        let mut symbols = self.recursive_collect(|symbol| {
            if let Some(in_module) = symbol.in_module()
                && symbol.is_used()
            {
                used_in_module.insert(in_module);
            }
            symbol.is_unused_private()
        });

        symbols.retain(|symbol| {
            if let Some(in_module) = symbol.in_module() {
                !used_in_module.contains(&in_module)
            } else {
                true
            }
        });
        symbols.sort_by_key(|s| s.full_name());
        symbols
    }

    /// Search all ids which require target mode (e.g. `assert_valid`)
    pub(super) fn search_target_mode_ids(&self) -> IdentifierSet {
        self.recursive_collect(|symbol| symbol.is_target_mode())
            .iter()
            .map(|symbol| symbol.id())
            .collect()
    }

    pub(super) fn recursive_collect<F>(&self, mut f: F) -> Symbols
    where
        F: FnMut(&Symbol) -> bool,
    {
        let mut result = vec![];
        self.values().for_each(|symbol| {
            symbol.recursive_collect(&mut f, &mut result);
        });
        result.into()
    }

    #[allow(dead_code)]
    pub(super) fn recursive_for_each<F>(&self, f: F)
    where
        F: Fn(&Identifier, &Symbol),
    {
        self.iter().for_each(|(id, symbol)| {
            symbol.recursive_for_each(id, &f);
        });
    }

    pub(super) fn recursive_for_each_mut<F>(&mut self, f: F)
    where
        F: Fn(&Identifier, &mut Symbol),
    {
        self.iter_mut().for_each(|(id, symbol)| {
            symbol.recursive_for_each_mut(id, &f);
        });
    }

    /// Search a *symbol* by it's *qualified name* **and** within a *symbol* given by name.
    ///
    /// If both are found
    /// # Arguments
    /// - `name`: *qualified name* to search for.
    /// - `within`: Searches in the *symbol* with this name too.
    /// - `target`: What to search for
    pub(crate) fn lookup_within_name(
        &self,
        name: &QualifiedName,
        within: &QualifiedName,
        target: LookupTarget,
    ) -> ResolveResult<Symbol> {
        self.lookup_within(name, &self.search(within, false)?, target)
    }
}

impl WriteToFile for SymbolTable {}

impl Lookup for SymbolTable {
    /// Lookup a symbol from global symbols.
    fn lookup(&self, name: &QualifiedName, target: LookupTarget) -> ResolveResult<Symbol> {
        log::trace!(
            "{lookup} for global symbol '{name:?}'",
            lookup = crate::mark!(LOOKUP)
        );
        self.deny_super(name)?;

        let symbol = match self.search(name, true) {
            Ok(symbol) => {
                if target.matches(&symbol) {
                    symbol
                } else {
                    log::trace!(
                        "{not_found} global symbol: {name:?}",
                        not_found = crate::mark!(NOT_FOUND_INTERIM),
                    );
                    return Err(ResolveError::WrongTarget);
                }
            }
            Err(err) => {
                log::trace!(
                    "{not_found} global symbol: {name:?}",
                    not_found = crate::mark!(NOT_FOUND_INTERIM),
                );
                return Err(err)?;
            }
        };
        symbol.set_check();
        log::trace!(
            "{found} global symbol: {symbol:?}",
            found = crate::mark!(FOUND_INTERIM),
        );
        Ok(symbol)
    }

    fn ambiguity_error(ambiguous: QualifiedName, others: QualifiedNames) -> ResolveError {
        ResolveError::AmbiguousSymbol(ambiguous, others)
    }
}

impl std::fmt::Display for SymbolTable {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(
            f,
            "{}",
            self.iter()
                .map(|(_, symbol)| symbol)
                .filter(|symbol| !symbol.is_deleted())
                .map(|symbol| symbol.full_name().to_string())
                .collect::<Vec<_>>()
                .join(", ")
        )
    }
}

impl std::fmt::Debug for SymbolTable {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "{:?}", self.0)
    }
}
