const std = @import("std");
const c_allocator = @import("std").heap.c_allocator;
const c = @import("c.zig");
const cfg = @import("config.zig");

fn tmpZ(str: []const u8) ![:0]const u8 {
    const state = struct {
        var buf: [256]u8 = undefined;
    };
    return try std.fmt.bufPrintZ(state.buf[0..], "{s}", .{str});
}

var image_unit_map = [_]bool{false} ** 1024;

pub const Texture = struct {
    pub const Type = enum(c.GLenum) {
        TEXTURE_1D = c.GL_TEXTURE_1D,
        TEXTURE_2D = c.GL_TEXTURE_2D,
        TEXTURE_3D = c.GL_TEXTURE_3D,
        TEXTURE_CUBEMAP = c.GL_TEXTURE_CUBE_MAP,
        TEXTURE_RECTANGLE = c.GL_TEXTURE_RECTANGLE,
        TEXTURE_2D_MULTISAMPLE = c.GL_TEXTURE_2D_MULTISAMPLE,
        TEXTURE_1D_ARRAY = c.GL_TEXTURE_1D_ARRAY,
        TEXTURE_2D_ARRAY = c.GL_TEXTURE_2D_ARRAY,
        TEXTURE_CUBEMAP_ARRAY = c.GL_TEXTURE_CUBE_MAP_ARRAY,
        TEXTURE_BUFFER = c.GL_TEXTURE_BUFFER,
        TEXTURE_2D_MULTISAMPLE_ARRAY = c.GL_TEXTURE_2D_MULTISAMPLE_ARRAY,
    };

    type: Type,
    id: c.GLuint,
    image_unit: ?usize,

    pub fn create(texture_type: Type) Texture {
        var self: Texture = .{
            .type = texture_type,
            .id = undefined,
            .image_unit = null,
        };
        c.glGenTextures(1, &self.id);

        c.glBindTexture(@intFromEnum(self.type), self.id);
        defer c.glBindTexture(@intFromEnum(self.type), 0);

        c.glTexParameteri(@intFromEnum(self.type), c.GL_TEXTURE_MIN_FILTER, c.GL_LINEAR);
        c.glTexParameteri(@intFromEnum(self.type), c.GL_TEXTURE_MAG_FILTER, c.GL_LINEAR);

        return self;
    }

    pub fn bind(self: *Texture) usize {
        if (self.image_unit == null) {
            for (image_unit_map[0..], 0..) |*slot, i| {
                if (slot.*) continue;

                slot.* = true;
                self.image_unit = @intCast(i);
                break;
            }
        }

        if (self.image_unit) |image_unit| {
            c.glActiveTexture(@intCast(c.GL_TEXTURE0 + image_unit));
            c.glBindTexture(@intFromEnum(self.type), self.id);
            return image_unit;
        } else unreachable;
    }

    pub fn destroy(self: *const Texture) void {
        if (self.image_unit) |image_unit| {
            image_unit_map[image_unit] = false;
        }
        c.glDeleteTextures(1, &self.id);
    }

    pub fn setData2D(self: *const Texture, width: i32, height: i32, data: [*]const u8) void {
        c.glBindTexture(@intFromEnum(self.type), self.id);
        defer c.glBindTexture(@intFromEnum(self.type), 0);

        c.glTexImage2D(
            @intFromEnum(self.type),
            0,
            c.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, // c.GL_RGBA,
            width,
            height,
            0,
            c.GL_RGBA,
            c.GL_UNSIGNED_BYTE,
            data,
        );
    }

    pub fn allocate3D(self: *const Texture, width: i32, height: i32, depth: i32) void {
        c.glBindTexture(@intFromEnum(self.type), self.id);
        defer c.glBindTexture(@intFromEnum(self.type), 0);

        c.glTexStorage3D(
            @intFromEnum(self.type),
            1,
            c.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, // c.GL_RGBA,
            width,
            height,
            depth,
        );
    }

    pub fn setLayer3D(self: *const Texture, width: i32, height: i32, layer: i32, data: [*]const u8) void {
        c.glBindTexture(@intFromEnum(self.type), self.id);
        defer c.glBindTexture(@intFromEnum(self.type), 0);

        c.glTexSubImage3D(
            @intFromEnum(self.type),
            0,
            0,
            0,
            layer,
            width,
            height,
            1,
            c.GL_RGBA,
            c.GL_UNSIGNED_BYTE,
            data,
        );
    }
};

