using System.Diagnostics;
using System.Text;
using NubLang.TypeChecking.Node;

namespace NubLang.Generation.QBE;

public class QBEGenerator
{
    private const int PTR_SIZE = 8;

    private readonly QBEWriter _writer;
    private readonly List<DefinitionNode> _definitions;
    private readonly HashSet<NubStructType> _structTypes;

    private readonly List<CStringLiteral> _cStringLiterals = [];
    private readonly List<StringLiteral> _stringLiterals = [];
    private readonly Stack<string> _breakLabels = [];
    private readonly Stack<string> _continueLabels = [];
    private readonly Stack<Scope> _scopes = new();
    private int _tmpIndex;
    private int _labelIndex;
    private int _cStringLiteralIndex;
    private int _stringLiteralIndex;

    private Scope Scope => _scopes.Peek();

    public QBEGenerator(List<DefinitionNode> definitions, HashSet<NubStructType> structTypes)
    {
        _definitions = definitions;
        _structTypes = structTypes;
        _writer = new QBEWriter();
    }

    public string Emit()
    {
        _cStringLiterals.Clear();
        _stringLiterals.Clear();
        _breakLabels.Clear();
        _continueLabels.Clear();
        _scopes.Clear();
        _tmpIndex = 0;
        _labelIndex = 0;
        _cStringLiteralIndex = 0;
        _stringLiteralIndex = 0;

        _writer.Comment("========== Builtin functions ==========");

        _writer.WriteLine("""
                          function l $.cstring_len(l %str) {
                          @start
                              %count =l copy 0
                          @loop
                              %address =l add %str, %count
                              %value =w loadub %address
                              jnz %value, @continue, @end
                          @continue
                              %count =l add %count, 1
                              jmp @loop
                          @end
                              ret %count
                          }

                          function $.memcpy(l %source, l %destination, l %length) {
                          @start
                              %count =l copy 0
                          @loop
                              %condition =w cultl %count, %length
                              jnz %condition, @continue, @end
                          @continue
                              %source_address =l add %source, %count
                              %destination_address =l add %destination, %count
                              %value =w loadub %source_address
                              storeb %value, %destination_address
                              %count =l add %count, 1
                              jmp @loop
                          @end
                              ret
                          }

                          function $.memset(l %destination, l %value, l %length) {
                          @start
                              %count =l copy 0
                          @loop
                              %condition =w cultl %count, %length
                              jnz %condition, @continue, @end
                          @continue
                              %destination_address =l add %destination, %count
                              storeb %value, %destination_address
                              %count =l add %count, 1
                              jmp @loop
                          @end
                              ret
                          }

                          function l $.array_size(l %array) {
                          @start
                              %size =l loadl %array
                              ret %size
                          }

                          """);

        _writer.Comment("========== Referenced structs ==========");

        foreach (var structType in _structTypes)
        {
            EmitStructType(structType);
        }

        _writer.NewLine();

        _writer.Comment("========== Struct definitions ==========");

        foreach (var structDef in _definitions.OfType<StructNode>())
        {
            EmitStructDefinition(structDef);
        }

        _writer.Comment("========== Function definitions ==========");

        foreach (var funcDef in _definitions.OfType<FuncNode>())
        {
            EmitFuncDefinition(funcDef);
        }

        _writer.Comment("========== cstring literals ==========");

        foreach (var cStringLiteral in _cStringLiterals)
        {
            _writer.WriteLine($"data {cStringLiteral.Name} = {{ b \"{cStringLiteral.Value}\", b 0 }}");
        }

        _writer.NewLine();
        _writer.Comment("========== string literals ==========");

        foreach (var stringLiteral in _stringLiterals)
        {
            var bytes = Encoding.UTF8.GetBytes(stringLiteral.Value).Select(b => $"b {b}");
            _writer.WriteLine($"data {stringLiteral.Name} = {{ l {stringLiteral.Value.Length}, {string.Join(", ", bytes)} }}");
        }

        return _writer.ToString();
    }

    private static string QBEAssign(NubType type)
    {
        return type switch
        {
            NubIntType { Width: <= 32 } => "=w",
            NubIntType { Width: 64 } => "=l",
            NubFloatType { Width: 32 } => "=s",
            NubFloatType { Width: 64 } => "=d",
            NubBoolType => "=w",
            NubPointerType => "=l",
            NubFuncType => "=l",
            NubCStringType => "=l",
            NubStringType => "=l",
            NubArrayType => "=l",
            NubStructType => "=l",
            NubVoidType => throw new InvalidOperationException("Void has no assignment"),
            _ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown type {type.GetType()}")
        };
    }

    private void EmitStore(NubType type, string value, string destination)
    {
        var store = type switch
        {
            NubBoolType => "storeb",
            NubIntType { Width: 8 } => "storeb",
            NubIntType { Width: 16 } => "storeh",
            NubIntType { Width: 32 } => "storew",
            NubIntType { Width: 64 } => "storel",
            NubFloatType { Width: 32 } => "stores",
            NubFloatType { Width: 64 } => "stored",
            NubPointerType => "storel",
            NubFuncType => "storel",
            NubCStringType => "storel",
            NubStringType => "storel",
            NubArrayType => "storel",
            NubStructType => throw new InvalidOperationException("Struct stores must use blit/memcpy"),
            NubVoidType => throw new InvalidOperationException("Cannot store void"),
            _ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown type {type.GetType()}")
        };

        _writer.Indented($"{store} {value}, {destination}");
    }

