const std = @import("std");
const fs = std.fs;
const debug = std.debug;
const panic = debug.panic;
const process = std.process;
const c = @import("c.zig");
const debug_gl = @import("debug_gl.zig");
const cfg = @import("config.zig");
const out = @import("output.zig");
const gl = @import("gl.zig");
const ctrl = @import("control.zig");
const c_allocator = std.heap.c_allocator;

var window: *c.GLFWwindow = undefined;

fn errorCallback(err: c_int, description: [*c]const u8) callconv(.C) void {
    panic("Error {}: {s}\n", .{ err, description });
}

pub fn main() !void {
    const progress = std.Progress.start(.{});
    defer progress.end();

    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();

    var args = process.args();
    _ = args.next();

    const filename = args.next() orelse "config.yaml";
    var config = try cfg.Config.parse(&arena.allocator(), filename);

    _ = c.glfwSetErrorCallback(errorCallback);

    if (c.glfwInit() == c.GL_FALSE) {
        panic("GLFW init failure\n", .{});
    }
    defer c.glfwTerminate();

    var monitor_count: c_int = 0;
    const monitors = c.glfwGetMonitors(&monitor_count);
    for (monitors[0..@as(usize, @intCast(monitor_count))], 0..) |monitor, i| {
        debug.print(
            "monitor {}: '{s}'\n",
            .{ i, @as([*:0]const u8, @ptrCast(c.glfwGetMonitorName(monitor))) },
        );
    }

    c.glfwWindowHint(c.GLFW_CONTEXT_VERSION_MAJOR, 4);
    c.glfwWindowHint(c.GLFW_CONTEXT_VERSION_MINOR, 2);
    c.glfwWindowHint(c.GLFW_OPENGL_FORWARD_COMPAT, c.GL_TRUE);
    c.glfwWindowHint(c.GLFW_OPENGL_DEBUG_CONTEXT, debug_gl.is_on);
    c.glfwWindowHint(c.GLFW_OPENGL_PROFILE, c.GLFW_OPENGL_CORE_PROFILE);
    c.glfwWindowHint(c.GLFW_DEPTH_BITS, 0);
    c.glfwWindowHint(c.GLFW_STENCIL_BITS, 0);
    c.glfwWindowHint(c.GLFW_VISIBLE, c.GLFW_FALSE);

    window = c.glfwCreateWindow(config.width, config.height, "glsl-view", null, null) orelse {
        panic("unable to create window\n", .{});
    };
    defer c.glfwDestroyWindow(window);

    c.glfwMakeContextCurrent(window);
    c.glfwSwapInterval(1);

    var constants = try gl.Constants.create(window, &config);
    defer constants.destroy();

    var outputs = try std.ArrayList(*out.Output).initCapacity(arena.allocator(), config.outputs.len);
    defer outputs.deinit();
    for (config.outputs) |output_config| {
        try outputs.append(out.Output.create(arena.allocator(), output_config, &constants));
    }
    defer for (outputs.items) |output| {
        output.destroy(output);
    };

    c.glfwMakeContextCurrent(window);
    c.glClearColor(0.0, 0.0, 0.0, 1.0);

    debug_gl.assertNoError();
    debug_gl.init();

    const start_time = c.glfwGetTime();
    var prev_time = start_time;

    constants.normalized_quad.bind(0);

    var main_program = try gl.ShaderProgram.create(
        \\#version 330 core
        \\
        \\layout(location = 0) in vec2 position;
        \\out vec2 uv;
        \\
        \\void main() {
        \\  uv = position / 2.0 + 0.5;
        \\  gl_Position = vec4(position, 0, 1);
        \\}
    ,
        \\#version 330 core
        \\
        \\in vec2 uv;
        \\out vec4 color;
        \\
        \\void main() {
        \\  color = vec4(0,0,0,1);
        \\}
    );
    defer main_program.destroy();

    var fbo = try gl.FramebufferObject.create(config.width, config.height);
    defer fbo.destroy();

    const shader_file = try config.project_root.openFile(config.fragment, .{});
    var last_stat = try shader_file.stat();
    try reloadShader(&config, &main_program, config.fragment, last_stat.size);

    var cache = gl.UniformCache.init(std.heap.c_allocator, &main_program);
    defer cache.deinit();

    const control = try ctrl.ControlServer.init(arena.allocator(), progress, &config, &cache);
    defer control.destroy();

    while (c.glfwWindowShouldClose(window) == c.GL_FALSE) {
        c.glfwMakeContextCurrent(window);
        c.glClear(c.GL_COLOR_BUFFER_BIT);

        const now_time = c.glfwGetTime();
        prev_time = now_time;

        control.update();

        const stat = try shader_file.stat();
        if (stat.mtime > last_stat.mtime or control.reload_requested) {
            try reloadShader(&config, &main_program, config.fragment, stat.size);
            try cache.refresh();
            last_stat = stat;

            control.reload_requested = false;
        }

        fbo.bind();
        c.glClear(c.GL_COLOR_BUFFER_BIT);

        main_program.bind();
        constants.normalized_quad.draw();
        fbo.unbind();

        for (outputs.items, 0..) |output, i| {
            const close = output.update(output, fbo.texture_id);
            if (close) {
                const removed = outputs.swapRemove(i);
                removed.destroy(removed);
                break;
            }
        }

        if (outputs.items.len == 0)
            break;

        c.glfwPollEvents();
    }
}

