using System.Diagnostics;
using System.Text;
using NubLang.Ast;
using NubLang.Syntax;

namespace NubLang.Generation;

public class Generator
{
    private readonly CompilationUnit _compilationUnit;
    private readonly IndentedTextWriter _writer;
    private readonly Stack<List<DeferNode>> _deferStack = [];
    private int _tmpIndex;

    public Generator(CompilationUnit compilationUnit)
    {
        _compilationUnit = compilationUnit;
        _writer = new IndentedTextWriter();
    }

    // todo(nub31): Handle name collisions
    private string NewTmp()
    {
        return $"_t{++_tmpIndex}";
    }

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

    private static string StructName(string module, string name)
    {
        return $"{module}_{name}";
    }

    public string Emit()
    {
        _writer.WriteLine("""
                          #include <stdint.h>
                          #include <stddef.h>

                          typedef struct
                          {
                              size_t length;
                              char *data;
                          } string;

                          typedef struct
                          {
                              size_t length;
                              void *data;
                          } slice;

                          """);

        foreach (var structType in _compilationUnit.ImportedStructTypes)
        {
            _writer.WriteLine("typedef struct");
            _writer.WriteLine("{");
            using (_writer.Indent())
            {
                foreach (var field in structType.Fields)
                {
                    _writer.WriteLine($"{CType.Create(field.Type, field.Name, constArraysAsPointers: false)};");
                }
            }

            _writer.WriteLine($"}} {StructName(structType.Module, structType.Name)};");
            _writer.WriteLine();
        }

        // note(nub31): Forward declarations
        foreach (var prototype in _compilationUnit.ImportedFunctions)
        {
            EmitLine(prototype.Tokens.FirstOrDefault());
            var parameters = prototype.Parameters.Count != 0
                ? string.Join(", ", prototype.Parameters.Select(x => CType.Create(x.Type, x.Name)))
                : "void";

            var name = FuncName(prototype.Module, prototype.Name, prototype.ExternSymbol);
            _writer.WriteLine($"{CType.Create(prototype.ReturnType, name)}({parameters});");
            _writer.WriteLine();
        }

        // note(nub31): Normal functions
        foreach (var funcNode in _compilationUnit.Functions)
        {
            if (funcNode.Body == null) continue;

            EmitLine(funcNode.Tokens.FirstOrDefault());
            var parameters = funcNode.Prototype.Parameters.Count != 0
                ? string.Join(", ", funcNode.Prototype.Parameters.Select(x => CType.Create(x.Type, x.Name)))
                : "void";

            var name = FuncName(funcNode.Module, funcNode.Name, funcNode.Prototype.ExternSymbol);
            _writer.WriteLine($"{CType.Create(funcNode.Prototype.ReturnType, name)}({parameters})");
            _writer.WriteLine("{");
            using (_writer.Indent())
            {
                EmitBlock(funcNode.Body);
            }

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

        return _writer.ToString();
    }

    private void EmitStatement(StatementNode statementNode)
    {
        EmitLine(statementNode.Tokens.FirstOrDefault());
        switch (statementNode)
        {
            case AssignmentNode assignmentNode:
                EmitAssignment(assignmentNode);
                break;
            case BlockNode blockNode:
                _writer.WriteLine("{");
                using (_writer.Indent())
                {
                    EmitBlock(blockNode);
                }

                _writer.WriteLine("}");
                break;
            case BreakNode breakNode:
                EmitBreak(breakNode);
                break;
            case ContinueNode continueNode:
                EmitContinue(continueNode);
                break;
            case DeferNode deferNode:
                EmitDefer(deferNode);
                break;
            case ForConstArrayNode forConstArrayNode:
                EmitForConstArray(forConstArrayNode);
                break;
            case ForSliceNode forSliceNode:
                EmitForSlice(forSliceNode);
                break;
            case IfNode ifNode:
                EmitIf(ifNode);
                break;
            case ReturnNode returnNode:
                EmitReturn(returnNode);
                break;
            case StatementFuncCallNode statementFuncCallNode:
                EmitStatementFuncCall(statementFuncCallNode);
                break;
            case VariableDeclarationNode variableDeclarationNode:
                EmitVariableDeclaration(variableDeclarationNode);
                break;
            case WhileNode whileNode:
                EmitWhile(whileNode);
                break;
            default:
                throw new ArgumentOutOfRangeException(nameof(statementNode));
        }
    }

    private void EmitLine(Token? token)
    {
        if (token == null) return;
        var file = token.Span.FilePath;
        var line = token.Span.Start.Line;
        _writer.WriteLine($"#line {line} \"{file}\"");
    }

    private void EmitAssignment(AssignmentNode assignmentNode)
    {
        var target = EmitExpression(assignmentNode.Target);
        var value = EmitExpression(assignmentNode.Value);
        _writer.WriteLine($"{target} = {value};");
    }

    private void EmitBreak(BreakNode _)
    {
        // todo(nub31): Emit deferred statements
        _writer.WriteLine("break;");
    }

    private void EmitContinue(ContinueNode _)
    {
        // todo(nub31): Emit deferred statements
        _writer.WriteLine("continue;");
    }

    private void EmitDefer(DeferNode deferNode)
    {
        _deferStack.Peek().Add(deferNode);
    }

    private void EmitForSlice(ForSliceNode forSliceNode)
    {
        var targetType = (NubSliceType)forSliceNode.Target.Type;
        var target = EmitExpression(forSliceNode.Target);
        var indexName = forSliceNode.IndexName ?? NewTmp();

        _writer.WriteLine($"for (size_t {indexName} = 0; {indexName} < {target}.length; ++{indexName})");
        _writer.WriteLine("{");
        using (_writer.Indent())
        {
            _writer.WriteLine($"{CType.Create(targetType.ElementType, forSliceNode.ElementName)} = (({CType.Create(targetType.ElementType)}*){target}.data)[{indexName}];");
            EmitBlock(forSliceNode.Body);
        }

        _writer.WriteLine("}");
    }

    private void EmitForConstArray(ForConstArrayNode forConstArrayNode)
    {
        var targetType = (NubConstArrayType)forConstArrayNode.Target.Type;
        var target = EmitExpression(forConstArrayNode.Target);
        var indexName = forConstArrayNode.IndexName ?? NewTmp();

        _writer.WriteLine($"for (size_t {indexName} = 0; {indexName} < {targetType.Size}; ++{indexName})");
        _writer.WriteLine("{");
        using (_writer.Indent())
        {
            _writer.WriteLine($"{CType.Create(targetType.ElementType, forConstArrayNode.ElementName)} = {target}[{indexName}];");
            EmitBlock(forConstArrayNode.Body);
        }

        _writer.WriteLine("}");
    }

    private void EmitIf(IfNode ifNode, bool elseIf = false)
    {
        var condition = EmitExpression(ifNode.Condition);
        _writer.WriteLine($"{(elseIf ? "else " : "")}if ({condition})");
        _writer.WriteLine("{");
        using (_writer.Indent())
        {
            EmitBlock(ifNode.Body);
        }

        _writer.WriteLine("}");
        ifNode.Else?.Match
        (
            elseIfNode => EmitIf(elseIfNode, true),
            elseNode =>
            {
                _writer.WriteLine("else");
                _writer.WriteLine("{");
                using (_writer.Indent())
                {
                    EmitBlock(elseNode);
                }

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

    private void EmitReturn(ReturnNode returnNode)
    {
        if (returnNode.Value == null)
        {
            var blockDefers = _deferStack.Peek();
            for (var i = blockDefers.Count - 1; i >= 0; i--)
            {
                EmitStatement(blockDefers[i].Statement);
            }

            _writer.WriteLine("return;");
        }
        else
        {
            var returnValue = EmitExpression(returnNode.Value);

            if (_deferStack.Peek().Count != 0)
            {
                var tmp = NewTmp();
                _writer.WriteLine($"{CType.Create(returnNode.Value.Type, tmp)} = {returnValue};");

                var blockDefers = _deferStack.Peek();
                for (var i = blockDefers.Count - 1; i >= 0; i--)
                {
                    EmitStatement(blockDefers[i].Statement);
                }

                EmitLine(returnNode.Tokens.FirstOrDefault());
                _writer.WriteLine($"return {tmp};");
            }
            else
            {
                EmitLine(returnNode.Tokens.FirstOrDefault());
                _writer.WriteLine($"return {returnValue};");
            }
        }
    }

    private void EmitStatementFuncCall(StatementFuncCallNode statementFuncCallNode)
    {
        var funcCall = EmitFuncCall(statementFuncCallNode.FuncCall);
        _writer.WriteLine($"{funcCall};");
    }

    private void EmitVariableDeclaration(VariableDeclarationNode variableDeclarationNode)
    {
        if (variableDeclarationNode.Assignment != null)
        {
            var value = EmitExpression(variableDeclarationNode.Assignment);
            _writer.WriteLine($"{CType.Create(variableDeclarationNode.Type, variableDeclarationNode.Name)} = {value};");
        }
        else
        {
            _writer.WriteLine($"{CType.Create(variableDeclarationNode.Type, variableDeclarationNode.Name)};");
        }
    }

    private void EmitWhile(WhileNode whileNode)
    {
        var condition = EmitExpression(whileNode.Condition);
        _writer.WriteLine($"while ({condition})");
        _writer.WriteLine("{");
        using (_writer.Indent())
        {
            EmitBlock(whileNode.Body);
        }

        _writer.WriteLine("}");
    }

    private string EmitExpression(ExpressionNode expressionNode)
    {
        if (expressionNode is IntermediateExpression)
        {
            throw new UnreachableException("Type checker fucked up");
        }

        var expr = expressionNode switch
        {
            ArrayIndexAccessNode arrayIndexAccessNode => EmitArrayIndexAccess(arrayIndexAccessNode),
            ArrayInitializerNode arrayInitializerNode => EmitArrayInitializer(arrayInitializerNode),
            BinaryExpressionNode binaryExpressionNode => EmitBinaryExpression(binaryExpressionNode),
            BoolLiteralNode boolLiteralNode => boolLiteralNode.Value ? "true" : "false",
            ConstArrayIndexAccessNode constArrayIndexAccessNode => EmitConstArrayIndexAccess(constArrayIndexAccessNode),
            ConstArrayInitializerNode constArrayInitializerNode => EmitConstArrayInitializer(constArrayInitializerNode),
            CStringLiteralNode cStringLiteralNode => $"\"{cStringLiteralNode.Value}\"",
            DereferenceNode dereferenceNode => EmitDereference(dereferenceNode),
            Float32LiteralNode float32LiteralNode => EmitFloat32Literal(float32LiteralNode),
            Float64LiteralNode float64LiteralNode => EmitFloat64Literal(float64LiteralNode),
            CastNode castNode => EmitCast(castNode),
            FuncCallNode funcCallNode => EmitFuncCall(funcCallNode),
            FuncIdentifierNode funcIdentifierNode => FuncName(funcIdentifierNode.Module, funcIdentifierNode.Name, funcIdentifierNode.ExternSymbol),
            AddressOfNode addressOfNode => EmitAddressOf(addressOfNode),
            SizeNode sizeBuiltinNode => $"sizeof({CType.Create(sizeBuiltinNode.TargetType)})",
            SliceIndexAccessNode sliceIndexAccessNode => EmitSliceArrayIndexAccess(sliceIndexAccessNode),
            StringLiteralNode stringLiteralNode => EmitStringLiteral(stringLiteralNode),
            StructFieldAccessNode structFieldAccessNode => EmitStructFieldAccess(structFieldAccessNode),
            StructInitializerNode structInitializerNode => EmitStructInitializer(structInitializerNode),
            I8LiteralNode i8LiteralNode => EmitI8Literal(i8LiteralNode),
            I16LiteralNode i16LiteralNode => EmitI16Literal(i16LiteralNode),
            I32LiteralNode i32LiteralNode => EmitI32Literal(i32LiteralNode),
            I64LiteralNode i64LiteralNode => EmitI64Literal(i64LiteralNode),
            U8LiteralNode u8LiteralNode => EmitU8Literal(u8LiteralNode),
            U16LiteralNode u16LiteralNode => EmitU16Literal(u16LiteralNode),
            U32LiteralNode u32LiteralNode => EmitU32Literal(u32LiteralNode),
            U64LiteralNode u64LiteralNode => EmitU64Literal(u64LiteralNode),
            UnaryExpressionNode unaryExpressionNode => EmitUnaryExpression(unaryExpressionNode),
            VariableIdentifierNode variableIdentifierNode => variableIdentifierNode.Name,
            _ => throw new ArgumentOutOfRangeException(nameof(expressionNode))
        };

        return $"({expr})";
    }

    private string EmitArrayIndexAccess(ArrayIndexAccessNode arrayIndexAccessNode)
    {
        var target = EmitExpression(arrayIndexAccessNode.Target);
        var index = EmitExpression(arrayIndexAccessNode.Index);
        return $"{target}[{index}]";
    }

    private string EmitArrayInitializer(ArrayInitializerNode arrayInitializerNode)
    {
        var values = new List<string>();
        foreach (var value in arrayInitializerNode.Values)
        {
            values.Add(EmitExpression(value));
        }

        var arrayType = (NubArrayType)arrayInitializerNode.Type;
        return $"({CType.Create(arrayType.ElementType)}[]){{{string.Join(", ", values)}}}";
    }

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

        var op = binaryExpressionNode.Operator switch
        {
            BinaryOperator.Plus => "+",
            BinaryOperator.Minus => "-",
            BinaryOperator.Multiply => "*",
            BinaryOperator.Divide => "/",
            BinaryOperator.Modulo => "%",
            BinaryOperator.Equal => "==",
            BinaryOperator.NotEqual => "!=",
            BinaryOperator.LessThan => "<",
            BinaryOperator.LessThanOrEqual => "<=",
            BinaryOperator.GreaterThan => ">",
            BinaryOperator.GreaterThanOrEqual => ">=",
            BinaryOperator.LogicalAnd => "&&",
            BinaryOperator.LogicalOr => "||",
            BinaryOperator.BitwiseAnd => "&",
            BinaryOperator.BitwiseOr => "|",
            BinaryOperator.BitwiseXor => "^",
            BinaryOperator.LeftShift => "<<",
            BinaryOperator.RightShift => ">>",
            _ => throw new ArgumentOutOfRangeException()
        };

        return $"{left} {op} {right}";
    }

    private string EmitConstArrayIndexAccess(ConstArrayIndexAccessNode constArrayIndexAccessNode)
    {
        var target = EmitExpression(constArrayIndexAccessNode.Target);
        var index = EmitExpression(constArrayIndexAccessNode.Index);
        // todo(nub31): We can emit bounds checking here
        return $"{target}[{index}]";
    }

    private string EmitConstArrayInitializer(ConstArrayInitializerNode arrayInitializerNode)
    {
        var values = new List<string>();
        foreach (var value in arrayInitializerNode.Values)
        {
            values.Add(EmitExpression(value));
        }

        var arrayType = (NubConstArrayType)arrayInitializerNode.Type;
        return $"({CType.Create(arrayType.ElementType)}[{arrayType.Size}]){{{string.Join(", ", values)}}}";
    }

    private string EmitDereference(DereferenceNode dereferenceNode)
    {
        var pointer = EmitExpression(dereferenceNode.Target);
        return $"*{pointer}";
    }

    private string EmitFloat32Literal(Float32LiteralNode float32LiteralNode)
    {
        var str = float32LiteralNode.Value.ToString("G9", System.Globalization.CultureInfo.InvariantCulture);
        if (!str.Contains('.') && !str.Contains('e') && !str.Contains('E'))
        {
            str += ".0";
        }

        return str + "f";
    }

    private string EmitFloat64Literal(Float64LiteralNode float64LiteralNode)
    {
        var str = float64LiteralNode.Value.ToString("G17", System.Globalization.CultureInfo.InvariantCulture);
        if (!str.Contains('.') && !str.Contains('e') && !str.Contains('E'))
        {
            str += ".0";
        }

        return str;
    }

    private string EmitCast(CastNode castNode)
    {
        var value = EmitExpression(castNode.Value);

        if (castNode is { Type: NubSliceType, Value.Type: NubConstArrayType arrayType })
        {
            return $"(slice){{.length = {arrayType.Size}, .data = (void*){value}}}";
        }

        // todo(nub31): Stop depending on libc
        if (castNode is { Type: NubCStringType, Value.Type: NubStringType })
        {
            return $"(string){{.length = strlen({value}), .data = {value}}}";
        }

        return $"({CType.Create(castNode.Type)}){value}";
    }

    private string EmitFuncCall(FuncCallNode funcCallNode)
    {
        var name = EmitExpression(funcCallNode.Expression);
        var parameterNames = funcCallNode.Parameters.Select(EmitExpression).ToList();
        return $"{name}({string.Join(", ", parameterNames)})";
    }

    private string EmitAddressOf(AddressOfNode addressOfNode)
    {
        var value = EmitExpression(addressOfNode.LValue);
        return $"&{value}";
    }

    private string EmitSliceArrayIndexAccess(SliceIndexAccessNode sliceIndexAccessNode)
    {
        var targetType = (NubSliceType)sliceIndexAccessNode.Target.Type;
        var target = EmitExpression(sliceIndexAccessNode.Target);
        var index = EmitExpression(sliceIndexAccessNode.Index);
        // todo(nub31): We can emit bounds checking here
        return $"(({CType.Create(targetType.ElementType)}*){target}.data)[{index}]";
    }

    private string EmitStringLiteral(StringLiteralNode stringLiteralNode)
    {
        var length = Encoding.UTF8.GetByteCount(stringLiteralNode.Value);
        return $"(string){{.length = {length}, .data = \"{stringLiteralNode.Value}\"}}";
    }

    private string EmitStructFieldAccess(StructFieldAccessNode structFieldAccessNode)
    {
        var structExpr = EmitExpression(structFieldAccessNode.Target);
        return $"{structExpr}.{structFieldAccessNode.Field}";
    }

    private string EmitStructInitializer(StructInitializerNode structInitializerNode)
    {
        var initValues = new List<string>();
        foreach (var initializer in structInitializerNode.Initializers)
        {
            var value = EmitExpression(initializer.Value);
            initValues.Add($".{initializer.Key} = {value}");
        }

        var initString = initValues.Count == 0
            ? "0"
            : string.Join(", ", initValues);

        return $"({CType.Create(structInitializerNode.Type)}){{{initString}}}";
    }

    private string EmitI8Literal(I8LiteralNode i8LiteralNode)
    {
        return i8LiteralNode.Value.ToString();
    }

    private string EmitI16Literal(I16LiteralNode i16LiteralNode)
    {
        return i16LiteralNode.Value.ToString();
    }

    private string EmitI32Literal(I32LiteralNode i32LiteralNode)
    {
        return i32LiteralNode.Value.ToString();
    }

    private string EmitI64Literal(I64LiteralNode i64LiteralNode)
    {
        return i64LiteralNode.Value + "LL";
    }

    private string EmitU8Literal(U8LiteralNode u8LiteralNode)
    {
        return u8LiteralNode.Value.ToString();
    }

    private string EmitU16Literal(U16LiteralNode u16LiteralNode)
    {
        return u16LiteralNode.Value.ToString();
    }

    private string EmitU32Literal(U32LiteralNode u32LiteralNode)
    {
        return u32LiteralNode.Value.ToString();
    }

    private string EmitU64Literal(U64LiteralNode u64LiteralNode)
    {
        return u64LiteralNode.Value + "ULL";
    }

    private string EmitUnaryExpression(UnaryExpressionNode unaryExpressionNode)
    {
        var value = EmitExpression(unaryExpressionNode.Operand);

        return unaryExpressionNode.Operator switch
        {
            UnaryOperator.Negate => $"-{value}",
            UnaryOperator.Invert => $"!{value}",
            _ => throw new ArgumentOutOfRangeException()
        };
    }

    private void EmitBlock(BlockNode blockNode)
    {
        _deferStack.Push([]);

        foreach (var statementNode in blockNode.Statements)
        {
            EmitStatement(statementNode);
        }

        var blockDefers = _deferStack.Pop();
        for (var i = blockDefers.Count - 1; i >= 0; i--)
        {
            EmitStatement(blockDefers[i].Statement);
        }
    }
}