#include "pxl8_gen.h"

#include <math.h>
#include <stdlib.h>
#include <string.h>

#include "pxl8_log.h"
#include "pxl8_rng.h"

#define CELL_SIZE 64.0f
#define PVS_MAX_DEPTH 40
#define WALL_HEIGHT 128.0f

typedef struct room_grid {
    u8* cells;
    i32 width;
    i32 height;
} room_grid;

typedef struct bsp_build_context {
    pxl8_bsp* bsp;
    const room_grid* grid;
    u32 node_count;
    u32 plane_offset;
} bsp_build_context;

typedef struct portal {
    f32 x0, z0;
    f32 x1, z1;
    u32 target_leaf;
} portal;

typedef struct cell_portals {
    portal portals[4];
    u8 num_portals;
} cell_portals;

typedef struct vis_window {
    f32 left_x, left_z;
    f32 right_x, right_z;
} vis_window;

typedef struct light_source {
    pxl8_vec3 position;
    f32 intensity;
    f32 radius;
} light_source;

static bool room_grid_init(room_grid* grid, i32 width, i32 height) {
    grid->width = width;
    grid->height = height;
    grid->cells = calloc(width * height, sizeof(u8));

    return grid->cells != NULL;
}

static u8 room_grid_get(const room_grid* grid, i32 x, i32 y) {
    if (x < 0 || x >= grid->width || y < 0 || y >= grid->height) {
        return 1;
    }
    return grid->cells[y * grid->width + x];
}

static void room_grid_set(room_grid* grid, i32 x, i32 y, u8 value) {
    if (x < 0 || x >= grid->width || y < 0 || y >= grid->height) {
        return;
    }
    grid->cells[y * grid->width + x] = value;
}

static inline void compute_face_aabb(pxl8_bsp_face* face, const pxl8_bsp_vertex* verts, u32 vert_idx) {
    face->aabb_min = (pxl8_vec3){1e30f, 1e30f, 1e30f};
    face->aabb_max = (pxl8_vec3){-1e30f, -1e30f, -1e30f};

    for (u32 i = 0; i < 4; i++) {
        pxl8_vec3 v = verts[vert_idx + i].position;
        if (v.x < face->aabb_min.x) face->aabb_min.x = v.x;
        if (v.x > face->aabb_max.x) face->aabb_max.x = v.x;
        if (v.y < face->aabb_min.y) face->aabb_min.y = v.y;
        if (v.y > face->aabb_max.y) face->aabb_max.y = v.y;
        if (v.z < face->aabb_min.z) face->aabb_min.z = v.z;
        if (v.z > face->aabb_max.z) face->aabb_max.z = v.z;
    }
}

static void room_grid_fill(room_grid* grid, u8 value) {
    for (i32 y = 0; y < grid->height; y++) {
        for (i32 x = 0; x < grid->width; x++) {
            room_grid_set(grid, x, y, value);
        }
    }
}

static f32 compute_vertex_light(
    pxl8_vec3 pos,
    pxl8_vec3 normal,
    const light_source* lights,
    u32 num_lights,
    f32 ambient
) {
    f32 total = ambient;

    for (u32 i = 0; i < num_lights; i++) {
        pxl8_vec3 to_light = {
            lights[i].position.x - pos.x,
            lights[i].position.y - pos.y,
            lights[i].position.z - pos.z
        };

        f32 dist_sq = to_light.x * to_light.x + to_light.y * to_light.y + to_light.z * to_light.z;
        f32 dist = sqrtf(dist_sq);

        if (dist > lights[i].radius) continue;
        if (dist < 1.0f) dist = 1.0f;

        f32 inv_dist = 1.0f / dist;
        pxl8_vec3 light_dir = {
            to_light.x * inv_dist,
            to_light.y * inv_dist,
            to_light.z * inv_dist
        };

        f32 ndotl = normal.x * light_dir.x + normal.y * light_dir.y + normal.z * light_dir.z;
        if (ndotl < 0) ndotl = 0;

        f32 attenuation = 1.0f - (dist / lights[i].radius);
        if (attenuation < 0) attenuation = 0;
        attenuation *= attenuation;

        total += lights[i].intensity * ndotl * attenuation;
    }

    if (total > 1.0f) total = 1.0f;
    return total;
}

