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

// Check duplicate types
comptime {
    @setEvalBranchQuota(types.len * types.len * 7);
    inline for (types) |t1, i| {
        inline for (types) |t2, j| {
            if (i != j) {
                if (std.mem.eql(u8, t1[0], t2[0])) {
                    @compileError("Duplicate types: " ++ t1[0]);
                } else if (std.mem.eql(u8, t1[1], t2[1])) {
                    @compileError("Duplicate enum literal: " ++ t1[1]);
                }
            }
        }
    }
}

/// The types needed for mocking
/// The format is as follows:
///     1. The type represented as a string. This is because @typeName doesn't play nicely with
///        all types so this way, what is put here is what you get when generated. There can only
///        be one of each type.
///     2. The enum to represent the type. See other below for example names. These have to be
///        unique.
///     3. The import name for a type (what would go in the @import()) without the .zig. This is
///        optional as some types won't need an import. If a type has already been imported, then
///        this can be omitted. Currently this is a single import, but this can be extended to have
///        a comma separated list of import with types that contain types from multiple places.
///     4. The sub import. This is what would come after the @import() but before the type to be
///        imported. An easy example is the Allocator where the sub import would be std.mem with no
///        import as @import("std") is already included. Another example is if including a type
///        from a struct.
///     5. The base type to include. This is different to the type in (1) as will exclude pointer.
///        This will be the name of the type to be included.
const types = .{
    .{ "bool", "BOOL", "", "", "" },
    .{ "u4", "U4", "", "", "" },
    .{ "u8", "U8", "", "", "" },
    .{ "u16", "U16", "", "", "" },
    .{ "u32", "U32", "", "", "" },
    .{ "usize", "USIZE", "", "", "" },
    .{ "StatusRegister", "STATUSREGISTER", "cmos_mock", "", "StatusRegister" },
    .{ "RtcRegister", "RTCREGISTER", "cmos_mock", "", "RtcRegister" },
    .{ "IdtPtr", "IDTPTR", "idt_mock", "", "IdtPtr" },
    .{ "*const GdtPtr", "PTR_CONST_GDTPTR", "gdt_mock", "", "GdtPtr" },
    .{ "*const IdtPtr", "PTR_CONST_IDTPTR", "idt_mock", "", "IdtPtr" },
    .{ "*Task", "PTR_TASK", "task_mock", "", "Task" },
    .{ "*Allocator", "PTR_ALLOCATOR", "", "std.mem", "Allocator" },
    .{ "*VirtualMemoryManager(u8)", "PTR_VMM", "vmm_mock", "", "VirtualMemoryManager" },

    .{ "IdtError!void", "ERROR_IDTERROR_RET_VOID", "idt_mock", "", "IdtError" },
    .{ "Allocator.Error!*Task", "ERROR_ALLOCATOR_RET_PTRTASK", "", "", "" },

    .{ "fn () callconv(.C) void", "FN_CCC_OVOID", "", "", "" },
    .{ "fn () callconv(.Naked) void", "FN_CCNAKED_OVOID", "", "", "" },
    .{ "fn () void", "FN_OVOID", "", "", "" },
    .{ "fn () u16", "FN_OU16", "", "", "" },
    .{ "fn () usize", "FN_OUSIZE", "", "", "" },
    .{ "fn () GdtPtr", "FN_OGDTPTR", "", "", "" },
    .{ "fn () IdtPtr", "FN_OIDTPTR", "", "", "" },

    .{ "fn (u8) void", "FN_IU8_OVOID", "", "", "" },
    .{ "fn (u8) bool", "FN_IU8_OBOOL", "", "", "" },
    .{ "fn (u16) void", "FN_IU16_OVOID", "", "", "" },
    .{ "fn (u16) u8", "FN_IU16_OU8", "", "", "" },
    .{ "fn (u16) u32", "FN_IU16_OU32", "", "", "" },
    .{ "fn (usize) bool", "FN_IUSIZE_OBOOL", "", "", "" },
    .{ "fn (RtcRegister) u8", "FN_IRTCREGISTER_OU8", "", "", "" },
    .{ "fn (IdtEntry) bool", "FN_IIDTENTRY_OBOOL", "idt_mock", "", "IdtEntry" },
    .{ "fn (*const GdtPtr) void", "FN_IPTRCONSTGDTPTR_OVOID", "", "", "" },
    .{ "fn (*const IdtPtr) void", "FN_IPTRCONSTIDTPTR_OVOID", "", "", "" },

    .{ "fn (u4, u4) u8", "FN_IU4_IU4_OU8", "", "", "" },
    .{ "fn (u8, u8) u16", "FN_IU8_IU8_OU16", "", "", "" },
    .{ "fn (u8, fn () callconv(.Naked) void) IdtError!void", "FN_IU8_IFNCCNAKEDOVOID_EIDTERROR_OVOID", "", "", "" },
    .{ "fn (u16, u8) void", "FN_IU16_IU8_OVOID", "", "", "" },
    .{ "fn (u16, u16) void", "FN_IU16_IU16_OVOID", "", "", "" },
    .{ "fn (u16, u32) void", "FN_IU16_IU32_OVOID", "", "", "" },
    .{ "fn (StatusRegister, bool) u8", "FN_ISTATUSREGISTER_IBOOL_OU8", "", "", "" },
    .{ "fn (*Task, usize) void", "FN_IPTRTASK_IUSIZE_OVOID", "", "", "" },
    .{ "fn (*Task, *Allocator) void", "FN_IPTRTASK_IPTRALLOCATOR_OVOID", "", "", "" },
    .{ "fn (fn () void, *Allocator) Allocator.Error!*Task", "FN_IFNOVOID_IPTRALLOCATOR_EALLOCATOR_OPTRTASK", "", "", "" },
    .{ "fn (usize, *Allocator, bool, *VirtualMemoryManager(u8)) Allocator.Error!*Task", "FN_IUSIZE_IPTRALLOCATOR_IBOOL_IVMM_EALLOCATOR_OVOID", "", "", "" },

    .{ "fn (StatusRegister, u8, bool) void", "FN_ISTATUSREGISTER_IU8_IBOOL_OVOID", "", "", "" },
};

