const c = @import("c.zig");
const gl = @import("gl.zig");
const video = @import("video.zig");
const std = @import("std");
const cfg = @import("config.zig");
const build_config = @import("build_config");

fn verify_args(expected: u8, got: []const u8) !void {
    for (got) |typ| {
        if (typ != expected) {
            std.debug.print("expected '{c}' but got '{c}' element (expected {s})\n", .{ expected, typ, got });
            return error.typeMismatch;
        }
    }
}

fn set_one(
    comptime T: type,
    dest: *T,
    argc: c_int,
    argv: [*c][*c]c.lo_arg,
    types: []const u8,
) !void {
    if (argc != 1)
        return error.sizeMismatch;

    switch (T) {
        f32 => {
            try verify_args('f', types);
            dest.* = argv[0].*.f;
        },
        f64 => {
            try verify_args('d', types);
            dest.* = argv[0].*.d;
        },
        i32 => {
            try verify_args('d', types);
            dest.* = argv[0].*.i;
        },
        u32 => {
            try verify_args('d', types);
            const val = argv[0].*.i;
            if (val < 0)
                return error.signDisallowed;
            dest.* = @as(u32, @intCast(val));
        },
        else => return error.invalidType,
    }
}

fn set_array(
    comptime T: type,
    dest: []T,
    argc: c_int,
    argv: [*c][*c]c.lo_arg,
    types: []const u8,
) !void {
    if (argc != dest.len)
        return error.sizeMismatch;

    switch (T) {
        f32 => {
            try verify_args('f', types);
            for (dest, 0..) |*v, i|
                v.* = argv[i].*.f;
        },
        f64 => {
            try verify_args('d', types);
            for (dest, 0..) |*v, i|
                v.* = argv[i].*.d;
        },
        i32 => {
            try verify_args('i', types);
            for (dest, 0..) |*v, i|
                v.* = argv[i].*.i;
        },
        u32 => {
            try verify_args('i', types);
            for (dest, 0..) |*v, i| {
                const val = argv[i].*.i;
                if (val < 0)
                    return error.signDisallowed;
                v.* = @as(u32, @intCast(argv[i].*.i));
            }
        },
        else => return error.invalidType,
    }
}

fn set_texture(
    progress: std.Progress.Node,
    project_root: std.fs.Dir,
    dest: *?gl.Texture,
    texture_type: gl.Texture.Type,
    argc: c_int,
    argv: [*c][*c]c.lo_arg,
    types: []const u8,
) !void {
    if (!build_config.have_ffmpeg) return error.texturesDisabled;
    if (argc != 1 or types[0] != 's') return error.invalidType;

    if (dest.*) |old| {
        old.destroy();
        dest.* = null;
    }

    var buffer: [1024]u8 = undefined;
    const filepath = try project_root.realpathZ(@ptrCast(&argv[0].*.s), buffer[0..]);
    buffer[filepath.len] = 0;

    dest.* = gl.Texture.create(texture_type);
    try video.loadVideo(progress, @ptrCast(filepath), &dest.*.?);
}

