const std = @import("std");
const Io = std.Io;
const Dir = std.Io.Dir;
const mem = std.mem;

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

pub fn main(init: std.process.Init) !void {
    const arena = init.arena.allocator();
    const io = init.io;
    const args = try init.minimal.args.toSlice(arena);
    return @"i like cheese"(arena, io, args);
}

fn @"i like cheese"(arena: std.mem.Allocator, io: Io, args: []const []const u8) !void {
    if (args.len <= 1) printUsageAndExit(args[0]);

    const zig_src_root = args[1];
    if (mem.startsWith(u8, zig_src_root, "-")) printUsageAndExit(args[0]);

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

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

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

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

    var zig_code_file_buffer: [4096]u8 = undefined;
    var zig_code_file_writer = zig_code_file.writer(io, &zig_code_file_buffer);
    const code_writer = &zig_code_file_writer.interface;

    try code_writer.writeAll(
        \\//! This file is auto-generated by tools/update_crc_catalog.zig.
        \\
        \\const builtin = @import("builtin");
        \\
    );

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

    var zig_test_file_buffer: [4096]u8 = undefined;
    var zig_test_file_writer = zig_test_file.writer(io, &zig_test_file_buffer);
    const test_writer = &zig_test_file_writer.interface;

    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 Crc = crc.@"CRC-32/ISO-HDLC";
        \\    try testing.expectEqual(Crc.hash(""), 0x00000000);
        \\    try testing.expectEqual(Crc.hash("a"), 0xe8b7be43);
        \\    try testing.expectEqual(Crc.hash("abc"), 0x352441c2);
        \\}
        \\
        \\test "crc32 castagnoli regression" {
        \\    const Crc = crc.@"CRC-32/ISCSI";
        \\    try testing.expectEqual(Crc.hash(""), 0x00000000);
        \\    try testing.expectEqual(Crc.hash("a"), 0xc1d04330);
        \\    try testing.expectEqual(Crc.hash("abc"), 0x364b3fb7);
        \\}
        \\
        \\test "crc32 koopman regression" {
        \\    const Crc = crc.@"CRC-32/KOOPMAN";
        \\    try testing.expectEqual(Crc.hash(""), 0x00000000);
        \\    try testing.expectEqual(Crc.hash("a"), 0x0da2aa8a);
        \\    try testing.expectEqual(Crc.hash("abc"), 0xba2322ac);
        \\}
        \\
    );

    var reader: std.Io.Reader = .fixed(catalog_txt);

    while (try reader.takeDelimiter('\n')) |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;
            }
        }

        if (mem.eql(u8, name, "CRC-32/ISCSI")) {
            try code_writer.print(
                \\
                \\pub const {f} = if (builtin.cpu.hasAll(.x86, &.{{ .@"64bit", .crc32 }}))
                \\    @import("crc/Crc32c.zig")
                \\else
                \\    Generic(u{s}, .{{
                \\        .polynomial = {s},
                \\        .initial = {s},
                \\        .reflect_input = {s},
                \\        .reflect_output = {s},
                \\        .xor_output = {s},
                \\    }});
                \\
            , .{ std.zig.fmtId(name), width, poly, init, refin, refout, xorout });
        } else {
            try code_writer.print(
                \\
                \\pub const {f} = Generic(u{s}, .{{
                \\    .polynomial = {s},
                \\    .initial = {s},
                \\    .reflect_input = {s},
                \\    .reflect_output = {s},
                \\    .xor_output = {s},
                \\}});
                \\
            , .{ std.zig.fmtId(name), width, poly, init, refin, refout, xorout });
        }

        try test_writer.print(
            \\
            \\test "{0s}" {{
            \\    const Crc = crc.{1f};
            \\
            \\    try testing.expectEqual(@as(u{2s}, {3s}), Crc.hash("123456789"));
            \\
            \\    var c = Crc.init();
            \\    c.update("1234");
            \\    c.update("56789");
            \\    try testing.expectEqual(@as(u{2s}, {3s}), c.final());
            \\}}
            \\
        , .{ name, std.zig.fmtId(name), width, check });
    }

    try code_writer.writeAll(
        \\
        \\pub fn Algorithm(comptime W: type) type {
        \\    return struct {
        \\        polynomial: W,
        \\        initial: W,
        \\        reflect_input: bool,
        \\        reflect_output: bool,
        \\        xor_output: W,
        \\    };
        \\}
        \\
        \\pub fn Generic(comptime W: type, comptime algorithm: Algorithm(W)) type {
        \\    return struct {
        \\        const Self = @This();
        \\        const I = if (@bitSizeOf(W) < 8) u8 else W;
        \\        const lookup_table = blk: {
        \\            @setEvalBranchQuota(2500);
        \\            const poly = reflect(algorithm.polynomial);
        \\            var table: [256]I = undefined;
        \\            for (&table, 0..) |*e, i| {
        \\                var crc: I = i;
        \\                if (algorithm.reflect_input) {
        \\                    var j: usize = 0;
        \\                    while (j < 8) : (j += 1) {
        \\                        crc = (crc >> 1) ^ ((crc & 1) * poly);
        \\                    }
        \\                } else {
        \\                    crc <<= @bitSizeOf(I) - 8;
        \\                    var j: usize = 0;
        \\                    while (j < 8) : (j += 1) {
        \\                        crc = (crc << 1) ^ (((crc >> (@bitSizeOf(I) - 1)) & 1) * poly);
        \\                    }
        \\                }
        \\                e.* = crc;
        \\            }
        \\            break :blk table;
        \\        };
        \\
        \\        crc: I,
        \\
        \\        pub fn init() Self {
        \\            const initial = reflect(algorithm.initial);
        \\            return .{ .crc = initial };
        \\        }
        \\
        \\        inline fn tableEntry(index: I) I {
        \\            const short: u8 = @truncate(index);
        \\            return lookup_table[short];
        \\        }
        \\
        \\        pub fn update(self: *Self, bytes: []const u8) void {
        \\            var i: usize = 0;
        \\            if (@bitSizeOf(I) <= 8) {
        \\                while (i < bytes.len) : (i += 1) {
        \\                    self.crc = tableEntry(self.crc ^ bytes[i]);
        \\                }
        \\            } else if (algorithm.reflect_input) {
        \\                while (i < bytes.len) : (i += 1) {
        \\                    const table_index = self.crc ^ bytes[i];
        \\                    self.crc = tableEntry(table_index) ^ (self.crc >> 8);
        \\                }
        \\            } else {
        \\                while (i < bytes.len) : (i += 1) {
        \\                    const table_index = (self.crc >> (@bitSizeOf(I) - 8)) ^ bytes[i];
        \\                    self.crc = tableEntry(table_index) ^ (self.crc << 8);
        \\                }
        \\            }
        \\        }
        \\
        \\        pub fn final(self: Self) W {
        \\            var c = self.crc;
        \\            if (algorithm.reflect_input != algorithm.reflect_output) {
        \\                c = @bitReverse(c);
        \\            }
        \\            if (!algorithm.reflect_output) {
        \\                c >>= @bitSizeOf(I) - @bitSizeOf(W);
        \\            }
        \\            return @intCast(c ^ algorithm.xor_output);
        \\        }
        \\
        \\        pub fn hash(bytes: []const u8) W {
        \\            var c = Self.init();
        \\            c.update(bytes);
        \\            return c.final();
        \\        }
        \\
        \\        fn reflect(x: I) I {
        \\            const offset = @bitSizeOf(I) - @bitSizeOf(W);
        \\            if (algorithm.reflect_input)
        \\                return @bitReverse(x) >> offset
        \\            else
        \\                return x << offset;
        \\        }
        \\    };
        \\}
        \\
        \\test {
        \\    _ = @import("crc/test.zig");
        \\}
        \\
    );

    try code_writer.flush();
    try test_writer.flush();
}

fn printUsageAndExit(arg0: []const u8) noreturn {
    const stderr = std.debug.lockStderr(&.{});
    const w = &stderr.file_writer.interface;
    printUsage(w, arg0) catch std.process.exit(2);
    std.process.exit(1);
}

fn printUsage(w: *std.Io.Writer, arg0: []const u8) std.Io.Writer.Error!void {
    return w.print(
        \\Usage: {s} /path/git/zig
        \\
    , .{arg0});
}
