const std = @import("std");
const assert = std.debug.assert;

const vsr = @import("vsr");
const constants = vsr.constants;
const IO = vsr.io.IO;
const Tracer = vsr.trace.TracerType(vsr.time.Time);

const Storage = vsr.storage.StorageType(IO, Tracer);
const StateMachine = vsr.state_machine.StateMachineType(Storage, constants.state_machine_config);

const tb = vsr.tigerbeetle;
const tb_client = vsr.tb_client;

// Needed to configure VSR
pub const vsr_options = @import("config").vsr_options;

const auto_generated_code_notice =
    \\#######################################################
    \\# This file was auto-generated by elixir_bindings.zig #
    \\#              Do not manually modify.                #
    \\#######################################################
    \\
;

const Constant = struct {
    docs: []const u8,
    name: []const u8,
    value: []const u8,
};

const TypeMapping = struct {
    file_name: []const u8,
    module_name: []const u8,
    hidden_fields: []const []const u8 = &.{},
    docs_link: ?[]const u8 = null,
    constants: []const Constant = &.{},

    pub fn hidden(comptime self: @This(), name: []const u8) bool {
        inline for (self.hidden_fields) |field| {
            if (std.mem.eql(u8, field, name)) {
                return true;
            }
        } else return false;
    }
};

const type_mappings = .{
    .{ tb.AccountFlags, TypeMapping{
        .file_name = "account_flags",
        .module_name = "AccountFlags",
        .hidden_fields = &.{"padding"},
        .docs_link = "reference/account#flags",
    } },
    .{ tb.TransferFlags, TypeMapping{
        .file_name = "transfer_flags",
        .module_name = "TransferFlags",
        .hidden_fields = &.{"padding"},
        .docs_link = "reference/transfer#flags",
    } },
    .{ tb.AccountFilterFlags, TypeMapping{
        .file_name = "account_filter_flags",
        .module_name = "AccountFilterFlags",
        .hidden_fields = &.{"padding"},
        .docs_link = "reference/account-filter#flags",
    } },
    .{ tb.QueryFilterFlags, TypeMapping{
        .file_name = "query_filter_flags",
        .module_name = "QueryFilterFlags",
        .hidden_fields = &.{"padding"},
        .docs_link = "reference/query-filter#flags",
    } },
    .{ tb.Account, TypeMapping{
        .file_name = "account",
        .module_name = "Account",
        .hidden_fields = &.{"reserved"},
        .docs_link = "reference/account#",
    } },
    .{ tb.AccountBalance, TypeMapping{
        .file_name = "account_balance",
        .module_name = "AccountBalance",
        .hidden_fields = &.{"reserved"},
        .docs_link = "reference/account-balance#",
    } },
    .{ tb.Transfer, TypeMapping{
        .file_name = "transfer",
        .module_name = "Transfer",
        .docs_link = "reference/transfer#",
        .constants = &.{
            .{
                .name = "amount_max",
                .value = "Integer.pow(2, 128) - 1",
                .docs = "The maximum amount for a transfer. Used in Two-Phase transfers to post the full pending amount.",
            },
        },
    } },
    .{ tb.QueryFilter, TypeMapping{
        .file_name = "query_filter",
        .module_name = "QueryFilter",
        .hidden_fields = &.{"reserved"},
        .docs_link = "reference/query-filter#",
    } },
    .{ tb.AccountFilter, TypeMapping{
        .file_name = "account_filter",
        .module_name = "AccountFilter",
        .hidden_fields = &.{"reserved"},
        .docs_link = "reference/account-filter#",
    } },
    .{ tb.CreateAccountResult, TypeMapping{
        .file_name = "create_account_result",
        .module_name = "CreateAccountResult",
        .docs_link = "reference/requests/create_accounts#result",
    } },
    .{ tb.CreateTransferResult, TypeMapping{
        .file_name = "create_transfer_result",
        .module_name = "CreateTransferResult",
        .docs_link = "reference/requests/create_transfers#result",
    } },
    .{ tb.CreateAccountsResult, TypeMapping{
        .file_name = "create_accounts_result",
        .module_name = "CreateAccountsResult",
        .docs_link = "reference/requests/create_accounts#",
    } },
    .{ tb.CreateTransfersResult, TypeMapping{
        .file_name = "create_transfers_result",
        .module_name = "CreateTransfersResult",
        .docs_link = "reference/requests/create_transfers#",
    } },
};

