use std::collections::{BTreeMap, BTreeSet};

use crate::{
    ast::types::{
        ArrayDimension, BinaryOp, BlockStmt, BreakStmt, CompileUnit, ContinueStmt, Expr, ExprValue, ForInit, ForStmt, FuncDeclStmt,
        GlobalDeclStmt, IfElseBranch, IfStmt, ReturnStmt, Statement, StorageClass, VarDeclStmt, WhileStmt,
    },
    diagnostic::{span::Span, Diagnositics},
    sema::{
        err::SemaError,
        hir::{
            HirBlockStmt, HirBreakStmt, HirCompileUnit, HirContinueStmt, HirExpr, HirExprValue,
            HirForInit, HirForStmt, HirFuncDeclStmt, HirGlobalDeclStmt, HirIfElseBranch, HirIfStmt, HirParam,
            HirReturnStmt, HirStatement, HirVarDeclStmt, HirVarDeclStmtValue, HirWhileStmt,
        },
        symbol::{FunctionId, FunctionSig, SymbolId, SymbolKind, SymbolTable},
        types::SemaType,
    },
};

pub struct Analyzer {
    symbols: SymbolTable,
    function_map: BTreeMap<String, FunctionId>,
    builtin_functions: BTreeSet<FunctionId>,
    functions: Vec<FunctionSig>,
    current_func_return_type: Option<SemaType>,
    diagnostic: Diagnositics,
    loop_depth: usize,
}

impl Analyzer {
    pub fn new() -> Self {
        let mut analyzer = Self {
            symbols: SymbolTable::new(),
            function_map: BTreeMap::new(),
            builtin_functions: BTreeSet::new(),
            functions: vec![],
            current_func_return_type: None,
            diagnostic: Diagnositics::new(),
            loop_depth: 0,
        };
        analyzer.declare_builtin_func("putint", vec![SemaType::I32], SemaType::Void);
        analyzer.declare_builtin_func("putch", vec![SemaType::I32], SemaType::Void);
        analyzer.declare_builtin_func("putarray", vec![SemaType::I32, SemaType::Array(Box::new(SemaType::I32), vec![0])], SemaType::Void);
        analyzer.declare_builtin_func("getint", vec![], SemaType::I32);
        analyzer.declare_builtin_func("getch", vec![], SemaType::I32);
        analyzer.declare_builtin_func("getarray", vec![SemaType::Array(Box::new(SemaType::I32), vec![0])], SemaType::I32);
        analyzer
    }

    pub fn analyze(&mut self, compile_unit: CompileUnit) -> HirCompileUnit {
        self.analyze_compile_unit(compile_unit)
    }

    pub fn get_diagnostics(&self) -> &Diagnositics {
        &self.diagnostic
    }

    pub fn get_symbol_type(&self, symbol: SymbolId) -> SemaType {
        self.symbols.get_symbol(symbol).ty.clone()
    }

    pub fn get_symbol_kind(&self, symbol: SymbolId) -> SymbolKind {
        self.symbols.get_symbol(symbol).kind
    }

    pub fn get_function_sig(&self, function: FunctionId) -> &FunctionSig {
        &self.functions[function.0]
    }

    fn declare_builtin_func(&mut self, name: &str, parameter_types: Vec<SemaType>, return_type: SemaType) {
        let id = FunctionId(self.functions.len());
        let sig = FunctionSig {
            id,
            name: name.to_string(),
            return_type,
            parameter_types,
        };
        self.function_map.insert(name.to_string(), id);
        self.builtin_functions.insert(id);
        self.functions.push(sig);
    }

    fn add_error(&mut self, error: SemaError, span: Span) {
        self.diagnostic.add_from_error(error, span);
    }

    fn analyze_compile_unit(&mut self, compile_unit: CompileUnit) -> HirCompileUnit {
        let mut global_decls = vec![];
        for decl in compile_unit.global_decls {
            match decl {
                GlobalDeclStmt::VarDecl(var_decl) => {
                    global_decls.push(HirGlobalDeclStmt::VarDecl(self.analyze_var_decl(var_decl, SymbolKind::Global)));
                }
                GlobalDeclStmt::FuncDecl(func_decl) => {
                    if let Some(func_decl) = self.analyze_func_decl(func_decl) {
                        global_decls.push(HirGlobalDeclStmt::FuncDecl(func_decl));
                    }
                }
            }
        }
        HirCompileUnit { global_decls }
    }