const UniformType = enum(c.GLint) {
    FLOAT = c.GL_FLOAT,
    FLOAT_VEC2 = c.GL_FLOAT_VEC2,
    FLOAT_VEC3 = c.GL_FLOAT_VEC3,
    FLOAT_VEC4 = c.GL_FLOAT_VEC4,
    DOUBLE = c.GL_DOUBLE,
    DOUBLE_VEC2 = c.GL_DOUBLE_VEC2,
    DOUBLE_VEC3 = c.GL_DOUBLE_VEC3,
    DOUBLE_VEC4 = c.GL_DOUBLE_VEC4,
    INT = c.GL_INT,
    INT_VEC2 = c.GL_INT_VEC2,
    INT_VEC3 = c.GL_INT_VEC3,
    INT_VEC4 = c.GL_INT_VEC4,
    UNSIGNED_INT = c.GL_UNSIGNED_INT,
    UNSIGNED_INT_VEC2 = c.GL_UNSIGNED_INT_VEC2,
    UNSIGNED_INT_VEC3 = c.GL_UNSIGNED_INT_VEC3,
    UNSIGNED_INT_VEC4 = c.GL_UNSIGNED_INT_VEC4,
    BOOL = c.GL_BOOL,
    BOOL_VEC2 = c.GL_BOOL_VEC2,
    BOOL_VEC3 = c.GL_BOOL_VEC3,
    BOOL_VEC4 = c.GL_BOOL_VEC4,
    FLOAT_MAT2 = c.GL_FLOAT_MAT2,
    FLOAT_MAT3 = c.GL_FLOAT_MAT3,
    FLOAT_MAT4 = c.GL_FLOAT_MAT4,
    FLOAT_MAT2x3 = c.GL_FLOAT_MAT2x3,
    FLOAT_MAT2x4 = c.GL_FLOAT_MAT2x4,
    FLOAT_MAT3x2 = c.GL_FLOAT_MAT3x2,
    FLOAT_MAT3x4 = c.GL_FLOAT_MAT3x4,
    FLOAT_MAT4x2 = c.GL_FLOAT_MAT4x2,
    FLOAT_MAT4x3 = c.GL_FLOAT_MAT4x3,
    DOUBLE_MAT2 = c.GL_DOUBLE_MAT2,
    DOUBLE_MAT3 = c.GL_DOUBLE_MAT3,
    DOUBLE_MAT4 = c.GL_DOUBLE_MAT4,
    DOUBLE_MAT2x3 = c.GL_DOUBLE_MAT2x3,
    DOUBLE_MAT2x4 = c.GL_DOUBLE_MAT2x4,
    DOUBLE_MAT3x2 = c.GL_DOUBLE_MAT3x2,
    DOUBLE_MAT3x4 = c.GL_DOUBLE_MAT3x4,
    DOUBLE_MAT4x2 = c.GL_DOUBLE_MAT4x2,
    DOUBLE_MAT4x3 = c.GL_DOUBLE_MAT4x3,
    SAMPLER_1D = c.GL_SAMPLER_1D,
    SAMPLER_2D = c.GL_SAMPLER_2D,
    SAMPLER_3D = c.GL_SAMPLER_3D,
    SAMPLER_CUBE = c.GL_SAMPLER_CUBE,
    SAMPLER_1D_SHADOW = c.GL_SAMPLER_1D_SHADOW,
    SAMPLER_2D_SHADOW = c.GL_SAMPLER_2D_SHADOW,
    SAMPLER_1D_ARRAY = c.GL_SAMPLER_1D_ARRAY,
    SAMPLER_2D_ARRAY = c.GL_SAMPLER_2D_ARRAY,
    SAMPLER_1D_ARRAY_SHADOW = c.GL_SAMPLER_1D_ARRAY_SHADOW,
    SAMPLER_2D_ARRAY_SHADOW = c.GL_SAMPLER_2D_ARRAY_SHADOW,
    SAMPLER_2D_MULTISAMPLE = c.GL_SAMPLER_2D_MULTISAMPLE,
    SAMPLER_2D_MULTISAMPLE_ARRAY = c.GL_SAMPLER_2D_MULTISAMPLE_ARRAY,
    SAMPLER_CUBE_SHADOW = c.GL_SAMPLER_CUBE_SHADOW,
    SAMPLER_BUFFER = c.GL_SAMPLER_BUFFER,
    SAMPLER_2D_RECT = c.GL_SAMPLER_2D_RECT,
    SAMPLER_2D_RECT_SHADOW = c.GL_SAMPLER_2D_RECT_SHADOW,
    INT_SAMPLER_1D = c.GL_INT_SAMPLER_1D,
    INT_SAMPLER_2D = c.GL_INT_SAMPLER_2D,
    INT_SAMPLER_3D = c.GL_INT_SAMPLER_3D,
    INT_SAMPLER_CUBE = c.GL_INT_SAMPLER_CUBE,
    INT_SAMPLER_1D_ARRAY = c.GL_INT_SAMPLER_1D_ARRAY,
    INT_SAMPLER_2D_ARRAY = c.GL_INT_SAMPLER_2D_ARRAY,
    INT_SAMPLER_2D_MULTISAMPLE = c.GL_INT_SAMPLER_2D_MULTISAMPLE,
    INT_SAMPLER_2D_MULTISAMPLE_ARRAY = c.GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY,
    INT_SAMPLER_BUFFER = c.GL_INT_SAMPLER_BUFFER,
    INT_SAMPLER_2D_RECT = c.GL_INT_SAMPLER_2D_RECT,
    UNSIGNED_INT_SAMPLER_1D = c.GL_UNSIGNED_INT_SAMPLER_1D,
    UNSIGNED_INT_SAMPLER_2D = c.GL_UNSIGNED_INT_SAMPLER_2D,
    UNSIGNED_INT_SAMPLER_3D = c.GL_UNSIGNED_INT_SAMPLER_3D,
    UNSIGNED_INT_SAMPLER_CUBE = c.GL_UNSIGNED_INT_SAMPLER_CUBE,
    UNSIGNED_INT_SAMPLER_1D_ARRAY = c.GL_UNSIGNED_INT_SAMPLER_1D_ARRAY,
    UNSIGNED_INT_SAMPLER_2D_ARRAY = c.GL_UNSIGNED_INT_SAMPLER_2D_ARRAY,
    UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE = c.GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE,
    UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY = c.GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY,
    UNSIGNED_INT_SAMPLER_BUFFER = c.GL_UNSIGNED_INT_SAMPLER_BUFFER,
    UNSIGNED_INT_SAMPLER_2D_RECT = c.GL_UNSIGNED_INT_SAMPLER_2D_RECT,
};