    private string EmitLoad(NubType type, string from)
    {
        string load = type switch
        {
            NubBoolType => "loadub",
            NubIntType { Signed: true, Width: 8 } => "loadsb",
            NubIntType { Signed: true, Width: 16 } => "loadsh",
            NubIntType { Signed: true, Width: 32 } => "loadw",
            NubIntType { Signed: true, Width: 64 } => "loadl",
            NubIntType { Signed: false, Width: 8 } => "loadsb",
            NubIntType { Signed: false, Width: 16 } => "loadsh",
            NubIntType { Signed: false, Width: 32 } => "loadw",
            NubIntType { Signed: false, Width: 64 } => "loadl",
            NubFloatType { Width: 32 } => "loads",
            NubFloatType { Width: 64 } => "loadd",
            NubPointerType => "loadl",
            NubFuncType => "loadl",
            NubCStringType => "loadl",
            NubStringType => "loadl",
            NubArrayType => "loadl",
            NubStructType => throw new InvalidOperationException("Struct loads must use blit/memcpy"),
            NubVoidType => throw new InvalidOperationException("Cannot load void"),
            _ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown type {type.GetType()}")
        };

        var into = TmpName();
        _writer.Indented($"{into} {QBEAssign(type)} {load} {from}");
        return into;
    }


    private static string StructDefQBEType(NubType type)
    {
        return type switch
        {
            NubBoolType => "b",
            NubIntType { Width: 8 } => "b",
            NubIntType { Width: 16 } => "h",
            NubIntType { Width: 32 } => "w",
            NubIntType { Width: 64 } => "l",
            NubFloatType { Width: 32 } => "s",
            NubFloatType { Width: 64 } => "d",
            NubPointerType => "l",
            NubFuncType => "l",
            NubCStringType => "l",
            NubStringType => "l",
            NubArrayType => "l",
            NubStructType st => StructTypeName(st.Module, st.Name),
            NubVoidType => throw new InvalidOperationException("Void has no QBE type"),
            _ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown type: {type.GetType()}")
        };
    }

    private string FuncQBETypeName(NubType type)
    {
        return type switch
        {
            NubBoolType => "ub",
            NubIntType { Signed: true, Width: 8 } => "sb",
            NubIntType { Signed: true, Width: 16 } => "sh",
            NubIntType { Signed: false, Width: 8 } => "ub",
            NubIntType { Signed: false, Width: 16 } => "uh",
            NubIntType { Width: 32 } => "w",
            NubIntType { Width: 64 } => "l",
            NubFloatType { Width: 32 } => "s",
            NubFloatType { Width: 64 } => "d",
            NubPointerType => "l",
            NubFuncType => "l",
            NubCStringType => "l",
            NubStringType => "l",
            NubArrayType => "l",
            NubStructType st => StructTypeName(st.Module, st.Name),
            NubVoidType => throw new InvalidOperationException("Void has no QBE type"),
            _ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown type: {type.GetType()}")
        };
    }

    private void EmitMemset(string destination, int value, string length)
    {
        _writer.Indented($"call $.memset(l {destination}, l {value}, l {length})");
    }

    private void EmitMemcpy(string source, string destination, string length)
    {
        _writer.Indented($"call $.memcpy(l {source}, l {destination}, l {length})");
    }

    private string EmitCStringSizeInBytes(string cstring)
    {
        var result = TmpName();
        _writer.Indented($"{result} =l call $.cstring_len(l {cstring})");
        _writer.Indented($"{result} =l add {result}, 1");
        return result;
    }

    private string EmitStringSizeInBytes(string nubstring)
    {
        var size = TmpName();
        _writer.Indented($"{size} =l loadl {nubstring}");
        _writer.Indented($"{size} =l add {size}, 8");
        return size;
    }

    // todo(nub31): EmitRValue should take a destination parameter so we can skip copying rvalues
    private void EmitCopyInto(ExpressionNode source, string destinationAddress, bool tryMove = true)
    {
        if (tryMove && source is RValueExpressionNode rValue)
        {
            EmitRValueInto(rValue, destinationAddress);
            return;
        }

        if (source.Type.IsScalar)
        {
            EmitStore(source.Type, EmitExpression(source), destinationAddress);
            return;
        }

        if (source.Type.IsValueType)
        {
            var value = EmitExpression(source);
            _writer.Indented($"blit {value}, {destinationAddress}, {SizeOf(source.Type)}");

            if (source.Type is NubStructType structType)
            {
                var copyFunc = structType.Functions.FirstOrDefault(x => x.Hook == "oncopy");
                if (copyFunc != null)
                {
                    _writer.Indented($"call {StructFuncName(structType.Module, structType.Name, copyFunc.Name)}(l {destinationAddress})");
                }
            }

            return;
        }

        switch (source.Type)
        {
            case NubCStringType:
            {
                var value = EmitExpression(source);
                var size = EmitCStringSizeInBytes(value);
                var buffer = TmpName();
                _writer.Indented($"{buffer} =l alloc8 {size}");
                EmitMemcpy(value, buffer, size);
                EmitStore(source.Type, buffer, destinationAddress);
                return;
            }
            case NubStringType:
            {
                var value = EmitExpression(source);
                var size = EmitStringSizeInBytes(value);
                var buffer = TmpName();
                _writer.Indented($"{buffer} =l alloc8 {size}");
                EmitMemcpy(value, buffer, size);
                EmitStore(source.Type, buffer, destinationAddress);
                return;
            }
            case NubVoidType:
                throw new InvalidOperationException("Cannot copy void");
            default:
                throw new ArgumentOutOfRangeException(nameof(source.Type), $"Unknown type {source.Type}");
        }
    }