fn emit_flags(
    buffer: *std.ArrayList(u8),
    comptime type_info: anytype,
    comptime mapping: TypeMapping,
) !void {
    assert(type_info.layout == .@"packed");

    try buffer.writer().print(
        \\{[notice]s}
        \\defmodule TigerBeetlex.{[module_name]s} do
        \\
    , .{
        .notice = auto_generated_code_notice,
        .module_name = mapping.module_name,
    });

    try emit_docs(buffer, mapping, null);

    try buffer.writer().print(
        \\
        \\
        \\  use TypedStruct
        \\
    , .{});

    try emit_typed_struct(buffer, type_info, mapping);

    try buffer.writer().print(
        \\
        \\  @doc """
        \\  Creates a `TigerBeetlex.{[module_name]s}` struct from its binary representation.
        \\  """
        \\  @spec from_binary(binary :: <<_::{[bit_size]}>>) :: t()
        \\  def from_binary(<<n::unsigned-little-{[bit_size]}>> = _bin) do
        \\    <<
        \\
    , .{
        .module_name = mapping.module_name,
        .bit_size = @bitSizeOf(type_info.backing_integer.?),
    });

    {
        // Here we have to reverse the fields because they are laid out in little-endian
        // order while when we match in Elixir we list things in big-endian order to
        // avoid having to manually swap bytes
        comptime var reversed_fields = std.mem.reverseIterator(type_info.fields);
        inline while (reversed_fields.next()) |field| {
            // Hidden == unused
            const prefix = if (comptime mapping.hidden(field.name)) "_" else "";

            try buffer.writer().print(
                \\      {[prefix]s}{[field_name]s}::{[bit_size]},
                \\
            , .{
                .prefix = prefix,
                .field_name = field.name,
                .bit_size = @bitSizeOf(field.type),
            });
        }
    }

    try buffer.writer().print(
        \\    >> = <<n::unsigned-big-{[bit_size]}>>
        \\
        \\    %__MODULE__{{
        \\
    , .{
        .bit_size = @bitSizeOf(type_info.backing_integer.?),
    });

    inline for (type_info.fields) |field| {
        // Hidden == unused
        if (comptime mapping.hidden(field.name)) continue;

        try buffer.writer().print(
            \\      {[field_name]s}: {[field_name]s} == 1,
            \\
        , .{
            .field_name = field.name,
        });
    }

    try buffer.writer().print(
        \\    }}
        \\  end
        \\
        \\  @doc """
        \\  Converts a `TigerBeetlex.{[module_name]s}` struct to its binary represenation.
        \\  """
        \\  @spec to_binary(flags :: t()) :: <<_::{[bit_size]}>>
        \\  def to_binary(flags) do
        \\    %__MODULE__{{
        \\
    , .{
        .module_name = mapping.module_name,
        .bit_size = @bitSizeOf(type_info.backing_integer.?),
    });

    inline for (type_info.fields) |field| {
        // Hidden == unused
        if (comptime mapping.hidden(field.name)) continue;

        try buffer.writer().print(
            \\      {[field_name]s}: {[field_name]s},
            \\
        , .{
            .field_name = field.name,
        });
    }

    try buffer.writer().print(
        \\    }} = flags
        \\
        \\    <<n::unsigned-big-{[bit_size]}>> =
        \\      <<
        \\
    , .{
        .bit_size = @bitSizeOf(type_info.backing_integer.?),
    });

    {
        // Here we have to reverse the fields because they are laid out in little-endian
        // order while when we match in Elixir we list things in big-endian order to
        // avoid having to manually swap bytes
        comptime var reversed_fields = std.mem.reverseIterator(type_info.fields);
        inline while (reversed_fields.next()) |field| {
            // We assume hidden == padded with zeroes
            if (comptime mapping.hidden(field.name)) {
                try buffer.writer().print(
                    \\        # {[field_name]s}
                    \\        0::{[bit_size]},
                    \\
                , .{
                    .field_name = field.name,
                    .bit_size = @bitSizeOf(field.type),
                });
            } else {
                try buffer.writer().print(
                    \\        bool_to_u1({[field_name]s})::1,
                    \\
                , .{
                    .field_name = field.name,
                });
            }
        }
    }

    try buffer.writer().print(
        \\    >>
        \\
        \\    <<n::unsigned-little-{[bit_size]}>>
        \\  end
        \\
        \\  @spec bool_to_u1(b :: boolean()) :: 0 | 1
        \\  defp bool_to_u1(true), do: 1
        \\  defp bool_to_u1(false), do: 0
        \\end
        \\
    , .{
        .bit_size = @bitSizeOf(type_info.backing_integer.?),
    });
}