const UniformValue = union(UniformType) {
    FLOAT: f32,
    FLOAT_VEC2: [2]f32,
    FLOAT_VEC3: [3]f32,
    FLOAT_VEC4: [4]f32,
    DOUBLE: f64,
    DOUBLE_VEC2: [2]f64,
    DOUBLE_VEC3: [3]f64,
    DOUBLE_VEC4: [4]f64,
    INT: c_int, // i32 but bug
    INT_VEC2: [2]c_int,
    INT_VEC3: [3]c_int,
    INT_VEC4: [4]c_int,
    UNSIGNED_INT: c_uint,
    UNSIGNED_INT_VEC2: [2]c_uint,
    UNSIGNED_INT_VEC3: [3]c_uint,
    UNSIGNED_INT_VEC4: [4]c_uint,
    BOOL: c_uint,
    BOOL_VEC2: [2]c_uint,
    BOOL_VEC3: [3]c_uint,
    BOOL_VEC4: [4]c_uint,
    FLOAT_MAT2: [2 * 2]f32,
    FLOAT_MAT3: [3 * 3]f32,
    FLOAT_MAT4: [4 * 4]f32,
    FLOAT_MAT2x3: [2 * 3]f32,
    FLOAT_MAT2x4: [2 * 4]f32,
    FLOAT_MAT3x2: [3 * 2]f32,
    FLOAT_MAT3x4: [3 * 4]f32,
    FLOAT_MAT4x2: [4 * 2]f32,
    FLOAT_MAT4x3: [4 * 3]f32,
    DOUBLE_MAT2: [2 * 2]f64,
    DOUBLE_MAT3: [3 * 3]f64,
    DOUBLE_MAT4: [4 * 4]f64,
    DOUBLE_MAT2x3: [2 * 3]f64,
    DOUBLE_MAT2x4: [2 * 4]f64,
    DOUBLE_MAT3x2: [3 * 2]f64,
    DOUBLE_MAT3x4: [3 * 4]f64,
    DOUBLE_MAT4x2: [4 * 2]f64,
    DOUBLE_MAT4x3: [4 * 3]f64,
    SAMPLER_1D: ?Texture,
    SAMPLER_2D: ?Texture,
    SAMPLER_3D: ?Texture,
    SAMPLER_CUBE: ?Texture,
    SAMPLER_1D_SHADOW: ?Texture,
    SAMPLER_2D_SHADOW: ?Texture,
    SAMPLER_1D_ARRAY: ?Texture,
    SAMPLER_2D_ARRAY: ?Texture,
    SAMPLER_1D_ARRAY_SHADOW: ?Texture,
    SAMPLER_2D_ARRAY_SHADOW: ?Texture,
    SAMPLER_2D_MULTISAMPLE: ?Texture,
    SAMPLER_2D_MULTISAMPLE_ARRAY: ?Texture,
    SAMPLER_CUBE_SHADOW: ?Texture,
    SAMPLER_BUFFER: ?Texture,
    SAMPLER_2D_RECT: ?Texture,
    SAMPLER_2D_RECT_SHADOW: ?Texture,
    INT_SAMPLER_1D: ?Texture,
    INT_SAMPLER_2D: ?Texture,
    INT_SAMPLER_3D: ?Texture,
    INT_SAMPLER_CUBE: ?Texture,
    INT_SAMPLER_1D_ARRAY: ?Texture,
    INT_SAMPLER_2D_ARRAY: ?Texture,
    INT_SAMPLER_2D_MULTISAMPLE: ?Texture,
    INT_SAMPLER_2D_MULTISAMPLE_ARRAY: ?Texture,
    INT_SAMPLER_BUFFER: ?Texture,
    INT_SAMPLER_2D_RECT: ?Texture,
    UNSIGNED_INT_SAMPLER_1D: ?Texture,
    UNSIGNED_INT_SAMPLER_2D: ?Texture,
    UNSIGNED_INT_SAMPLER_3D: ?Texture,
    UNSIGNED_INT_SAMPLER_CUBE: ?Texture,
    UNSIGNED_INT_SAMPLER_1D_ARRAY: ?Texture,
    UNSIGNED_INT_SAMPLER_2D_ARRAY: ?Texture,
    UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE: ?Texture,
    UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: ?Texture,
    UNSIGNED_INT_SAMPLER_BUFFER: ?Texture,
    UNSIGNED_INT_SAMPLER_2D_RECT: ?Texture,

    pub fn deinit(self: *const UniformValue) void {
        switch (self.*) {
            .SAMPLER_1D,
            .SAMPLER_2D,
            .SAMPLER_3D,
            .SAMPLER_CUBE,
            .SAMPLER_1D_SHADOW,
            .SAMPLER_2D_SHADOW,
            .SAMPLER_1D_ARRAY,
            .SAMPLER_2D_ARRAY,
            .SAMPLER_1D_ARRAY_SHADOW,
            .SAMPLER_2D_ARRAY_SHADOW,
            .SAMPLER_2D_MULTISAMPLE,
            .SAMPLER_2D_MULTISAMPLE_ARRAY,
            .SAMPLER_CUBE_SHADOW,
            .SAMPLER_BUFFER,
            .SAMPLER_2D_RECT,
            .SAMPLER_2D_RECT_SHADOW,
            .INT_SAMPLER_1D,
            .INT_SAMPLER_2D,
            .INT_SAMPLER_3D,
            .INT_SAMPLER_CUBE,
            .INT_SAMPLER_1D_ARRAY,
            .INT_SAMPLER_2D_ARRAY,
            .INT_SAMPLER_2D_MULTISAMPLE,
            .INT_SAMPLER_2D_MULTISAMPLE_ARRAY,
            .INT_SAMPLER_BUFFER,
            .INT_SAMPLER_2D_RECT,
            .UNSIGNED_INT_SAMPLER_1D,
            .UNSIGNED_INT_SAMPLER_2D,
            .UNSIGNED_INT_SAMPLER_3D,
            .UNSIGNED_INT_SAMPLER_CUBE,
            .UNSIGNED_INT_SAMPLER_1D_ARRAY,
            .UNSIGNED_INT_SAMPLER_2D_ARRAY,
            .UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE,
            .UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY,
            .UNSIGNED_INT_SAMPLER_BUFFER,
            .UNSIGNED_INT_SAMPLER_2D_RECT,
            => |value| {
                if (value) |texture| texture.destroy();
            },
            else => {},
        }
    }

    pub fn getPointer(self: *UniformValue) UniformPointer {
        return switch (self.*) {
            inline else => |*value, tag| @unionInit(UniformPointer, @tagName(tag), value),
        };
    }
};

