use std::collections::HashSet;

use indexmap::IndexSet;
use rue_diagnostic::DiagnosticKind;
use rue_hir::{
    BindingSymbol, ConstantSymbol, Declaration, FunctionKind, FunctionSymbol, Items,
    ParameterSymbol, Symbol,
};
use rue_types::{Alias, Generic, Struct, Type};

use crate::Compiler;

pub fn check_unused(ctx: &mut Compiler, entrypoints: &HashSet<Declaration>) {
    let mut used_declarations = IndexSet::new();
    let mut unused_declarations = IndexSet::new();

    for declaration in ctx.relevant_declarations().collect::<Vec<_>>() {
        let Declaration::Symbol(symbol) = declaration else {
            continue;
        };

        if !ctx.declaration_parents(declaration).is_empty()
            && !ctx.reference_parents(declaration).is_empty()
        {
            used_declarations.insert(declaration);
            continue;
        }

        let mut visited = HashSet::new();
        let mut stack = vec![declaration];

        while let Some(current) = stack.pop() {
            if !visited.insert(current) {
                if let Declaration::Symbol(current_symbol) = current
                    && current_symbol == symbol
                    && let Some(name) = ctx.symbol(symbol).name()
                {
                    if let Symbol::Function(FunctionSymbol {
                        kind: FunctionKind::Inline,
                        ..
                    }) = ctx.symbol(symbol).clone()
                    {
                        ctx.diagnostic_name(
                            &name,
                            DiagnosticKind::RecursiveInlineFunction(name.text().to_string()),
                        );
                    } else if let Symbol::Constant(ConstantSymbol { .. }) =
                        ctx.symbol(symbol).clone()
                    {
                        ctx.diagnostic_name(
                            &name,
                            DiagnosticKind::RecursiveConstant(name.text().to_string()),
                        );
                    }
                }

                continue;
            }

            for parent in ctx.reference_parents(current) {
                stack.push(parent);

                match parent {
                    Declaration::Symbol(parent_symbol) => {
                        if entrypoints.contains(&Declaration::Symbol(parent_symbol)) {
                            used_declarations.insert(declaration);
                        }
                    }
                    Declaration::Type(parent_ty) => {
                        if entrypoints.contains(&Declaration::Type(parent_ty)) {
                            used_declarations.insert(declaration);
                        }
                    }
                }
            }
        }

        if entrypoints.contains(&declaration) {
            used_declarations.insert(declaration);
            continue;
        }

        if !used_declarations.contains(&declaration) {
            unused_declarations.insert(declaration);
        }
    }

    for declaration in ctx.relevant_declarations() {
        if !matches!(declaration, Declaration::Type(_)) {
            continue;
        }

        if entrypoints.contains(&declaration) {
            used_declarations.insert(declaration);
            continue;
        }

        let mut visited = HashSet::new();
        let mut stack = vec![declaration];

        while let Some(current) = stack.pop() {
            if !visited.insert(current) {
                continue;
            }

            for parent in ctx.reference_parents(current) {
                stack.push(parent);

                match parent {
                    Declaration::Symbol(parent_symbol) => {
                        if used_declarations.contains(&Declaration::Symbol(parent_symbol)) {
                            used_declarations.insert(declaration);
                            break;
                        }
                    }
                    Declaration::Type(parent_ty) => {
                        if entrypoints.contains(&Declaration::Type(parent_ty)) {
                            used_declarations.insert(declaration);
                            break;
                        }
                    }
                }
            }

            if used_declarations.contains(&declaration) {
                break;
            }
        }

        if !used_declarations.contains(&declaration) {
            unused_declarations.insert(declaration);
        }
    }

    for import in ctx.relevant_imports().collect::<Vec<_>>() {
        let references = ctx
            .import_references(import)
            .into_iter()
            .collect::<HashSet<_>>();

        let import = ctx.import(import);

        if import.exported {
            continue;
        }

        let item = match &import.items {
            Items::All(star) => {
                let star = star.clone();
                if !import.declarations.is_empty() && references.is_empty() {
                    ctx.diagnostic_name(&star, DiagnosticKind::UnusedGlobImport);
                }
                continue;
            }
            Items::Named(item) => item,
        };

        let mut unused = Vec::new();

        for (name, declaration) in import.declarations.clone() {
            if !references.contains(&declaration) {
                if item.text() != name {
                    continue;
                }

                unused.push(item.clone());
            }
        }

        for name in unused {
            ctx.diagnostic_name(&name, DiagnosticKind::UnusedImport(name.text().to_string()));
        }
    }

    for &declaration in &unused_declarations {
        match declaration {
            Declaration::Symbol(symbol) => match ctx.symbol(symbol).clone() {
                Symbol::Unresolved | Symbol::Module(_) | Symbol::Builtin(_) => {}
                Symbol::Function(FunctionSymbol { name, .. }) => {
                    if let Some(name) = name {
                        if name.text().starts_with('_') {
                            continue;
                        }

                        ctx.diagnostic_name(
                            &name,
                            DiagnosticKind::UnusedFunction(name.text().to_string()),
                        );
                    }
                }
                Symbol::Binding(BindingSymbol { name, .. }) => {
                    if let Some(name) = name {
                        if name.text().starts_with('_') {
                            continue;
                        }

                        ctx.diagnostic_name(
                            &name,
                            DiagnosticKind::UnusedBinding(name.text().to_string()),
                        );
                    }
                }
                Symbol::Constant(ConstantSymbol { name, .. }) => {
                    if let Some(name) = name {
                        if name.text().starts_with('_') {
                            continue;
                        }

                        ctx.diagnostic_name(
                            &name,
                            DiagnosticKind::UnusedConstant(name.text().to_string()),
                        );
                    }
                }
                Symbol::Parameter(ParameterSymbol { name, .. }) => {
                    if let Some(name) = name {
                        if name.text().starts_with('_') {
                            continue;
                        }

                        ctx.diagnostic_name(
                            &name,
                            DiagnosticKind::UnusedParameter(name.text().to_string()),
                        );
                    }
                }
            },
            Declaration::Type(ty) => match ctx.ty(ty).clone() {
                Type::Generic(Generic {
                    name: Some(name), ..
                }) => {
                    if name.text().starts_with('_') {
                        continue;
                    }

                    ctx.diagnostic_name(
                        &name,
                        DiagnosticKind::UnusedGenericType(name.text().to_string()),
                    );
                }
                Type::Struct(Struct {
                    name: Some(name), ..
                }) => {
                    if name.text().starts_with('_') {
                        continue;
                    }

                    ctx.diagnostic_name(
                        &name,
                        DiagnosticKind::UnusedStruct(name.text().to_string()),
                    );
                }
                Type::Alias(Alias {
                    name: Some(name), ..
                }) => {
                    if name.text().starts_with('_') {
                        continue;
                    }

                    ctx.diagnostic_name(
                        &name,
                        DiagnosticKind::UnusedTypeAlias(name.text().to_string()),
                    );
                }
                _ => {}
            },
        }
    }
}
