use indexmap::IndexMap;
use log::debug;
use rue_ast::{AstFunctionItem, AstNode};
use rue_diagnostic::DiagnosticKind;
use rue_hir::{Declaration, FunctionKind, FunctionSymbol, ParameterSymbol, Symbol, SymbolId, Test};
use rue_types::{FunctionType, Type};

use crate::{
    Compiler, CompletionContext, SyntaxItemKind, compile_block, compile_generic_parameters,
    compile_type, create_binding,
};

pub fn declare_function(ctx: &mut Compiler, function: &AstFunctionItem) -> SymbolId {
    ctx.add_syntax(
        SyntaxItemKind::CompletionContext(CompletionContext::Item),
        function.syntax().text_range(),
    );

    let symbol = ctx.alloc_symbol(Symbol::Unresolved);

    if function.test().is_some() {
        let path = ctx.source().kind.clone();

        ctx.add_test(Test {
            name: function.name().map(|name| name.text().to_string()),
            path,
            symbol,
        });
    }

    ctx.push_declaration(Declaration::Symbol(symbol));

    let scope = ctx.alloc_child_scope();

    let vars = if let Some(generic_parameters) = function.generic_parameters() {
        compile_generic_parameters(ctx, scope, &generic_parameters)
    } else {
        vec![]
    };

    let range = function.syntax().text_range();
    ctx.push_scope(scope, range.start());

    let return_type = if let Some(return_type) = function.return_type() {
        compile_type(ctx, &return_type)
    } else {
        ctx.builtins().nil.ty
    };

    let mut parameters = IndexMap::new();
    let mut param_types = IndexMap::new();
    let mut nil_terminated = true;

    let len = function.parameters().count();

    for (i, parameter) in function.parameters().enumerate() {
        let is_spread = if let Some(spread) = parameter.spread() {
            if i == len - 1 {
                true
            } else {
                ctx.diagnostic(&spread, DiagnosticKind::NonFinalSpread);
                false
            }
        } else {
            false
        };

        if is_spread {
            nil_terminated = false;
        }

        let symbol = ctx.alloc_symbol(Symbol::Unresolved);

        ctx.push_declaration(Declaration::Symbol(symbol));

        let ty = if let Some(ty) = parameter.ty() {
            compile_type(ctx, &ty)
        } else {
            debug!("Unresolved function parameter type");
            ctx.builtins().unresolved.ty
        };

        *ctx.symbol_mut(symbol) = Symbol::Parameter(ParameterSymbol { name: None, ty });

        let name = parameter
            .binding()
            .map_or(String::new(), |binding| binding.syntax().text().to_string());

        param_types.insert(name.clone(), ty);
        parameters.insert(name, symbol);

        ctx.pop_declaration();
    }

    ctx.pop_scope(range.end());

    let body = ctx.builtins().unresolved.hir;

    let ty = ctx.alloc_type(Type::Function(FunctionType {
        params: param_types,
        nil_terminated,
        ret: return_type,
    }));

    let name = function.name().map(|name| ctx.local_name(&name));

    *ctx.symbol_mut(symbol) = Symbol::Function(FunctionSymbol {
        name,
        ty,
        scope,
        vars,
        parameters,
        nil_terminated,
        return_type,
        body,
        kind: if function.inline().is_some() {
            FunctionKind::Inline
        } else if function.extern_kw().is_some() {
            FunctionKind::Sequential
        } else {
            FunctionKind::BinaryTree
        },
    });

    if let Some(name) = function.name() {
        if ctx.last_scope().symbol(name.text()).is_some() {
            ctx.diagnostic(
                &name,
                DiagnosticKind::DuplicateSymbol(name.text().to_string()),
            );
        } else {
            ctx.last_scope_mut().insert_symbol(
                name.text().to_string(),
                symbol,
                function.export().is_some(),
            );
        }

        ctx.declaration_span(Declaration::Symbol(symbol), name.text_range());
    }

    ctx.pop_declaration();

    symbol
}

pub fn compile_function(ctx: &mut Compiler, function: &AstFunctionItem, symbol: SymbolId) {
    ctx.push_declaration(Declaration::Symbol(symbol));

    let Symbol::Function(FunctionSymbol {
        scope,
        parameters,
        return_type,
        ..
    }) = ctx.symbol(symbol).clone()
    else {
        unreachable!();
    };

    let range = function.syntax().text_range();
    ctx.push_scope(scope, range.start());

    for (i, parameter) in function.parameters().enumerate() {
        let symbol = parameters[i];

        ctx.push_declaration(Declaration::Symbol(symbol));

        if let Some(binding) = parameter.binding() {
            create_binding(ctx, symbol, &binding);
        }

        ctx.pop_declaration();
    }

    let resolved_body = if let Some(body) = function.body() {
        let value = compile_block(
            ctx,
            &body,
            true,
            Some(return_type),
            function.return_type().is_some(),
        );
        ctx.assign_type(body.syntax(), value.ty, return_type);
        value
    } else {
        debug!("Unresolved function body");
        ctx.builtins().unresolved.clone()
    };

    ctx.pop_scope(range.end());

    let Symbol::Function(FunctionSymbol { body, .. }) = ctx.symbol_mut(symbol) else {
        unreachable!();
    };

    *body = resolved_body.hir;

    ctx.pop_declaration();
}
