const std = @import("std");
const raylib_helper = @import("../lib_helpers/raylib_helper.zig");
const raylib = raylib_helper.raylib;
const v3 = raylib_helper.v3;
const A7r = std.mem.Allocator;
const comptimePrint = std.fmt.comptimePrint;

const VERTICES_BLOCK_SIZE = 2 * 3 * 3;
const TEXCOORDS_BLOCK_SIZE = 2 * 2 * 3;

const RawQuad = struct {
    tile: u32,
    top_left: raylib.Vector3,
    top_right: raylib.Vector3,
    bottom_right: raylib.Vector3,
    bottom_left: raylib.Vector3,
    normal: raylib.Vector3,
    width: f32,
    height: f32,

    top_obscuring_pattern: u32,
    left_obscuring_pattern: u32,
    right_obscuring_pattern: u32,
    bottom_obscuring_pattern: u32,
    top_left_obscured: bool,
    top_right_obscured: bool,
    bottom_right_obscured: bool,
    bottom_left_obscured: bool,
};

const Metadata1 = packed struct {
    ambient_occlusion_1: u32,
    ambient_occlusion_2: u32,
    ambient_occlusion_corner1: bool,
    ambient_occlusion_corner2: bool,
    ambient_occlusion_corner3: bool,
    quad_height: u6,
    quad_width: u6,
    unused: u17 = 0,
    unused_2: u32 = 0,
};

comptime {
    if (@bitSizeOf(Metadata1) != 128) {
        @compileError(comptimePrint("Metadata 1 has wrong size. Expected 128 bits, found {}", .{@bitSizeOf(Metadata1)}));
    }
}

