pub const CookResultEnum = enum {
    Success,
    Failure,
};

pub const FileBytes = struct {
    path: []const u8,
    bytes: []u8,
};

pub const ExtraFiles = std.ArrayList(FileBytes);

pub const CookResult = struct {
    bytes: std.ArrayList(u8),
    result: CookResultEnum = .Success,
    extraFiles: ?ExtraFiles = null,

    pub fn deinit(self: *@This()) void {
        self.bytes.deinit();
    }
};

pub const CookParams = struct {
    noSave: bool = false,
    fileName: []const u8,
    cookFileName: []const u8,
};
pub const CookInfo = struct {
    assetType: []const u8 = "none",
};

pub const CookRegistry = struct {
    allocator: std.mem.Allocator,
    cooks: std.StringHashMapUnmanaged(CookFunction) = .{},
    generates: std.StringHashMapUnmanaged(CookGenerateFunction) = .{},

    typesByExt: std.StringHashMapUnmanaged([]const u8) = .{},

    pub fn create(allocator: std.mem.Allocator) !*@This() {
        const self = try allocator.create(@This());
        self.* = .{
            .allocator = allocator,
        };

        gCookRegistry = self;
        return self;
    }

    pub fn install(self: *@This(), typeName: []const u8, generate: CookGenerateFunction, cook: CookFunction, extensions: []const []const u8) !void {
        try self.generates.put(self.allocator, typeName, generate);
        try self.cooks.put(self.allocator, typeName, cook);

        for (extensions) |ext| {
            try self.typesByExt.put(self.allocator, ext, typeName);
        }
    }

    pub fn destroy(self: *@This()) void {
        self.cooks.deinit(self.allocator);
        self.typesByExt.deinit(self.allocator);
        self.generates.deinit(self.allocator);

        self.allocator.destroy(self);
    }
};

var gCookRegistry: *CookRegistry = undefined;
var gCooking: bool = false;

pub fn getRegistry() *CookRegistry {
    return gCookRegistry;
}

pub fn startup(allocator: std.mem.Allocator) !void {
    gCooking = true;
    _ = try CookRegistry.create(allocator);
}

pub fn shutdown() void {
    if (gCooking) {
        gCookRegistry.destroy();
        gCooking = false;
    }
}

pub const GenerateError = error{UnableToGenerate};
pub const LoadError = error{BadFile};

pub const CookGenerateFunction = *const fn (allocator: std.mem.Allocator, filePath: []const u8, out: *std.ArrayList(u8)) GenerateError!void;
pub const CookFunction = *const fn (allocator: std.mem.Allocator, dir: std.fs.Dir, path: []const u8, params: CookParams) CookResult;

pub fn loadFileAlloc(allocator: std.mem.Allocator, dir: std.fs.Dir, path: []const u8) ![]align(8) u8 {
    var file = try dir.openFile(path, .{});
    defer file.close();
    const filesize = (try file.stat()).size + 1; // add null byte
    const buffer: []align(8) u8 = try allocator.alignedAlloc(u8, 8, filesize);
    errdefer allocator.free(buffer);
    try file.reader().readNoEof(buffer[0 .. buffer.len - 1]);
    buffer[buffer.len - 1] = 0;

    return buffer;
}

pub fn getExtension(path: []const u8) ?[]const u8 {
    var extension: []const u8 = path;
    var i: i32 = @intCast(path.len - 1);
    while (i > 0) : (i -= 1) {
        if (path[@intCast(i)] == '.') {
            extension = path[@intCast(i)..];
            return extension;
        }
    }

    return null;
}

pub fn generateCookFile(allocator: std.mem.Allocator, dir: std.fs.Dir, path: []const u8) !void {
    if (getExtension(path)) |extension| {
        if (gCookRegistry.typesByExt.get(extension)) |assetType| {
            var cookFilePath = std.ArrayList(u8).init(allocator);
            defer cookFilePath.deinit();
            const generateFunction = gCookRegistry.generates.get(assetType).?;

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

            try outPath.appendSlice(allocator, path);
            try outPath.appendSlice(allocator, ".cook");

            try generateFunction(allocator, path, &cookFilePath);

            const file = try dir.createFile(outPath.items, .{});
            defer file.close();

            try file.writeAll(cookFilePath.items);
        }
    }
}