const UniformPointer = union(UniformType) {
    FLOAT: *f32,
    FLOAT_VEC2: *[2]f32,
    FLOAT_VEC3: *[3]f32,
    FLOAT_VEC4: *[4]f32,
    DOUBLE: *f64,
    DOUBLE_VEC2: *[2]f64,
    DOUBLE_VEC3: *[3]f64,
    DOUBLE_VEC4: *[4]f64,
    INT: *c_int, // i32 but bug
    INT_VEC2: *[2]c_int,
    INT_VEC3: *[3]c_int,
    INT_VEC4: *[4]c_int,
    UNSIGNED_INT: *c_uint,
    UNSIGNED_INT_VEC2: *[2]c_uint,
    UNSIGNED_INT_VEC3: *[3]c_uint,
    UNSIGNED_INT_VEC4: *[4]c_uint,
    BOOL: *c_uint,
    BOOL_VEC2: *[2]c_uint,
    BOOL_VEC3: *[3]c_uint,
    BOOL_VEC4: *[4]c_uint,
    FLOAT_MAT2: *[2 * 2]f32,
    FLOAT_MAT3: *[3 * 3]f32,
    FLOAT_MAT4: *[4 * 4]f32,
    FLOAT_MAT2x3: *[2 * 3]f32,
    FLOAT_MAT2x4: *[2 * 4]f32,
    FLOAT_MAT3x2: *[3 * 2]f32,
    FLOAT_MAT3x4: *[3 * 4]f32,
    FLOAT_MAT4x2: *[4 * 2]f32,
    FLOAT_MAT4x3: *[4 * 3]f32,
    DOUBLE_MAT2: *[2 * 2]f64,
    DOUBLE_MAT3: *[3 * 3]f64,
    DOUBLE_MAT4: *[4 * 4]f64,
    DOUBLE_MAT2x3: *[2 * 3]f64,
    DOUBLE_MAT2x4: *[2 * 4]f64,
    DOUBLE_MAT3x2: *[3 * 2]f64,
    DOUBLE_MAT3x4: *[3 * 4]f64,
    DOUBLE_MAT4x2: *[4 * 2]f64,
    DOUBLE_MAT4x3: *[4 * 3]f64,
    SAMPLER_1D: *?Texture,
    SAMPLER_2D: *?Texture,
    SAMPLER_3D: *?Texture,
    SAMPLER_CUBE: *?Texture,
    SAMPLER_1D_SHADOW: *?Texture,
    SAMPLER_2D_SHADOW: *?Texture,
    SAMPLER_1D_ARRAY: *?Texture,
    SAMPLER_2D_ARRAY: *?Texture,
    SAMPLER_1D_ARRAY_SHADOW: *?Texture,
    SAMPLER_2D_ARRAY_SHADOW: *?Texture,
    SAMPLER_2D_MULTISAMPLE: *?Texture,
    SAMPLER_2D_MULTISAMPLE_ARRAY: *?Texture,
    SAMPLER_CUBE_SHADOW: *?Texture,
    SAMPLER_BUFFER: *?Texture,
    SAMPLER_2D_RECT: *?Texture,
    SAMPLER_2D_RECT_SHADOW: *?Texture,
    INT_SAMPLER_1D: *?Texture,
    INT_SAMPLER_2D: *?Texture,
    INT_SAMPLER_3D: *?Texture,
    INT_SAMPLER_CUBE: *?Texture,
    INT_SAMPLER_1D_ARRAY: *?Texture,
    INT_SAMPLER_2D_ARRAY: *?Texture,
    INT_SAMPLER_2D_MULTISAMPLE: *?Texture,
    INT_SAMPLER_2D_MULTISAMPLE_ARRAY: *?Texture,
    INT_SAMPLER_BUFFER: *?Texture,
    INT_SAMPLER_2D_RECT: *?Texture,
    UNSIGNED_INT_SAMPLER_1D: *?Texture,
    UNSIGNED_INT_SAMPLER_2D: *?Texture,
    UNSIGNED_INT_SAMPLER_3D: *?Texture,
    UNSIGNED_INT_SAMPLER_CUBE: *?Texture,
    UNSIGNED_INT_SAMPLER_1D_ARRAY: *?Texture,
    UNSIGNED_INT_SAMPLER_2D_ARRAY: *?Texture,
    UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE: *?Texture,
    UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: *?Texture,
    UNSIGNED_INT_SAMPLER_BUFFER: *?Texture,
    UNSIGNED_INT_SAMPLER_2D_RECT: *?Texture,

    pub fn index(self: *const UniformPointer, i: u8) !UniformPointer {
        const length: u8 = switch (self.*) {
            .FLOAT_VEC2, .DOUBLE_VEC2, .INT_VEC2, .BOOL_VEC2, .UNSIGNED_INT_VEC2 => 2,
            .FLOAT_VEC3, .DOUBLE_VEC3, .INT_VEC3, .UNSIGNED_INT_VEC3 => 3,
            .BOOL_VEC3, .FLOAT_VEC4, .DOUBLE_VEC4, .INT_VEC4, .UNSIGNED_INT_VEC4, .BOOL_VEC4 => 4,
            else => return error.notIndexable,
        };

        if (i >= length) {
            return error.invalidIndex;
        }

        return switch (self.*) {
            inline .FLOAT_VEC2, .FLOAT_VEC3, .FLOAT_VEC4 => |v| .{ .FLOAT = &v[i] },
            inline .DOUBLE_VEC2, .DOUBLE_VEC3, .DOUBLE_VEC4 => |v| .{ .DOUBLE = &v[i] },
            inline .INT_VEC2, .INT_VEC3, .INT_VEC4 => |v| .{ .INT = &v[i] },
            inline .UNSIGNED_INT_VEC2, .UNSIGNED_INT_VEC3, .UNSIGNED_INT_VEC4 => |v| .{ .UNSIGNED_INT = &v[i] },
            inline .BOOL_VEC2, .BOOL_VEC3, .BOOL_VEC4 => |v| .{ .BOOL = &v[i] },
            else => unreachable,
        };
    }
};

