const std = @import("std");
const root = @import("root");

pub const Window = @import("nu/Window.zig");
pub const Render = @import("nu/Render.zig");
pub const ImGui = @import("nu/ImGui.zig");
pub const ctx = @import("nu/Render/ctx.zig");

// pub const Bus = @import("nu/Bus.zig");

pub const Config = struct {
    window: Window.Config = .{},
    render: Render.Config = .{},
};

pub const config: Config = if (@hasDecl(root, "nu_config")) root.nu_config else .{};
pub const engine = Engine(Window, Render, root.nu_modules);

// Hooks: setup, teardown, fixed, frame, present

pub fn main() void {
    std.log.info("use_debug_messenger: {}", .{config.render.use_debug_messenger});

    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const alloc = gpa.allocator();

    std.debug.print("Engine modules: ", .{});
    inline for (engine.modules) |module| std.debug.print("{s} ", .{@typeName(module)});
    std.debug.print("\n", .{});

    engine.try_invoke("setup", .{alloc}) catch @panic("Startup failed");
    defer engine.rinvoke("teardown", .{});

    std.debug.print("Setup complete.\n", .{});

    while (engine.next()) {
        engine.try_invoke("fixed", .{}) catch @panic("fixed failed!");
        engine.try_invoke("frame", .{}) catch @panic("frame failed!");

        engine.render() catch @panic("render failed!");
    }
}

pub fn Engine(comptime D: type, comptime R: type, comptime M: anytype) type {
    return struct {
        pub const modules = Graph.sort(.{ D, R } ++ M);

        pub fn next() bool {
            return D.next();
        }

        pub fn render() !void {
            return R.render();
        }

        // todo remove this
        pub fn try_invoke(comptime name: []const u8, args: anytype) !void {
            inline for (modules) |mod| {
                if (@hasDecl(mod, name)) {
                    try @call(.auto, @field(mod, name), args);
                }
            }
        }

        pub fn invoke(comptime name: []const u8, args: anytype) void {
            inline for (modules) |mod| {
                if (@hasDecl(mod, name)) {
                    @call(.auto, @field(mod, name), args);
                }
            }
        }

        // todo remove this
        pub fn try_rinvoke(comptime name: []const u8, args: anytype) !void {
            comptime var it = std.mem.reverseIterator(modules);
            inline while (it.next()) |mod| {
                if (@hasDecl(mod, name)) {
                    try @call(.auto, @field(mod, name), args);
                }
            }
        }

        pub fn rinvoke(comptime name: []const u8, args: anytype) void {
            comptime var it = std.mem.reverseIterator(modules);
            inline while (it.next()) |mod| {
                if (@hasDecl(mod, name)) {
                    @call(.auto, @field(mod, name), args);
                }
            }
        }
    };
}

const Graph = struct {
    fn visit(
        comptime mod: type,
        comptime mark: *[]const type,
        comptime result: *[]const type,
    ) void {
        if (std.mem.indexOfScalar(type, result.*, mod)) |_| return;
        if (std.mem.indexOfScalar(type, mark.*, mod)) |_| @compileError("Cycle");
        mark.* = mark.* ++ .{mod};
        if (@hasDecl(mod, "depends"))
            for (mod.depends) |dep|
                visit(dep, mark, result);
        result.* = result.* ++ .{mod};
    }

    fn sort(comptime modules: anytype) []const type {
        var mark: []const type = &.{};
        var result: []const type = &.{};
        for (modules) |mod| visit(mod, &mark, &result);
        return result;
    }
};

test {}