fn emit_struct(
    buffer: *std.ArrayList(u8),
    comptime type_info: anytype,
    comptime mapping: TypeMapping,
    comptime struct_size: comptime_int,
) !void {
    assert(type_info.layout == .@"extern");

    try buffer.writer().print(
        \\{[notice]s}
        \\defmodule TigerBeetlex.{[module_name]s} do
        \\
    , .{
        .notice = auto_generated_code_notice,
        .module_name = mapping.module_name,
    });

    try emit_docs(buffer, mapping, null);

    try buffer.writer().print(
        \\
        \\
        \\ use TypedStruct
        \\
    , .{});

    inline for (type_info.fields) |field| {
        if (comptime mapping.hidden(field.name)) continue;

        switch (@typeInfo(field.type)) {
            .@"struct", .@"enum" => {
                try buffer.writer().print(
                    \\
                    \\  alias TigerBeetlex.{[module_name]s}
                , .{
                    .module_name = comptime module_name_for(type_mappings, field.type),
                });
            },
            else => {},
        }
    }

    try emit_typed_struct(buffer, type_info, mapping);

    try emit_constants(buffer, mapping.constants);

    try buffer.writer().print(
        \\
        \\  @doc """
        \\  Creates a `TigerBeetlex.{[module_name]s}` struct from its binary representation.
        \\  """
        \\  @spec from_binary(binary :: <<_::{[bit_size]}>>) :: t()
        \\  def from_binary(<<_::binary-size({[byte_size]})>> = bin) do
        \\    <<
        \\
    , .{
        .module_name = mapping.module_name,
        .bit_size = struct_size * 8,
        .byte_size = struct_size,
    });

    inline for (type_info.fields) |field| {
        const prefix = if (comptime mapping.hidden(field.name)) "_" else "";

        try buffer.writer().print(
            \\      {[prefix]s}{[field_name]s}::{[bitstring_options]s},
            \\
        , .{
            .prefix = prefix,
            .field_name = field.name,
            .bitstring_options = bitstring_options(field.name, field.type),
        });
    }

    try buffer.writer().print(
        \\    >> = bin
        \\
        \\    %__MODULE__{{
        \\
    , .{});

    inline for (type_info.fields) |field| {
        if (comptime mapping.hidden(field.name)) continue;

        try buffer.writer().print(
            \\      {[field_name]s}: {[nested_deserialization]s},
            \\
        , .{
            .field_name = field.name,
            .nested_deserialization = deserialize_struct_field(field.name, field.type),
        });
    }

    try buffer.writer().print(
        \\    }}
        \\  end
        \\
        \\  @doc """
        \\  Converts a `TigerBeetlex.{[module_name]s}` struct to its binary representation.
        \\  """
        \\  @spec to_binary(struct :: t()) :: <<_::{[bit_size]}>>
        \\  def to_binary(struct) do
        \\    %__MODULE__{{
        \\
    , .{
        .module_name = mapping.module_name,
        .bit_size = struct_size * 8,
    });

    inline for (type_info.fields) |field| {
        if (comptime mapping.hidden(field.name)) continue;

        try buffer.writer().print(
            \\      {[field_name]s}: {[field_name]s},
            \\
        , .{
            .field_name = field.name,
        });
    }

    try buffer.writer().print(
        \\    }} = struct
        \\
        \\    <<
        \\
    , .{});

    inline for (type_info.fields) |field| {
        // We assume hidden == padded with zeroes
        if (comptime mapping.hidden(field.name)) {
            try buffer.writer().print(
                \\      # {[field_name]s}
                \\      0::unit(8)-size({[byte_size]}),
                \\
            , .{
                .field_name = field.name,
                .byte_size = @sizeOf(field.type),
            });
            continue;
        }

        try buffer.writer().print(
            \\      {[serialized_value]s}::{[bitstring_options]s},
            \\
        , .{
            .serialized_value = serialized_value(field.name, field.type),
            .bitstring_options = bitstring_options(field.name, field.type),
        });
    }

    try buffer.writer().print(
        \\    >>
        \\  end
        \\end
        \\
    , .{});
}