pub const CachedUniform = struct {
    value: ?*UniformValue,
    location: c.GLint,

    pub fn init(allocator: std.mem.Allocator, shader: ShaderProgram, name: []const u8) !CachedUniform {
        var index: c.GLuint = undefined;

        const nameZ = try tmpZ(name);

        c.glGetUniformIndices(shader.program_id, 1, &nameZ.ptr, &index);
        if (index == c.GL_INVALID_INDEX)
            return error.invalidUniform;

        var _utype: c.GLint = undefined;
        c.glGetActiveUniformsiv(shader.program_id, 1, &index, c.GL_UNIFORM_TYPE, &_utype);

        switch (@as(UniformType, @enumFromInt(_utype))) {
            inline else => |utype| {
                const location = try shader.uniformLocation(nameZ);
                const value = try allocator.create(UniformValue);
                errdefer allocator.destroy(value);

                value.* = @unionInit(UniformValue, @tagName(utype), undefined);
                return .{ .value = value, .location = location };
            },
        }
    }

    pub fn deinit(self: *CachedUniform, allocator: std.mem.Allocator) void {
        if (self.value) |value| {
            value.deinit();
            allocator.destroy(value);
            self.value = null;
        }
    }

    // deinit self WITHOUT freeing contained resources
    pub fn move(self: *CachedUniform, to: *CachedUniform, allocator: std.mem.Allocator) CachedUniform {
        to.deinit(allocator);
        to.buffer = self.buffer;
        to.value = self.value;

        self.buffer = null;

        return self;
    }

    pub fn tryMove(self: *CachedUniform, dest: *CachedUniform, allocator: std.mem.Allocator) bool {
        if (@as(UniformType, self.value.?.*) != dest.value.?.*) return false;

        dest.deinit(allocator);
        dest.value = self.value;
        self.value = null;
        return true;
    }

    pub fn setShaderValue(self: *const CachedUniform, shader: ShaderProgram) void {
        const program = shader.program_id;
        const location = self.location;

        switch (self.value.?.*) {
            .FLOAT => |*value| c.glProgramUniform1fv(program, location, 1, value),
            .FLOAT_VEC2 => |*value| c.glProgramUniform2fv(program, location, 1, value),
            .FLOAT_VEC3 => |*value| c.glProgramUniform3fv(program, location, 1, value),
            .FLOAT_VEC4 => |*value| c.glProgramUniform4fv(program, location, 1, value),
            .DOUBLE => |*value| c.glProgramUniform1dv(program, location, 1, value),
            .DOUBLE_VEC2 => |*value| c.glProgramUniform1dv(program, location, 1, value),
            .DOUBLE_VEC3 => |*value| c.glProgramUniform2dv(program, location, 1, value),
            .DOUBLE_VEC4 => |*value| c.glProgramUniform3dv(program, location, 1, value),
            .INT => |*value| c.glProgramUniform1iv(program, location, 1, value),
            .INT_VEC2 => |*value| c.glProgramUniform2iv(program, location, 1, value),
            .INT_VEC3 => |*value| c.glProgramUniform3iv(program, location, 1, value),
            .INT_VEC4 => |*value| c.glProgramUniform4iv(program, location, 1, value),
            .UNSIGNED_INT => |*value| c.glProgramUniform1uiv(program, location, 1, value),
            .UNSIGNED_INT_VEC2 => |*value| c.glProgramUniform2uiv(program, location, 1, value),
            .UNSIGNED_INT_VEC3 => |*value| c.glProgramUniform3uiv(program, location, 1, value),
            .UNSIGNED_INT_VEC4 => |*value| c.glProgramUniform4uiv(program, location, 1, value),
            .BOOL => |*value| c.glProgramUniform1uiv(program, location, 1, value),
            .BOOL_VEC2 => |*value| c.glProgramUniform2uiv(program, location, 1, value),
            .BOOL_VEC3 => |*value| c.glProgramUniform3uiv(program, location, 1, value),
            .BOOL_VEC4 => |*value| c.glProgramUniform4uiv(program, location, 1, value),
            .FLOAT_MAT2 => |*value| c.glProgramUniformMatrix2fv(program, location, 1, c.GL_FALSE, value),
            .FLOAT_MAT3 => |*value| c.glProgramUniformMatrix3fv(program, location, 1, c.GL_FALSE, value),
            .FLOAT_MAT4 => |*value| c.glProgramUniformMatrix4fv(program, location, 1, c.GL_FALSE, value),
            .FLOAT_MAT2x3 => |*value| c.glProgramUniformMatrix2x3fv(program, location, 1, c.GL_FALSE, value),
            .FLOAT_MAT2x4 => |*value| c.glProgramUniformMatrix2x4fv(program, location, 1, c.GL_FALSE, value),
            .FLOAT_MAT3x2 => |*value| c.glProgramUniformMatrix3x2fv(program, location, 1, c.GL_FALSE, value),
            .FLOAT_MAT3x4 => |*value| c.glProgramUniformMatrix3x4fv(program, location, 1, c.GL_FALSE, value),
            .FLOAT_MAT4x2 => |*value| c.glProgramUniformMatrix4x2fv(program, location, 1, c.GL_FALSE, value),
            .FLOAT_MAT4x3 => |*value| c.glProgramUniformMatrix4x3fv(program, location, 1, c.GL_FALSE, value),
            .DOUBLE_MAT2 => |*value| c.glProgramUniformMatrix2dv(program, location, 1, c.GL_FALSE, value),
            .DOUBLE_MAT3 => |*value| c.glProgramUniformMatrix3dv(program, location, 1, c.GL_FALSE, value),
            .DOUBLE_MAT4 => |*value| c.glProgramUniformMatrix4dv(program, location, 1, c.GL_FALSE, value),
            .DOUBLE_MAT2x3 => |*value| c.glProgramUniformMatrix2x3dv(program, location, 1, c.GL_FALSE, value),
            .DOUBLE_MAT2x4 => |*value| c.glProgramUniformMatrix2x4dv(program, location, 1, c.GL_FALSE, value),
            .DOUBLE_MAT3x2 => |*value| c.glProgramUniformMatrix3x2dv(program, location, 1, c.GL_FALSE, value),
            .DOUBLE_MAT3x4 => |*value| c.glProgramUniformMatrix3x4dv(program, location, 1, c.GL_FALSE, value),
            .DOUBLE_MAT4x2 => |*value| c.glProgramUniformMatrix4x2dv(program, location, 1, c.GL_FALSE, value),
            .DOUBLE_MAT4x3 => |*value| c.glProgramUniformMatrix4x3dv(program, location, 1, c.GL_FALSE, value),
            .SAMPLER_1D,
            .SAMPLER_2D,
            .SAMPLER_3D,
            .SAMPLER_CUBE,
            .SAMPLER_1D_SHADOW,
            .SAMPLER_2D_SHADOW,
            .SAMPLER_1D_ARRAY,
            .SAMPLER_2D_ARRAY,
            .SAMPLER_1D_ARRAY_SHADOW,
            .SAMPLER_2D_ARRAY_SHADOW,
            .SAMPLER_2D_MULTISAMPLE,
            .SAMPLER_2D_MULTISAMPLE_ARRAY,
            .SAMPLER_CUBE_SHADOW,
            .SAMPLER_BUFFER,
            .SAMPLER_2D_RECT,
            .SAMPLER_2D_RECT_SHADOW,
            .INT_SAMPLER_1D,
            .INT_SAMPLER_2D,
            .INT_SAMPLER_3D,
            .INT_SAMPLER_CUBE,
            .INT_SAMPLER_1D_ARRAY,
            .INT_SAMPLER_2D_ARRAY,
            .INT_SAMPLER_2D_MULTISAMPLE,
            .INT_SAMPLER_2D_MULTISAMPLE_ARRAY,
            .INT_SAMPLER_BUFFER,
            .INT_SAMPLER_2D_RECT,
            .UNSIGNED_INT_SAMPLER_1D,
            .UNSIGNED_INT_SAMPLER_2D,
            .UNSIGNED_INT_SAMPLER_3D,
            .UNSIGNED_INT_SAMPLER_CUBE,
            .UNSIGNED_INT_SAMPLER_1D_ARRAY,
            .UNSIGNED_INT_SAMPLER_2D_ARRAY,
            .UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE,
            .UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY,
            .UNSIGNED_INT_SAMPLER_BUFFER,
            .UNSIGNED_INT_SAMPLER_2D_RECT,
            => |*value| {
                if (value.*) |*texture| {
                    const index = texture.bind();
                    c.glProgramUniform1i(program, location, @intCast(index));
                }
            },
        }
    }

    pub fn getShaderValue(self: *CachedUniform, shader: ShaderProgram) void {
        const program = shader.program_id;
        const location = self.location;

        switch (self.value.?.*) {
            .FLOAT => |*value| c.glGetnUniformfv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .FLOAT_VEC2 => |*value| c.glGetnUniformfv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .FLOAT_VEC3 => |*value| c.glGetnUniformfv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .FLOAT_VEC4 => |*value| c.glGetnUniformfv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .DOUBLE => |*value| c.glGetnUniformdv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .DOUBLE_VEC2 => |*value| c.glGetnUniformdv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .DOUBLE_VEC3 => |*value| c.glGetnUniformdv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .DOUBLE_VEC4 => |*value| c.glGetnUniformdv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .INT => |*value| c.glGetnUniformiv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .INT_VEC2 => |*value| c.glGetnUniformiv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .INT_VEC3 => |*value| c.glGetnUniformiv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .INT_VEC4 => |*value| c.glGetnUniformiv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .UNSIGNED_INT => |*value| c.glGetnUniformuiv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .UNSIGNED_INT_VEC2 => |*value| c.glGetnUniformuiv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .UNSIGNED_INT_VEC3 => |*value| c.glGetnUniformuiv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .UNSIGNED_INT_VEC4 => |*value| c.glGetnUniformuiv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .BOOL => |*value| c.glGetnUniformuiv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .BOOL_VEC2 => |*value| c.glGetnUniformuiv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .BOOL_VEC3 => |*value| c.glGetnUniformuiv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .BOOL_VEC4 => |*value| c.glGetnUniformuiv(program, location, @sizeOf(@TypeOf(value.*)), value),
            .FLOAT_MAT2 => |*value| c.glGetUniformfv(program, location, value),
            .FLOAT_MAT3 => |*value| c.glGetUniformfv(program, location, value),
            .FLOAT_MAT4 => |*value| c.glGetUniformfv(program, location, value),
            .FLOAT_MAT2x3 => |*value| c.glGetUniformfv(program, location, value),
            .FLOAT_MAT2x4 => |*value| c.glGetUniformfv(program, location, value),
            .FLOAT_MAT3x2 => |*value| c.glGetUniformfv(program, location, value),
            .FLOAT_MAT3x4 => |*value| c.glGetUniformfv(program, location, value),
            .FLOAT_MAT4x2 => |*value| c.glGetUniformfv(program, location, value),
            .FLOAT_MAT4x3 => |*value| c.glGetUniformfv(program, location, value),
            .DOUBLE_MAT2 => |*value| c.glGetUniformdv(program, location, value),
            .DOUBLE_MAT3 => |*value| c.glGetUniformdv(program, location, value),
            .DOUBLE_MAT4 => |*value| c.glGetUniformdv(program, location, value),
            .DOUBLE_MAT2x3 => |*value| c.glGetUniformdv(program, location, value),
            .DOUBLE_MAT2x4 => |*value| c.glGetUniformdv(program, location, value),
            .DOUBLE_MAT3x2 => |*value| c.glGetUniformdv(program, location, value),
            .DOUBLE_MAT3x4 => |*value| c.glGetUniformdv(program, location, value),
            .DOUBLE_MAT4x2 => |*value| c.glGetUniformdv(program, location, value),
            .DOUBLE_MAT4x3 => |*value| c.glGetUniformdv(program, location, value),
            .SAMPLER_1D,
            .SAMPLER_2D,
            .SAMPLER_3D,
            .SAMPLER_CUBE,
            .SAMPLER_1D_SHADOW,
            .SAMPLER_2D_SHADOW,
            .SAMPLER_1D_ARRAY,
            .SAMPLER_2D_ARRAY,
            .SAMPLER_1D_ARRAY_SHADOW,
            .SAMPLER_2D_ARRAY_SHADOW,
            .SAMPLER_2D_MULTISAMPLE,
            .SAMPLER_2D_MULTISAMPLE_ARRAY,
            .SAMPLER_CUBE_SHADOW,
            .SAMPLER_BUFFER,
            .SAMPLER_2D_RECT,
            .SAMPLER_2D_RECT_SHADOW,
            .INT_SAMPLER_1D,
            .INT_SAMPLER_2D,
            .INT_SAMPLER_3D,
            .INT_SAMPLER_CUBE,
            .INT_SAMPLER_1D_ARRAY,
            .INT_SAMPLER_2D_ARRAY,
            .INT_SAMPLER_2D_MULTISAMPLE,
            .INT_SAMPLER_2D_MULTISAMPLE_ARRAY,
            .INT_SAMPLER_BUFFER,
            .INT_SAMPLER_2D_RECT,
            .UNSIGNED_INT_SAMPLER_1D,
            .UNSIGNED_INT_SAMPLER_2D,
            .UNSIGNED_INT_SAMPLER_3D,
            .UNSIGNED_INT_SAMPLER_CUBE,
            .UNSIGNED_INT_SAMPLER_1D_ARRAY,
            .UNSIGNED_INT_SAMPLER_2D_ARRAY,
            .UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE,
            .UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY,
            .UNSIGNED_INT_SAMPLER_BUFFER,
            .UNSIGNED_INT_SAMPLER_2D_RECT,
            => |*value| value.* = null,
        }
    }
};