static void compute_bsp_vertex_lighting(
    pxl8_bsp* bsp,
    const light_source* lights,
    u32 num_lights,
    f32 ambient
) {
    if (!bsp || bsp->num_vertices == 0) return;

    bsp->vertex_lights = calloc(bsp->num_vertices, sizeof(u32));
    if (!bsp->vertex_lights) return;
    bsp->num_vertex_lights = bsp->num_vertices;

    for (u32 f = 0; f < bsp->num_faces; f++) {
        const pxl8_bsp_face* face = &bsp->faces[f];
        pxl8_vec3 normal = {0, 1, 0};

        if (face->plane_id < bsp->num_planes) {
            normal = bsp->planes[face->plane_id].normal;
        }

        for (u32 e = 0; e < face->num_edges; e++) {
            i32 surfedge_idx = face->first_edge + e;
            if (surfedge_idx >= (i32)bsp->num_surfedges) continue;

            i32 edge_idx = bsp->surfedges[surfedge_idx];
            u32 vert_idx;

            if (edge_idx >= 0) {
                if ((u32)edge_idx >= bsp->num_edges) continue;
                vert_idx = bsp->edges[edge_idx].vertex[0];
            } else {
                edge_idx = -edge_idx;
                if ((u32)edge_idx >= bsp->num_edges) continue;
                vert_idx = bsp->edges[edge_idx].vertex[1];
            }

            if (vert_idx >= bsp->num_vertices) continue;

            pxl8_vec3 pos = bsp->vertices[vert_idx].position;
            f32 light = compute_vertex_light(pos, normal, lights, num_lights, ambient);

            u8 light_byte = (u8)(light * 255.0f);
            bsp->vertex_lights[vert_idx] = ((u32)light_byte << 24) | 0x00FFFFFF;
        }
    }

    pxl8_debug("Computed vertex lighting: %u vertices, %u lights", bsp->num_vertices, num_lights);
}

static cell_portals* build_cell_portals(const room_grid* grid, f32 cell_size) {
    i32 total_cells = grid->width * grid->height;
    cell_portals* portals = calloc(total_cells, sizeof(cell_portals));
    if (!portals) return NULL;

    for (i32 y = 0; y < grid->height; y++) {
        for (i32 x = 0; x < grid->width; x++) {
            if (room_grid_get(grid, x, y) != 0) continue;

            i32 c = y * grid->width + x;
            f32 cx = x * cell_size;
            f32 cz = y * cell_size;

            if (x > 0 && room_grid_get(grid, x - 1, y) == 0) {
                portal* p = &portals[c].portals[portals[c].num_portals++];
                p->x0 = cx;
                p->z0 = cz;
                p->x1 = cx;
                p->z1 = cz + cell_size;
                p->target_leaf = y * grid->width + (x - 1);
            }
            if (x < grid->width - 1 && room_grid_get(grid, x + 1, y) == 0) {
                portal* p = &portals[c].portals[portals[c].num_portals++];
                p->x0 = cx + cell_size;
                p->z0 = cz + cell_size;
                p->x1 = cx + cell_size;
                p->z1 = cz;
                p->target_leaf = y * grid->width + (x + 1);
            }
            if (y > 0 && room_grid_get(grid, x, y - 1) == 0) {
                portal* p = &portals[c].portals[portals[c].num_portals++];
                p->x0 = cx + cell_size;
                p->z0 = cz;
                p->x1 = cx;
                p->z1 = cz;
                p->target_leaf = (y - 1) * grid->width + x;
            }
            if (y < grid->height - 1 && room_grid_get(grid, x, y + 1) == 0) {
                portal* p = &portals[c].portals[portals[c].num_portals++];
                p->x0 = cx;
                p->z0 = cz + cell_size;
                p->x1 = cx + cell_size;
                p->z1 = cz + cell_size;
                p->target_leaf = (y + 1) * grid->width + x;
            }
        }
    }
    return portals;
}

static inline f32 cross_2d(f32 ax, f32 az, f32 bx, f32 bz) {
    return ax * bz - az * bx;
}

static bool clip_window_through_portal(
    f32 src_x, f32 src_z,
    const vis_window* window,
    const portal* p,
    vis_window* clipped
) {
    f32 wl_x = window->left_x - src_x;
    f32 wl_z = window->left_z - src_z;
    f32 wr_x = window->right_x - src_x;
    f32 wr_z = window->right_z - src_z;

    f32 p0_x = p->x0 - src_x;
    f32 p0_z = p->z0 - src_z;
    f32 p1_x = p->x1 - src_x;
    f32 p1_z = p->z1 - src_z;

    f32 p0_vs_wl = cross_2d(p0_x, p0_z, wl_x, wl_z);
    f32 p1_vs_wl = cross_2d(p1_x, p1_z, wl_x, wl_z);
    f32 p0_vs_wr = cross_2d(p0_x, p0_z, wr_x, wr_z);
    f32 p1_vs_wr = cross_2d(p1_x, p1_z, wr_x, wr_z);

    if (p1_vs_wl <= 0.0f || p0_vs_wr >= 0.0f) {
        return false;
    }

    if (p0_vs_wl > 0.0f) {
        clipped->left_x = p->x0;
        clipped->left_z = p->z0;
    } else {
        clipped->left_x = window->left_x;
        clipped->left_z = window->left_z;
    }

    if (p1_vs_wr < 0.0f) {
        clipped->right_x = p->x1;
        clipped->right_z = p->z1;
    } else {
        clipped->right_x = window->right_x;
        clipped->right_z = window->right_z;
    }

    f32 cl_x = clipped->left_x - src_x;
    f32 cl_z = clipped->left_z - src_z;
    f32 cr_x = clipped->right_x - src_x;
    f32 cr_z = clipped->right_z - src_z;

    if (cross_2d(cl_x, cl_z, cr_x, cr_z) >= -0.0001f) {
        return false;
    }

    return true;
}