    fn analyze_var_decl(&mut self, var_decl: VarDeclStmt, kind: SymbolKind) -> HirVarDeclStmt {
        let base_type: SemaType = var_decl.data_type.into();
        let is_static_local = kind == SymbolKind::Local && var_decl.storage_class == Some(StorageClass::Static);
        let has_static_storage = kind == SymbolKind::Global || is_static_local;
        let actual_kind = if is_static_local {
            SymbolKind::StaticLocal
        } else {
            kind
        };
        let mut values = vec![];
        for value in var_decl.values {
            let data_type = self.build_var_type(base_type.clone(), &value.dimensions, false);
            let initializer_is_not_constant = has_static_storage
                && value
                    .value
                    .as_ref()
                    .is_some_and(|expr| Self::eval_const_expr(expr).is_none());
            if initializer_is_not_constant {
                if let Some(expr) = &value.value {
                    self.add_error(SemaError::InitializerNotConstant, expr.span);
                }
            }
            match self.symbols.declare_variable(&value.name, actual_kind, data_type) {
                Ok(symbol) => {
                    let symbol_type = self.symbols.get_symbol(symbol).ty.clone();
                    let init = if initializer_is_not_constant {
                        None
                    } else {
                        value.value.and_then(|expr| {
                        let span = expr.span;
                        let init = self.analyze_expr(expr)?;
                        if init.ty == SemaType::Void {
                            self.add_error(SemaError::InvalidOperand(SemaType::Void), span);
                        } else if !self.type_matches(&symbol_type, &init.ty) {
                            self.add_error(SemaError::TypeMismatch(symbol_type.clone(), init.ty.clone()), span);
                        }
                        Some(init)
                        })
                    };
                    values.push(HirVarDeclStmtValue {
                        symbol,
                        name_span: value.name_span,
                        value: init,
                        is_static_local,
                    });
                }
                Err(e) => self.add_error(e, value.name_span),
            }
        }
        HirVarDeclStmt {
            values,
            data_type: base_type,
            type_span: var_decl.type_span,
        }
    }

    fn analyze_func_decl(&mut self, func_decl: FuncDeclStmt) -> Option<HirFuncDeclStmt> {
        let is_static = func_decl.storage_class == Some(StorageClass::Static);
        let existing_function = self.function_map.get(&func_decl.name).cloned();
        let can_shadow_builtin = is_static
            && existing_function.is_some_and(|function| self.builtin_functions.contains(&function));
        if existing_function.is_some() && !can_shadow_builtin {
            self.add_error(SemaError::FunctionHasBeenDefined(func_decl.name.clone()), func_decl.name_span);
            return None;
        }

        let function_id = FunctionId(self.functions.len());
        let mut parameter_types = vec![];
        for param in &func_decl.params {
            parameter_types.push(self.build_var_type(param.param_type.into(), &param.dimensions, true));
        }
        let return_type: SemaType = func_decl.return_type.into();
        let ir_name = if is_static {
            format!("__static_func_{}_{}", function_id.0, func_decl.name)
        } else {
            func_decl.name.clone()
        };
        let sig = FunctionSig {
            id: function_id,
            name: ir_name,
            return_type: return_type.clone(),
            parameter_types,
        };
        self.function_map.insert(func_decl.name.clone(), function_id);
        self.functions.push(sig.clone());

        self.current_func_return_type = Some(return_type);
        self.symbols.enter_scope();

        let mut params = vec![];
        for param in func_decl.params {
            let param_type = self.build_var_type(param.param_type.into(), &param.dimensions, true);
            match self.symbols.declare_variable(&param.name, SymbolKind::Param, param_type.clone()) {
                Ok(symbol) => params.push(HirParam {
                    symbol,
                    param_type,
                    name_span: param.name_span,
                    type_span: param.type_span,
                }),
                Err(e) => self.add_error(e, param.name_span),
            }
        }

        let body = self.analyze_block_stmt(func_decl.body);
        self.symbols.exit_scope();
        self.current_func_return_type = None;

        Some(HirFuncDeclStmt {
            sig,
            params,
            body,
            ret_type_span: func_decl.ret_type_span,
            name_span: func_decl.name_span,
        })
    }