pub const ShaderProgram = struct {
    program_id: c.GLuint,
    vert_id: c.GLuint,
    frag_id: c.GLuint,

    pub fn bind(self: ShaderProgram) void {
        c.glUseProgram(self.program_id);
    }

    pub fn uniformLocation(self: ShaderProgram, nameZ: [:0]const u8) !c.GLint {
        const id = c.glGetUniformLocation(self.program_id, nameZ);
        if (id == -1)
            return error.invalidUniform;

        return id;
    }

    pub fn create(vert_source: []const u8, frag_source: []const u8) !ShaderProgram {
        var self: ShaderProgram = undefined;

        self.vert_id = try initGlShader(vert_source, "vertex", c.GL_VERTEX_SHADER);
        errdefer c.glDeleteShader(self.vert_id);

        self.frag_id = try initGlShader(frag_source, "fragment", c.GL_FRAGMENT_SHADER);
        errdefer c.glDeleteShader(self.frag_id);

        self.program_id = c.glCreateProgram();
        c.glAttachShader(self.program_id, self.vert_id);
        c.glAttachShader(self.program_id, self.frag_id);
        c.glLinkProgram(self.program_id);

        errdefer c.glDeleteProgram(self.program_id);
        errdefer c.glDetachShader(self.program_id, self.frag_id);
        errdefer c.glDetachShader(self.program_id, self.vert_id);

        var ok: c.GLint = undefined;
        c.glGetProgramiv(self.program_id, c.GL_LINK_STATUS, &ok);
        if (ok != 0) return self;

        var error_size: c.GLint = undefined;
        c.glGetProgramiv(self.program_id, c.GL_INFO_LOG_LENGTH, &error_size);
        const message = try c_allocator.alloc(u8, @as(usize, @intCast(error_size)));
        c.glGetProgramInfoLog(self.program_id, error_size, &error_size, message.ptr);
        std.debug.print("Error linking shader program: {s}\n", .{@as([*:0]const u8, @ptrCast(message.ptr))});
        return error.LinkError;
    }

    pub fn destroy(self: *ShaderProgram) void {
        c.glDetachShader(self.program_id, self.frag_id);
        c.glDetachShader(self.program_id, self.vert_id);

        c.glDeleteShader(self.frag_id);
        c.glDeleteShader(self.vert_id);

        c.glDeleteProgram(self.program_id);
    }
};

