const std = @import("std");
const LazyPath = std.Build.LazyPath;

pub const AddElementOptions = struct {
    pub const ElementDef = struct {
        tag: []const u8,
        content: []const u8 = "",
        attributes: []const u8 = "",
    };

    pub const Parent = enum { body, head };
    pub const Position = enum { starting, ending };

    parent: Parent,
    position: Position,
    element: ElementDef,
    priority: u8 = 128,
};

/// A custom build step that generates a Zig module containing pre-rendered HTML injections.
pub const InjectionsGenStep = struct {
    step: std.Build.Step,
    injections: std.ArrayListUnmanaged(AddElementOptions),
    output_file: std.Build.GeneratedFile,

    pub fn create(b: *std.Build) !*InjectionsGenStep {
        const self = b.allocator.create(InjectionsGenStep) catch @panic("OOM");
        self.* = .{
            .step = std.Build.Step.init(.{
                .id = .custom,
                .name = "generate zx injections",
                .owner = b,
                .makeFn = make,
            }),
            .injections = .{},
            .output_file = .{ .step = &self.step },
        };
        return self;
    }

    pub fn add(self: *InjectionsGenStep, options: AddElementOptions) void {
        const allocator = self.step.owner.allocator;
        self.injections.append(allocator, options) catch @panic("OOM");
    }

    pub fn getOutput(self: *InjectionsGenStep) LazyPath {
        return .{ .generated = .{ .file = &self.output_file } };
    }

    fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void {
        const self: *InjectionsGenStep = @fieldParentPtr("step", step);
        const b = step.owner;
        const allocator = b.allocator;

        var content = std.ArrayListUnmanaged(u8){};
        defer content.deinit(allocator);

        try content.appendSlice(allocator, "// Generated by Ziex build system\n");
        const points = [_]struct { name: []const u8, parent: AddElementOptions.Parent, pos: AddElementOptions.Position }{
            .{ .name = "head_starting", .parent = .head, .pos = .starting },
            .{ .name = "head_ending", .parent = .head, .pos = .ending },
            .{ .name = "body_starting", .parent = .body, .pos = .starting },
            .{ .name = "body_ending", .parent = .body, .pos = .ending },
        };

        for (points) |p| {
            const html = renderInjections(allocator, self.injections.items, p.parent, p.pos);
            try content.writer(allocator).print("pub const {s}: []const u8 = ", .{p.name});

            // Serialize string literal
            try content.appendSlice(allocator, "\"");
            for (html) |c| {
                switch (c) {
                    '\"' => try content.appendSlice(allocator, "\\\""),
                    '\\' => try content.appendSlice(allocator, "\\\\"),
                    '\n' => try content.appendSlice(allocator, "\\n"),
                    '\r' => try content.appendSlice(allocator, "\\r"),
                    '\t' => try content.appendSlice(allocator, "\\t"),
                    else => {
                        if (std.ascii.isPrint(c)) {
                            try content.append(allocator, c);
                        } else {
                            try content.writer(allocator).print("\\x{x:0>2}", .{c});
                        }
                    },
                }
            }
            try content.appendSlice(allocator, "\";\n");
        }

        const name = "zx_injections.zig";
        const path = try b.cache_root.join(allocator, &.{name});
        try b.cache_root.handle.writeFile(.{ .sub_path = name, .data = content.items });

        self.output_file.path = path;
    }
};

fn renderInjections(
    allocator: std.mem.Allocator,
    injections: []const AddElementOptions,
    parent: AddElementOptions.Parent,
    position: AddElementOptions.Position,
) []const u8 {
    var matching = std.ArrayListUnmanaged(AddElementOptions){};
    for (injections) |inj| {
        if (inj.parent == parent and inj.position == position) {
            matching.append(allocator, inj) catch @panic("OOM");
        }
    }

    std.mem.sortUnstable(AddElementOptions, matching.items, {}, struct {
        fn lessThan(_: void, a: AddElementOptions, b_item: AddElementOptions) bool {
            return a.priority < b_item.priority;
        }
    }.lessThan);

    var result = std.ArrayListUnmanaged(u8){};
    for (matching.items) |inj| {
        result.appendSlice(allocator, renderSingleElement(allocator, inj.element)) catch @panic("OOM");
    }
    return result.items;
}

fn renderSingleElement(allocator: std.mem.Allocator, elem: AddElementOptions.ElementDef) []const u8 {
    var html = std.ArrayListUnmanaged(u8){};
    html.appendSlice(allocator, "<") catch @panic("OOM");
    html.appendSlice(allocator, elem.tag) catch @panic("OOM");
    if (elem.attributes.len > 0) {
        html.appendSlice(allocator, " ") catch @panic("OOM");
        html.appendSlice(allocator, elem.attributes) catch @panic("OOM");
    }
    if (isVoidElement(elem.tag)) {
        html.appendSlice(allocator, " />") catch @panic("OOM");
    } else {
        html.appendSlice(allocator, ">") catch @panic("OOM");
        html.appendSlice(allocator, elem.content) catch @panic("OOM");
        html.appendSlice(allocator, "</") catch @panic("OOM");
        html.appendSlice(allocator, elem.tag) catch @panic("OOM");
        html.appendSlice(allocator, ">") catch @panic("OOM");
    }
    return html.items;
}

fn isVoidElement(tag: []const u8) bool {
    const void_tags = [_][]const u8{
        "area", "base", "br", "col", "embed", "hr",
        "img", "input", "link", "meta", "param", "source",
        "track", "wbr",
    };
    for (void_tags) |vt| {
        if (std.mem.eql(u8, tag, vt)) return true;
    }
    return false;
}