    fn eval_const_expr(expr: &Expr) -> Option<i32> {
        match &expr.value {
            ExprValue::IntLit(value) => Some(*value as i32),
            ExprValue::UnaryOp { op, operand } => {
                let value = Self::eval_const_expr(operand)?;
                match op {
                    crate::ast::types::UnaryOp::Add => Some(value),
                    crate::ast::types::UnaryOp::Sub => Some(value.wrapping_neg()),
                    crate::ast::types::UnaryOp::Not => Some((value == 0) as i32),
                }
            }
            ExprValue::BinaryOp { lhs, op, rhs } => {
                let lhs = Self::eval_const_expr(lhs)?;
                match op {
                    BinaryOp::And if lhs == 0 => return Some(0),
                    BinaryOp::Or if lhs != 0 => return Some(1),
                    _ => {}
                }
                let rhs = Self::eval_const_expr(rhs)?;
                match op {
                    BinaryOp::Add => Some(lhs.wrapping_add(rhs)),
                    BinaryOp::Sub => Some(lhs.wrapping_sub(rhs)),
                    BinaryOp::Mul => Some(lhs.wrapping_mul(rhs)),
                    BinaryOp::Div if rhs != 0 => Some(lhs.wrapping_div(rhs)),
                    BinaryOp::Div => None,
                    BinaryOp::Mod if rhs != 0 => Some(lhs.wrapping_rem(rhs)),
                    BinaryOp::Mod => None,
                    BinaryOp::Equal => Some((lhs == rhs) as i32),
                    BinaryOp::NotEqual => Some((lhs != rhs) as i32),
                    BinaryOp::Less => Some((lhs < rhs) as i32),
                    BinaryOp::LessEqual => Some((lhs <= rhs) as i32),
                    BinaryOp::Greater => Some((lhs > rhs) as i32),
                    BinaryOp::GreaterEqual => Some((lhs >= rhs) as i32),
                    BinaryOp::And => Some((lhs != 0 && rhs != 0) as i32),
                    BinaryOp::Or => Some((lhs != 0 || rhs != 0) as i32),
                }
            }
            _ => None,
        }
    }

    fn build_var_type(&mut self, base_type: SemaType, dimensions: &[ArrayDimension], is_param: bool) -> SemaType {
        if dimensions.is_empty() {
            return base_type;
        }
        let mut dims = vec![];
        for (i, dimension) in dimensions.iter().enumerate() {
            match &dimension.value {
                Some(expr) => {
                    if let Some(value) = Self::eval_const_expr(expr) {
                        if value > 0 {
                            dims.push(value as usize);
                            continue;
                        }
                        self.add_error(SemaError::InvalidArrayDimension, dimension.span);
                    } else {
                        self.add_error(SemaError::InvalidArrayDimension, dimension.span);
                    }
                }
                None if is_param && i == 0 => dims.push(0),
                None => self.add_error(SemaError::InvalidArrayDimension, dimension.span),
            }
        }
        SemaType::Array(Box::new(base_type), dims)
    }

    fn analyze_block_stmt(&mut self, block_stmt: BlockStmt) -> HirBlockStmt {
        let mut statements = vec![];
        for stmt in block_stmt.statements {
            if let Some(stmt) = self.analyze_statement(stmt) {
                statements.push(stmt);
            }
        }
        HirBlockStmt { statements }
    }

    fn analyze_scoped_block_stmt(&mut self, block_stmt: BlockStmt) -> HirBlockStmt {
        self.symbols.enter_scope();
        let block = self.analyze_block_stmt(block_stmt);
        self.symbols.exit_scope();
        block
    }