// Create the imports
fn genImports() []const u8 {
    @setEvalBranchQuota(types.len * types.len * 7);
    comptime var str: []const u8 = "";
    comptime var seen_imports: []const u8 = &[_]u8{};
    comptime var seen_types: []const u8 = &[_]u8{};

    inline for (types) |t| {
        const has_import = !std.mem.eql(u8, t[2], "");
        const seen = if (std.mem.indexOf(u8, seen_imports, t[2])) |_| true else false;
        if (has_import and !seen) {
            str = str ++ "const " ++ t[2] ++ " = @import(\"" ++ t[2] ++ ".zig\");\n";
            seen_imports = seen_imports ++ t[2];
        }
    }

    inline for (types) |t| {
        const has_import = !std.mem.eql(u8, t[2], "");
        const has_base = !std.mem.eql(u8, t[3], "");
        const has_type = !std.mem.eql(u8, t[4], "");
        const seen = if (std.mem.indexOf(u8, seen_types, t[4])) |_| true else false;
        if (!seen and has_type and (has_import or has_base)) {
            str = str ++ "const " ++ t[4] ++ " = ";
            if (has_import) {
                str = str ++ t[2] ++ ".";
            }
            if (has_base) {
                str = str ++ t[3] ++ ".";
            }
            str = str ++ t[4] ++ ";\n";
            seen_types = seen_types ++ t[4];
        }
    }
    // Remove trailing new line
    return str;
}

// Create the DataElementType
fn genDataElementType() []const u8 {
    comptime var str: []const u8 = "const DataElementType = enum {\n";
    inline for (types) |t| {
        const spaces = " " ** 4;
        str = str ++ spaces ++ t[1] ++ ",\n";
    }
    return str ++ "};\n";
}

// Create the DataElement
fn genDataElement() []const u8 {
    comptime var str: []const u8 = "const DataElement = union(DataElementType) {\n";
    inline for (types) |t| {
        const spaces = " " ** 4;
        str = str ++ spaces ++ t[1] ++ ": " ++ t[0] ++ ",\n";
    }
    return str ++ "};\n";
}

// All the function generation parts are the same apart from 3 things
fn genGenericFunc(comptime intermediate: []const u8, comptime trail: []const u8, comptime end: []const u8) []const u8 {
    comptime var str: []const u8 = "";
    inline for (types) |t, i| {
        const spaces = if (i == 0) " " ** 4 else " " ** 16;
        str = str ++ spaces ++ t[0] ++ intermediate ++ t[1] ++ trail;
    }
    return str ++ " " ** 16 ++ end;
}

// Create the createDataElement
fn genCreateDataElement() []const u8 {
    return genGenericFunc(" => DataElement{ .", " = arg },\n", "else => @compileError(\"Type not supported: \" ++ @typeName(@TypeOf(arg))),");
}