typedef struct portal_vis_context {
    u8* pvs;
    u8* visited;
    const cell_portals* portals;
    const pxl8_bsp_leaf* leafs;
    f32 src_x;
    f32 src_z;
    u32 num_leafs;
    u32 max_depth;
} portal_vis_context;

static void portal_flood_recursive(
    portal_vis_context* ctx,
    u32 current_leaf,
    const vis_window* window,
    u32 depth
) {
    if (depth > ctx->max_depth) return;

    u32 byte = current_leaf >> 3;
    u32 bit = current_leaf & 7;

    if (ctx->visited[byte] & (1 << bit)) return;
    ctx->visited[byte] |= (1 << bit);

    if (ctx->leafs[current_leaf].contents == -1) return;

    ctx->pvs[byte] |= (1 << bit);

    const cell_portals* cp = &ctx->portals[current_leaf];
    for (u8 i = 0; i < cp->num_portals; i++) {
        const portal* p = &cp->portals[i];

        u32 target_byte = p->target_leaf >> 3;
        u32 target_bit = p->target_leaf & 7;
        if (ctx->visited[target_byte] & (1 << target_bit)) continue;

        vis_window clipped;
        if (clip_window_through_portal(ctx->src_x, ctx->src_z, window, p, &clipped)) {
            portal_flood_recursive(ctx, p->target_leaf, &clipped, depth + 1);
        }
    }
}

typedef struct vis_entry {
    u32 leaf;
    vis_window window;
} vis_entry;

static void portal_flood_bfs(
    u32 start_leaf,
    const cell_portals* portals,
    const pxl8_bsp_leaf* leafs,
    u8* pvs,
    u32 num_leafs,
    f32 cell_size,
    i32 grid_width
) {
    u32 pvs_bytes = (num_leafs + 7) / 8;
    u8* visited = calloc(pvs_bytes, 1);
    vis_entry* queue = malloc(num_leafs * 4 * sizeof(vis_entry));
    if (!visited || !queue) {
        free(visited);
        free(queue);
        return;
    }

    i32 start_x = start_leaf % grid_width;
    i32 start_y = start_leaf / grid_width;
    f32 src_x = (start_x + 0.5f) * cell_size;
    f32 src_z = (start_y + 0.5f) * cell_size;

    u32 head = 0, tail = 0;

    pvs[start_leaf >> 3] |= (1 << (start_leaf & 7));
    visited[start_leaf >> 3] |= (1 << (start_leaf & 7));

    const cell_portals* start_cp = &portals[start_leaf];
    for (u8 i = 0; i < start_cp->num_portals; i++) {
        const portal* p = &start_cp->portals[i];
        vis_window initial = {p->x0, p->z0, p->x1, p->z1};
        queue[tail++] = (vis_entry){p->target_leaf, initial};
    }

    while (head < tail) {
        vis_entry e = queue[head++];

        if (leafs[e.leaf].contents == -1) continue;

        u32 byte = e.leaf >> 3;
        u32 bit = e.leaf & 7;

        pvs[byte] |= (1 << bit);

        if (visited[byte] & (1 << bit)) continue;
        visited[byte] |= (1 << bit);

        const cell_portals* cp = &portals[e.leaf];
        for (u8 i = 0; i < cp->num_portals; i++) {
            const portal* p = &cp->portals[i];

            vis_window clipped;
            if (clip_window_through_portal(src_x, src_z, &e.window, p, &clipped)) {
                queue[tail++] = (vis_entry){p->target_leaf, clipped};
            }
        }
    }

    free(visited);
    free(queue);
}

