const std = @import("std");
const reg = @import("registry.zig");
const xml = @import("../xml.zig");
const renderRegistry = @import("render.zig").render;
const parseXml = @import("parse.zig").parseXml;
const IdRenderer = @import("../id_render.zig").IdRenderer;
const mem = std.mem;
const Allocator = mem.Allocator;
const FeatureLevel = reg.FeatureLevel;

const EnumFieldMerger = struct {
    const EnumExtensionMap = std.StringArrayHashMapUnmanaged(std.ArrayListUnmanaged(reg.Enum.Field));
    const FieldSet = std.StringArrayHashMapUnmanaged(void);

    arena: Allocator,
    registry: *reg.Registry,
    enum_extensions: EnumExtensionMap,
    field_set: FieldSet,

    fn init(arena: Allocator, registry: *reg.Registry) EnumFieldMerger {
        return .{
            .arena = arena,
            .registry = registry,
            .enum_extensions = .{},
            .field_set = .{},
        };
    }

    fn putEnumExtension(self: *EnumFieldMerger, enum_name: []const u8, field: reg.Enum.Field) !void {
        const res = try self.enum_extensions.getOrPut(self.arena, enum_name);
        if (!res.found_existing) {
            res.value_ptr.* = std.ArrayListUnmanaged(reg.Enum.Field){};
        }

        try res.value_ptr.append(self.arena, field);
    }

    fn addRequires(self: *EnumFieldMerger, reqs: []const reg.Require) !void {
        for (reqs) |req| {
            for (req.extends) |enum_ext| {
                try self.putEnumExtension(enum_ext.extends, enum_ext.field);
            }
        }
    }

    fn mergeEnumFields(self: *EnumFieldMerger, name: []const u8, base_enum: *reg.Enum) !void {
        // If there are no extensions for this enum, assume its valid.
        const extensions = self.enum_extensions.get(name) orelse return;

        self.field_set.clearRetainingCapacity();

        const n_fields_upper_bound = base_enum.fields.len + extensions.items.len;
        const new_fields = try self.arena.alloc(reg.Enum.Field, n_fields_upper_bound);
        var i: usize = 0;

        for (base_enum.fields) |field| {
            const res = try self.field_set.getOrPut(self.arena, field.name);
            if (!res.found_existing) {
                new_fields[i] = field;
                i += 1;
            }
        }

        // Assume that if a field name clobbers, the value is the same
        for (extensions.items) |field| {
            const res = try self.field_set.getOrPut(self.arena, field.name);
            if (!res.found_existing) {
                new_fields[i] = field;
                i += 1;
            }
        }

        // Existing base_enum.fields was allocated by `self.arena`, so
        // it gets cleaned up whenever that is deinited.
        base_enum.fields = try self.arena.realloc(new_fields, i);
    }

    fn merge(self: *EnumFieldMerger) !void {
        for (self.registry.features) |feature| {
            try self.addRequires(feature.requires);
        }

        for (self.registry.extensions) |ext| {
            try self.addRequires(ext.requires);
        }

        // Merge all the enum fields.
        // Assume that all keys of enum_extensions appear in `self.registry.decls`
        for (self.registry.decls) |*decl| {
            if (decl.decl_type == .enumeration) {
                try self.mergeEnumFields(decl.name, &decl.decl_type.enumeration);
            }
        }
    }
};

pub const Generator = struct {
    arena: std.heap.ArenaAllocator,
    registry: reg.Registry,
    id_renderer: IdRenderer,

    fn init(allocator: Allocator, spec: *xml.Element) !Generator {
        const result = try parseXml(allocator, spec);

        const tags = try allocator.alloc([]const u8, result.registry.tags.len);
        for (tags, 0..) |*tag, i| tag.* = result.registry.tags[i].name;

        return Generator{
            .arena = result.arena,
            .registry = result.registry,
            .id_renderer = IdRenderer.init(allocator, tags),
        };
    }

    fn deinit(self: Generator) void {
        self.arena.deinit();
    }

    fn stripFlagBits(self: Generator, name: []const u8) []const u8 {
        const tagless = self.id_renderer.stripAuthorTag(name);
        return tagless[0 .. tagless.len - "FlagBits".len];
    }

    fn stripFlags(self: Generator, name: []const u8) []const u8 {
        const tagless = self.id_renderer.stripAuthorTag(name);
        return tagless[0 .. tagless.len - "Flags".len];
    }

    // Solve `registry.declarations` according to `registry.extensions` and `registry.features`.
    fn mergeEnumFields(self: *Generator) !void {
        var merger = EnumFieldMerger.init(self.arena.allocator(), &self.registry);
        try merger.merge();
    }

    // https://github.com/KhronosGroup/Vulkan-Docs/pull/1556
    fn fixupBitFlags(self: *Generator) !void {
        var seen_bits = std.StringArrayHashMap(void).init(self.arena.allocator());
        defer seen_bits.deinit();

        for (self.registry.decls) |decl| {
            const bitmask = switch (decl.decl_type) {
                .bitmask => |bm| bm,
                else => continue,
            };

            if (bitmask.bits_enum) |bits_enum| {
                try seen_bits.put(bits_enum, {});
            }
        }

        var i: usize = 0;

        for (self.registry.decls) |decl| {
            switch (decl.decl_type) {
                .enumeration => |e| {
                    if (e.is_bitmask and seen_bits.get(decl.name) == null)
                        continue;
                },
                else => {},
            }
            self.registry.decls[i] = decl;
            i += 1;
        }

        self.registry.decls.len = i;
    }

    fn render(self: *Generator, writer: anytype) !void {
        try renderRegistry(writer, self.arena.allocator(), &self.registry, &self.id_renderer);
    }
};

/// Main function for generating the Vulkan bindings. vk.xml is to be provided via `spec_xml`,
/// and the resulting binding is written to `writer`. `allocator` will be used to allocate temporary
/// internal datastructures - mostly via an ArenaAllocator, but sometimes a hashmap uses this allocator
/// directly.
pub fn generate(allocator: Allocator, spec_xml: []const u8, writer: anytype) !void {
    const spec = try xml.parse(allocator, spec_xml);
    defer spec.deinit();

    var gen = try Generator.init(allocator, spec.root);
    defer gen.deinit();

    try gen.mergeEnumFields();
    try gen.fixupBitFlags();
    try gen.render(writer);
}