fn emit_typed_struct(
    buffer: *std.ArrayList(u8),
    comptime type_info: anytype,
    comptime mapping: TypeMapping,
) !void {
    try buffer.writer().print(
        \\
        \\
        \\  typedstruct do
        \\
    , .{});

    inline for (type_info.fields) |field| {
        if (comptime mapping.hidden(field.name)) continue;

        const default_opt = if (comptime default_field_value(field.name, field.type)) |default|
            std.fmt.comptimePrint(", default: {s}", .{default})
        else
            "";

        try buffer.writer().print(
            \\    field :{[field_name]s}, {[typespec_type]s}{[default_opt]s}
            \\
        , .{
            .field_name = field.name,
            .typespec_type = typespec_type(field.name, field.type),
            .default_opt = default_opt,
        });
    }

    try buffer.writer().print(
        \\  end
        \\
    , .{});
}

fn emit_constants(
    buffer: *std.ArrayList(u8),
    comptime consts: []const Constant,
) !void {
    for (consts) |constant| {
        try buffer.writer().print(
            \\
            \\  @doc "{[docs]s}"
            \\  def {[name]s} do
            \\    {[value]s}
            \\  end
            \\
        , .{
            .docs = constant.docs,
            .name = constant.name,
            .value = constant.value,
        });
    }
}

fn typespec_type(comptime field_name: []const u8, comptime Type: type) []const u8 {
    switch (@typeInfo(Type)) {
        .bool => return "boolean()",
        .int => |int| {
            if (int.bits == 128 and !big_integer.contains(field_name))
                return "<<_::128>>";

            return if (int.signedness == .unsigned) "non_neg_integer()" else "integer()";
        },
        .@"enum" => return "atom()",
        .@"struct" => return std.fmt.comptimePrint("{[module_name]s}.t()", .{
            .module_name = comptime module_name_for(type_mappings, Type),
        }),
        else => unreachable,
    }
}

fn bitstring_options(comptime field_name: []const u8, comptime Type: type) []const u8 {
    switch (@typeInfo(Type)) {
        .int => |info| {
            if (info.bits == 128 and !big_integer.contains(field_name))
                return "binary-size(16)";

            return int_bitstring_options(info);
        },
        .@"enum" => |info| {
            const tag_info = @typeInfo(info.tag_type);
            assert(tag_info == .int);
            return int_bitstring_options(tag_info.int);
        },
        .@"struct" => |info| {
            assert(info.layout == .@"packed");

            return std.fmt.comptimePrint("binary-size({[byte_size]})", .{
                .byte_size = @sizeOf(Type),
            });
        },
        // This should always be padding
        .array => |info| {
            assert(info.child == u8);

            return std.fmt.comptimePrint("binary-size({[byte_size]})", .{
                .byte_size = info.len,
            });
        },
        else => unreachable,
    }
}

fn int_bitstring_options(comptime info: std.builtin.Type.Int) []const u8 {
    const signedness = if (info.signedness == .signed) "signed" else "unsigned";
    return std.fmt.comptimePrint("{[signedness]s}-little-{[bits]}", .{
        .signedness = signedness,
        .bits = info.bits,
    });
}

