//! Comptime-generated metadata describing the layout of all C API
//! extern structs for the current target.
//!
//! This is embedded in the binary as a const string and exposed via
//! `ghostty_type_json` so that WASM (and other FFI) consumers can
//! build structs without hardcoding byte offsets.
const std = @import("std");
const lib = @import("../lib.zig");
const color = @import("../color.zig");
const sgr = @import("../sgr.zig");
const color_c = @import("color.zig");
const mouse_event = @import("mouse_event.zig");
const point = @import("../point.zig");
const size_report = @import("size_report.zig");

const terminal = @import("terminal.zig");
const formatter = @import("formatter.zig");
const selection = @import("selection.zig");
const selection_gesture = @import("selection_gesture.zig");
const render = @import("render.zig");
const style_c = @import("style.zig");
const kitty_graphics = @import("kitty_graphics.zig");
const mouse_encode = @import("mouse_encode.zig");
const grid_ref = @import("grid_ref.zig");
const io = @import("io.zig");
const sys = @import("sys.zig");

/// C: GhosttySurfacePosition
pub const SurfacePosition = extern struct {
    x: f64,
    y: f64,
};

/// C: GhosttyCodepoints
pub const Codepoints = extern struct {
    ptr: ?[*]const u32 = null,
    len: usize = 0,
};

/// All C API structs and their Ghostty C names.
pub const structs: std.StaticStringMap(StructInfo) = structs: {
    @setEvalBranchQuota(10_000);
    break :structs .initComptime(.{
        .{ "GhosttyAllocator", StructInfo.init(lib.alloc.Allocator) },
        .{ "GhosttyAllocatorVtable", StructInfo.init(lib.alloc.VTable) },
        .{ "GhosttyBuffer", StructInfo.init(lib.Buffer) },
        .{ "GhosttyClipboardContent", StructInfo.init(terminal.ClipboardContent) },
        .{ "GhosttyClipboardWrite", StructInfo.init(terminal.ClipboardWrite) },
        .{ "GhosttyCodepoints", StructInfo.init(Codepoints) },
        .{ "GhosttyColorPaletteMask", StructInfo.init(color_c.PaletteMask) },
        .{ "GhosttyColorRgb", StructInfo.init(color.RGB.C) },
        .{ "GhosttyColorX11Entry", StructInfo.init(color_c.X11Entry) },
        .{ "GhosttyDeviceAttributes", StructInfo.init(terminal.DeviceAttributes) },
        .{ "GhosttyDeviceAttributesPrimary", StructInfo.init(terminal.DeviceAttributes.Primary) },
        .{ "GhosttyDeviceAttributesSecondary", StructInfo.init(terminal.DeviceAttributes.Secondary) },
        .{ "GhosttyDeviceAttributesTertiary", StructInfo.init(terminal.DeviceAttributes.Tertiary) },
        .{ "GhosttyFormatterTerminalOptions", StructInfo.init(formatter.TerminalOptions) },
        .{ "GhosttySelection", StructInfo.init(selection.CSelection) },
        .{ "GhosttyTerminalSelectWordOptions", StructInfo.init(selection.SelectWordOptions) },
        .{ "GhosttyTerminalSelectWordBetweenOptions", StructInfo.init(selection.SelectWordBetweenOptions) },
        .{ "GhosttyTerminalSelectLineOptions", StructInfo.init(selection.SelectLineOptions) },
        .{ "GhosttyTerminalSelectionFormatOptions", StructInfo.init(selection.FormatOptions) },
        .{ "GhosttyFormatterTerminalExtra", StructInfo.init(formatter.TerminalOptions.Extra) },
        .{ "GhosttyFormatterScreenExtra", StructInfo.init(formatter.ScreenOptions.Extra) },
        .{ "GhosttyGridRef", StructInfo.init(grid_ref.CGridRef) },
        .{ "GhosttyKittyGraphicsPlacementRenderInfo", StructInfo.init(kitty_graphics.PlacementRenderInfo) },
        .{ "GhosttyMouseEncoderSize", StructInfo.init(mouse_encode.Size) },
        .{ "GhosttyMousePosition", StructInfo.init(mouse_event.Position) },
        .{ "GhosttyPoint", StructInfo.init(point.Point.C) },
        .{ "GhosttyPointCoordinate", StructInfo.init(point.Coordinate) },
        .{ "GhosttyReader", StructInfo.init(io.Reader) },
        .{ "GhosttyRenderStateColors", StructInfo.init(render.Colors) },
        .{ "GhosttyRenderStateRowSelection", StructInfo.init(render.RowSelection) },
        .{ "GhosttySelectionGestureBehaviors", StructInfo.init(selection_gesture.Behaviors) },
        .{ "GhosttySelectionGestureGeometry", StructInfo.init(selection_gesture.Geometry) },
        .{ "GhosttySgrAttribute", StructInfo.init(sgr.Attribute.C) },
        .{ "GhosttySgrUnknown", StructInfo.init(sgr.Attribute.Unknown.C) },
        .{ "GhosttySizeReportSize", StructInfo.init(size_report.Size) },
        .{ "GhosttyString", StructInfo.init(lib.String) },
        .{ "GhosttySurfacePosition", StructInfo.init(SurfacePosition) },
        .{ "GhosttyStyle", StructInfo.init(style_c.Style) },
        .{ "GhosttyStyleColor", StructInfo.init(style_c.Color) },
        .{ "GhosttySysImage", StructInfo.init(sys.Image) },
        .{ "GhosttyTerminalDesktopNotification", StructInfo.init(terminal.DesktopNotification) },
        .{ "GhosttyTerminalModeConfig", StructInfo.init(terminal.ModeConfig) },
        .{ "GhosttyTerminalProgressReport", StructInfo.init(terminal.ProgressReport) },
        .{ "GhosttyTerminalScrollbar", StructInfo.init(terminal.TerminalScrollbar) },
        .{ "GhosttyTerminalScrollViewport", StructInfo.init(terminal.ScrollViewport) },
        .{ "GhosttyTerminalUnknownSequence", StructInfo.init(terminal.UnknownSequence.C) },
        .{ "GhosttyTerminalUnknownStringSequence", StructInfo.init(terminal.UnknownStringSequence) },
        .{ "GhosttyWriter", StructInfo.init(io.Writer) },
    });
};