    private void EmitStructType(NubStructType structType)
    {
        // todo(nub31): qbe expects structs to be declared in order. We must Check the dependencies of the struct to see if a type need to be declared before this one
        // qbe allows multiple declarations of the same struct, but we should keep track and only emit an new one if necessary
        _writer.Write($"type {StructTypeName(structType.Module, structType.Name)} = {{ ");

        foreach (var field in structType.Fields)
        {
            _writer.Write($"{StructDefQBEType(field.Type)},");
        }

        _writer.WriteLine(" }");
    }

    private void EmitStructDefinition(StructNode structDef)
    {
        _writer.Comment($"{structDef.Module}::{structDef.Name}");
        _writer.WriteLine($"export function {StructCtorName(structDef.Module, structDef.Name)}(l %struct) {{");
        _writer.WriteLine("@start");

        foreach (var field in structDef.Fields)
        {
            if (field.Value != null)
            {
                var offset = OffsetOf(TypeOfStruct(structDef), field.Name);
                var destination = TmpName();
                _writer.Indented($"{destination} =l add %struct, {offset}");
                EmitCopyInto(field.Value, destination);
            }
        }

        _writer.Indented("ret");
        _writer.WriteLine("}");
        _writer.NewLine();

        foreach (var function in structDef.Functions)
        {
            _writer.Comment($"{structDef.Module}::{structDef.Name}.{function.Name}");
            _labelIndex = 0;
            _tmpIndex = 0;

            _writer.Write("export function ");

            if (function.Signature.ReturnType is not NubVoidType)
            {
                _writer.Write(FuncQBETypeName(function.Signature.ReturnType) + ' ');
            }

            _writer.Write(StructFuncName(structDef.Module, structDef.Name, function.Name));

            _writer.Write("(l %this, ");
            foreach (var parameter in function.Signature.Parameters)
            {
                _writer.Write(FuncQBETypeName(parameter.Type) + $" %{parameter.Name}, ");
            }

            _writer.WriteLine(") {");
            _writer.WriteLine("@start");

            var scope = new Scope();
            foreach (var parameter in function.Signature.Parameters)
            {
                scope.Variables.Push(new Variable(parameter.Name, parameter.Type));
            }

            EmitBlock(function.Body, scope);

            // Implicit return for void functions if no explicit return has been set
            if (function.Signature.ReturnType is NubVoidType && function.Body.Statements.LastOrDefault() is not ReturnNode)
            {
                _writer.Indented("ret");
            }

            _writer.WriteLine("}");
            _writer.NewLine();
        }
    }

    private void EmitFuncDefinition(FuncNode funcDef)
    {
        if (funcDef.Body == null) return;

        _labelIndex = 0;
        _tmpIndex = 0;

        _writer.Write(funcDef.ExternSymbol != null ? "export function " : "function ");

        if (funcDef.Signature.ReturnType is not NubVoidType)
        {
            _writer.Write(FuncQBETypeName(funcDef.Signature.ReturnType) + ' ');
        }

        _writer.Write(FuncName(funcDef.Module, funcDef.Name, funcDef.ExternSymbol));

        _writer.Write("(");
        foreach (var parameter in funcDef.Signature.Parameters)
        {
            _writer.Write(FuncQBETypeName(parameter.Type) + $" %{parameter.Name}");
        }

        _writer.WriteLine(") {");
        _writer.WriteLine("@start");

        var scope = new Scope();
        foreach (var parameter in funcDef.Signature.Parameters)
        {
            scope.Variables.Push(new Variable(parameter.Name, parameter.Type));
        }

        EmitBlock(funcDef.Body, scope);

        _writer.WriteLine("}");
        _writer.NewLine();
    }

    private void EmitBlock(BlockNode block, Scope? scope = null)
    {
        scope ??= new Scope();
        _scopes.Push(scope);

        foreach (var statement in block.Statements)
        {
            EmitStatement(statement);
        }

        EmitScopeCleanup();

        _scopes.Pop();
    }

    private void EmitStatement(StatementNode statement)
    {
        switch (statement)
        {
            case AssignmentNode assignment:
                EmitCopyInto(assignment.Value, EmitAddressOf(assignment.Target));
                break;
            case BlockNode block:
                EmitBlock(block);
                break;
            case BreakNode:
                EmitScopeCleanup();
                _writer.Indented($"jmp {_breakLabels.Peek()}");
                break;
            case ContinueNode:
                EmitScopeCleanup();
                _writer.Indented($"jmp {_continueLabels.Peek()}");
                break;
            case DeferNode defer:
                Scope.Defers.Push(defer);
                break;
            case IfNode ifStatement:
                EmitIf(ifStatement);
                break;
            case ReturnNode @return:
                EmitReturn(@return);
                break;
            case StatementFuncCallNode statementFuncCall:
                EmitFuncCall(statementFuncCall.FuncCall);
                break;
            case StatementStructFuncCallNode statementStructFuncCall:
                EmitStructFuncCall(statementStructFuncCall.StructFuncCall);
                break;
            case VariableDeclarationNode variableDeclaration:
                EmitVariableDeclaration(variableDeclaration);
                break;
            case WhileNode whileStatement:
                EmitWhile(whileStatement);
                break;
            default:
                throw new ArgumentOutOfRangeException(nameof(statement));
        }
    }