    fn analyze_statement(&mut self, stmt: Statement) -> Option<HirStatement> {
        match stmt {
            Statement::Return(stmt) => Some(HirStatement::Return(self.analyze_return_stmt(stmt))),
            Statement::If(stmt) => Some(HirStatement::If(self.analyze_if_stmt(stmt))),
            Statement::While(stmt) => Some(HirStatement::While(self.analyze_while_stmt(stmt))),
            Statement::For(stmt) => Some(HirStatement::For(self.analyze_for_stmt(stmt))),
            Statement::Break(stmt) => Some(HirStatement::Break(self.analyze_break_stmt(stmt))),
            Statement::Continue(stmt) => Some(HirStatement::Continue(self.analyze_continue_stmt(stmt))),
            Statement::Block(stmt) => {
                self.symbols.enter_scope();
                let block = self.analyze_block_stmt(stmt);
                self.symbols.exit_scope();
                Some(HirStatement::Block(block))
            }
            Statement::Expr(expr) => self.analyze_expr(expr).map(HirStatement::Expr),
            Statement::VarDecl(var_decl) => Some(HirStatement::VarDecl(self.analyze_var_decl(var_decl, SymbolKind::Local))),
        }
    }

    fn analyze_return_stmt(&mut self, return_stmt: ReturnStmt) -> HirReturnStmt {
        let expected_ty = self.current_func_return_type.clone().unwrap();
        let value = match return_stmt.value {
            Some(expr) => {
                if expected_ty == SemaType::Void {
                    self.add_error(SemaError::ReturnExpressionOnVoidFunction, return_stmt.span);
                    None
                } else {
                    match self.analyze_expr(expr) {
                        Some(expr) => {
                            if expr.ty == SemaType::Void {
                                self.add_error(SemaError::InvalidOperand(SemaType::Void), return_stmt.span);
                            } else if !self.type_matches(&expected_ty, &expr.ty) {
                                self.add_error(SemaError::TypeMismatch(expected_ty.clone(), expr.ty.clone()), return_stmt.span);
                            }
                            Some(expr)
                        }
                        None => None,
                    }
                }
            }
            None => {
                if expected_ty != SemaType::Void {
                    self.add_error(SemaError::TypeMismatch(expected_ty, SemaType::Void), return_stmt.span);
                }
                None
            }
        };
        HirReturnStmt {
            value,
            span: return_stmt.span,
        }
    }

    fn analyze_if_stmt(&mut self, if_stmt: IfStmt) -> HirIfStmt {
        let condition = self.analyze_condition_expr(if_stmt.condition);
        let then_branch = self.analyze_scoped_block_stmt(if_stmt.then_branch);
        let mut ifelse_branch = vec![];
        for branch in if_stmt.ifelse_branch {
            let IfElseBranch { condition, then_branch } = branch;
            ifelse_branch.push(HirIfElseBranch {
                condition: self.analyze_condition_expr(condition),
                then_branch: self.analyze_scoped_block_stmt(then_branch),
            });
        }
        let else_branch = if_stmt.else_branch.map(|block| self.analyze_scoped_block_stmt(block));
        HirIfStmt {
            condition,
            then_branch,
            ifelse_branch,
            else_branch,
        }
    }

    fn analyze_while_stmt(&mut self, while_stmt: WhileStmt) -> HirWhileStmt {
        let condition = self.analyze_condition_expr(while_stmt.condition);
        self.loop_depth += 1;
        let body = self.analyze_scoped_block_stmt(while_stmt.body);
        self.loop_depth -= 1;
        HirWhileStmt {
            condition,
            body,
        }
    }

    fn analyze_for_stmt(&mut self, for_stmt: ForStmt) -> HirForStmt {
        self.symbols.enter_scope();
        let init = for_stmt.init.and_then(|init| match init {
            ForInit::Expr(expr) => self.analyze_expr(expr).map(HirForInit::Expr),
            ForInit::VarDecl(var_decl) => Some(HirForInit::VarDecl(self.analyze_var_decl(var_decl, SymbolKind::Local))),
        });
        let condition = for_stmt.condition.map(|expr| self.analyze_condition_expr(expr));
        let update = for_stmt.update.and_then(|expr| self.analyze_expr(expr));
        self.loop_depth += 1;
        let body = self.analyze_scoped_block_stmt(for_stmt.body);
        self.loop_depth -= 1;
        self.symbols.exit_scope();
        HirForStmt {
            init,
            condition,
            update,
            body,
        }
    }