/// The comptime-generated JSON string of all structs.
pub const json: [:0]const u8 = json: {
    @setEvalBranchQuota(200_000);
    var counter: std.Io.Writer.Discarding = .init(&.{});
    jsonWriteAll(&counter.writer) catch unreachable;

    var buf: [counter.count:0]u8 = undefined;
    var writer: std.Io.Writer = .fixed(&buf);
    jsonWriteAll(&writer) catch unreachable;
    const final = buf;
    break :json final[0..writer.end :0];
};

/// Returns a pointer to the comptime-generated JSON string describing
/// the layout of all C API extern structs, and writes its length to `len`.
/// Exported as `ghostty_type_json` for FFI consumers.
pub fn get_json() callconv(lib.calling_conv) [*:0]const u8 {
    return json.ptr;
}

/// Meta information about a struct that we expose to ease writing
/// bindings in some languages, particularly WASM where we can't
/// easily share struct definitions and need to hardcode byte offsets.
pub const StructInfo = struct {
    name: []const u8,
    size: usize,
    @"align": usize,
    fields: []const FieldInfo,

    pub const FieldInfo = struct {
        name: []const u8,
        offset: usize,
        size: usize,
        type: []const u8,
    };

    pub fn init(comptime T: type) StructInfo {
        comptime {
            const fields = @typeInfo(T).@"struct".fields;
            const field_infos: [fields.len]FieldInfo = blk: {
                var infos: [fields.len]FieldInfo = undefined;
                for (fields, 0..) |field, i| infos[i] = .{
                    .name = field.name,
                    .offset = @offsetOf(T, field.name),
                    .size = @sizeOf(field.type),
                    .type = typeName(field.type),
                };
                break :blk infos;
            };

            return .{
                .name = @typeName(T),
                .size = @sizeOf(T),
                .@"align" = @alignOf(T),
                .fields = &field_infos,
            };
        }
    }

    pub fn jsonStringify(
        self: *const StructInfo,
        jws: anytype,
    ) std.Io.Writer.Error!void {
        try jws.beginObject();
        try jws.objectField("size");
        try jws.write(self.size);
        try jws.objectField("align");
        try jws.write(self.@"align");
        try jws.objectField("fields");
        try jws.beginObject();
        for (self.fields) |field| {
            try jws.objectField(field.name);
            try jws.beginObject();
            try jws.objectField("offset");
            try jws.write(field.offset);
            try jws.objectField("size");
            try jws.write(field.size);
            try jws.objectField("type");
            try jws.write(field.type);
            try jws.endObject();
        }
        try jws.endObject();
        try jws.endObject();
    }
};

fn jsonWriteAll(writer: *std.Io.Writer) std.Io.Writer.Error!void {
    var jws: std.json.Stringify = .{ .writer = writer };
    try jws.beginObject();
    for (structs.keys(), structs.values()) |name, *info| {
        try jws.objectField(name);
        try info.jsonStringify(&jws);
    }
    try jws.endObject();
}

fn typeName(comptime T: type) []const u8 {
    return switch (@typeInfo(T)) {
        .bool => "bool",
        .float => |info| switch (info.bits) {
            32 => "f32",
            64 => "f64",
            else => @compileError("unsupported float size"),
        },
        .int => |info| switch (info.signedness) {
            .signed => switch (info.bits) {
                8 => "i8",
                16 => "i16",
                32 => "i32",
                64 => "i64",
                else => @compileError("unsupported signed int size"),
            },
            .unsigned => switch (info.bits) {
                8 => "u8",
                16 => "u16",
                32 => "u32",
                64 => "u64",
                else => @compileError("unsupported unsigned int size"),
            },
        },
        .@"enum" => "enum",
        .@"struct" => "struct",
        .pointer => "pointer",
        .array => "array",
        else => "opaque",
    };
}