fn deserialize_struct_field(comptime field_name: []const u8, comptime Type: type) []const u8 {
    switch (@typeInfo(Type)) {
        .@"enum" => {
            return std.fmt.comptimePrint("TigerBeetlex.{[module_name]s}.to_atom({[field_name]s})", .{
                .module_name = comptime module_name_for(type_mappings, Type),
                .field_name = field_name,
            });
        },
        .@"struct" => |info| {
            assert(info.layout == .@"packed");

            return std.fmt.comptimePrint("TigerBeetlex.{[module_name]s}.from_binary({[field_name]s})", .{
                .module_name = comptime module_name_for(type_mappings, Type),
                .field_name = field_name,
            });
        },
        .int => return field_name,
        else => unreachable,
    }
}

fn default_field_value(comptime field_name: []const u8, comptime Type: type) ?[]const u8 {
    switch (@typeInfo(Type)) {
        .bool => return "false",

        .int => |info| {
            if (info.bits == 128 and !big_integer.contains(field_name))
                return "<<0::size(128)>>";

            return "0";
        },

        .@"struct" => |info| {
            assert(info.layout == .@"packed");

            return std.fmt.comptimePrint("%{[module_name]s}{{}}", .{
                .module_name = comptime module_name_for(type_mappings, Type),
            });
        },

        else => return null,
    }
}

fn serialized_value(comptime field_name: []const u8, comptime Type: type) []const u8 {
    switch (@typeInfo(Type)) {
        .int => return field_name,

        .@"enum" => {
            return std.fmt.comptimePrint("{[module_name]s}.from_atom({[field_name]s})", .{
                .module_name = comptime module_name_for(type_mappings, Type),
                .field_name = field_name,
            });
        },

        .@"struct" => |info| {
            assert(info.layout == .@"packed");

            return std.fmt.comptimePrint(
                "{[module_name]s}.to_binary({[field_name]s})",
                .{
                    .module_name = comptime module_name_for(type_mappings, Type),
                    .field_name = field_name,
                },
            );
        },

        else => unreachable,
    }
}

fn module_name_for(mappings: anytype, comptime Type: type) []const u8 {
    comptime for (mappings) |mapping| {
        const ZigType, const type_mapping = mapping;
        if (ZigType == Type) return type_mapping.module_name;
    };

    unreachable;
}

/// Some 128-bit fields are better represented as numbers,
/// otherwise they are considered IDs and exposed as binaries.
const big_integer = struct {
    const fields = .{
        "credits_posted",
        "credits_pending",
        "debits_posted",
        "debits_pending",
        "amount",
    };

    fn contains(comptime field: []const u8) bool {
        return comptime blk: for (fields) |value| {
            if (std.mem.eql(u8, field, value)) break :blk true;
        } else false;
    }
};

fn emit_enum(
    buffer: *std.ArrayList(u8),
    comptime Type: type,
    comptime mapping: TypeMapping,
) !void {
    try buffer.writer().print(
        \\{[notice]s}
        \\defmodule TigerBeetlex.{[module_name]s} do
        \\    @moduledoc false
        \\
        \\
    , .{
        .notice = auto_generated_code_notice,
        .module_name = mapping.module_name,
    });

    try buffer.writer().print(
        \\
        \\  @doc """
        \\  Obtains the atom representation of a result from its integer value.
        \\  """
        \\
    , .{});

    const type_info = @typeInfo(Type).@"enum";
    inline for (type_info.fields) |field| {
        if (comptime std.mem.startsWith(u8, field.name, "deprecated_")) continue;

        if (comptime mapping.hidden(field.name)) continue;

        try buffer.writer().print(
            \\  def to_atom({[int_value]d}), do: :{[field]s}
            \\
        , .{
            .int_value = @intFromEnum(@field(Type, field.name)),
            .field = field.name,
        });
    }

    try buffer.writer().print(
        \\
        \\  @doc """
        \\  Obtains the integer representation of a result reason from its atom value.
        \\  """
        \\
    , .{});

    inline for (type_info.fields) |field| {
        if (comptime std.mem.startsWith(u8, field.name, "deprecated_")) continue;
        if (comptime mapping.hidden(field.name)) continue;

        try buffer.writer().print(
            \\
            \\  def from_atom(:{[field]s}), do: {[int_value]}
        , .{
            .int_value = @intFromEnum(@field(Type, field.name)),
            .field = field.name,
        });
    }

    try buffer.writer().print(
        \\
        \\end
        \\
    , .{});
}