static u8* compute_leaf_pvs(u32 start_leaf, const cell_portals* portals,
                            u32 num_leafs, const pxl8_bsp_leaf* leafs,
                            const room_grid* grid, f32 cell_size) {
    u32 pvs_bytes = (num_leafs + 7) / 8;
    u8* pvs = calloc(pvs_bytes, 1);
    if (!pvs) return NULL;

    portal_flood_bfs(start_leaf, portals, leafs, pvs, num_leafs, cell_size, grid->width);

    return pvs;
}

static u32 rle_compress_pvs(const u8* pvs, u32 pvs_bytes, u8* out) {
    u32 out_pos = 0;
    u32 i = 0;

    while (i < pvs_bytes) {
        if (pvs[i] != 0) {
            out[out_pos++] = pvs[i++];
        } else {
            u32 count = 0;
            while (i < pvs_bytes && pvs[i] == 0 && count < 255) {
                count++;
                i++;
            }
            out[out_pos++] = 0;
            out[out_pos++] = (u8)count;
        }
    }
    return out_pos;
}

static pxl8_result build_pvs_data(pxl8_bsp* bsp, const cell_portals* portals,
                                  const room_grid* grid, f32 cell_size) {
    u32 num_leafs = bsp->num_leafs;
    u32 pvs_bytes = (num_leafs + 7) / 8;

    u32 max_visdata = num_leafs * pvs_bytes * 2;
    u8* visdata = malloc(max_visdata);
    if (!visdata) return PXL8_ERROR_OUT_OF_MEMORY;

    u32 visdata_pos = 0;

    u8* compressed = malloc(pvs_bytes * 2);
    if (!compressed) {
        free(visdata);
        return PXL8_ERROR_OUT_OF_MEMORY;
    }

    u32 debug_count = 0;
    for (u32 leaf = 0; leaf < num_leafs; leaf++) {
        if (bsp->leafs[leaf].contents == -1) {
            bsp->leafs[leaf].visofs = -1;
            continue;
        }

        u8* pvs = compute_leaf_pvs(leaf, portals, num_leafs, bsp->leafs, grid, cell_size);
        if (!pvs) {
            free(compressed);
            free(visdata);
            return PXL8_ERROR_OUT_OF_MEMORY;
        }

        if (debug_count < 3) {
            u32 visible = 0;
            for (u32 b = 0; b < pvs_bytes; b++) {
                for (u32 i = 0; i < 8; i++) {
                    if (pvs[b] & (1 << i)) visible++;
                }
            }
            pxl8_debug("Leaf %u PVS: %u cells visible (portals: %u)", leaf, visible, portals[leaf].num_portals);
            debug_count++;
        }

        u32 compressed_size = rle_compress_pvs(pvs, pvs_bytes, compressed);

        bsp->leafs[leaf].visofs = visdata_pos;
        memcpy(visdata + visdata_pos, compressed, compressed_size);
        visdata_pos += compressed_size;

        free(pvs);
    }

    free(compressed);
    bsp->visdata = realloc(visdata, visdata_pos > 0 ? visdata_pos : 1);
    bsp->visdata_size = visdata_pos;

    pxl8_debug("Built PVS: %u leafs, %u bytes visdata", num_leafs, visdata_pos);
    return PXL8_OK;
}

static i32 build_bsp_node(bsp_build_context* ctx, i32 x0, i32 y0, i32 x1, i32 y1, i32 depth) {
    if (x1 - x0 == 1 && y1 - y0 == 1) {
        i32 leaf_idx = y0 * ctx->grid->width + x0;
        return -(leaf_idx + 1);
    }

    i32 node_idx = ctx->node_count++;
    pxl8_bsp_node* node = &ctx->bsp->nodes[node_idx];

    i32 plane_idx = ctx->plane_offset++;
    pxl8_bsp_plane* plane = &ctx->bsp->planes[plane_idx];
    node->plane_id = plane_idx;

    if (depth % 2 == 0) {
        i32 mid_x = (x0 + x1) / 2;
        f32 split_pos = mid_x * CELL_SIZE;

        plane->normal = (pxl8_vec3){1, 0, 0};
        plane->dist = split_pos;

        node->children[0] = build_bsp_node(ctx, mid_x, y0, x1, y1, depth + 1);
        node->children[1] = build_bsp_node(ctx, x0, y0, mid_x, y1, depth + 1);
    } else {
        i32 mid_y = (y0 + y1) / 2;
        f32 split_pos = mid_y * CELL_SIZE;

        plane->normal = (pxl8_vec3){0, 0, 1};
        plane->dist = split_pos;

        node->children[0] = build_bsp_node(ctx, x0, mid_y, x1, y1, depth + 1);
        node->children[1] = build_bsp_node(ctx, x0, y0, x1, mid_y, depth + 1);
    }

    return node_idx;
}