pub fn cookFile(allocator: std.mem.Allocator, dir: std.fs.Dir, path: []const u8) !void {
    std.debug.print("{s}\n", .{path});
    const extension = getExtension(path).?;

    if (std.mem.eql(u8, extension, ".cook")) {
        return;
    }

    var cookFilePath = std.ArrayList(u8).init(allocator);
    defer cookFilePath.deinit();

    try cookFilePath.appendSlice(path);
    try cookFilePath.appendSlice(".cook");

    if (gCookRegistry.typesByExt.get(extension)) |assetType| {
        const cookFunction = gCookRegistry.cooks.get(assetType).?;

        dir.access(cookFilePath.items, .{}) catch return;

        var results = cookFunction(allocator, dir, path, .{
            .cookFileName = cookFilePath.items,
            .fileName = path,
        });
        defer results.deinit();

        var outPath = std.ArrayList(u8).init(allocator);
        defer outPath.deinit();

        try outPath.appendSlice("_cooked/");
        try outPath.appendSlice(path);
        try outPath.appendSlice(".");
        try outPath.appendSlice(assetType);

        {
            var basePath: []const u8 = outPath.items;
            var index: usize = basePath.len - 1;
            while (basePath[index] != '/' and basePath[index] != '\\') : (index -= 1) {}
            basePath = basePath[0..index];
            try dir.makePath(basePath);
        }

        if (results.result == .Success) {
            const file = try dir.createFile(outPath.items, .{});
            defer file.close();
            try file.writeAll(results.bytes.items);
        }
        return;
    } else {
        unreachable;
    }
}

pub fn generateAllCookFiles(allocator: std.mem.Allocator, dir: std.fs.Dir) !void {
    var iter = dir.iterate();
    while (try iter.next()) |next| {
        switch (next.kind) {
            .file => {
                try generateCookFile(allocator, dir, next.name);
            },
            .directory => {
                if (!std.mem.eql(u8, "_cooked", next.name)) {
                    var subDir = try dir.openDir(next.name, .{ .iterate = true });
                    defer subDir.close();

                    try generateAllCookFiles(allocator, subDir);
                }
            },
            else => {},
        }
    }
}

pub fn cookAllFiles(allocator: std.mem.Allocator, dir: std.fs.Dir) !void {
    var walker = try dir.walk(allocator);
    defer walker.deinit();

    while (try walker.next()) |next| {
        switch (next.kind) {
            .file => {
                if (std.mem.startsWith(u8, next.path, "_cooked")) {
                    continue;
                }

                if (std.mem.startsWith(u8, next.path, ".gitignore")) {
                    continue;
                }

                try cookFile(allocator, dir, next.path);
            },
            else => {},
        }
    }
}

test "testing cooking" {
    const alloc = std.testing.allocator;

    const Test = struct {
        const TestConfig = struct {
            info: CookInfo = .{ .assetType = "Texture" }, // there must always be a CookInfo field
            sourceType: []const u8 = "png",
        };

        pub fn generateFunction(allocator: std.mem.Allocator, path: []const u8, out: *std.ArrayList(u8)) GenerateError!void {
            _ = allocator;
            _ = path;
            out.clearRetainingCapacity();

            std.json.stringify(TestConfig{}, .{ .whitespace = .indent_4 }, out.writer()) catch return GenerateError.UnableToGenerate;
        }

        pub fn cookFunction(allocator: std.mem.Allocator, dir: std.fs.Dir, path: []const u8, params: CookParams) CookResult {
            _ = params;
            const rawFileBytes = loadFileAlloc(allocator, dir, path) catch unreachable;
            defer allocator.free(rawFileBytes);
            const rv = std.ArrayList(u8).init(allocator);

            return .{
                .bytes = rv,
                .result = .Success,
            };
        }
    };

    const dir = try std.fs.cwd().openDir("test", .{ .iterate = true });

    const registry = try CookRegistry.create(alloc);
    defer registry.destroy();
    try registry.generates.put(registry.allocator, "Texture", Test.generateFunction);
    try registry.cooks.put(registry.allocator, "Texture", Test.cookFunction);
    try registry.typesByExt.put(registry.allocator, ".png", "Texture");

    // scan and generate all texture files
    try generateAllCookFiles(alloc, dir);
    try cookAllFiles(alloc, dir);
    // try generateCookFile(alloc, dir, "icon.png");
    // try cookFile(alloc, dir, "icon.png");
}

const std = @import("std");