fn emit_response_module(
    buffer: *std.ArrayList(u8),
) !void {
    try buffer.writer().print(
        \\defmodule TigerBeetlex.Response do
        \\  @moduledoc """
        \\  NIF Response parsing.
        \\  This module is responsible for converting a response received from the TigerBeetle NIF to either
        \\  an error or a list of results.
        \\  """
        \\
        \\  @doc """
        \\  Converts a NIF message response to a list of results.
        \\
        \\  If the response contains an error, `{{:error, reason}}` is returned.
        \\
        \\  If successful, it returns `{{:ok, list}}`. The type of the items of the list depend on the
        \\  operation.
        \\
        \\
    , .{});

    {
        const operation_info = @typeInfo(tb_client.Operation).@"enum";
        inline for (operation_info.fields) |field| {
            if (comptime std.mem.startsWith(u8, field.name, "deprecated_")) continue;

            const operation: tb_client.Operation = @enumFromInt(field.value);
            if (operation == .pulse or operation == .get_change_events) continue;
            const result_type = StateMachine.ResultType(operation);

            try buffer.writer().print(
                \\  - `{[operation_name]s}`: a list of `%TigerBeetlex.{[result_module]s}{{}}`
                \\
            , .{
                .operation_name = field.name,
                .result_module = comptime module_name_for(type_mappings, result_type),
            });
        }
    }

    try buffer.writer().print(
        \\  """
        \\
    , .{});

    {
        const packet_status_info = @typeInfo(tb_client.PacketStatus).@"enum";
        inline for (packet_status_info.fields) |field| {
            const status: tb_client.PacketStatus = @enumFromInt(field.value);
            if (status == .ok) {
                try buffer.writer().print(
                    \\  def decode({{{[ok_value]}, operation, batch}} = _response) do
                    \\    {{:ok, build_result_list(operation, batch)}}
                    \\  end
                    \\
                    \\
                , .{
                    .ok_value = field.value,
                });
            } else {
                try buffer.writer().print(
                    \\  def decode({{{[error_value]}, _operation, _batch}}) do
                    \\    {{:error, :{[error_name]s}}}
                    \\  end
                    \\
                    \\
                , .{
                    .error_value = field.value,
                    .error_name = field.name,
                });
            }
        }
    }

    {
        const operation_info = @typeInfo(tb_client.Operation).@"enum";
        inline for (operation_info.fields) |field| {
            if (comptime std.mem.startsWith(u8, field.name, "deprecated_")) continue;

            const operation: tb_client.Operation = @enumFromInt(field.value);
            if (operation == .pulse or operation == .get_change_events) continue;
            const result_type = StateMachine.ResultType(operation);

            try buffer.writer().print(
                \\  defp build_result_list({[operation_value]}, batch) when rem(bit_size(batch), {[result_bit_size]}) == 0 do
                \\    for <<item::binary-size({[result_byte_size]}) <- batch>> do
                \\      TigerBeetlex.{[result_module]s}.from_binary(item)
                \\    end
                \\  end
                \\
                \\
            , .{
                .operation_value = field.value,
                .result_bit_size = @bitSizeOf(result_type),
                .result_byte_size = @sizeOf(result_type),
                .result_module = comptime module_name_for(type_mappings, result_type),
            });
        }
    }

    {
        const packet_status_info = @typeInfo(tb_client.PacketStatus).@"enum";

        try buffer.writer().print(
            \\  @doc false
            \\  def status_map do
            \\    %{{
            \\
        , .{});

        inline for (packet_status_info.fields) |field| {
            try buffer.writer().print(
                \\      {[error_name]s}: {[error_value]},
                \\
            , .{
                .error_name = field.name,
                .error_value = field.value,
            });
        }

        try buffer.writer().print(
            \\    }}
            \\  end
            \\
        , .{});
    }

    try buffer.writer().print(
        \\end
        \\
    , .{});
}