// Create the getDataElementType
fn genGetDataElementType() []const u8 {
    return genGenericFunc(" => DataElement.", ",\n", "else => @compileError(\"Type not supported: \" ++ @typeName(T)),");
}

// Create the getDataValue
fn genGetDataValue() []const u8 {
    return genGenericFunc(" => element.", ",\n", "else => @compileError(\"Type not supported: \" ++ @typeName(T)),");
}

///
/// Generate the mocking framework file from the template file and the type.
///
/// Error: Allocator.Error || File.OpenError || File.WriteError || File.ReadError
///     Allocator.Error - If there wasn't enough memory for reading in the mocking template file.
///     File.OpenError  - Error opening the mocking template and output file.
///     File.WriteError - Error writing to the output mocking file.
///     File.ReadError  - Error reading the mocking template file.
///
pub fn main() (Allocator.Error || File.OpenError || File.WriteError || File.ReadError)!void {
    // Create the file output mocking framework file
    const mock_file = try std.fs.cwd().createFile("test/mock/kernel/mock_framework.zig", .{});
    defer mock_file.close();

    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = &gpa.allocator;

    // All the string
    const imports_str = comptime genImports();
    const data_element_type_str = comptime genDataElementType();
    const data_element_str = comptime genDataElement();
    const create_data_element_str = comptime genCreateDataElement();
    const get_data_element_type_str = comptime genGetDataElementType();
    const get_data_value_str = comptime genGetDataValue();

    // Read the mock template file
    const mock_template = try std.fs.cwd().openFile("test/mock/kernel/mock_framework_template.zig", .{});
    defer mock_template.close();
    const mock_framework_str = try mock_template.readToEndAlloc(allocator, 1024 * 1024 * 1024);
    defer allocator.free(mock_framework_str);

    // The index where to write the templates
    const imports_delimiter = "////Imports////";
    const imports_index = (std.mem.indexOf(u8, mock_framework_str, imports_delimiter) orelse unreachable);

    const data_element_type_delimiter = "////DataElementType////";
    const data_element_type_index = (std.mem.indexOf(u8, mock_framework_str, data_element_type_delimiter) orelse unreachable);

    const data_element_delimiter = "////DataElement////";
    const data_element_index = (std.mem.indexOf(u8, mock_framework_str, data_element_delimiter) orelse unreachable);

    const create_data_elem_delimiter = "////createDataElement////";
    const create_data_elem_index = (std.mem.indexOf(u8, mock_framework_str, create_data_elem_delimiter) orelse unreachable);

    const get_data_elem_type_delimiter = "////getDataElementType////";
    const get_data_elem_type_index = (std.mem.indexOf(u8, mock_framework_str, get_data_elem_type_delimiter) orelse unreachable);

    const get_data_value_delimiter = "////getDataValue////";
    const get_data_value_index = (std.mem.indexOf(u8, mock_framework_str, get_data_value_delimiter) orelse unreachable);

    // Write the beginning of the file
    try mock_file.writer().writeAll(mock_framework_str[0..imports_index]);

    // Write the Imports
    try mock_file.writer().writeAll(imports_str);

    // Write the up to DataElementType
    try mock_file.writer().writeAll(mock_framework_str[imports_index + imports_delimiter.len .. data_element_type_index]);

    // Write the DataElementType
    try mock_file.writer().writeAll(data_element_type_str);

    // Write the up to DataElement
    try mock_file.writer().writeAll(mock_framework_str[data_element_type_index + data_element_type_delimiter.len .. data_element_index]);

    // Write the DataElement
    try mock_file.writer().writeAll(data_element_str);

    // Write the up to createDataElement
    try mock_file.writer().writeAll(mock_framework_str[data_element_index + data_element_delimiter.len .. create_data_elem_index]);

    // Write the createDataElement
    try mock_file.writer().writeAll(create_data_element_str);

    // Write the up to getDataElementType
    try mock_file.writer().writeAll(mock_framework_str[create_data_elem_index + create_data_elem_delimiter.len .. get_data_elem_type_index]);

    // Write the getDataElementType
    try mock_file.writer().writeAll(get_data_element_type_str);

    // Write the up to getDataValue
    try mock_file.writer().writeAll(mock_framework_str[get_data_elem_type_index + get_data_elem_type_delimiter.len .. get_data_value_index]);

    // Write the getDataValue
    try mock_file.writer().writeAll(get_data_value_str);

    // Write the rest of the file
    try mock_file.writer().writeAll(mock_framework_str[get_data_value_index + get_data_value_delimiter.len ..]);
}