test "json parses" {
    const parsed = try std.json.parseFromSlice(
        std.json.Value,
        std.testing.allocator,
        json,
        .{},
    );
    defer parsed.deinit();

    const root = parsed.value.object;

    // Verify we have every public struct declared by the C API headers.
    const expected_structs = [_][]const u8{
        "GhosttyAllocator",
        "GhosttyAllocatorVtable",
        "GhosttyBuffer",
        "GhosttyClipboardContent",
        "GhosttyClipboardWrite",
        "GhosttyCodepoints",
        "GhosttyColorPaletteMask",
        "GhosttyColorRgb",
        "GhosttyColorX11Entry",
        "GhosttyDeviceAttributes",
        "GhosttyDeviceAttributesPrimary",
        "GhosttyDeviceAttributesSecondary",
        "GhosttyDeviceAttributesTertiary",
        "GhosttyFormatterScreenExtra",
        "GhosttyFormatterTerminalExtra",
        "GhosttyFormatterTerminalOptions",
        "GhosttyGridRef",
        "GhosttyKittyGraphicsPlacementRenderInfo",
        "GhosttyMouseEncoderSize",
        "GhosttyMousePosition",
        "GhosttyPoint",
        "GhosttyPointCoordinate",
        "GhosttyReader",
        "GhosttyRenderStateColors",
        "GhosttyRenderStateRowSelection",
        "GhosttySelection",
        "GhosttySelectionGestureBehaviors",
        "GhosttySelectionGestureGeometry",
        "GhosttySgrAttribute",
        "GhosttySgrUnknown",
        "GhosttySizeReportSize",
        "GhosttyString",
        "GhosttyStyle",
        "GhosttyStyleColor",
        "GhosttySurfacePosition",
        "GhosttySysImage",
        "GhosttyTerminalDesktopNotification",
        "GhosttyTerminalModeConfig",
        "GhosttyTerminalProgressReport",
        "GhosttyTerminalScrollbar",
        "GhosttyTerminalScrollViewport",
        "GhosttyTerminalSelectLineOptions",
        "GhosttyTerminalSelectWordBetweenOptions",
        "GhosttyTerminalSelectWordOptions",
        "GhosttyTerminalSelectionFormatOptions",
        "GhosttyTerminalUnknownSequence",
        "GhosttyTerminalUnknownStringSequence",
        "GhosttyWriter",
    };
    try std.testing.expectEqual(expected_structs.len, root.count());
    for (expected_structs) |name| {
        try std.testing.expect(root.contains(name));
    }

    const clipboard_content = root.get("GhosttyClipboardContent").?.object;
    const clipboard_content_fields = clipboard_content.get("fields").?.object;
    try std.testing.expect(clipboard_content_fields.contains("mime"));
    try std.testing.expect(clipboard_content_fields.contains("data"));

    const clipboard_write = root.get("GhosttyClipboardWrite").?.object;
    const clipboard_write_fields = clipboard_write.get("fields").?.object;
    try std.testing.expect(clipboard_write_fields.contains("size"));
    try std.testing.expect(clipboard_write_fields.contains("location"));
    try std.testing.expect(clipboard_write_fields.contains("contents"));
    try std.testing.expect(clipboard_write_fields.contains("contents_len"));

    const unknown_sequence = root.get("GhosttyTerminalUnknownSequence").?.object;
    const unknown_sequence_fields = unknown_sequence.get("fields").?.object;
    try std.testing.expect(unknown_sequence_fields.contains("tag"));
    try std.testing.expect(unknown_sequence_fields.contains("value"));

    const unknown_string = root.get("GhosttyTerminalUnknownStringSequence").?.object;
    const unknown_string_fields = unknown_string.get("fields").?.object;
    try std.testing.expect(unknown_string_fields.contains("truncated"));
    try std.testing.expect(unknown_string_fields.contains("content"));

    const reader_fields = root.get("GhosttyReader").?.object
        .get("fields").?.object;
    try std.testing.expect(reader_fields.contains("read"));
    try std.testing.expect(reader_fields.contains("userdata"));

    const writer_fields = root.get("GhosttyWriter").?.object
        .get("fields").?.object;
    try std.testing.expect(writer_fields.contains("write"));
    try std.testing.expect(writer_fields.contains("userdata"));

    try std.testing.expect(!root.contains("GhosttyTerminalOptions"));
}

test "struct sizes are non-zero" {
    const parsed = try std.json.parseFromSlice(
        std.json.Value,
        std.testing.allocator,
        json,
        .{},
    );
    defer parsed.deinit();

    var it = parsed.value.object.iterator();
    while (it.next()) |entry| {
        const struct_info = entry.value_ptr.object;
        const size = struct_info.get("size").?.integer;
        try std.testing.expect(size > 0);
    }
}