    private void EmitScopeCleanup()
    {
        while (Scope.Defers.TryPop(out var defer))
        {
            EmitStatement(defer.Statement);
        }

        while (Scope.Variables.TryPop(out var variable))
        {
            if (variable.Type is NubStructType structType)
            {
                var destroyFunc = structType.Functions.FirstOrDefault(x => x.Hook == "ondestroy");
                if (destroyFunc != null)
                {
                    _writer.Indented($"call {StructFuncName(structType.Module, structType.Name, destroyFunc.Name)}(l %{variable.Name})");
                }
            }
        }
    }

    private void EmitIf(IfNode ifStatement)
    {
        var trueLabel = LabelName();
        var falseLabel = LabelName();
        var endLabel = LabelName();

        var result = EmitExpression(ifStatement.Condition);
        _writer.Indented($"jnz {result}, {trueLabel}, {falseLabel}");
        _writer.WriteLine(trueLabel);
        EmitBlock(ifStatement.Body);
        _writer.Indented($"jmp {endLabel}");
        _writer.WriteLine(falseLabel);
        if (ifStatement.Else.HasValue)
        {
            ifStatement.Else.Value.Match(EmitIf, b => EmitBlock(b));
        }

        _writer.WriteLine(endLabel);
    }

    private void EmitReturn(ReturnNode @return)
    {
        if (@return.Value != null)
        {
            var result = EmitExpression(@return.Value);
            EmitScopeCleanup();
            _writer.Indented($"ret {result}");
        }
        else
        {
            EmitScopeCleanup();
            _writer.Indented("ret");
        }
    }

    private void EmitVariableDeclaration(VariableDeclarationNode variableDeclaration)
    {
        var name = $"%{variableDeclaration.Name}";
        _writer.Indented($"{name} =l alloc8 {SizeOf(variableDeclaration.Type)}");

        if (variableDeclaration.Assignment != null)
        {
            EmitCopyInto(variableDeclaration.Assignment, name);
        }

        Scope.Variables.Push(new Variable(variableDeclaration.Name, variableDeclaration.Type));
    }

    private void EmitWhile(WhileNode whileStatement)
    {
        var conditionLabel = LabelName();
        var iterationLabel = LabelName();
        var endLabel = LabelName();

        _breakLabels.Push(endLabel);
        _continueLabels.Push(conditionLabel);

        _writer.Indented($"jmp {conditionLabel}");
        _writer.WriteLine(iterationLabel);
        EmitBlock(whileStatement.Body);
        _writer.WriteLine(conditionLabel);
        var result = EmitExpression(whileStatement.Condition);
        _writer.Indented($"jnz {result}, {iterationLabel}, {endLabel}");
        _writer.WriteLine(endLabel);

        _continueLabels.Pop();
        _breakLabels.Pop();
    }

    private string EmitExpression(ExpressionNode expr)
    {
        return expr switch
        {
            RValueExpressionNode rValue => EmitRValue(rValue),
            LValueExpressionNode lValue => EmitLValue(lValue),
            _ => throw new ArgumentOutOfRangeException(nameof(expr))
        };
    }

    private string EmitRValue(RValueExpressionNode rValue)
    {
        return rValue switch
        {
            StringLiteralNode expr => EmitStringLiteral(expr),
            CStringLiteralNode expr => EmitCStringLiteral(expr),
            IntLiteralNode expr => expr.Value.ToString(),
            UIntLiteralNode expr => expr.Value.ToString(),
            Float32LiteralNode expr => BitConverter.SingleToInt32Bits(expr.Value).ToString(),
            Float64LiteralNode expr => BitConverter.DoubleToInt64Bits(expr.Value).ToString(),
            BoolLiteralNode expr => expr.Value ? "1" : "0",
            AddressOfNode expr => EmitAddressOf(expr.LValue),
            ArrayInitializerNode expr => EmitArrayInitializer(expr),
            BinaryExpressionNode expr => EmitBinaryExpression(expr),
            ConvertFloatNode expr => EmitConvertFloat(expr),
            ConvertIntNode expr => EmitConvertInt(expr),
            FuncCallNode expr => EmitFuncCall(expr),
            FuncIdentifierNode expr => FuncName(expr.Module, expr.Name, expr.ExternSymbol),
            RValueIdentifierNode expr => $"%{expr.Name}",
            StructFuncCallNode expr => EmitStructFuncCall(expr),
            StructInitializerNode expr => EmitStructInitializer(expr),
            UnaryExpressionNode expr => EmitUnaryExpression(expr),
            SizeBuiltinNode expr => $"{SizeOf(expr.TargetType)}",
            FloatToIntBuiltinNode expr => EmitFloatToIntBuiltin(expr),
            _ => throw new ArgumentOutOfRangeException(nameof(rValue))
        };
    }