pub const Chunk = struct {
    tiles: []u32,
    a7r: A7r,

    pub fn init(a7r: A7r) !Chunk {
        const self = Chunk{
            .a7r = a7r,
            .tiles = try a7r.alloc(u32, 32 * 32 * 32),
        };
        @memset(self.tiles, 0);
        return self;
    }

    pub fn deinit(self: Chunk) void {
        self.a7r.free(self.tiles);
    }

    pub fn getTile(self: Chunk, x: u5, y: u5, z: u5) u32 {
        // Fetch the tile at (x, y, z), but with potential side effects. If you imagine tiles to be a 3-dimensional array, this would be tiles[x][y][z].
        return self.tiles[@as(u15, x) << 10 | @as(u15, y) << 5 | @as(u15, z)];
    }

    pub fn setTile(self: Chunk, x: u5, y: u5, z: u5, tile: u32) void {
        // Set the tile at (x, y, z). If you imagine tiles to be a 3-dimensional array, this would be tiles[x][y][z] = tile.
        self.tiles[@as(u15, x) << 10 | @as(u15, y) << 5 | @as(u15, z)] = tile;
    }

    fn getTileRaw(self: Chunk, x: u5, y: u5, z: u5) u32 {
        // Fetch the tile at (x, y, z) without changin anything. If you imagine tiles to be a 3-dimensional array, this would be tiles[x][y][z].
        return self.tiles[@as(u15, x) << 10 | @as(u15, y) << 5 | @as(u15, z)];
    }

    inline fn getTileRawShifted(self: Chunk, x: u5, y: u5, z: u5, comptime d: comptime_int) u32 {
        // This cyclicaly permutes the x, y, z coordinates at compile time. Useful when iterating over x, y, and z axis.
        if (d % 3 == 0) {
            return self.getTileRaw(x, y, z);
        } else if (d % 3 == 1) {
            return self.getTileRaw(y, z, x);
        } else if (d % 3 == 2) {
            return self.getTileRaw(z, x, y);
        }
    }

    fn pack_raw_quad(y: usize, y2: usize, z: usize, z2: usize, sign: comptime_int, d: comptime_int, surface: u32, xf: f32) RawQuad {
        const ymin: f32 = @as(f32, @floatFromInt(y)) - 0.5;
        const ymax: f32 = @as(f32, @floatFromInt(y2)) + 0.5;
        const zmin: f32 = @as(f32, @floatFromInt(z)) - 0.5;
        const zmax: f32 = @as(f32, @floatFromInt(z2)) + 0.5;
        const yleft: f32 = if (sign == 1) ymin else ymax;
        const yright: f32 = if (sign == 1) ymax else ymin;
        const zleft: f32 = if (sign == 1) zmin else zmax;
        const zright: f32 = if (sign == 1) zmax else zmin;
        var raw_quad: RawQuad = undefined;
        switch (d) {
            0 => {
                raw_quad = .{
                    .tile = surface,
                    .top_left = v3.new(xf + 0.5 * sign, ymax, zright),
                    .top_right = v3.new(xf + 0.5 * sign, ymax, zleft),
                    .bottom_left = v3.new(xf + 0.5 * sign, ymin, zright),
                    .bottom_right = v3.new(xf + 0.5 * sign, ymin, zleft),
                    .normal = v3.new(sign, 0, 0),
                    .width = zmax - zmin,
                    .height = ymax - ymin,

                    .top_obscuring_pattern = 0,
                    .left_obscuring_pattern = 0,
                    .right_obscuring_pattern = 0,
                    .bottom_obscuring_pattern = 0,
                    .top_left_obscured = false,
                    .top_right_obscured = false,
                    .bottom_right_obscured = false,
                    .bottom_left_obscured = false,
                };
            },
            1 => {
                raw_quad = .{
                    .tile = surface,
                    .bottom_left = v3.new(yleft, zmin, xf + 0.5 * sign),
                    .top_left = v3.new(yleft, zmax, xf + 0.5 * sign),
                    .bottom_right = v3.new(yright, zmin, xf + 0.5 * sign),
                    .top_right = v3.new(yright, zmax, xf + 0.5 * sign),
                    .normal = v3.new(0, 0, sign),
                    .height = zmax - zmin,
                    .width = ymax - ymin,

                    .top_obscuring_pattern = 0,
                    .left_obscuring_pattern = 0,
                    .right_obscuring_pattern = 0,
                    .bottom_obscuring_pattern = 0,
                    .top_left_obscured = false,
                    .top_right_obscured = false,
                    .bottom_right_obscured = false,
                    .bottom_left_obscured = false,
                };
            },
            2 => {
                raw_quad = .{
                    .tile = surface,
                    .top_left = v3.new(zleft, xf + 0.5 * sign, ymin),
                    .top_right = v3.new(zright, xf + 0.5 * sign, ymin),
                    .bottom_left = v3.new(zleft, xf + 0.5 * sign, ymax),
                    .bottom_right = v3.new(zright, xf + 0.5 * sign, ymax),
                    .normal = v3.new(0, sign, 0),
                    .width = zmax - zmin,
                    .height = ymax - ymin,

                    .top_obscuring_pattern = 0xffffffff,
                    .left_obscuring_pattern = 0xffffffff,
                    .right_obscuring_pattern = 0xffffffff,
                    .bottom_obscuring_pattern = 0xffffffff,
                    .top_left_obscured = false,
                    .top_right_obscured = false,
                    .bottom_right_obscured = false,
                    .bottom_left_obscured = false,
                };
            },
            else => unreachable,
        }
        return raw_quad;
    }

    pub fn createMesh(chunk: Chunk, tile_rows: u32, tile_columns: u32) !raylib.Mesh {
        var raw_quads = try std.ArrayList(RawQuad).initCapacity(chunk.a7r, 4096);
        defer raw_quads.deinit();

        // Begin scanning the chunk for block surfaces to make raw quads.
        inline for (0..3) |d| { // For each of the 3 dimensions,
            for (0..32) |raw_x| {
                const x: u5 = @intCast(raw_x);
                var positive_tile_surfaces: [32][32]u32 = .{.{0} ** 32} ** 32;
                var negative_tile_surfaces: [32][32]u32 = .{.{0} ** 32} ** 32;
                for (0..32) |raw_y| for (0..32) |raw_z| {
                    const y: u5 = @intCast(raw_y);
                    const z: u5 = @intCast(raw_z);
                    const tile: u32 = chunk.getTileRawShifted(x, y, z, d);
                    if (tile == 0) continue;
                    if (x == 31 or chunk.getTileRawShifted(x + 1, y, z, d) == 0) positive_tile_surfaces[y][z] = tile;
                    if (x == 0 or chunk.getTileRawShifted(x - 1, y, z, d) == 0) negative_tile_surfaces[y][z] = tile;
                };
                const xf: f32 = @floatFromInt(raw_x);
                inline for (.{ -1, 1 }) |sign| {
                    var tile_surfaces = if (sign == 1) positive_tile_surfaces else negative_tile_surfaces;
                    for (0..32) |y| for (0..32) |z| {
                        const surface = tile_surfaces[y][z];
                        if (surface == 0) continue;
                        var y2 = y + 1;
                        var z2 = z + 1;
                        while (y2 <= 31 and tile_surfaces[y2][z] == surface) : (y2 += 1) {
                            tile_surfaces[y2][z] = 0;
                        }
                        zloop: while (z2 <= 31) : (z2 += 1) {
                            for (y..y2) |ytmp| if (tile_surfaces[ytmp][z2] != surface) break :zloop;
                            for (y..y2) |ytmp| tile_surfaces[ytmp][z2] = 0;
                        }
                        // todo: scan tiles around quad surface for ambient occlusion
                        y2 -= 1;
                        z2 -= 1;
                        tile_surfaces[y][z] = 0;
                        const raw_quad = pack_raw_quad(y, y2, z, z2, sign, d, surface, xf);
                        try raw_quads.append(raw_quad);
                    };
                }
            }
        }

        // Create OpenGL buffers
        const triangle_count: i32 = @as(i32, @intCast(raw_quads.items.len)) * 2;

        const arr_size: u32 = @as(u32, @intCast(triangle_count)) * 3 * @sizeOf(f32);

        const vertices: [*]f32 = @ptrCast(@alignCast(raylib.MemAlloc(arr_size * 3)));
        const texcoords: [*]f32 = @ptrCast(@alignCast(raylib.MemAlloc(arr_size * 2)));
        const texcoords2: [*]f32 = @ptrCast(@alignCast(raylib.MemAlloc(arr_size * 2)));
        const normals: [*]f32 = @ptrCast(@alignCast(raylib.MemAlloc(arr_size * 3)));
        const metadata1_packed: [*]f32 = @ptrCast(@alignCast(raylib.MemAlloc(arr_size * 4)));

        for (raw_quads.items, 0..) |raw_quad, i| {
            if (raw_quad.tile <= 0) continue; // air tile, no texture
            const tile = raw_quad.tile;

            // Set normals for the quads (same as the triangles.)
            for (0..6) |j| {
                normals[18 * i + 3 * j + 0] = raw_quad.normal.x;
                normals[18 * i + 3 * j + 1] = raw_quad.normal.y;
                normals[18 * i + 3 * j + 2] = raw_quad.normal.z;
            }

            // Find UV coordinates of corresponding tiles.
            const left_uv = @as(f32, @floatFromInt(tile % tile_columns)) / @as(f32, @floatFromInt(tile_columns));
            const right_uv = @as(f32, @floatFromInt(tile % tile_columns + 1)) / @as(f32, @floatFromInt(tile_columns));
            const top_uv = @as(f32, @floatFromInt(tile / tile_columns)) / @as(f32, @floatFromInt(tile_rows));
            const bottom_uv = @as(f32, @floatFromInt(tile / tile_columns + 1)) / @as(f32, @floatFromInt(tile_rows));

            // Unwrap raw quads vertex coordinates and UV coordinates into OpenGL buffers.
            const vertex_corners = .{ raw_quad.top_left, raw_quad.bottom_left, raw_quad.top_right, raw_quad.bottom_right, raw_quad.top_right, raw_quad.bottom_left };
            const texcoords_x = .{ left_uv, left_uv, right_uv, right_uv, right_uv, left_uv };
            const texcoords_y = .{ top_uv, bottom_uv } ** 3;
            const texcoords2_x = .{ 0.0, 0.0, raw_quad.width, raw_quad.width, raw_quad.width, 0.0 };
            const texcoords2_y = .{ 0.0, raw_quad.height } ** 3;

            inline for (0..6) |corner_id| {
                vertices[VERTICES_BLOCK_SIZE * i + corner_id * 3 + 0] = vertex_corners[corner_id].x;
                vertices[VERTICES_BLOCK_SIZE * i + corner_id * 3 + 1] = vertex_corners[corner_id].y;
                vertices[VERTICES_BLOCK_SIZE * i + corner_id * 3 + 2] = vertex_corners[corner_id].z;
                texcoords[TEXCOORDS_BLOCK_SIZE * i + corner_id * 2 + 0] = texcoords_x[corner_id];
                texcoords[TEXCOORDS_BLOCK_SIZE * i + corner_id * 2 + 1] = texcoords_y[corner_id];
                texcoords2[TEXCOORDS_BLOCK_SIZE * i + corner_id * 2 + 0] = texcoords2_x[corner_id];
                texcoords2[TEXCOORDS_BLOCK_SIZE * i + corner_id * 2 + 1] = texcoords2_y[corner_id];
            }

            // Store metadata into OpenGL buffers.
            for (0..3) |j| {
                const metadata1 = Metadata1{
                    .ambient_occlusion_1 = raw_quad.left_obscuring_pattern,
                    .ambient_occlusion_2 = raw_quad.right_obscuring_pattern,
                    .ambient_occlusion_corner1 = raw_quad.top_left_obscured,
                    .ambient_occlusion_corner2 = raw_quad.bottom_left_obscured,
                    .ambient_occlusion_corner3 = raw_quad.top_right_obscured,
                    .quad_height = @intFromFloat(raw_quad.height),
                    .quad_width = @intFromFloat(raw_quad.width),
                };
                const metadata1_baked: [4]f32 = @bitCast(metadata1);
                for (0..4) |k| {
                    metadata1_packed[24 * i + 4 * j + k] = @bitCast(@as(f32, metadata1_baked[k]));
                }
            }
            for (3..6) |j| {
                const metadata1 = Metadata1{
                    .ambient_occlusion_1 = raw_quad.right_obscuring_pattern,
                    .ambient_occlusion_2 = raw_quad.bottom_obscuring_pattern,
                    .ambient_occlusion_corner1 = raw_quad.bottom_right_obscured,
                    .ambient_occlusion_corner2 = raw_quad.top_right_obscured,
                    .ambient_occlusion_corner3 = raw_quad.bottom_left_obscured,
                    .quad_height = @intFromFloat(raw_quad.height),
                    .quad_width = @intFromFloat(raw_quad.width),
                };
                const metadata1_baked: [4]f32 = @bitCast(metadata1);
                for (0..4) |k| {
                    metadata1_packed[24 * i + 4 * j + k] = @bitCast(@as(f32, metadata1_baked[k]));
                }
            }
        }

        // Create mesh.
        var mesh = raylib.Mesh{
            .triangleCount = triangle_count,
            .vertexCount = triangle_count * 3,

            .vertices = vertices,
            .texcoords = texcoords,
            .texcoords2 = texcoords2,
            .normals = normals,
            .tangents = metadata1_packed,

            .vaoId = 0,
            .vboId = null,
        };

        raylib.UploadMesh(@ptrCast(&mesh), false);

        return mesh;
    }
};
