const std = @import("std");

const max_size = 100_000_000;
var gpa = std.heap.GeneralPurposeAllocator(.{}){};

const Hash = std.crypto.hash.Md5;
const hashes_file = "template/hashes.bin";

fn instantiateTemplate(template: []const u8, day: u32) ![]const u8 {
    var list = std.ArrayList(u8).init(gpa.allocator());
    errdefer list.deinit();

    try list.ensureTotalCapacity(template.len + 100);
    var rest: []const u8 = template;
    while (std.mem.indexOfScalar(u8, rest, '$')) |index| {
        try list.appendSlice(rest[0..index]);
        try std.fmt.format(list.writer(), "{d:0>2}", .{day});
        rest = rest[index+1..];
    }
    try list.appendSlice(rest);
    return list.toOwnedSlice();
}

fn readHashes() !*[25][Hash.digest_length]u8 {
    const hash_bytes = std.fs.cwd().readFileAlloc(gpa.allocator(), hashes_file, 25 * Hash.digest_length) catch |err| switch (err) {
        error.FileTooBig => return error.InvalidFormat,
        else => |e| return e,
    };
    errdefer gpa.allocator().free(hash_bytes);

    if (hash_bytes.len != 25 * Hash.digest_length)
        return error.InvalidFormat;

    return @ptrCast(*[25][Hash.digest_length]u8, hash_bytes.ptr);
}

pub fn main() !void {
    const template = try std.fs.cwd().readFileAlloc(gpa.allocator(), "template/template.zig", max_size);

    const hashes: *[25][Hash.digest_length]u8 = readHashes() catch |err| switch (err) {
        error.FileNotFound => blk: {
            std.debug.print("{s} doesn't exist, will assume all files have been modified.\nDelete src/dayXX.zig and rerun `zig build generate` to regenerate it.\n", .{hashes_file});
            const mem = try gpa.allocator().create([25][Hash.digest_length]u8);
            @memset(@ptrCast([*]u8, mem), 0, @sizeOf(@TypeOf(mem.*)));
            break :blk mem;
        },
        error.InvalidFormat => {
            std.debug.print("{s} is corrupted, delete it to silence this warning and assume all days have been modified.\n", .{hashes_file});
            std.os.exit(1);
        },
        else => |e| {
            std.debug.print("Failed to open {s}: {}\n", .{hashes_file, e});
            return e;
        },
    };

    var skipped_any = false;
    var updated_hashes = false;
    var day: u32 = 1;
    while (day <= 25) : (day += 1) {
        const filename = try std.fmt.allocPrint(gpa.allocator(), "src/day{d:0>2}.zig", .{day});
        defer gpa.allocator().free(filename);

        var new_file = false;
        const file = std.fs.cwd().openFile(filename, .{.mode = .read_write}) catch |err| switch (err) {
            error.FileNotFound => blk: {
                new_file = true;
                break :blk try std.fs.cwd().createFile(filename, .{});
            },
            else => |e| return e,
        };
        defer file.close();

        var regenerate = false;
        if (!new_file) {
            const contents = file.readToEndAlloc(gpa.allocator(), max_size) catch |err| switch (err) {
                error.FileTooBig => {
                    std.debug.print("Skipping modified day {s}\n", .{filename});
                    skipped_any = true;
                    continue;
                },
                else => |e| return e,
            };
            defer gpa.allocator().free(contents);

            var hash: [Hash.digest_length]u8 = undefined;
            Hash.hash(contents, &hash, .{});

            regenerate = std.mem.eql(u8, &hash, &hashes[day-1]);
        } else {
            regenerate = true;
        }

        if (regenerate) {
            if (!new_file) {
                try file.seekTo(0);
                try file.setEndPos(0);
            }

            const text = try instantiateTemplate(template, day);
            defer gpa.allocator().free(text);

            Hash.hash(text, &hashes[day-1], .{});
            updated_hashes = true;

            try file.writeAll(text);
            if (new_file) {
                std.debug.print("Creating new file {s} from template.\n", .{filename});
            } else {
                std.debug.print("Updated {s}\n", .{filename});
            }
        } else {
            std.debug.print("Skipping modified day {s}\n", .{filename});
            skipped_any = true;
        }
    }

    if (updated_hashes) {
        try std.fs.cwd().writeFile(hashes_file, std.mem.asBytes(hashes));
        if (skipped_any) {
            std.debug.print("Some days were skipped. Delete them to force regeneration.\n",.{});
        }
    } else {
        std.debug.print("No updates made, all days were modified. Delete src/dayXX.zig to force regeneration.\n", .{});
    }
}