    private void EmitRValueInto(RValueExpressionNode rValue, string destinationAddress)
    {
        if (rValue.Type.IsScalar)
        {
            EmitStore(rValue.Type, EmitRValue(rValue), destinationAddress);
            return;
        }

        switch (rValue)
        {
            case StructInitializerNode expr:
            {
                EmitStructInitializer(expr, destinationAddress);
                return;
            }
            case StringLiteralNode expr:
            {
                // String literals are read only, so safe to use without copy
                EmitStore(expr.Type, EmitStringLiteral(expr), destinationAddress);
                return;
            }
            case CStringLiteralNode expr:
            {
                // String literals are read only, so safe to use without copy
                EmitStore(expr.Type, EmitCStringLiteral(expr), destinationAddress);
                return;
            }
            default:
            {
                // If no special case is provided, fall back to copying
                Console.WriteLine($"No fast initialization for {rValue.GetType().Name}. Using copy fallback");
                EmitCopyInto(rValue, destinationAddress, false);
                break;
            }
        }
    }

    private string EmitLValue(LValueExpressionNode lValue)
    {
        var address = EmitAddressOf(lValue);

        if (lValue.Type is { IsValueType: true, IsScalar: false })
        {
            return address;
        }

        return EmitLoad(lValue.Type, address);
    }

    private string EmitStringLiteral(StringLiteralNode expr)
    {
        var stringLiteral = new StringLiteral(expr.Value, StringName());
        _stringLiterals.Add(stringLiteral);
        return stringLiteral.Name;
    }

    private string EmitCStringLiteral(CStringLiteralNode expr)
    {
        var cStringLiteral = new CStringLiteral(expr.Value, CStringName());
        _cStringLiterals.Add(cStringLiteral);
        return cStringLiteral.Name;
    }

    private string EmitArrayInitializer(ArrayInitializerNode arrayInitializer)
    {
        var capacity = EmitExpression(arrayInitializer.Capacity);
        var elementSize = SizeOf(arrayInitializer.ElementType);

        var capacityInBytes = TmpName();
        _writer.Indented($"{capacityInBytes} =l mul {capacity}, {elementSize}");
        var totalSize = TmpName();
        _writer.Indented($"{totalSize} =l add {capacityInBytes}, 8");

        var arrayPointer = TmpName();
        _writer.Indented($"{arrayPointer} =l alloc8 {totalSize}");
        _writer.Indented($"storel {capacity}, {arrayPointer}");

        var dataPointer = TmpName();
        _writer.Indented($"{dataPointer} =l add {arrayPointer}, 8");
        EmitMemset(dataPointer, 0, capacityInBytes);

        return arrayPointer;
    }

    private string EmitAddressOf(LValueExpressionNode lval)
    {
        return lval switch
        {
            ArrayIndexAccessNode arrayIndexAccess => EmitAddressOfArrayIndexAccess(arrayIndexAccess),
            DereferenceNode dereference => EmitExpression(dereference.Expression),
            StructFieldAccessNode structFieldAccess => EmitAddressOfStructFieldAccess(structFieldAccess),
            LValueIdentifierNode variableIdent => $"%{variableIdent.Name}",
            _ => throw new ArgumentOutOfRangeException(nameof(lval))
        };
    }

    private string EmitAddressOfArrayIndexAccess(ArrayIndexAccessNode arrayIndexAccess)
    {
        var array = EmitExpression(arrayIndexAccess.Target);
        var index = EmitExpression(arrayIndexAccess.Index);

        var elementType = ((NubArrayType)arrayIndexAccess.Target.Type).ElementType;

        var offset = TmpName();
        _writer.Indented($"{offset} =l mul {index}, {SizeOf(elementType)}");
        _writer.Indented($"{offset} =l add {offset}, 8");
        _writer.Indented($"{offset} =l add {array}, {offset}");
        return offset;
    }

    private string EmitAddressOfStructFieldAccess(StructFieldAccessNode structFieldAccess)
    {
        var structType = (NubStructType)structFieldAccess.Target.Type;
        var target = EmitExpression(structFieldAccess.Target);
        var offset = OffsetOf(structType, structFieldAccess.Field);

        var address = TmpName();
        _writer.Indented($"{address} =l add {target}, {offset}");
        return address;
    }

    private string EmitBinaryExpression(BinaryExpressionNode binaryExpression)
    {
        var left = EmitExpression(binaryExpression.Left);
        var right = EmitExpression(binaryExpression.Right);

        var outputName = TmpName();

        var instruction = EmitBinaryInstructionForOperator(binaryExpression.Operator, binaryExpression.Left.Type);

        _writer.Indented($"{outputName} {QBEAssign(binaryExpression.Left.Type)} {instruction} {left}, {right}");
        return outputName;
    }

