const std = @import("std");
const fs = std.fs;
const mem = std.mem;
const ascii = std.ascii;

const catalog_txt = @embedFile("crc/catalog.txt");

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

    const args = try std.process.argsAlloc(arena);
    if (args.len <= 1) {
        usageAndExit(std.io.getStdErr(), args[0], 1);
    }

    const zig_src_root = args[1];
    if (mem.startsWith(u8, zig_src_root, "-")) {
        usageAndExit(std.io.getStdErr(), args[0], 1);
    }

    var zig_src_dir = try fs.cwd().openDir(zig_src_root, .{});
    defer zig_src_dir.close();

    const hash_sub_path = try fs.path.join(arena, &.{ "lib", "std", "hash" });
    var hash_target_dir = try zig_src_dir.makeOpenPath(hash_sub_path, .{});
    defer hash_target_dir.close();

    const crc_sub_path = try fs.path.join(arena, &.{ "lib", "std", "hash", "crc" });
    var crc_target_dir = try zig_src_dir.makeOpenPath(crc_sub_path, .{});
    defer crc_target_dir.close();

    var zig_code_file = try hash_target_dir.createFile("crc.zig", .{});
    defer zig_code_file.close();

    var cbw = std.io.bufferedWriter(zig_code_file.writer());
    defer cbw.flush() catch unreachable;
    const code_writer = cbw.writer();

    try code_writer.writeAll(
        \\//! This file is auto-generated by tools/update_crc_catalog.zig.
        \\
        \\const impl = @import("crc/impl.zig");
        \\
        \\pub const Crc = impl.Crc;
        \\pub const Polynomial = impl.Polynomial;
        \\pub const Crc32WithPoly = impl.Crc32WithPoly;
        \\pub const Crc32SmallWithPoly = impl.Crc32SmallWithPoly;
        \\
        \\pub const Crc32 = Crc32IsoHdlc;
        \\
        \\test {
        \\    _ = @import("crc/test.zig");
        \\}
        \\
    );

    var zig_test_file = try crc_target_dir.createFile("test.zig", .{});
    defer zig_test_file.close();

    var tbw = std.io.bufferedWriter(zig_test_file.writer());
    defer tbw.flush() catch unreachable;
    const test_writer = tbw.writer();

    try test_writer.writeAll(
        \\//! This file is auto-generated by tools/update_crc_catalog.zig.
        \\
        \\const std = @import("std");
        \\const testing = std.testing;
        \\const verify = @import("../verify.zig");
        \\const crc = @import("../crc.zig");
        \\
        \\test "crc32 ieee regression" {
        \\    const crc32 = crc.Crc32IsoHdlc;
        \\    try testing.expectEqual(crc32.hash(""), 0x00000000);
        \\    try testing.expectEqual(crc32.hash("a"), 0xe8b7be43);
        \\    try testing.expectEqual(crc32.hash("abc"), 0x352441c2);
        \\}
        \\
        \\test "crc32 castagnoli regression" {
        \\    const crc32 = crc.Crc32Iscsi;
        \\    try testing.expectEqual(crc32.hash(""), 0x00000000);
        \\    try testing.expectEqual(crc32.hash("a"), 0xc1d04330);
        \\    try testing.expectEqual(crc32.hash("abc"), 0x364b3fb7);
        \\}
        \\
        \\test "crc32 koopman regression" {
        \\    const crc32 = crc.Koopman;
        \\    try testing.expectEqual(crc32.hash(""), 0x00000000);
        \\    try testing.expectEqual(crc32.hash("a"), 0x0da2aa8a);
        \\    try testing.expectEqual(crc32.hash("abc"), 0xba2322ac);
        \\}
        \\
    );

    var stream = std.io.fixedBufferStream(catalog_txt);
    const reader = stream.reader();

    while (try reader.readUntilDelimiterOrEofAlloc(arena, '\n', std.math.maxInt(usize))) |line| {
        if (line.len == 0 or line[0] == '#')
            continue;

        var width: []const u8 = undefined;
        var poly: []const u8 = undefined;
        var init: []const u8 = undefined;
        var refin: []const u8 = undefined;
        var refout: []const u8 = undefined;
        var xorout: []const u8 = undefined;
        var check: []const u8 = undefined;
        var residue: []const u8 = undefined;
        var name: []const u8 = undefined;

        var it = mem.splitSequence(u8, line, "  ");
        while (it.next()) |property| {
            const i = mem.indexOf(u8, property, "=").?;
            const key = property[0..i];
            const value = property[i + 1 ..];
            if (mem.eql(u8, key, "width")) {
                width = value;
            } else if (mem.eql(u8, key, "poly")) {
                poly = value;
            } else if (mem.eql(u8, key, "init")) {
                init = value;
            } else if (mem.eql(u8, key, "refin")) {
                refin = value;
            } else if (mem.eql(u8, key, "refout")) {
                refout = value;
            } else if (mem.eql(u8, key, "xorout")) {
                xorout = value;
            } else if (mem.eql(u8, key, "check")) {
                check = value;
            } else if (mem.eql(u8, key, "residue")) {
                residue = value;
            } else if (mem.eql(u8, key, "name")) {
                name = mem.trim(u8, value, "\"");
            } else {
                unreachable;
            }
        }

        const snakecase = try ascii.allocLowerString(arena, name);
        defer arena.free(snakecase);

        _ = mem.replace(u8, snakecase, "-", "_", snakecase);
        _ = mem.replace(u8, snakecase, "/", "_", snakecase);

        var buf = try std.ArrayList(u8).initCapacity(arena, snakecase.len);
        defer buf.deinit();

        var prev: u8 = 0;
        for (snakecase, 0..) |c, i| {
            if (c == '_') {
                // do nothing
            } else if (i == 0) {
                buf.appendAssumeCapacity(ascii.toUpper(c));
            } else if (prev == '_') {
                buf.appendAssumeCapacity(ascii.toUpper(c));
            } else {
                buf.appendAssumeCapacity(c);
            }
            prev = c;
        }

        const camelcase = buf.items;

        try code_writer.writeAll(try std.fmt.allocPrint(arena,
            \\
            \\pub const {s} = Crc(u{s}, .{{
            \\    .polynomial = {s},
            \\    .initial = {s},
            \\    .reflect_input = {s},
            \\    .reflect_output = {s},
            \\    .xor_output = {s},
            \\}});
            \\
        , .{ camelcase, width, poly, init, refin, refout, xorout }));

        try test_writer.writeAll(try std.fmt.allocPrint(arena,
            \\
            \\test "{0s}" {{
            \\    const {1s} = crc.{1s};
            \\
            \\    try testing.expectEqual(@as(u{2s}, {3s}), {1s}.hash("123456789"));
            \\
            \\    var c = {1s}.init();
            \\    c.update("1234");
            \\    c.update("56789");
            \\    try testing.expectEqual(@as(u{2s}, {3s}), c.final());
            \\}}
            \\
        , .{ name, camelcase, width, check }));
    }
}

fn usageAndExit(file: fs.File, arg0: []const u8, code: u8) noreturn {
    file.writer().print(
        \\Usage: {s} /path/git/zig
        \\
    , .{arg0}) catch std.process.exit(1);
    std.process.exit(code);
}