pub const ControlServer = struct {
    server: c.lo_server,
    reload_requested: bool,

    cache: *gl.UniformCache,
    config: *cfg.Config,
    progress: std.Progress.Node,
    allocator: std.mem.Allocator,

    pub fn init(
        allocator: std.mem.Allocator,
        progress: std.Progress.Node,
        config: *cfg.Config,
        cache: *gl.UniformCache,
    ) !*ControlServer {
        var self: *ControlServer = try allocator.create(ControlServer);
        self.* = .{
            .server = null,
            .reload_requested = false,

            .cache = cache,
            .config = config,
            .progress = progress,
            .allocator = allocator,
        };

        switch (config.osc) {
            .Manual => |conf| {
                var port = [_]u8{0} ** 6;
                _ = std.fmt.formatIntBuf(port[0..], conf.port, 10, .lower, std.fmt.FormatOptions{});
                const proto: c_int = switch (conf.protocol) {
                    .udp => c.LO_UDP,
                    .tcp => c.LO_TCP,
                    .unix => c.LO_UNIX,
                };

                std.debug.print(
                    "listening for OSC messages on {} port {}\n",
                    .{ conf.protocol, conf.port },
                );
                self.server = c.lo_server_new_with_proto(port[0..], proto, handle_error);
            },
            .URL => |url| {
                std.debug.print("listening for OSC messages at {s}\n", .{url});
                self.server = c.lo_server_new_from_url(url[0..].ptr, handle_error);
            },
        }

        if (self.server == null)
            return error.serverInitializationError;
        _ = c.lo_server_add_method(self.server, null, null, handle_method, @as(*anyopaque, @ptrCast(self)));

        return self;
    }

    pub fn update(self: *ControlServer) void {
        while (c.lo_server_recv_noblock(self.server, 0) > 0) {}
    }

    pub fn destroy(self: ControlServer) void {
        c.lo_server_free(self.server);
    }

    fn handle_error(num: c_int, msg: [*c]const u8, where: [*c]const u8) callconv(.C) void {
        std.debug.print(
            "OSC error {} @ {s}: {s}\n",
            .{ num, @as([*:0]const u8, @ptrCast(where)), @as([*:0]const u8, @ptrCast(msg)) },
        );
    }

    fn set_uniform(
        self: *ControlServer,
        path: []const u8,
        argv: [*c][*c]c.lo_arg,
        argc: c_int,
        types: []const u8,
    ) !void {
        var parts = std.mem.tokenizeScalar(u8, path, '/');
        var uniform: *gl.CachedUniform = undefined;

        if (parts.next()) |uniform_name| {
            const nameZ = try self.allocator.dupeZ(u8, uniform_name);
            defer self.allocator.free(nameZ);

            const res = try self.cache.get(nameZ);
            uniform = res orelse return error.notFound;
        } else {
            return error.invalidMessage;
        }

        var value = (uniform.value orelse unreachable).getPointer();
        while (parts.next()) |component| {
            const i = switch (component[0]) {
                'x', 'r', 's' => 0,
                'y', 'g', 't' => 1,
                'z', 'b', 'p' => 2,
                'w', 'a', 'q' => 3,
                else => std.fmt.parseUnsigned(u8, component, 10) catch null,
            };

            if (i) |ii| {
                value = try value.index(ii);
            } else {
                return error.invalidIndex;
            }
        }

        try switch (value) {
            .FLOAT => |val| set_one(f32, val, argc, argv, types),
            .DOUBLE => |val| set_one(f64, val, argc, argv, types),
            .INT => |val| set_one(i32, val, argc, argv, types),
            .UNSIGNED_INT => |val| set_one(u32, val, argc, argv, types),
            .BOOL => |val| set_one(u32, val, argc, argv, types),
            .FLOAT_VEC2 => |val| set_array(f32, val.*[0..], argc, argv, types),
            .FLOAT_VEC3 => |val| set_array(f32, val.*[0..], argc, argv, types),
            .FLOAT_VEC4 => |val| set_array(f32, val.*[0..], argc, argv, types),
            .DOUBLE_VEC2 => |val| set_array(f64, val.*[0..], argc, argv, types),
            .DOUBLE_VEC3 => |val| set_array(f64, val.*[0..], argc, argv, types),
            .DOUBLE_VEC4 => |val| set_array(f64, val.*[0..], argc, argv, types),
            .INT_VEC2 => |val| set_array(i32, val.*[0..], argc, argv, types),
            .INT_VEC3 => |val| set_array(i32, val.*[0..], argc, argv, types),
            .INT_VEC4 => |val| set_array(i32, val.*[0..], argc, argv, types),
            .UNSIGNED_INT_VEC2 => |val| set_array(u32, val.*[0..], argc, argv, types),
            .UNSIGNED_INT_VEC3 => |val| set_array(u32, val.*[0..], argc, argv, types),
            .UNSIGNED_INT_VEC4 => |val| set_array(u32, val.*[0..], argc, argv, types),
            .BOOL_VEC2 => |val| set_array(u32, val.*[0..], argc, argv, types),
            .BOOL_VEC3 => |val| set_array(u32, val.*[0..], argc, argv, types),
            .BOOL_VEC4 => |val| set_array(u32, val.*[0..], argc, argv, types),
            .FLOAT_MAT2 => |val| set_array(f32, val.*[0..], argc, argv, types),
            .FLOAT_MAT3 => |val| set_array(f32, val.*[0..], argc, argv, types),
            .FLOAT_MAT4 => |val| set_array(f32, val.*[0..], argc, argv, types),
            .FLOAT_MAT2x3 => |val| set_array(f32, val.*[0..], argc, argv, types),
            .FLOAT_MAT2x4 => |val| set_array(f32, val.*[0..], argc, argv, types),
            .FLOAT_MAT3x2 => |val| set_array(f32, val.*[0..], argc, argv, types),
            .FLOAT_MAT3x4 => |val| set_array(f32, val.*[0..], argc, argv, types),
            .FLOAT_MAT4x2 => |val| set_array(f32, val.*[0..], argc, argv, types),
            .FLOAT_MAT4x3 => |val| set_array(f32, val.*[0..], argc, argv, types),
            .DOUBLE_MAT2 => |val| set_array(f64, val.*[0..], argc, argv, types),
            .DOUBLE_MAT3 => |val| set_array(f64, val.*[0..], argc, argv, types),
            .DOUBLE_MAT4 => |val| set_array(f64, val.*[0..], argc, argv, types),
            .DOUBLE_MAT2x3 => |val| set_array(f64, val.*[0..], argc, argv, types),
            .DOUBLE_MAT2x4 => |val| set_array(f64, val.*[0..], argc, argv, types),
            .DOUBLE_MAT3x2 => |val| set_array(f64, val.*[0..], argc, argv, types),
            .DOUBLE_MAT3x4 => |val| set_array(f64, val.*[0..], argc, argv, types),
            .DOUBLE_MAT4x2 => |val| set_array(f64, val.*[0..], argc, argv, types),
            .DOUBLE_MAT4x3 => |val| set_array(f64, val.*[0..], argc, argv, types),

            .SAMPLER_2D,
            .SAMPLER_2D_SHADOW,
            .INT_SAMPLER_2D,
            .UNSIGNED_INT_SAMPLER_2D,
            => |val| set_texture(self.progress, self.config.project_root, val, .TEXTURE_2D, argc, argv, types),

            .SAMPLER_2D_ARRAY,
            .SAMPLER_2D_ARRAY_SHADOW,
            .INT_SAMPLER_2D_ARRAY,
            .UNSIGNED_INT_SAMPLER_2D_ARRAY,
            => |val| set_texture(self.progress, self.config.project_root, val, .TEXTURE_2D_ARRAY, argc, argv, types),

            .SAMPLER_3D,
            .INT_SAMPLER_3D,
            .UNSIGNED_INT_SAMPLER_3D,
            => |val| set_texture(self.progress, self.config.project_root, val, .TEXTURE_3D, argc, argv, types),

            else => error.uniformNotSupported,
        };

        uniform.setShaderValue(self.cache.shader.*);
    }

    fn handle_method(
        _path: [*c]const u8,
        _types: [*c]const u8,
        argv: [*c][*c]c.lo_arg,
        argc: c_int,
        msg: c.lo_message,
        userdata: ?*anyopaque,
    ) callconv(.C) c_int {
        _ = msg;

        const self = @as(*ControlServer, @ptrCast(@alignCast(userdata.?)));
        const path = _path[0..c.strlen(_path)];
        const types = _types[0..@as(u32, @intCast(argc))];

        if (std.mem.eql(u8, path, "/-/reload")) {
            self.reload_requested = true;
            return 1;
        }

        self.set_uniform(path, argv, argc, types) catch |err| {
            std.debug.print("{} while processing {s}\n", .{ err, path });
            return 1;
        };
        return 0;
    }

    fn handle_bundle_start(time: c.lo_timetag, userdata: ?*anyopaque) callconv(.C) c_int {
        _ = time;
        _ = userdata;
        return 1;
    }

    fn handle_bundle_end(userdata: ?*anyopaque) callconv(.C) c_int {
        _ = userdata;
        return 1;
    }
};