    fn analyze_break_stmt(&mut self, stmt: BreakStmt) -> HirBreakStmt {
        if self.loop_depth == 0 {
            self.add_error(SemaError::BreakOutsideLoop, stmt.span);
        }
        HirBreakStmt { span: stmt.span }
    }

    fn analyze_continue_stmt(&mut self, stmt: ContinueStmt) -> HirContinueStmt {
        if self.loop_depth == 0 {
            self.add_error(SemaError::ContinueOutsideLoop, stmt.span);
        }
        HirContinueStmt { span: stmt.span }
    }

    fn analyze_condition_expr(&mut self, expr: Expr) -> HirExpr {
        let span = expr.span;
        match self.analyze_expr(expr) {
            Some(expr) => {
                if expr.ty == SemaType::Void {
                    self.add_error(SemaError::InvalidOperand(SemaType::Void), span);
                }
                expr
            }
            None => HirExpr {
                value: HirExprValue::IntLit(0),
                ty: SemaType::I32,
                span,
            },
        }
    }

    fn analyze_expr(&mut self, expr: Expr) -> Option<HirExpr> {
        let span = expr.span;
        match expr.value {
            ExprValue::IntLit(value) => Some(HirExpr {
                value: HirExprValue::IntLit(value),
                ty: SemaType::I32,
                span,
            }),
            ExprValue::Var(name) => {
                let symbol = match self.symbols.get_variable(&name) {
                    Some(symbol) => symbol,
                    None => {
                        self.add_error(SemaError::VariableNotFound(name), span);
                        return None;
                    }
                };
                Some(HirExpr {
                    value: HirExprValue::Var(symbol),
                    ty: self.symbols.get_symbol(symbol).ty.clone(),
                    span,
                })
            }
            ExprValue::Assign { lvalue, rvalue } => self.analyze_assign_expr(*lvalue, *rvalue, span),
            ExprValue::CompoundAssign { lvalue, op, rvalue } => self.analyze_compound_assign_expr(*lvalue, op, *rvalue, span),
            ExprValue::ArrayAccess { array, index } => self.analyze_array_access_expr(*array, *index, span),
            ExprValue::UnaryOp { op, operand } => {
                let operand_span = operand.span;
                let operand = self.analyze_expr(*operand)?;
                if operand.ty == SemaType::Void {
                    self.add_error(SemaError::InvalidOperand(SemaType::Void), operand_span);
                    return None;
                }
                let ty = match op {
                    crate::ast::types::UnaryOp::Not => SemaType::I1,
                    crate::ast::types::UnaryOp::Add | crate::ast::types::UnaryOp::Sub if operand.ty == SemaType::I1 => SemaType::I32,
                    _ => operand.ty.clone(),
                };
                Some(HirExpr {
                    value: HirExprValue::UnaryOp {
                        op,
                        operand: Box::new(operand),
                    },
                    ty,
                    span,
                })
            }
            ExprValue::IncDec { op, operand, is_prefix } => {
                if !matches!(operand.value, ExprValue::Var(_) | ExprValue::ArrayAccess { .. }) {
                    self.add_error(SemaError::InvalidAssignmentTarget, operand.span);
                    return None;
                }
                let operand_span = operand.span;
                let operand = self.analyze_expr(*operand)?;
                if !operand.ty.is_scalar() {
                    self.add_error(SemaError::InvalidOperand(operand.ty.clone()), operand_span);
                    return None;
                }
                Some(HirExpr {
                    ty: operand.ty.clone(),
                    value: HirExprValue::IncDec {
                        op,
                        operand: Box::new(operand),
                        is_prefix,
                    },
                    span,
                })
            }
            ExprValue::BinaryOp { lhs, op, rhs } => self.analyze_binary_expr(*lhs, op, *rhs, span),
            ExprValue::FuncCall(func_name, args) => self.analyze_func_call_expr(func_name, args, span),
        }
    }