    private static string EmitBinaryInstructionForOperator(BinaryOperator op, NubType type)
    {
        // todo(nub31): Add support for string concatenation. Currently this expects ints or floats and will treat strings as ints
        return op switch
        {
            BinaryOperator.RightShift => type switch
            {
                NubIntType { Signed: true } => "sar",
                NubIntType { Signed: false } => "shr",
                _ => throw new NotSupportedException($"Right shift not supported for type '{type}'")
            },
            BinaryOperator.BitwiseAnd => "and",
            BinaryOperator.BitwiseOr => "or",
            BinaryOperator.BitwiseXor => "xor",
            BinaryOperator.LeftShift => "shl",
            BinaryOperator.Divide => type switch
            {
                NubIntType { Signed: true } => "div",
                NubIntType { Signed: false } => "udiv",
                NubFloatType => "div",
                _ => throw new NotSupportedException($"Division not supported for type '{type}'")
            },
            BinaryOperator.Modulo => type switch
            {
                NubIntType { Signed: true } => "rem",
                NubIntType { Signed: false } => "urem",
                _ => throw new NotSupportedException($"Modulo not supported for type '{type}'")
            },
            BinaryOperator.Plus => "add",
            BinaryOperator.Minus => "sub",
            BinaryOperator.Multiply => "mul",
            BinaryOperator.Equal => type switch
            {
                NubIntType intType => intType.Width switch
                {
                    <= 32 => "ceqw",
                    64 => "ceql",
                    _ => throw new ArgumentOutOfRangeException()
                },
                NubFloatType floatType => floatType.Width switch
                {
                    32 => "ceqs",
                    64 => "ceqd",
                    _ => throw new ArgumentOutOfRangeException()
                },
                _ => throw new NotSupportedException($"Equality comparison not supported for type '{type}'")
            },
            BinaryOperator.NotEqual => type switch
            {
                NubIntType intType => intType.Width switch
                {
                    <= 32 => "cnew",
                    64 => "cnel",
                    _ => throw new ArgumentOutOfRangeException()
                },
                NubFloatType floatType => floatType.Width switch
                {
                    32 => "cnes",
                    64 => "cned",
                    _ => throw new ArgumentOutOfRangeException()
                },
                _ => throw new NotSupportedException($"Inequality comparison not supported for type '{type}'")
            },
            BinaryOperator.LessThan => type switch
            {
                NubIntType { Signed: true } intType => intType.Width switch
                {
                    <= 32 => "csltw",
                    64 => "csltl",
                    _ => throw new ArgumentOutOfRangeException()
                },
                NubIntType { Signed: false } intType => intType.Width switch
                {
                    <= 32 => "cultw",
                    64 => "cultl",
                    _ => throw new ArgumentOutOfRangeException()
                },
                NubFloatType floatType => floatType.Width switch
                {
                    32 => "clts",
                    64 => "cltd",
                    _ => throw new ArgumentOutOfRangeException()
                },
                _ => throw new NotSupportedException($"Less than comparison not supported for type '{type}'")
            },
            BinaryOperator.LessThanOrEqual => type switch
            {
                NubIntType { Signed: true } intType => intType.Width switch
                {
                    <= 32 => "cslew",
                    64 => "cslel",
                    _ => throw new ArgumentOutOfRangeException()
                },
                NubIntType { Signed: false } intType => intType.Width switch
                {
                    <= 32 => "culew",
                    64 => "culel",
                    _ => throw new ArgumentOutOfRangeException()
                },
                NubFloatType floatType => floatType.Width switch
                {
                    32 => "cles",
                    64 => "cled",
                    _ => throw new ArgumentOutOfRangeException()
                },
                _ => throw new NotSupportedException($"Less than or equal comparison not supported for type '{type}'")
            },
            BinaryOperator.GreaterThan => type switch
            {
                NubIntType { Signed: true } intType => intType.Width switch
                {
                    <= 32 => "csgtw",
                    64 => "csgtl",
                    _ => throw new ArgumentOutOfRangeException()
                },
                NubIntType { Signed: false } intType => intType.Width switch
                {
                    <= 32 => "cugtw",
                    64 => "cugtl",
                    _ => throw new ArgumentOutOfRangeException()
                },
                NubFloatType floatType => floatType.Width switch
                {
                    32 => "cgts",
                    64 => "cgtd",
                    _ => throw new ArgumentOutOfRangeException()
                },
                _ => throw new NotSupportedException($"Greater than comparison not supported for type '{type}'")
            },
            BinaryOperator.GreaterThanOrEqual => type switch
            {
                NubIntType { Signed: true } intType => intType.Width switch
                {
                    <= 32 => "csgew",
                    64 => "csgel",
                    _ => throw new ArgumentOutOfRangeException()
                },
                NubIntType { Signed: false } intType => intType.Width switch
                {
                    <= 32 => "cugew",
                    64 => "cugel",
                    _ => throw new ArgumentOutOfRangeException()
                },
                NubFloatType floatType => floatType.Width switch
                {
                    32 => "cges",
                    64 => "cged",
                    _ => throw new ArgumentOutOfRangeException()
                },
                _ => throw new NotSupportedException($"Greater than or equal comparison not supported for type '{type}'")
            },
            // todo(nub31): Implement short circuiting
            BinaryOperator.LogicalAnd => "and",
            BinaryOperator.LogicalOr => "or",
            _ => throw new ArgumentOutOfRangeException(nameof(op))
        };
    }

    private string EmitStructInitializer(StructInitializerNode structInitializer)
    {
        var destinationAddress = TmpName();
        var size = SizeOf(structInitializer.StructType);
        _writer.Indented($"{destinationAddress} =l alloc8 {size}");
        EmitStructInitializer(structInitializer, destinationAddress);
        return destinationAddress;
    }