static pxl8_result grid_to_bsp(pxl8_bsp* bsp, const room_grid* grid) {
    i32 vertex_count = 0;
    i32 face_count = 0;
    i32 floor_ceiling_count = 0;

    for (i32 y = 0; y < grid->height; y++) {
        for (i32 x = 0; x < grid->width; x++) {
            if (room_grid_get(grid, x, y) == 0) {
                if (room_grid_get(grid, x - 1, y) == 1) face_count++;
                if (room_grid_get(grid, x + 1, y) == 1) face_count++;
                if (room_grid_get(grid, x, y - 1) == 1) face_count++;
                if (room_grid_get(grid, x, y + 1) == 1) face_count++;
                floor_ceiling_count++;
            }
        }
    }

    face_count += floor_ceiling_count;
    vertex_count = face_count * 4;

    pxl8_debug("Level generation: %dx%d grid -> %d faces, %d vertices",
               grid->width, grid->height, face_count, vertex_count);

    i32 total_cells = grid->width * grid->height;
    u32 max_nodes = 2 * total_cells;
    u32 total_planes = face_count + max_nodes;

    bsp->vertices = calloc(vertex_count, sizeof(pxl8_bsp_vertex));
    bsp->faces = calloc(face_count, sizeof(pxl8_bsp_face));
    bsp->planes = calloc(total_planes, sizeof(pxl8_bsp_plane));
    bsp->edges = calloc(vertex_count, sizeof(pxl8_bsp_edge));
    bsp->surfedges = calloc(vertex_count, sizeof(i32));
    bsp->nodes = calloc(max_nodes, sizeof(pxl8_bsp_node));

    u32* face_cell = calloc(face_count, sizeof(u32));

    if (!bsp->vertices || !bsp->faces || !bsp->planes || !bsp->edges ||
        !bsp->surfedges || !bsp->nodes || !face_cell) {
        free(face_cell);
        return PXL8_ERROR_OUT_OF_MEMORY;
    }

    bsp->texinfo = NULL;
    bsp->num_texinfo = 0;

    i32 vert_idx = 0;
    i32 face_idx = 0;
    i32 edge_idx = 0;

    const f32 cell_size = CELL_SIZE;
    const f32 wall_height = WALL_HEIGHT;

    for (i32 y = 0; y < grid->height; y++) {
        for (i32 x = 0; x < grid->width; x++) {
            if (room_grid_get(grid, x, y) == 0) {
                f32 fx = (f32)x * cell_size;
                f32 fy = (f32)y * cell_size;
                i32 cell_idx = y * grid->width + x;

                if (room_grid_get(grid, x - 1, y) == 1) {
                    bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx, 0, fy};
                    bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx, wall_height, fy};
                    bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx, wall_height, fy + cell_size};
                    bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx, 0, fy + cell_size};

                    bsp->planes[face_idx].normal = (pxl8_vec3){1, 0, 0};
                    bsp->planes[face_idx].dist = fx;

                    bsp->faces[face_idx].plane_id = face_idx;
                    bsp->faces[face_idx].num_edges = 4;
                    bsp->faces[face_idx].first_edge = edge_idx;
                    bsp->faces[face_idx].texinfo_id = 0;

                    for (i32 i = 0; i < 4; i++) {
                        bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
                        bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
                        bsp->surfedges[edge_idx + i] = edge_idx + i;
                    }

                    compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
                    face_cell[face_idx] = cell_idx;

                    vert_idx += 4;
                    edge_idx += 4;
                    face_idx++;
                }

                if (room_grid_get(grid, x + 1, y) == 1) {
                    bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx + cell_size, 0, fy};
                    bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx + cell_size, 0, fy + cell_size};
                    bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx + cell_size, wall_height, fy + cell_size};
                    bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx + cell_size, wall_height, fy};

                    bsp->planes[face_idx].normal = (pxl8_vec3){-1, 0, 0};
                    bsp->planes[face_idx].dist = -(fx + cell_size);

                    bsp->faces[face_idx].plane_id = face_idx;
                    bsp->faces[face_idx].num_edges = 4;
                    bsp->faces[face_idx].first_edge = edge_idx;
                    bsp->faces[face_idx].texinfo_id = 0;

                    for (i32 i = 0; i < 4; i++) {
                        bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
                        bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
                        bsp->surfedges[edge_idx + i] = edge_idx + i;
                    }

                    compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
                    face_cell[face_idx] = cell_idx;

                    vert_idx += 4;
                    edge_idx += 4;
                    face_idx++;
                }

                if (room_grid_get(grid, x, y - 1) == 1) {
                    bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx, 0, fy};
                    bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx + cell_size, 0, fy};
                    bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx + cell_size, wall_height, fy};
                    bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx, wall_height, fy};

                    bsp->planes[face_idx].normal = (pxl8_vec3){0, 0, 1};
                    bsp->planes[face_idx].dist = fy;

                    bsp->faces[face_idx].plane_id = face_idx;
                    bsp->faces[face_idx].num_edges = 4;
                    bsp->faces[face_idx].first_edge = edge_idx;
                    bsp->faces[face_idx].texinfo_id = 0;

                    for (i32 i = 0; i < 4; i++) {
                        bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
                        bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
                        bsp->surfedges[edge_idx + i] = edge_idx + i;
                    }

                    compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
                    face_cell[face_idx] = cell_idx;

                    vert_idx += 4;
                    edge_idx += 4;
                    face_idx++;
                }

                if (room_grid_get(grid, x, y + 1) == 1) {
                    bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx, 0, fy + cell_size};
                    bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx, wall_height, fy + cell_size};
                    bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx + cell_size, wall_height, fy + cell_size};
                    bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx + cell_size, 0, fy + cell_size};

                    bsp->planes[face_idx].normal = (pxl8_vec3){0, 0, -1};
                    bsp->planes[face_idx].dist = -(fy + cell_size);

                    bsp->faces[face_idx].plane_id = face_idx;
                    bsp->faces[face_idx].num_edges = 4;
                    bsp->faces[face_idx].first_edge = edge_idx;
                    bsp->faces[face_idx].texinfo_id = 0;

                    for (i32 i = 0; i < 4; i++) {
                        bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
                        bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
                        bsp->surfedges[edge_idx + i] = edge_idx + i;
                    }

                    compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
                    face_cell[face_idx] = cell_idx;

                    vert_idx += 4;
                    edge_idx += 4;
                    face_idx++;
                }
            }
        }
    }

    for (i32 y = 0; y < grid->height; y++) {
        for (i32 x = 0; x < grid->width; x++) {
            if (room_grid_get(grid, x, y) == 0) {
                f32 fx = (f32)x * cell_size;
                f32 fy = (f32)y * cell_size;
                i32 cell_idx = y * grid->width + x;

                bsp->vertices[vert_idx + 0].position = (pxl8_vec3){fx, 0, fy};
                bsp->vertices[vert_idx + 1].position = (pxl8_vec3){fx, 0, fy + cell_size};
                bsp->vertices[vert_idx + 2].position = (pxl8_vec3){fx + cell_size, 0, fy + cell_size};
                bsp->vertices[vert_idx + 3].position = (pxl8_vec3){fx + cell_size, 0, fy};

                bsp->planes[face_idx].normal = (pxl8_vec3){0, 1, 0};
                bsp->planes[face_idx].dist = 0;

                bsp->faces[face_idx].plane_id = face_idx;
                bsp->faces[face_idx].num_edges = 4;
                bsp->faces[face_idx].first_edge = edge_idx;
                bsp->faces[face_idx].texinfo_id = 0;

                for (i32 i = 0; i < 4; i++) {
                    bsp->edges[edge_idx + i].vertex[0] = vert_idx + i;
                    bsp->edges[edge_idx + i].vertex[1] = vert_idx + ((i + 1) % 4);
                    bsp->surfedges[edge_idx + i] = edge_idx + i;
                }

                compute_face_aabb(&bsp->faces[face_idx], bsp->vertices, vert_idx);
                face_cell[face_idx] = cell_idx;

                vert_idx += 4;
                edge_idx += 4;
                face_idx++;
            }
        }
    }

    bsp->num_vertices = vertex_count;
    bsp->num_faces = face_count;
    bsp->num_edges = vertex_count;
    bsp->num_surfedges = vertex_count;

    bsp->leafs = calloc(total_cells, sizeof(pxl8_bsp_leaf));
    bsp->marksurfaces = calloc(face_count, sizeof(u16));

    if (!bsp->leafs || !bsp->marksurfaces) {
        free(face_cell);
        return PXL8_ERROR_OUT_OF_MEMORY;
    }

    bsp->num_leafs = total_cells;
    bsp->num_marksurfaces = face_count;

    u32* faces_per_cell = calloc(total_cells, sizeof(u32));
    u32* cell_offset = calloc(total_cells, sizeof(u32));
    u32* cell_cursor = calloc(total_cells, sizeof(u32));

    if (!faces_per_cell || !cell_offset || !cell_cursor) {
        free(faces_per_cell);
        free(cell_offset);
        free(cell_cursor);
        free(face_cell);
        return PXL8_ERROR_OUT_OF_MEMORY;
    }

    for (i32 i = 0; i < face_count; i++) {
        faces_per_cell[face_cell[i]]++;
    }

    u32 offset = 0;
    for (i32 c = 0; c < total_cells; c++) {
        cell_offset[c] = offset;
        offset += faces_per_cell[c];
    }

    for (i32 i = 0; i < face_count; i++) {
        u32 c = face_cell[i];
        bsp->marksurfaces[cell_offset[c] + cell_cursor[c]++] = i;
    }

    for (i32 y = 0; y < grid->height; y++) {
        for (i32 x = 0; x < grid->width; x++) {
            i32 c = y * grid->width + x;
            pxl8_bsp_leaf* leaf = &bsp->leafs[c];

            f32 fx = (f32)x * cell_size;
            f32 fz = (f32)y * cell_size;

            leaf->mins[0] = (i16)fx;
            leaf->mins[1] = 0;
            leaf->mins[2] = (i16)fz;
            leaf->maxs[0] = (i16)(fx + cell_size);
            leaf->maxs[1] = (i16)wall_height;
            leaf->maxs[2] = (i16)(fz + cell_size);

            if (room_grid_get(grid, x, y) == 0) {
                leaf->contents = -2;
                leaf->first_marksurface = cell_offset[c];
                leaf->num_marksurfaces = faces_per_cell[c];
            } else {
                leaf->contents = -1;
                leaf->first_marksurface = 0;
                leaf->num_marksurfaces = 0;
            }
            leaf->visofs = -1;
        }
    }

    free(faces_per_cell);
    free(cell_offset);
    free(cell_cursor);
    free(face_cell);

    bsp_build_context ctx = {
        .bsp = bsp,
        .grid = grid,
        .node_count = 0,
        .plane_offset = face_count,
    };

    build_bsp_node(&ctx, 0, 0, grid->width, grid->height, 0);
    bsp->num_nodes = ctx.node_count;
    bsp->num_planes = ctx.plane_offset;

    pxl8_debug("Built BSP tree: %u nodes, %u leafs, %u planes",
               bsp->num_nodes, bsp->num_leafs, bsp->num_planes);

    cell_portals* portals = build_cell_portals(grid, cell_size);
    if (!portals) {
        return PXL8_ERROR_OUT_OF_MEMORY;
    }

    u32 walkable_cells = 0;
    u32 total_portals = 0;
    i32 first_walkable = -1;
    for (i32 c = 0; c < total_cells; c++) {
        if (bsp->leafs[c].contents == -2) {
            walkable_cells++;
            if (first_walkable < 0) first_walkable = c;
        }
        total_portals += portals[c].num_portals;
    }
    pxl8_debug("Portal stats: %u walkable cells, %u total portals (avg %.1f per cell)",
               walkable_cells, total_portals, (f32)total_portals / walkable_cells);

    if (first_walkable >= 0) {
        u32 pvs_bytes = (total_cells + 7) / 8;
        u8* visited = calloc(pvs_bytes, 1);
        u8* queue = malloc(total_cells * sizeof(u32));
        u32 head = 0, tail = 0;
        ((u32*)queue)[tail++] = first_walkable;
        visited[first_walkable >> 3] |= (1 << (first_walkable & 7));
        u32 reachable = 0;
        while (head < tail) {
            u32 c = ((u32*)queue)[head++];
            reachable++;
            for (u8 i = 0; i < portals[c].num_portals; i++) {
                u32 n = portals[c].portals[i].target_leaf;
                u32 nb = n >> 3, ni = n & 7;
                if (!(visited[nb] & (1 << ni))) {
                    visited[nb] |= (1 << ni);
                    ((u32*)queue)[tail++] = n;
                }
            }
        }
        pxl8_debug("Connectivity: %u/%u walkable cells reachable from leaf %d",
                   reachable, walkable_cells, first_walkable);
        free(visited);
        free(queue);
    }

    pxl8_result pvs_result = build_pvs_data(bsp, portals, grid, cell_size);
    free(portals);

    if (pvs_result != PXL8_OK) {
        return pvs_result;
    }

    return PXL8_OK;
}

