use log::debug;
use rue_ast::{AstBlock, AstNode, AstStmt, AstStmtOrExpr};
use rue_diagnostic::{DiagnosticKind, SrcLoc};
use rue_hir::{
    BindingSymbol, Block, Declaration, ExprStatement, Hir, IfStatement, Statement, Symbol, Value,
};
use rue_types::{Type, TypeId, Union};

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

pub fn compile_block(
    ctx: &mut Compiler,
    block: &AstBlock,
    is_expr: bool,
    expected_type: Option<TypeId>,
    require_return: bool,
) -> Value {
    ctx.add_syntax(
        SyntaxItemKind::CompletionContext(CompletionContext::Expression),
        block.syntax().text_range(),
    );

    let index = ctx.mapping_checkpoint();

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

    let mut statements = Vec::new();
    let mut return_value = None;
    let mut return_types = Vec::new();
    let mut implicit_return = false;

    for stmt in block.items() {
        let stmt = match stmt {
            AstStmtOrExpr::Stmt(stmt) => {
                ctx.add_syntax(
                    SyntaxItemKind::CompletionContext(CompletionContext::Statement),
                    stmt.syntax().text_range(),
                );

                stmt
            }
            AstStmtOrExpr::Expr(expr) => {
                if !is_expr {
                    ctx.diagnostic(expr.syntax(), DiagnosticKind::UnexpectedImplicitReturn);
                }

                let value = compile_expr(ctx, &expr, expected_type);

                return_types.push(value.ty);

                if return_value.is_none() {
                    return_value = Some(value);
                    implicit_return = true;
                }

                continue;
            }
        };

        let compiled = match stmt {
            AstStmt::ExprStmt(stmt) => {
                let value = if let Some(expr) = stmt.expr() {
                    compile_expr(ctx, &expr, None)
                } else {
                    debug!("Unresolved expr stmt expr");
                    ctx.builtins().unresolved.clone()
                };

                let always_nil = ctx.is_castable(value.ty, ctx.builtins().types.nil);

                if !always_nil {
                    ctx.diagnostic(stmt.syntax(), DiagnosticKind::UnusedStatementValue);
                }

                Statement::Expr(ExprStatement {
                    hir: value.hir,
                    always_nil,
                })
            }
            AstStmt::LetStmt(stmt) => {
                let symbol = ctx.alloc_symbol(Symbol::Unresolved);

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

                let expected_type = stmt.ty().map(|ty| compile_type(ctx, &ty));

                let value = if let Some(expr) = stmt.value() {
                    compile_expr(ctx, &expr, expected_type)
                } else {
                    debug!("Unresolved let binding value");
                    ctx.diagnostic(stmt.syntax(), DiagnosticKind::MissingLetValue);
                    ctx.builtins().unresolved.clone()
                };

                let ty = if let Some(expected_type) = expected_type {
                    if let Some(expr) = stmt.value() {
                        ctx.assign_type(expr.syntax(), value.ty, expected_type);
                    }
                    expected_type
                } else {
                    value.ty
                };

                *ctx.symbol_mut(symbol) = Symbol::Binding(BindingSymbol {
                    name: None,
                    value: value.with_type(ty),
                    inline: stmt.inline().is_some(),
                });

                if let Some(binding) = stmt.binding() {
                    let scope = ctx.alloc_child_scope();

                    ctx.push_scope(scope, stmt.syntax().text_range().start());

                    create_binding(ctx, symbol, &binding);
                }

                ctx.pop_declaration();

                Statement::Let(symbol)
            }
            AstStmt::IfStmt(stmt) => {
                let condition = if let Some(condition) = stmt.condition() {
                    let value = compile_expr(ctx, &condition, None);
                    ctx.check_condition(condition.syntax(), value.ty);
                    value
                } else {
                    debug!("Unresolved if stmt condition");
                    ctx.builtins().unresolved.clone()
                };

                let then_block = if let Some(then_block) = stmt.then_block() {
                    if stmt.inline().is_some() {
                        compile_block(ctx, &then_block, false, expected_type, true)
                    } else {
                        let range = then_block.syntax().text_range();
                        let index = ctx.push_mappings(condition.then_map.clone(), range.start());
                        let value = compile_block(ctx, &then_block, false, expected_type, true);
                        ctx.revert_mappings(index, range.end());
                        value
                    }
                } else {
                    debug!("Unresolved if stmt then block");
                    ctx.builtins().unresolved.clone()
                };

                if stmt.inline().is_none() {
                    ctx.push_mappings(condition.else_map, stmt.syntax().text_range().end());
                }

                if !is_expr {
                    return_types.push(then_block.ty);
                }

                Statement::If(IfStatement {
                    condition: condition.hir,
                    then: then_block.hir,
                    inline: stmt.inline().is_some(),
                })
            }
            AstStmt::ReturnStmt(stmt) => {
                let value = if let Some(expr) = stmt.expr() {
                    compile_expr(ctx, &expr, expected_type)
                } else {
                    ctx.builtins().nil.clone()
                };

                if is_expr {
                    ctx.diagnostic(stmt.syntax(), DiagnosticKind::UnexpectedExplicitReturn);
                }

                if !is_expr {
                    return_types.push(value.ty);
                }

                if return_value.is_none() {
                    return_value = Some(value.clone());
                }

                Statement::Return(value.hir)
            }
            AstStmt::AssertStmt(stmt) => {
                let value = if let Some(expr) = stmt.expr() {
                    let value = compile_expr(ctx, &expr, None);
                    ctx.check_condition(expr.syntax(), value.ty);
                    value
                } else {
                    debug!("Unresolved assert expr");
                    ctx.builtins().unresolved.clone()
                };

                ctx.push_mappings(value.then_map, stmt.syntax().text_range().end());

                Statement::Assert(
                    value.hir,
                    SrcLoc::new(ctx.source().clone(), stmt.syntax().text_range().into()),
                )
            }
            AstStmt::RaiseStmt(stmt) => {
                let value = stmt.expr().map(|expr| compile_expr(ctx, &expr, None));

                if return_value.is_none() {
                    return_value = Some(Value::new(
                        ctx.alloc_hir(Hir::Unresolved),
                        ctx.builtins().types.never,
                    ));
                }

                Statement::Raise(
                    value.map(|value| value.hir),
                    SrcLoc::new(ctx.source().clone(), stmt.syntax().text_range().into()),
                )
            }
            AstStmt::DebugStmt(stmt) => {
                let value = if let Some(expr) = stmt.expr() {
                    compile_expr(ctx, &expr, None)
                } else {
                    debug!("Unresolved print expr");
                    ctx.builtins().unresolved.clone()
                };

                Statement::Debug(
                    value.hir,
                    SrcLoc::new(ctx.source().clone(), stmt.syntax().text_range().into()),
                )
            }
        };

        statements.push(compiled);
    }

    if return_value.is_none() {
        return_types.push(ctx.builtins().types.nil);
    }

    let return_type = if return_types.is_empty() {
        ctx.builtins().types.never
    } else if return_types.len() == 1 {
        return_types[0]
    } else {
        ctx.alloc_type(Type::Union(Union::new(return_types)))
    };

    let mut return_value = return_value.map(|value| value.with_type(return_type));

    if return_value.is_none() && require_return {
        ctx.diagnostic(block.syntax(), DiagnosticKind::MissingReturn);
        return_value = Some(Value::new(ctx.alloc_hir(Hir::Unresolved), return_type));
    }

    let hir = ctx.alloc_hir(Hir::Block(Block {
        statements,
        body: return_value
            .as_ref()
            .and_then(|value| implicit_return.then_some(value.hir)),
    }));

    ctx.revert_mappings(index, range.end());

    if let Some(return_value) = return_value {
        return_value.with_hir(hir)
    } else {
        Value::new(hir, return_type)
    }
}