    private void EmitStructInitializer(StructInitializerNode structInitializer, string destinationAddress)
    {
        _writer.Indented($"call {StructCtorName(structInitializer.StructType.Module, structInitializer.StructType.Name)}(l {destinationAddress})");

        var createFunc = structInitializer.StructType.Functions.FirstOrDefault(x => x.Hook == "oncreate");
        if (createFunc != null)
        {
            _writer.Indented($"call {StructFuncName(structInitializer.StructType.Module, structInitializer.StructType.Name, createFunc.Name)}(l {destinationAddress})");
        }

        foreach (var (field, value) in structInitializer.Initializers)
        {
            var offset = TmpName();
            _writer.Indented($"{offset} =l add {destinationAddress}, {OffsetOf(structInitializer.StructType, field)}");
            EmitCopyInto(value, offset);
        }
    }

    private string EmitUnaryExpression(UnaryExpressionNode unaryExpression)
    {
        var operand = EmitExpression(unaryExpression.Operand);
        var outputName = TmpName();

        switch (unaryExpression.Operator)
        {
            case UnaryOperator.Negate:
            {
                switch (unaryExpression.Operand.Type)
                {
                    case NubIntType { Signed: true, Width: 64 }:
                        _writer.Indented($"{outputName} =l neg {operand}");
                        return outputName;
                    case NubIntType { Signed: true, Width: 8 or 16 or 32 }:
                        _writer.Indented($"{outputName} =w neg {operand}");
                        return outputName;
                    case NubFloatType { Width: 64 }:
                        _writer.Indented($"{outputName} =d neg {operand}");
                        return outputName;
                    case NubFloatType { Width: 32 }:
                        _writer.Indented($"{outputName} =s neg {operand}");
                        return outputName;
                }

                break;
            }
            case UnaryOperator.Invert:
            {
                switch (unaryExpression.Operand.Type)
                {
                    case NubBoolType:
                        _writer.Indented($"{outputName} =w xor {operand}, 1");
                        return outputName;
                }

                break;
            }
            default:
            {
                throw new ArgumentOutOfRangeException();
            }
        }

        throw new NotSupportedException($"Unary operator {unaryExpression.Operator} for type {unaryExpression.Operand.Type} not supported");
    }

    private string EmitStructFuncCall(StructFuncCallNode structFuncCall)
    {
        var func = StructFuncName(structFuncCall.Module, structFuncCall.StructName, structFuncCall.FuncName);

        var thisParameter = EmitExpression(structFuncCall.StructExpression);

        List<string> parameterStrings = [$"l {thisParameter}"];

        foreach (var parameter in structFuncCall.Parameters)
        {
            var value = EmitExpression(parameter);

            if (parameter.Type is NubStructType structType)
            {
                var copyFunc = structType.Functions.FirstOrDefault(x => x.Hook == "oncopy");
                if (copyFunc != null)
                {
                    _writer.Indented($"call {StructFuncName(structType.Module, structType.Name, copyFunc.Name)}(l {value})");
                }
            }

            parameterStrings.Add($"{FuncQBETypeName(parameter.Type)} {value}");
        }

        if (structFuncCall.Type is NubVoidType)
        {
            _writer.Indented($"call {func}({string.Join(", ", parameterStrings)})");
            return string.Empty;
        }
        else
        {
            var outputName = TmpName();
            _writer.Indented($"{outputName} {QBEAssign(structFuncCall.Type)} call {func}({string.Join(", ", parameterStrings)})");
            return outputName;
        }
    }

    private string EmitConvertInt(ConvertIntNode convertInt)
    {
        var value = EmitExpression(convertInt.Value);

        if (convertInt.ValueType.Width >= convertInt.TargetType.Width)
        {
            return value;
        }

        var method = convertInt.ValueType.Signed switch
        {
            true => convertInt.ValueType.Width switch
            {
                8 => "extsb",
                16 => "extsh",
                32 => "extsw",
                _ => throw new ArgumentOutOfRangeException()
            },
            false => convertInt.ValueType.Width switch
            {
                8 => "extub",
                16 => "extuh",
                32 => "extuw",
                _ => throw new ArgumentOutOfRangeException()
            }
        };

        var result = TmpName();
        _writer.Indented($"{result} {QBEAssign(convertInt.TargetType)} {method} {value}");
        return result;
    }

    private string EmitConvertFloat(ConvertFloatNode convertFloat)
    {
        var value = EmitExpression(convertFloat.Value);

        if (convertFloat.ValueType.Width == convertFloat.TargetType.Width)
        {
            return value;
        }

        var method = convertFloat.ValueType.Width switch
        {
            32 => "exts",
            64 => "truncd",
            _ => throw new ArgumentOutOfRangeException()
        };

        var result = TmpName();
        _writer.Indented($"{result} {QBEAssign(convertFloat.TargetType)} {method} {value}");
        return result;
    }

    private string EmitFloatToIntBuiltin(FloatToIntBuiltinNode floatToInt)
    {
        var value = EmitExpression(floatToInt.Value);

        var method = floatToInt.TargetType.Signed switch
        {
            true => floatToInt.ValueType.Width switch
            {
                32 => "stosi",
                64 => "dtosi",
                _ => throw new ArgumentOutOfRangeException()
            },
            false => floatToInt.ValueType.Width switch
            {
                32 => "stoui",
                64 => "dtoui",
                _ => throw new ArgumentOutOfRangeException()
            }
        };

        var result = TmpName();
        _writer.Indented($"{result} {QBEAssign(floatToInt.TargetType)} {method} {value}");
        return result;
    }