static bool bounds_intersects(const pxl8_bounds* a, const pxl8_bounds* b) {
    return !(a->x + a->w <= b->x || b->x + b->w <= a->x ||
             a->y + a->h <= b->y || b->y + b->h <= a->y);
}

static void carve_corridor_h(room_grid* grid, i32 x1, i32 x2, i32 y) {
    i32 start = (x1 < x2) ? x1 : x2;
    i32 end = (x1 > x2) ? x1 : x2;
    for (i32 x = start; x <= end; x++) {
        room_grid_set(grid, x, y, 0);
        room_grid_set(grid, x, y - 1, 0);
        room_grid_set(grid, x, y + 1, 0);
    }
}

static void carve_corridor_v(room_grid* grid, i32 y1, i32 y2, i32 x) {
    i32 start = (y1 < y2) ? y1 : y2;
    i32 end = (y1 > y2) ? y1 : y2;
    for (i32 y = start; y <= end; y++) {
        room_grid_set(grid, x, y, 0);
        room_grid_set(grid, x - 1, y, 0);
        room_grid_set(grid, x + 1, y, 0);
    }
}

static pxl8_result procgen_rooms(pxl8_bsp* bsp, const pxl8_procgen_params* params) {
    pxl8_debug("procgen_rooms called: %dx%d, seed=%u, min=%d, max=%d, num=%d",
               params->width, params->height, params->seed,
               params->min_room_size, params->max_room_size, params->num_rooms);

    pxl8_rng rng;
    pxl8_rng_seed(&rng, params->seed);

    room_grid grid;
    if (!room_grid_init(&grid, params->width, params->height)) {
        pxl8_error("Failed to allocate room grid");
        return PXL8_ERROR_OUT_OF_MEMORY;
    }

    room_grid_fill(&grid, 1);

    pxl8_bounds rooms[256];
    i32 room_count = 0;
    i32 max_attempts = params->num_rooms * 10;

    const f32 cell_size = CELL_SIZE;
    const f32 light_height = 80.0f;

    for (i32 attempt = 0; attempt < max_attempts && room_count < params->num_rooms && room_count < 256; attempt++) {
        i32 w = params->min_room_size + (pxl8_rng_next(&rng) % (params->max_room_size - params->min_room_size + 1));
        i32 h = params->min_room_size + (pxl8_rng_next(&rng) % (params->max_room_size - params->min_room_size + 1));
        i32 x = 1 + (pxl8_rng_next(&rng) % (params->width - w - 2));
        i32 y = 1 + (pxl8_rng_next(&rng) % (params->height - h - 2));

        pxl8_bounds new_room = {x, y, w, h};

        bool overlaps = false;
        for (i32 i = 0; i < room_count; i++) {
            if (bounds_intersects(&new_room, &rooms[i])) {
                overlaps = true;
                break;
            }
        }

        if (!overlaps) {
            for (i32 ry = y; ry < y + h; ry++) {
                for (i32 rx = x; rx < x + w; rx++) {
                    room_grid_set(&grid, rx, ry, 0);
                }
            }

            if (room_count > 0) {
                i32 new_cx = x + w / 2;
                i32 new_cy = y + h / 2;
                i32 prev_cx = rooms[room_count - 1].x + rooms[room_count - 1].w / 2;
                i32 prev_cy = rooms[room_count - 1].y + rooms[room_count - 1].h / 2;

                if (pxl8_rng_next(&rng) % 2 == 0) {
                    carve_corridor_h(&grid, prev_cx, new_cx, prev_cy);
                    carve_corridor_v(&grid, prev_cy, new_cy, new_cx);
                } else {
                    carve_corridor_v(&grid, prev_cy, new_cy, prev_cx);
                    carve_corridor_h(&grid, prev_cx, new_cx, new_cy);
                }
            }

            rooms[room_count++] = new_room;
        }
    }

    pxl8_debug("Room generation: %dx%d grid -> %d rooms created",
               params->width, params->height, room_count);

    pxl8_result result = grid_to_bsp(bsp, &grid);
    free(grid.cells);

    if (result != PXL8_OK) {
        return result;
    }

    light_source lights[256];
    u32 num_lights = 0;

    for (i32 i = 0; i < room_count && num_lights < 256; i++) {
        f32 cx = (rooms[i].x + rooms[i].w / 2.0f) * cell_size;
        f32 cy = (rooms[i].y + rooms[i].h / 2.0f) * cell_size;

        lights[num_lights++] = (light_source){
            .position = {cx, light_height, cy},
            .intensity = 0.8f,
            .radius = 300.0f,
        };
    }

    compute_bsp_vertex_lighting(bsp, lights, num_lights, 0.1f);

    return result;
}

pxl8_result pxl8_procgen(pxl8_bsp* bsp, const pxl8_procgen_params* params) {
    if (!bsp || !params) {
        return PXL8_ERROR_NULL_POINTER;
    }

    switch (params->type) {
        case PXL8_PROCGEN_ROOMS:
            return procgen_rooms(bsp, params);

        case PXL8_PROCGEN_TERRAIN:
            pxl8_error("Terrain generation not yet implemented");
            return PXL8_ERROR_NOT_INITIALIZED;

        default:
            pxl8_error("Unknown procgen type: %d", params->type);
            return PXL8_ERROR_INVALID_ARGUMENT;
    }
}