pub const UniformCache = struct {
    allocator: std.mem.Allocator,
    uniforms: std.StringHashMap(CachedUniform),
    shader: *ShaderProgram,

    pub fn init(allocator: std.mem.Allocator, shader: *ShaderProgram) UniformCache {
        return UniformCache{
            .allocator = allocator,
            .uniforms = std.StringHashMap(CachedUniform).init(allocator),
            .shader = shader,
        };
    }

    pub fn deinit(self: *UniformCache) void {
        var it = self.uniforms.iterator();
        while (it.next()) |entry| {
            entry.value_ptr.deinit(self.allocator);
            self.allocator.free(entry.key_ptr.*);
        }

        self.uniforms.deinit();
    }

    pub fn get(self: *UniformCache, name: []const u8) !?*CachedUniform {
        if (!self.uniforms.contains(name)) {
            const cloned_name = try self.allocator.dupe(u8, name);
            errdefer self.allocator.free(cloned_name);

            var uniform = CachedUniform.init(self.allocator, self.shader.*, name) catch return null;
            errdefer uniform.deinit(self.allocator);

            uniform.getShaderValue(self.shader.*);
            try self.uniforms.put(cloned_name, uniform);
        }

        if (self.uniforms.getPtr(name)) |ptr| {
            return ptr;
        }

        return null;
    }

    pub fn refresh(self: *UniformCache) !void {
        var old_uniforms = self.uniforms.move();
        defer {
            var it = old_uniforms.iterator();
            while (it.next()) |entry| {
                self.allocator.free(entry.key_ptr.*);
                entry.value_ptr.deinit(self.allocator);
            }
            old_uniforms.deinit();
        }

        {
            var it = old_uniforms.iterator();
            while (it.next()) |old_entry| {
                if (try self.get(old_entry.key_ptr.*)) |uniform| {
                    if (old_entry.value_ptr.tryMove(uniform, self.allocator)) {
                        uniform.setShaderValue(self.shader.*);
                    }
                }
            }
        }
    }
};