    fn analyze_assign_expr(&mut self, lvalue: Expr, rvalue: Expr, span: Span) -> Option<HirExpr> {
        if !matches!(lvalue.value, ExprValue::Var(_) | ExprValue::ArrayAccess { .. }) {
            self.add_error(SemaError::InvalidAssignmentTarget, lvalue.span);
            return None;
        }
        let lvalue = self.analyze_expr(lvalue)?;
        if lvalue.ty.is_array_like() {
            self.add_error(SemaError::InvalidAssignmentTarget, lvalue.span);
            return None;
        }
        let rvalue_span = rvalue.span;
        let rvalue = self.analyze_expr(rvalue)?;
        if rvalue.ty == SemaType::Void {
            self.add_error(SemaError::InvalidOperand(SemaType::Void), rvalue_span);
            return None;
        }
        if !self.type_matches(&lvalue.ty, &rvalue.ty) {
            self.add_error(SemaError::TypeMismatch(lvalue.ty.clone(), rvalue.ty.clone()), span);
            return None;
        }
        Some(HirExpr {
            ty: lvalue.ty.clone(),
            value: HirExprValue::Assign {
                lvalue: Box::new(lvalue),
                rvalue: Box::new(rvalue),
            },
            span,
        })
    }

    fn analyze_compound_assign_expr(&mut self, lvalue: Expr, op: BinaryOp, rvalue: Expr, span: Span) -> Option<HirExpr> {
        if !matches!(lvalue.value, ExprValue::Var(_) | ExprValue::ArrayAccess { .. }) {
            self.add_error(SemaError::InvalidAssignmentTarget, lvalue.span);
            return None;
        }
        let lvalue_span = lvalue.span;
        let lvalue = self.analyze_expr(lvalue)?;
        if lvalue.ty.is_array_like() {
            self.add_error(SemaError::InvalidAssignmentTarget, lvalue.span);
            return None;
        }
        let rvalue_span = rvalue.span;
        let rvalue = self.analyze_expr(rvalue)?;
        if !lvalue.ty.is_scalar() {
            self.add_error(SemaError::InvalidOperand(lvalue.ty.clone()), lvalue_span);
            return None;
        }
        if !rvalue.ty.is_scalar() {
            self.add_error(SemaError::InvalidOperand(rvalue.ty.clone()), rvalue_span);
            return None;
        }
        let result_ty = match SemaType::get_elevate_result(&lvalue.ty, &rvalue.ty) {
            Some(ty) => ty,
            None => {
                self.add_error(SemaError::IncompatiableOperand(lvalue.ty.clone(), rvalue.ty.clone()), lvalue_span);
                self.add_error(SemaError::IncompatiableOperand(lvalue.ty.clone(), rvalue.ty.clone()), rvalue_span);
                return None;
            }
        };
        if !self.type_matches(&lvalue.ty, &result_ty) {
            self.add_error(SemaError::TypeMismatch(lvalue.ty.clone(), result_ty), span);
            return None;
        }
        Some(HirExpr {
            ty: lvalue.ty.clone(),
            value: HirExprValue::CompoundAssign {
                lvalue: Box::new(lvalue),
                op,
                rvalue: Box::new(rvalue),
            },
            span,
        })
    }

