const std = @import("std");

const Watch = @import("Watch.zig");
const serveFn = *const fn () void;

var shutdown = false;
const timeout = 250;

const Executor = struct {
    path: [:0]const u8,
    library: ?*anyopaque = null,
    serve: ?serveFn = null,
    watch: ?usize = null,
    reload_lock: bool = false,
};

var executors = [_]Executor{
    .{ .path = "zig-out/lib/libfaas-proxy-sample-lib.so" },
    .{ .path = "zig-out/lib/libfaas-proxy-sample-lib2.so" },
};

var watcher = Watch.init(executorChanged);
const log = std.log.scoped(.main);
pub const std_options = struct {
    pub const log_level = .debug;

    pub const log_scope_levels = &[_]std.log.ScopeLevel{
        .{ .scope = .watch, .level = .info },
    };
};
const SERVE_FN_NAME = "serve";

fn serve() !void {
    // if (some path routing thing) {

    (try getExecutor(0))();
    // if (inx > 4) {
    //     if (inx % 2 == 0)
    //         (try getExecutor(0))()
    //     else
    //         (try getExecutor(1))();
    // }
}
fn getExecutor(key: usize) !serveFn {
    var executor = &executors[key];
    if (executor.serve) |s| return s;

    executor.library = blk: {
        if (executor.library) |l|
            break :blk l;

        while (executor.reload_lock) // system is reloading the library
            std.time.sleep(1);

        if (executor.library) |l| // check again to see where we are at
            break :blk l;

        log.info("library {s} requested but not loaded. Loading library", .{executor.path});
        const l = try dlopen(executor.path);
        errdefer if (std.c.dlclose(l) != 0)
            @panic("System unstable: Error after library open and cannot close");
        executor.watch = executor.watch orelse try watcher.addFileWatch(&executor.path);
        break :blk l;
    };

    // std.c.dlerror();
    const serve_function = std.c.dlsym(executor.library.?, SERVE_FN_NAME);
    if (serve_function == null) return error.CouldNotLoadSymbolServe;

    executor.serve = @ptrCast(serveFn, serve_function.?);
    return executor.serve.?;
}

fn executorChanged(watch: usize) void {
    // NOTE: This will be called off the main thread
    log.debug("executor with watch {d} changed", .{watch});
    for (&executors) |*executor| {
        if (executor.watch) |w| {
            if (w == watch) {
                if (executor.library) |l| {
                    executor.reload_lock = true;
                    defer executor.reload_lock = false;

                    if (std.c.dlclose(l) != 0)
                        @panic("System unstable: Error after library open and cannot close");
                    log.debug("closed old library. reloading executor at: {s}", .{executor.path});
                    executor.library = dlopen(executor.path) catch {
                        log.warn("could not reload! error opening library", .{});
                        return;
                    };
                    executor.serve = @ptrCast(serveFn, std.c.dlsym(executor.library.?, SERVE_FN_NAME));
                    if (executor.serve == null) {
                        log.warn("could not reload! error finding symbol", .{});
                        if (std.c.dlclose(executor.library.?) != 0)
                            @panic("System unstable: Error after library open and cannot close");
                        return;
                    }
                }
            }
        }
    }
}

fn dlopen(path: [:0]const u8) !*anyopaque {
    // We need now (and local) because we're about to call it
    const lib = std.c.dlopen(path, std.c.RTLD.NOW);
    if (lib) |l| return l;
    return error.CouldNotOpenDynamicLibrary;
}

var inx: usize = 0;
pub fn main() !void {
    defer watcher.deinit();

    // stdout is for the actual output of your application, for example if you
    // are implementing gzip, then only the compressed bytes should be sent to
    // stdout, not any debugging messages.
    const stdout_file = std.io.getStdOut().writer();
    var bw = std.io.bufferedWriter(stdout_file);
    const stdout = bw.writer();

    const stderr_file = std.io.getStdErr().writer();
    var bw_stderr = std.io.bufferedWriter(stderr_file);
    const stderr = bw_stderr.writer();

    try stdout.print("Run `zig build test` to run the tests.\n", .{});

    try bw.flush(); // don't forget to flush!
    const watcher_thread = try std.Thread.spawn(.{}, Watch.startWatch, .{&watcher});

    while (true) {
        std.time.sleep(std.time.ns_per_s * 2);
        inx += 1;
        if (inx == 10) {
            log.debug("forcing stop to make sure it works", .{});
            try watcher.stopWatch();
            break;
        }
        try stdout.print("Serving...", .{});
        try bw.flush();
        serve() catch |err| {
            try stderr.print("Error serving request ({any})\n", .{err});
            try bw_stderr.flush();
        };
        try stdout.print("served\n", .{});
        try bw.flush();
    }
    shutdown = true;
    watcher_thread.join();
}
test {
    // To run nested container tests, either, call `refAllDecls` which will
    // reference all declarations located in the given argument.
    // `@This()` is a builtin function that returns the innermost container it is called from.
    // In this example, the innermost container is this file (implicitly a struct).
    std.testing.refAllDecls(@This());
}
test "simple test" {
    var list = std.ArrayList(i32).init(std.testing.allocator);
    defer list.deinit(); // try commenting this out and see if zig detects the memory leak!
    try list.append(42);
    try std.testing.expectEqual(@as(i32, 42), list.pop());
}