    private string EmitFuncCall(FuncCallNode funcCall)
    {
        var funcPointer = EmitExpression(funcCall.Expression);

        var parameterStrings = new List<string>();

        foreach (var parameter in funcCall.Parameters)
        {
            var value = EmitExpression(parameter);

            if (parameter.Type is NubStructType structType)
            {
                var copyFunc = structType.Functions.FirstOrDefault(x => x.Hook == "oncopy");
                if (copyFunc != null)
                {
                    _writer.Indented($"call {StructFuncName(structType.Module, structType.Name, copyFunc.Name)}(l {value})");
                }
            }

            parameterStrings.Add($"{FuncQBETypeName(parameter.Type)} {value}");
        }

        if (funcCall.Type is NubVoidType)
        {
            _writer.Indented($"call {funcPointer}({string.Join(", ", parameterStrings)})");
            return string.Empty;
        }
        else
        {
            var outputName = TmpName();
            _writer.Indented($"{outputName} {QBEAssign(funcCall.Type)} call {funcPointer}({string.Join(", ", parameterStrings)})");
            return outputName;
        }
    }

    private static int SizeOf(NubType type)
    {
        return type switch
        {
            NubIntType intType => intType.Width / 8,
            NubFloatType fType => fType.Width / 8,
            NubBoolType => 1,
            NubPointerType => PTR_SIZE,
            NubFuncType => PTR_SIZE,
            NubStructType structType => CalculateStructSize(structType),
            NubVoidType => throw new InvalidOperationException("Void type has no size"),
            NubCStringType => PTR_SIZE,
            NubStringType => PTR_SIZE,
            NubArrayType => PTR_SIZE,
            _ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown type: {type.GetType()}")
        };
    }

    private static int CalculateStructSize(NubStructType structType)
    {
        var offset = 0;

        foreach (var field in structType.Fields)
        {
            var fieldAlignment = AlignmentOf(field.Type);
            offset = AlignTo(offset, fieldAlignment);
            offset += SizeOf(field.Type);
        }

        var structAlignment = CalculateStructAlignment(structType);
        return AlignTo(offset, structAlignment);
    }

    private static int AlignmentOf(NubType type)
    {
        return type switch
        {
            NubIntType intType => intType.Width / 8,
            NubFloatType fType => fType.Width / 8,
            NubBoolType => 1,
            NubPointerType => PTR_SIZE,
            NubFuncType => PTR_SIZE,
            NubStructType st => CalculateStructAlignment(st),
            NubCStringType => PTR_SIZE,
            NubStringType => PTR_SIZE,
            NubArrayType => PTR_SIZE,
            NubVoidType => throw new InvalidOperationException("Void has no alignment"),
            _ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown type: {type.GetType()}")
        };
    }


    private static int CalculateStructAlignment(NubStructType structType)
    {
        var maxAlignment = 1;

        foreach (var field in structType.Fields)
        {
            var fieldAlignment = AlignmentOf(field.Type);
            maxAlignment = Math.Max(maxAlignment, fieldAlignment);
        }

        return maxAlignment;
    }

    private static int AlignTo(int offset, int alignment)
    {
        return (offset + alignment - 1) & ~(alignment - 1);
    }

    private static int OffsetOf(NubStructType type, string member)
    {
        var offset = 0;

        foreach (var field in type.Fields)
        {
            var fieldAlignment = AlignmentOf(field.Type);
            offset = AlignTo(offset, fieldAlignment);

            if (field.Name == member)
            {
                return offset;
            }

            offset += SizeOf(field.Type);
        }

        throw new UnreachableException($"Member '{member}' not found in struct");
    }

    private NubStructType TypeOfStruct(StructNode definition)
    {
        var fieldTypes = definition.Fields
            .Select(x => new NubStructFieldType(x.Name, x.Type, x.Value != null))
            .ToList();

        var functionTypes = new List<NubStructFuncType>();
        foreach (var function in definition.Functions)
        {
            var parameters = function.Signature.Parameters.Select(x => x.Type).ToList();
            functionTypes.Add(new NubStructFuncType(function.Name, function.Hook, parameters, function.Signature.ReturnType));
        }

        return new NubStructType(definition.Module, definition.Name, fieldTypes, functionTypes);
    }

    private string TmpName()
    {
        return $"%.t{++_tmpIndex}";
    }

    private string LabelName()
    {
        return $"@.l{++_labelIndex}";
    }

    private string CStringName()
    {
        return $"$.cstr{++_cStringLiteralIndex}";
    }

    private string StringName()
    {
        return $"$.str{++_stringLiteralIndex}";
    }

    private static string FuncName(string module, string name, string? externSymbol)
    {
        return $"${externSymbol ?? $".{module}.{name}"}";
    }

    private static string StructTypeName(string module, string name)
    {
        return $":{module}.{name}";
    }

    private static string StructCtorName(string module, string name)
    {
        return $"$.{module}.{name}.ctor";
    }

    private static string StructFuncName(string module, string structName, string funcName)
    {
        return $"$.{module}.{structName}.func.{funcName}";
    }
}

public class Scope
{
    public readonly Stack<Variable> Variables = [];
    public readonly Stack<DeferNode> Defers = [];
}

public record Variable(string Name, NubType Type);

public class StringLiteral(string value, string name)
{
    public string Value { get; } = value;
    public string Name { get; } = name;
}

public class CStringLiteral(string value, string name)
{
    public string Value { get; } = value;
    public string Name { get; } = name;
}