    fn analyze_binary_expr(&mut self, lhs: Expr, op: BinaryOp, rhs: Expr, span: Span) -> Option<HirExpr> {
        let lhs_span = lhs.span;
        let rhs_span = rhs.span;
        let lhs = self.analyze_expr(lhs)?;
        let rhs = self.analyze_expr(rhs)?;
        if !lhs.ty.is_scalar() {
            self.add_error(SemaError::InvalidOperand(lhs.ty.clone()), lhs_span);
            return None;
        }
        if !rhs.ty.is_scalar() {
            self.add_error(SemaError::InvalidOperand(rhs.ty.clone()), rhs_span);
            return None;
        }

        let result_ty = if op.is_logical() {
            SemaType::I1
        } else {
            match SemaType::get_elevate_result(&lhs.ty, &rhs.ty) {
                Some(_) if op.is_cmp() => SemaType::I1,
                Some(ty) => ty,
                None => {
                    self.add_error(SemaError::IncompatiableOperand(lhs.ty.clone(), rhs.ty.clone()), lhs_span);
                    self.add_error(SemaError::IncompatiableOperand(lhs.ty.clone(), rhs.ty.clone()), rhs_span);
                    return None;
                }
            }
        };

        Some(HirExpr {
            value: HirExprValue::BinaryOp {
                lhs: Box::new(lhs),
                op,
                rhs: Box::new(rhs),
            },
            ty: result_ty,
            span,
        })
    }

    fn analyze_func_call_expr(&mut self, func_name: String, args: Vec<Expr>, span: Span) -> Option<HirExpr> {
        let func_id = match self.function_map.get(&func_name).cloned() {
            Some(func_id) => func_id,
            None => {
                self.add_error(SemaError::FunctionNotFound(func_name), span);
                return None;
            }
        };
        let func_def = self.functions[func_id.0].clone();

        if args.len() < func_def.parameter_types.len() {
            self.add_error(SemaError::TooFewArguments(func_def.parameter_types.len(), args.len()), span);
            return None;
        }
        if args.len() > func_def.parameter_types.len() {
            self.add_error(SemaError::TooManyArguments(func_def.parameter_types.len(), args.len()), span);
            return None;
        }

        let mut has_error = false;
        for parameter_type in &func_def.parameter_types {
            if matches!(parameter_type, SemaType::Void) {
                self.add_error(SemaError::InvalidParameterType(SemaType::Void), span);
                has_error = true;
            }
        }
        if has_error {
            return None;
        }

        let mut hir_args = vec![];
        for (i, arg) in args.into_iter().enumerate() {
            let arg = self.analyze_expr(arg)?;
            let parameter_type = func_def.parameter_types[i].clone();
            if !self.type_matches(&parameter_type, &arg.ty) {
                self.add_error(SemaError::TypeMismatch(parameter_type, arg.ty.clone()), span);
                has_error = true;
                continue;
            }
            hir_args.push(arg);
        }
        if has_error {
            return None;
        }

        Some(HirExpr {
            value: HirExprValue::FuncCall(func_id, hir_args),
            ty: func_def.return_type,
            span,
        })
    }

    fn analyze_array_access_expr(&mut self, array: Expr, index: Expr, span: Span) -> Option<HirExpr> {
        let index_span = index.span;
        let array = self.analyze_expr(array)?;
        let index = self.analyze_expr(index)?;
        if !index.ty.is_scalar() {
            self.add_error(SemaError::InvalidOperand(index.ty.clone()), index_span);
            return None;
        }
        let ty = match array.ty.indexed_type() {
            Some(ty) => ty,
            None => {
                self.add_error(SemaError::NotSubscriptable(array.ty.clone()), span);
                return None;
            }
        };
        Some(HirExpr {
            value: HirExprValue::ArrayAccess {
                array: Box::new(array),
                index: Box::new(index),
            },
            ty,
            span,
        })
    }

    fn type_matches(&self, expected: &SemaType, actual: &SemaType) -> bool {
        if expected == actual {
            return true;
        }
        if matches!((expected, actual), (SemaType::I32, SemaType::I1)) {
            return true;
        }
        if let (SemaType::Array(expected_elem, expected_dims), SemaType::Array(actual_elem, actual_dims)) = (expected, actual) {
            return expected_elem == actual_elem
                && expected_dims.len() == actual_dims.len()
                && expected_dims.iter().zip(actual_dims.iter()).all(|(expected_dim, actual_dim)| *expected_dim == 0 || expected_dim == actual_dim);
        }
        matches!((expected, actual), (SemaType::Ptr(expected_elem), SemaType::Array(_, _)) if actual.indexed_type().is_some_and(|ty| &ty == expected_elem.as_ref()))
    }
}