fn initGlShader(source: []const u8, name: []const u8, kind: c.GLenum) !c.GLuint {
    const shader_id = c.glCreateShader(kind);
    errdefer c.glDeleteShader(shader_id);
    const source_ptr: ?[*]const u8 = source.ptr;
    const source_len = @as(c.GLint, @intCast(source.len));
    c.glShaderSource(shader_id, 1, &source_ptr, &source_len);
    c.glCompileShader(shader_id);

    var ok: c.GLint = undefined;
    c.glGetShaderiv(shader_id, c.GL_COMPILE_STATUS, &ok);
    if (ok != 0) return shader_id;

    var error_size: c.GLint = undefined;
    c.glGetShaderiv(shader_id, c.GL_INFO_LOG_LENGTH, &error_size);

    const message = try c_allocator.alloc(u8, @as(usize, @intCast(error_size)));
    c.glGetShaderInfoLog(shader_id, error_size, &error_size, message.ptr);
    std.debug.print(
        "Error compiling {s} shader:\n{s}\n",
        .{ name, @as([*:0]const u8, @ptrCast(message.ptr)) },
    );
    return error.CompileError;
}

pub const FramebufferObject = struct {
    buffer_id: c.GLuint,
    texture_id: c.GLuint,

    pub fn bind(self: FramebufferObject) void {
        c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.buffer_id);
    }

    pub fn unbind(self: FramebufferObject) void {
        _ = self;
        c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0);
    }

    pub fn create(width: i32, height: i32) !FramebufferObject {
        var self: FramebufferObject = undefined;
        c.glGenFramebuffers(1, &self.buffer_id);
        c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.buffer_id);
        defer c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0);

        c.glGenTextures(1, &self.texture_id);
        c.glBindTexture(c.GL_TEXTURE_2D, self.texture_id);
        defer c.glBindTexture(c.GL_TEXTURE_2D, 0);
        c.glTexImage2D(
            c.GL_TEXTURE_2D,
            0,
            c.GL_RGBA,
            width,
            height,
            0,
            c.GL_RGBA,
            c.GL_UNSIGNED_BYTE,
            null,
        );
        c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_LINEAR);
        c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_LINEAR);

        c.glFramebufferTexture2D(
            c.GL_FRAMEBUFFER,
            c.GL_COLOR_ATTACHMENT0,
            c.GL_TEXTURE_2D,
            self.texture_id,
            0,
        );

        if (c.glCheckFramebufferStatus(c.GL_FRAMEBUFFER) != c.GL_FRAMEBUFFER_COMPLETE)
            return error.FBONotComplete;

        return self;
    }

    pub fn destroy(self: *FramebufferObject) void {
        c.glDeleteFramebuffers(1, &self.*.buffer_id);
        c.glDeleteTextures(1, &self.*.texture_id);
    }
};

pub const VertexObject = struct {
    array_id: c.GLuint,
    buffer_id: c.GLuint,
    vertex_size: i32,
    vertex_count: i32,

    pub fn bind(self: VertexObject, attrib: u32) void {
        c.glBindBuffer(c.GL_ARRAY_BUFFER, self.buffer_id);
        c.glEnableVertexAttribArray(attrib);
        c.glVertexAttribPointer(attrib, self.vertex_size, c.GL_FLOAT, c.GL_FALSE, 0, null);
    }

    pub fn draw(self: VertexObject) void {
        c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, self.vertex_count);
    }

    pub fn create(comptime T: type, vertices: []const T, vertex_size: i32) VertexObject {
        var self: VertexObject = undefined;

        self.vertex_size = vertex_size;
        self.vertex_count = @as(i32, @intCast(vertices.len));

        c.glGenVertexArrays(1, &self.array_id);
        c.glBindVertexArray(self.array_id);

        c.glGenBuffers(1, &self.buffer_id);
        c.glBindBuffer(c.GL_ARRAY_BUFFER, self.buffer_id);
        c.glBufferData(
            c.GL_ARRAY_BUFFER,
            @as(c_long, @intCast(@sizeOf(T) * vertices.len)),
            @as(*const anyopaque, @ptrCast(vertices.ptr)),
            c.GL_STATIC_DRAW,
        );

        return self;
    }

    pub fn destroy(self: *VertexObject) void {
        defer c.glDeleteBuffers(1, &self.*.buffer_id);
        defer c.glDeleteVertexArrays(1, &self.*.array_id);
    }
};

pub const Constants = struct {
    texture_shader: ShaderProgram,
    normalized_quad: VertexObject,
    main_window: *c.GLFWwindow,
    aspect: f32,
    config: *cfg.Config,

    pub fn create(main_window: *c.GLFWwindow, config: *cfg.Config) !Constants {
        c.glfwMakeContextCurrent(main_window);

        const shader = try 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;
            \\
            \\uniform sampler2D colorTexture;
            \\
            \\void main(){
            \\  color = texture(colorTexture, uv);
            \\}
        );

        const vertices = [_]c.GLfloat{
            -1.0, -1.0,
            -1.0, 1.0,
            1.0,  -1.0,
            1.0,  1.0,
        };

        return Constants{
            .main_window = main_window,
            .texture_shader = shader,
            .normalized_quad = VertexObject.create(f32, vertices[0..], 2),
            .config = config,
            .aspect = @as(f32, @floatFromInt(config.width)) / @as(f32, @floatFromInt(config.height)),
        };
    }

    pub fn destroy(self: *Constants) void {
        self.normalized_quad.destroy();
        self.texture_shader.destroy();
    }
};
