const std = @import("std");
const testing = std.testing;
const Target = @import("target.zig").Target;

/// Create a tagged union type that supports a C ABI and maintains
/// C ABI compatibility when adding new tags. This returns a set of types
/// and functions to augment the given Union type, not create a wholly new
/// union type.
///
/// The C ABI compatible types and functions are only available when the
/// target produces C values.
///
/// The `Union` type should be a standard Zig tagged union. The tag type
/// should be explicit (i.e. not `union(enum)`) and the tag type should
/// be an enum created with the `Enum` function in this library, so that
/// automatic C ABI compatibility is ensured.
///
/// `options.padding` is a type that is always added to the C union with the key
/// `_padding`. It should have the size and alignment needed to pad the C union
/// to the expected size and should never change to ensure ABI compatibility.
///
/// Each tag has an optional field in `options.field_renames` that may rename its
/// C value field. Multiple tags may map to the same field when their C value
/// types are identical. Tags with no payload are omitted. This is used
/// for metadata and should match the C headers directly.
pub fn TaggedUnion(
    comptime target: Target,
    comptime Union: type,
    comptime options: TaggedUnionOptions(Union),
) type {
    return struct {
        comptime {
            switch (target) {
                .zig => {},

                // For ABI compatibility, we expect that this is our union size.
                .c => if (@sizeOf(CValue) != @sizeOf(options.padding)) {
                    @compileLog(@sizeOf(CValue));
                    @compileError("TaggedUnion CValue size does not match expected fixed size");
                },
            }
        }

        /// The tag type.
        pub const Tag = @typeInfo(Union).@"union".tag_type.?;

        /// The Zig union.
        pub const Zig = Union;

        /// The C ABI compatible tagged union type.
        pub const C = switch (target) {
            .zig => struct {},
            .c => extern struct {
                tag: Tag,
                value: CValue,

                /// Returns the public C value-union field name for `tag`, or
                /// null when the tag has no value field. This is metadata only;
                /// it does not rename fields in `CValue`.
                pub fn cFieldRename(comptime tag: Tag) ?[]const u8 {
                    const tag_name = @tagName(tag);
                    const value = @field(@unionInit(Union, tag_name, undefined), tag_name);

                    if (@field(options.field_renames, tag_name)) |name| return name;
                    if (@sizeOf(@TypeOf(value)) == 0) return null;

                    return tag_name;
                }
            },
        };

        /// The C ABI compatible union value type.
        pub const CValue = cvalue: {
            @setEvalBranchQuota(10_000);
            switch (target) {
                .zig => break :cvalue extern struct {},
                .c => {},
            }

            const tag_fields = @typeInfo(Tag).@"enum".fields;
            var names: [tag_fields.len + 1][]const u8 = undefined;
            var types: [tag_fields.len + 1]type = undefined;
            var attrs: [tag_fields.len + 1]std.builtin.Type.UnionField.Attributes = undefined;

            for (tag_fields, 0..) |field, i| {
                const action = @unionInit(Union, field.name, undefined);
                const Type = t: {
                    const Type = @TypeOf(@field(action, field.name));
                    // Types can provide custom types for their CValue.
                    switch (@typeInfo(Type)) {
                        .@"enum", .@"struct", .@"union" => if (@hasDecl(Type, "C")) break :t Type.C,
                        else => {},
                    }

                    break :t Type;
                };

                names[i] = field.name;
                types[i] = Type;
                attrs[i] = .{ .@"align" = @alignOf(Type) };
            }

            names[tag_fields.len] = "_padding";
            types[tag_fields.len] = options.padding;
            attrs[tag_fields.len] = .{ .@"align" = @alignOf(options.padding) };

            break :cvalue @Union(.@"extern", null, &names, &types, &attrs);
        };

        /// Convert to C union.
        pub fn cval(self: Union) C {
            const value: CValue = switch (self) {
                inline else => |v, tag| @unionInit(
                    CValue,
                    @tagName(tag),
                    value: {
                        switch (@typeInfo(@TypeOf(v))) {
                            .@"enum", .@"struct", .@"union" => if (@hasDecl(@TypeOf(v), "cval")) break :value v.cval(),
                            else => {},
                        }

                        break :value v;
                    },
                ),
            };

            return .{
                .tag = @as(Tag, self),
                .value = value,
            };
        }

        /// Returns the value type for the given tag.
        pub fn Value(comptime tag: Tag) type {
            @setEvalBranchQuota(10000);
            inline for (@typeInfo(Union).@"union".fields) |field| {
                const field_tag = @field(Tag, field.name);
                if (field_tag == tag) return field.type;
            }

            unreachable;
        }
    };
}

/// Options for generating the C representation of a tagged union.
pub fn TaggedUnionOptions(comptime Union: type) type {
    const Tag = @typeInfo(Union).@"union".tag_type.?;
    const tag_fields = @typeInfo(Tag).@"enum".fields;
    const FieldRenames: type = field_renames: {
        const default_rename: ?[]const u8 = null;

        var names: [tag_fields.len][]const u8 = undefined;
        var types: [tag_fields.len]type = undefined;
        var attrs: [tag_fields.len]std.builtin.Type.StructField.Attributes = undefined;

        for (tag_fields, 0..) |field, i| {
            names[i] = field.name;
            types[i] = ?[]const u8;
            attrs[i] = .{ .default_value_ptr = &default_rename };
        }

        break :field_renames @Struct(.auto, null, &names, &types, &attrs);
    };
    return struct {
        padding: type,
        field_renames: FieldRenames = .{},
    };
}
test "TaggedUnion: matching size" {
    const Tag = enum(c_int) { a, b };
    const U = TaggedUnion(
        .c,
        union(Tag) {
            a: u32,
            b: u64,
        },
        .{ .padding = u64 },
    );

    try testing.expectEqual(8, @sizeOf(U.CValue));
}

test "TaggedUnion: padded size" {
    const Tag = enum(c_int) { a };
    const U = TaggedUnion(
        .c,
        union(Tag) {
            a: u32,
        },
        .{ .padding = u64 },
    );

    try testing.expectEqual(8, @sizeOf(U.CValue));
}

test "TaggedUnion: c conversion" {
    const Tag = enum(c_int) { a, b };
    const U = TaggedUnion(.c, union(Tag) {
        a: u32,
        b: u64,
    }, .{ .padding = u64 });

    const c = U.cval(.{ .a = 42 });
    try testing.expectEqual(Tag.a, c.tag);
    try testing.expectEqual(42, c.value.a);
}

test "TaggedUnion: custom C value fields" {
    const Tag = enum(c_int) { a, b, none };
    const Union = union(Tag) {
        a: u32,
        b: u32,
        none,
    };
    const U = TaggedUnion(.c, Union, .{
        .padding = u64,
        .field_renames = .{
            .a = "value",
            .b = "value",
        },
    });

    try testing.expect(!@hasField(U.CValue, "value"));
    try testing.expect(@hasField(U.CValue, "a"));
    try testing.expect(@hasField(U.CValue, "b"));
    try testing.expect(@hasField(U.CValue, "none"));
    try testing.expectEqualStrings("value", U.C.cFieldRename(.a).?);
    try testing.expect(U.C.cFieldRename(.none) == null);

    const a = U.cval(.{ .a = 42 });
    try testing.expectEqual(Tag.a, a.tag);
    try testing.expectEqual(42, a.value.a);

    const none = U.cval(.none);
    try testing.expectEqual(Tag.none, none.tag);
}
