using System.Text;

namespace Compiler;

public sealed class Generator(List<NodeDefinition> nodes)
{
    public static string Emit(List<NodeDefinition> nodes)
    {
        return new Generator(nodes).Emit();
    }

    private IndentedTextWriter writer = new();

    private string Emit()
    {
        writer.WriteLine("""
                         #include <float.h>
                         #include <stdarg.h>
                         #include <stddef.h>
                         #include <stdbool.h>
                         #include <stdint.h>

                         struct string {
                            const char *data;
                            int length;
                         };

                         """);

        foreach (var node in nodes.OfType<NodeDefinitionFunc>())
        {
            var parameters = node.Parameters.Select(x => CType(x.Type, x.Name.Ident));
            writer.WriteLine($"{CType(node.ReturnType, node.Name.Ident)}({string.Join(", ", parameters)});");
        }

        writer.WriteLine();

        foreach (var node in nodes.OfType<NodeDefinitionFunc>())
        {
            var parameters = node.Parameters.Select(x => CType(x.Type, x.Name.Ident));
            writer.WriteLine($"{CType(node.ReturnType, node.Name.Ident)}({string.Join(", ", parameters)})");
            writer.WriteLine("{");
            using (writer.Indent())
            {
                EmitStatement(node.Body);
            }

            writer.WriteLine("}");
            writer.WriteLine();
        }

        return writer.ToString();
    }

    private void EmitStatement(NodeStatement node)
    {
        switch (node)
        {
            case NodeStatementBlock statement:
                EmitStatementBlock(statement);
                break;
            case NodeStatementFuncCall statement:
                EmitStatementFuncCall(statement);
                break;
            case NodeStatementReturn statement:
                EmitStatementReturn(statement);
                break;
            case NodeStatementVariableDeclaration statement:
                EmitStatementVariableDeclaration(statement);
                break;
            case NodeStatementAssignment statement:
                EmitStatementAssignment(statement);
                break;
            case NodeStatementIf statement:
                EmitStatementIf(statement);
                break;
            case NodeStatementWhile statement:
                EmitStatementWhile(statement);
                break;
            default:
                throw new ArgumentOutOfRangeException(nameof(node), node, null);
        }
    }

    private void EmitStatementBlock(NodeStatementBlock node)
    {
        writer.WriteLine("{");
        using (writer.Indent())
        {
            foreach (var statement in node.Statements)
                EmitStatement(statement);
        }

        writer.WriteLine("}");
    }

    private void EmitStatementFuncCall(NodeStatementFuncCall node)
    {
        var name = EmitExpression(node.Target);
        var parameterValues = node.Parameters.Select(EmitExpression).ToList();
        writer.WriteLine($"{name}({string.Join(", ", parameterValues)});");
    }

    private void EmitStatementReturn(NodeStatementReturn statement)
    {
        var value = EmitExpression(statement.Value);
        writer.WriteLine($"return {value};");
    }

    private void EmitStatementVariableDeclaration(NodeStatementVariableDeclaration statement)
    {
        var value = EmitExpression(statement.Value);
        writer.WriteLine($"{CType(statement.Type)} {statement.Name.Ident} = {value};");
    }

    private void EmitStatementAssignment(NodeStatementAssignment statement)
    {
        var target = EmitExpression(statement.Target);
        var value = EmitExpression(statement.Value);
        writer.WriteLine($"{target} = {value};");
    }

    private void EmitStatementIf(NodeStatementIf statement)
    {
        var condition = EmitExpression(statement.Condition);
        writer.WriteLine($"if ({condition})");
        writer.WriteLine("{");
        using (writer.Indent())
        {
            EmitStatement(statement.ThenBlock);
        }

        writer.WriteLine("}");

        if (statement.ElseBlock != null)
        {
            writer.Write("else");
            if (statement.ElseBlock is NodeStatementIf)
                writer.Write(" ");
            else
                writer.WriteLine();

            writer.WriteLine("{");
            using (writer.Indent())
            {
                EmitStatement(statement.ElseBlock);
            }

            writer.WriteLine("}");
        }
    }

    private void EmitStatementWhile(NodeStatementWhile statement)
    {
        var condition = EmitExpression(statement.Condition);
        writer.WriteLine($"while ({condition})");
        writer.WriteLine("{");
        using (writer.Indent())
        {
            EmitStatement(statement.Block);
        }
        writer.WriteLine("}");
    }

    private string EmitExpression(NodeExpression node)
    {
        return node switch
        {
            NodeExpressionBoolLiteral expression => expression.Value.Value ? "true" : "false",
            NodeExpressionIntLiteral expression => expression.Value.Value.ToString(),
            NodeExpressionStringLiteral expression => $"(struct string){{ \"{expression.Value.Value}\", {expression.Value.Value.Length} }}",
            NodeExpressionIdent expression => expression.Value.Ident,
            _ => throw new ArgumentOutOfRangeException(nameof(node), node, null)
        };
    }

    private static string CType(NodeType node, string? varName = null)
    {
        return node switch
        {
            NodeTypeVoid => "void" + (varName != null ? $" {varName}" : ""),
            NodeTypeBool => "bool" + (varName != null ? $" {varName}" : ""),
            NodeTypeCustom type => $"struct {type}" + (varName != null ? $" {varName}" : ""),
            NodeTypeSInt type => $"int{type.Width}_t" + (varName != null ? $" {varName}" : ""),
            NodeTypeUInt type => $"uint{type.Width}_t" + (varName != null ? $" {varName}" : ""),
            NodeTypePointer type => CType(type.To) + (varName != null ? $" *{varName}" : "*"),
            NodeTypeString => "struct string" + (varName != null ? $" {varName}" : ""),
            NodeTypeFunc type => $"{CType(type.ReturnType)} (*{varName})({string.Join(", ", type.Parameters.Select(p => CType(p)))})",
            _ => throw new ArgumentOutOfRangeException(nameof(node), node, null)
        };
    }
}

internal class IndentedTextWriter
{
    private readonly StringBuilder builder = new();
    private int indentLevel;

    public IDisposable Indent()
    {
        indentLevel++;
        return new IndentScope(this);
    }

    public void WriteLine(string text)
    {
        WriteIndent();
        builder.AppendLine(text);
    }

    public void Write(string text)
    {
        WriteIndent();
        builder.Append(text);
    }

    public void WriteLine()
    {
        builder.AppendLine();
    }

    public override string ToString()
    {
        return builder.ToString();
    }

    private void WriteIndent()
    {
        if (builder.Length > 0)
        {
            var lastChar = builder[^1];
            if (lastChar != '\n' && lastChar != '\r')
                return;
        }

        for (var i = 0; i < indentLevel; i++)
        {
            builder.Append("    ");
        }
    }

    private class IndentScope(IndentedTextWriter writer) : IDisposable
    {
        private bool disposed;

        public void Dispose()
        {
            if (disposed) return;
            writer.indentLevel--;
            disposed = true;
        }
    }
}