fn escape_dquote(str: []const u8) []const u8 {
    const state = struct {
        var buf: [1024]u8 = undefined;
    };

    const len = std.mem.replacementSize(u8, str, "\"", "\\\"");
    _ = std.mem.replace(u8, str, "\"", "\\\"", state.buf[0..len]);
    return state.buf[0..len];
}

// given a file and the directory it is in, resolve references
fn loadFile(config: *const cfg.Config, writer: anytype, dir: []const u8, filename: []const u8) !void {
    const file_dir = try fs.path.resolve(c_allocator, &[_][]const u8{
        dir,
        fs.path.dirname(filename) orelse "",
    });
    defer c_allocator.free(file_dir);

    const file_path = try fs.path.resolve(c_allocator, &[_][]const u8{ dir, filename });
    defer c_allocator.free(file_path);

    var file = try config.project_root.openFile(file_path, .{});
    defer file.close();
    var reader = file.reader();

    var line_number: u32 = 1;
    var buf: [1024]u8 = undefined;
    var wrote_line = false;
    while (try reader.readUntilDelimiterOrEof(&buf, '\n')) |line| {
        if (std.mem.startsWith(u8, line, "#pragma ")) {
            var parts = std.mem.splitScalar(u8, line, ' ');
            _ = parts.next();
            const pragma = parts.next().?;

            if (std.mem.eql(u8, pragma, "include")) {
                var include_path = parts.next().?;
                if (include_path[0] != '"' or include_path[include_path.len - 1] != '"') {
                    debug.print("{s}:{}: Invalid #pragma include\n", .{ file_path, line_number });
                    debug.print("{s}:{}:   {s}\n", .{ file_path, line_number, line });
                    continue;
                }
                include_path = include_path[1 .. include_path.len - 1];

                loadFile(config, writer, file_dir, include_path) catch unreachable;
                _ = try writer.print("#line {d} \"{s}\"\n", .{ line_number, escape_dquote(filename) });

                continue;
            } else {
                debug.print("{s}:{}: Unknown #pragma directive '{s}'\n", .{ file_path, line_number, pragma });
                debug.print("{s}:{}:   {s}\n", .{ file_path, line_number, line });
            }
        }

        if (!std.mem.startsWith(u8, line, "#version") and !wrote_line) {
            _ = try writer.print("#line {d} \"{s}\"\n", .{ line_number - 1, escape_dquote(filename) });
            wrote_line = true;
        }

        _ = try writer.write(line);
        _ = try writer.write("\n");

        line_number += 1;
    }
}

fn reloadShader(config: *const cfg.Config, current: *gl.ShaderProgram, frag_filename: []const u8, size: u64) !void {
    var buffer = try std.ArrayList(u8).initCapacity(c_allocator, @as(usize, @intCast(size)));
    errdefer buffer.deinit();

    const writer = buffer.writer();
    _ = try writer.write(
        \\#version 330 core
        \\#extension GL_ARB_shading_language_include : require
        \\
    );
    try loadFile(config, writer, "", frag_filename);

    const frag_source = try buffer.toOwnedSlice();
    defer c_allocator.free(frag_source);

    const vert_source =
        \\#version 330 core
        \\
        \\layout(location = 0) in vec2 position;
        \\out vec2 uv;
        \\
        \\void main() {
        \\  uv = position / 2.0 + 0.5;
        \\  gl_Position = vec4(position, 0, 1);
        \\}
    ;

    if (gl.ShaderProgram.create(vert_source, frag_source)) |new_program| {
        current.destroy();
        current.* = new_program;
    } else |err| {
        debug.print("Error while reloading shader: {}\n", .{err});
    }
}
