const std = @import("std");
const ArrayList = std.ArrayListUnmanaged; // this should be deleted when zig 0.14 support is no longer necessary
const StringHashMap = std.StringHashMapUnmanaged; // this should be deleted when zig 0.14 support is no longer necessary

const String = ArrayList(u8);
const Database = StringHashMap(void);

pub const DefineState = struct {
    database: Database,
    definitions: ArrayList(String),

    pub const empty: DefineState = .{
        .database = .empty,
        .definitions = .empty,
    };

    pub fn deinit(self: *DefineState, gpa: std.mem.Allocator) void {
        for (self.definitions.items) |*def| {
            def.deinit(gpa);
        }
        self.database.deinit(gpa);
        self.definitions.deinit(gpa);
        self.* = undefined;
    }
};

pub fn define(
    io: std.Io,
    gpa: std.mem.Allocator,
    absolute_output_path: []const u8,
    comptime to_define: []const type,
) !void {
    var state: DefineState = .empty;
    defer state.deinit(gpa);

    inline for (to_define) |T| {
        _ = try addClass(&state, gpa, T);
    }

    var file = try std.Io.Dir.createFileAbsolute(io, absolute_output_path, .{});
    defer file.close(io);

    try file.writeStreamingAll(io, file_header);

    for (state.definitions.items) |def| {
        try file.writeStreamingAll(io, def.items);
        try file.writeStreamingAll(io, "\n");
    }
}

const file_header: []const u8 =
    \\---@meta
    \\
    \\--- This is an autogenerated file,
    \\--- Do not modify
    \\
    \\
;

fn name(comptime T: type) []const u8 {
    return (comptime std.fs.path.extension(@typeName(T)))[1..];
}

fn addEnum(
    state: *DefineState,
    gpa: std.mem.Allocator,
    comptime T: type,
) !void {
    if (state.database.contains(@typeName(T)) == false) {
        try state.database.put(gpa, @typeName(T), {});
        const item = try state.definitions.addOne(gpa);
        item.* = .empty;

        try item.appendSlice(gpa, "---@alias ");
        try item.appendSlice(gpa, name(T));
        try item.appendSlice(gpa, "\n");

        inline for (@typeInfo(T).@"enum".field_names) |fname| {
            try item.appendSlice(gpa, "---|\' \"");
            try item.appendSlice(gpa, fname);
            try item.appendSlice(gpa, "\" \'\n");
        }
    }
}

pub fn addClass(
    state: *DefineState,
    gpa: std.mem.Allocator,
    comptime T: type,
) !void {
    if (state.database.contains(@typeName(T)) == false) {
        try state.database.put(gpa, @typeName(T), {});
        const item = try state.definitions.addOne(gpa);
        const idx = state.definitions.items.len - 1;
        item.* = .empty;

        try item.appendSlice(gpa, "---@class (exact) ");
        try item.appendSlice(gpa, name(T));
        try item.appendSlice(gpa, "\n");

        const str = @typeInfo(T).@"struct";

        inline for (str.field_names, str.field_types, str.field_attrs) |fname, ftype, attr| {
            try item.appendSlice(gpa, "---@field ");
            try item.appendSlice(gpa, fname);

            if (attr.defaultValue(ftype) != null) {
                try item.appendSlice(gpa, "?");
            }
            try item.appendSlice(gpa, " ");
            try luaTypeName(state, gpa, idx, ftype);
            try state.definitions.items[idx].appendSlice(gpa, "\n");
        }
    }
}

fn luaTypeName(
    state: *DefineState,
    gpa: std.mem.Allocator,
    index: usize,
    comptime T: type,
) !void {
    switch (@typeInfo(T)) {
        .@"struct" => {
            try state.definitions.items[index].appendSlice(gpa, name(T));
            try addClass(state, gpa, T);
        },
        .pointer => |info| {
            if (info.child == u8 and info.size == .slice) {
                try state.definitions.items[index].appendSlice(gpa, "string");
            } else switch (info.size) {
                .one => {
                    try state.definitions.items[index].appendSlice(gpa, "lightuserdata");
                },
                .c, .many, .slice => {
                    try luaTypeName(state, gpa, index, info.child);
                    try state.definitions.items[index].appendSlice(gpa, "[]");
                },
            }
        },
        .array => |info| {
            try luaTypeName(state, gpa, index, info.child);
            try state.definitions.items[index].appendSlice(gpa, "[]");
        },

        .vector => |info| {
            try luaTypeName(state, gpa, index, info.child);
            try state.definitions.items[index].appendSlice(gpa, "[]");
        },
        .optional => |info| {
            try luaTypeName(state, gpa, index, info.child);
            try state.definitions.items[index].appendSlice(gpa, " | nil");
        },
        .@"enum" => {
            try state.definitions.items[index].appendSlice(gpa, name(T));
            try addEnum(state, gpa, T);
        },
        .int => {
            try state.definitions.items[index].appendSlice(gpa, "integer");
        },
        .float => {
            try state.definitions.items[index].appendSlice(gpa, "number");
        },
        .bool => {
            try state.definitions.items[index].appendSlice(gpa, "boolean");
        },
        else => {
            @compileLog(T);
            @compileError("Type not supported");
        },
    }
}