fn emit_operation_module(
    buffer: *std.ArrayList(u8),
) !void {
    try buffer.writer().print(
        \\defmodule TigerBeetlex.Operation do
        \\  @moduledoc false
        \\
        \\
    , .{});

    {
        const operation_info = @typeInfo(tb_client.Operation).@"enum";

        try buffer.writer().print(
            \\  def available_operations do
            \\    [
            \\
        , .{});

        inline for (operation_info.fields) |field| {
            if (comptime std.mem.startsWith(u8, field.name, "deprecated_")) continue;

            const operation: tb_client.Operation = @enumFromInt(field.value);
            if (operation == .pulse or operation == .get_change_events) continue;

            try buffer.writer().print(
                \\      :{[operation_name]s},
                \\
            , .{ .operation_name = field.name });
        }

        try buffer.writer().print(
            \\    ]
            \\  end
            \\
            \\
        , .{});
    }

    {
        const operation_info = @typeInfo(tb_client.Operation).@"enum";
        inline for (operation_info.fields) |field| {
            if (comptime std.mem.startsWith(u8, field.name, "deprecated_")) continue;

            const operation: tb_client.Operation = @enumFromInt(field.value);
            if (operation == .pulse or operation == .get_change_events) continue;

            try buffer.writer().print(
                \\  def from_atom(:{[operation_name]s}), do: {[operation_value]}
                \\
            , .{
                .operation_name = field.name,
                .operation_value = field.value,
            });
        }
    }

    try buffer.writer().print(
        \\end
        \\
    , .{});
}

fn emit_docs(
    buffer: anytype,
    comptime mapping: TypeMapping,
    comptime field: ?[]const u8,
) !void {
    if (mapping.docs_link) |docs_link| {
        try buffer.writer().print(
            \\  @{[doc_type]s} """
            \\  See [{[name]s}](https://docs.tigerbeetle.com/{[docs_link]s}{[field]s}).
            \\  """
        , .{
            .doc_type = if (field == null) "moduledoc" else "doc",
            .name = field orelse mapping.module_name,
            .docs_link = docs_link,
            .field = field orelse "",
        });
    }
}

pub fn generate_bindings(
    comptime ZigType: type,
    comptime mapping: TypeMapping,
    buffer: *std.ArrayList(u8),
) !void {
    @setEvalBranchQuota(100_000);

    switch (@typeInfo(ZigType)) {
        .@"struct" => |info| switch (info.layout) {
            .auto => @compileError(
                "Only packed or extern structs are supported: " ++ @typeName(ZigType),
            ),
            .@"packed" => try emit_flags(buffer, info, mapping),
            .@"extern" => try emit_struct(buffer, info, mapping, @sizeOf(ZigType)),
        },
        .@"enum" => try emit_enum(buffer, ZigType, mapping),
        else => @compileError("Type cannot be represented: " ++ @typeName(ZigType)),
    }
}

// TODO: accept this from the build system
const target_dir_path = "lib/tigerbeetlex/bindings";

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();

    const allocator = arena.allocator();

    var target_dir = try std.fs.cwd().openDir(target_dir_path, .{});
    defer target_dir.close();

    // Emit Elixir declarations.
    inline for (type_mappings) |type_mapping| {
        const ZigType = type_mapping[0];
        const mapping = type_mapping[1];

        var buffer = std.ArrayList(u8).init(allocator);
        try generate_bindings(ZigType, mapping, &buffer);

        try target_dir.writeFile(.{
            .sub_path = mapping.file_name ++ ".ex",
            .data = buffer.items,
        });
    }

    {
        var buffer = std.ArrayList(u8).init(allocator);
        try emit_response_module(&buffer);

        try target_dir.writeFile(.{
            .sub_path = "response.ex",
            .data = buffer.items,
        });
    }

    {
        var buffer = std.ArrayList(u8).init(allocator);
        try emit_operation_module(&buffer);

        try target_dir.writeFile(.{
            .sub_path = "operation.ex",
            .data = buffer.items,
        });
    }
}
