const std = @import("std");
const linux = std.os.linux;
const core = @import("core.zig");

const ModLib = struct {
    path: []const u8,
    lib: std.DynLib,
    mod: *core.Module(anyopaque),

    fn open(realpath: []const u8, version: u32, alloc: std.mem.Allocator) !ModLib {
        const basename = std.fs.path.basename(realpath);
        const name = try std.fmt.allocPrint(
            alloc,
            ".{s}.{d}",
            .{ basename, version },
        );
        defer alloc.free(name);

        const path = if (std.fs.path.dirname(realpath)) |dirname|
            try std.fs.path.join(alloc, &.{ dirname, name })
        else
            try alloc.dupe(u8, name);
        errdefer alloc.free(path);

        try std.fs.copyFileAbsolute(realpath, path, .{});

        var lib = try std.DynLib.open(path);
        errdefer lib.close();

        const mod = lib.lookup(*core.Module(anyopaque), "MODULE") orelse
            return error.MissingModuleDefinition;

        return .{
            .path = path,
            .lib = lib,
            .mod = mod,
        };
    }

    fn close(self: *ModLib, alloc: std.mem.Allocator) void {
        self.lib.close();
        //         std.fs.deleteFileAbsolute(self.path) catch std.log.warn("Failed to delete {s}", .{self.path});
        alloc.free(self.path);
    }
};

const DynamicModule = struct {
    gpa: std.heap.GeneralPurposeAllocator(.{}),
    realpath: []const u8,
    curr: ModLib,
    state: *anyopaque,
    next: u32,

    pub fn init(path: []const u8, alloc: std.mem.Allocator) !DynamicModule {
        const realpath = try std.fs.cwd().realpathAlloc(alloc, path);
        errdefer alloc.free(realpath);

        var gpa = std.heap.GeneralPurposeAllocator(.{}).init;
        errdefer if (gpa.deinit() == .leak) {
            std.log.warn("Module {s} leaked.", .{realpath});
        };

        var self: DynamicModule = .{
            .gpa = gpa,
            .realpath = realpath,
            .curr = undefined,
            .state = undefined,
            .next = 0,
        };

        self.curr = try ModLib.open(realpath, self.next, alloc);
        errdefer self.curr.close(alloc);

        self.state = try self.curr.mod.startup.invoke(.{self.gpa.allocator()});
        errdefer self.curr.mod.shutdown.invoke(.{ self.state, self.gpa.allocator() });

        // todo deserialize from disk.
        try self.curr.mod.reload.invoke(.{ self.state, self.gpa.allocator(), &.{} });

        self.next += 1;

        return self;
    }

    pub fn reload(self: *@This(), alloc: std.mem.Allocator) !void {
        var next = try ModLib.open(self.realpath, self.next, alloc);
        errdefer next.close(alloc);

        const data = try self.curr.mod.unload.invoke(.{ self.state, self.gpa.allocator() });
        errdefer self.curr.mod.reload.invoke(.{ self.state, self.gpa.allocator(), data }) catch
            std.debug.panic("Failed to rollback to {s}", .{self.curr.path});

        try next.mod.reload.invoke(.{ self.state, self.gpa.allocator(), data });

        self.curr.close(alloc);
        self.next += 1;
        self.curr = next;
    }

    pub fn deinit(self: *DynamicModule, alloc: std.mem.Allocator) void {
        const data = self.curr.mod.unload.invoke(.{ self.state, self.gpa.allocator() }) catch
            std.debug.panic("Failed to unload {s}", .{self.curr.path});

        _ = data; // todo serialize to disk.

        self.curr.mod.shutdown.invoke(.{ self.state, self.gpa.allocator() });

        self.curr.close(alloc);

        if (self.gpa.deinit() == .leak) {
            std.log.warn("Module {s} leaked.", .{self.realpath});
        }

        alloc.free(self.realpath);
    }
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}).init;
    defer _ = gpa.deinit();
    const alloc = gpa.allocator();

    const self = try std.fs.selfExeDirPathAlloc(alloc);
    defer alloc.free(self);
    std.log.debug("running from '{s}'", .{self});

    var env = try std.process.getEnvMap(alloc);
    defer env.deinit();

    const mods_path = if (env.get("MODS_DIR")) |envvar| blk: {
        break :blk try alloc.dupe(u8, envvar);
    } else blk: {
        const dir = try std.fs.selfExeDirPathAlloc(alloc);
        defer alloc.free(dir);
        break :blk try std.fs.path.resolve(alloc, &.{ dir, "..", "mods" });
    };
    defer alloc.free(mods_path);

    var mods: std.StringHashMapUnmanaged(DynamicModule) = .{};
    defer {
        var it = mods.valueIterator();
        while (it.next()) |mod| {
            mod.deinit(alloc);
        }
        mods.deinit(alloc);
    }

    std.log.debug("loading mods in '{s}'", .{mods_path});
    {
        var dir = try std.fs.openDirAbsolute(mods_path, .{ .iterate = true });
        defer dir.close();

        var it = dir.iterate();
        while (try it.next()) |entry| {
            const mod_path = try std.fs.path.resolve(alloc, &.{ mods_path, entry.name });
            defer alloc.free(mod_path);

            if (std.mem.startsWith(u8, std.fs.path.basename(mod_path), "."))
                continue;

            var mod = try DynamicModule.init(mod_path, alloc);
            errdefer mod.deinit(alloc);

            try mods.putNoClobber(alloc, entry.name, mod);
        }
    }

    const fd: i32 = blk: {
        const ret: isize = @bitCast(linux.inotify_init1(linux.IN.NONBLOCK));
        if (ret < 0) return error.inotify_init_error;
        break :blk @intCast(ret);
    };
    defer _ = linux.close(fd);

    const wd: i32 = blk: {
        const pathz = try alloc.dupeZ(u8, mods_path);
        defer alloc.free(pathz);
        const ret: isize = @bitCast(linux.inotify_add_watch(
            fd,
            pathz,
            linux.IN.MOVED_TO,
        ));
        if (ret < 0) return error.inotify_add_watch_error;
        break :blk @intCast(ret);
    };
    defer _ = linux.inotify_rm_watch(fd, wd);

    var fds = [_]linux.pollfd{
        .{ .fd = fd, .events = linux.POLL.IN, .revents = undefined },
    };

    const eventbuf = try alloc.alloc(u8, 5 * (@sizeOf(linux.inotify_event) + linux.NAME_MAX));
    defer alloc.free(eventbuf);

    for (0..60 * 10) |_| {
        const n = linux.poll(&fds, fds.len, std.time.ms_per_s);

        if (n > 0) {
            const c = linux.read(fd, eventbuf.ptr, eventbuf.len);

            var i: usize = 0;
            while (i < c) {
                const event: *linux.inotify_event = @alignCast(@ptrCast(eventbuf.ptr + i));
                const name = event.getName().?; // Impossible as IN.MOVE_TO includes a name.

                if (mods.getPtr(name)) |mod| {
                    if (mod.reload(alloc)) |_| {
                        std.log.info("Reloaded {s}.", .{name});
                    } else |_| {
                        std.log.err("Failed to reload {s}. Rolled back.", .{mod.realpath});
                    }
                } else {
                    // Ignore unloaded module.
                }

                i += @sizeOf(linux.inotify_event) + event.len;
            }
        }
    }

    std.log.debug("-" ** 80, .{});
}
