#include "pxl8_bsp.h"

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

#include "pxl8_color.h"
#include "pxl8_gfx.h"
#include "pxl8_io.h"
#include "pxl8_log.h"

#define BSP_VERSION 29

typedef enum {
    CHUNK_ENTITIES = 0,
    CHUNK_PLANES = 1,
    CHUNK_TEXTURES = 2,
    CHUNK_VERTICES = 3,
    CHUNK_VISIBILITY = 4,
    CHUNK_NODES = 5,
    CHUNK_TEXINFO = 6,
    CHUNK_FACES = 7,
    CHUNK_LIGHTING = 8,
    CHUNK_CLIPNODES = 9,
    CHUNK_LEAFS = 10,
    CHUNK_MARKSURFACES = 11,
    CHUNK_EDGES = 12,
    CHUNK_SURFEDGES = 13,
    CHUNK_MODELS = 14,
    CHUNK_COUNT = 15
} pxl8_bsp_chunk_type;

typedef struct {
    u32 offset;
    u32 size;
} pxl8_bsp_chunk;

typedef struct {
    u32 version;
    pxl8_bsp_chunk chunks[CHUNK_COUNT];
} pxl8_bsp_header;

static inline pxl8_vec3 read_vec3(pxl8_stream* stream) {
    pxl8_vec3 v;
    v.x = pxl8_read_f32(stream);
    v.y = pxl8_read_f32(stream);
    v.z = pxl8_read_f32(stream);
    return v;
}

static bool validate_chunk(const pxl8_bsp_chunk* chunk, u32 element_size, size_t file_size) {
    if (chunk->size == 0) return true;
    if (chunk->offset >= file_size) return false;
    if (chunk->offset + chunk->size > file_size) return false;
    if (chunk->size % element_size != 0) return false;
    return true;
}

static inline bool pxl8_bsp_get_edge_vertex(const pxl8_bsp* bsp, i32 surfedge_idx, u32* out_vert_idx) {
    if (surfedge_idx >= (i32)bsp->num_surfedges) return false;

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

    if (edge_idx >= 0) {
        if ((u32)edge_idx >= bsp->num_edges) return false;
        vertex_index = 0;
    } else {
        edge_idx = -edge_idx;
        if ((u32)edge_idx >= bsp->num_edges) return false;
        vertex_index = 1;
    }

    *out_vert_idx = bsp->edges[edge_idx].vertex[vertex_index];
    return *out_vert_idx < bsp->num_vertices;
}

static inline bool pxl8_bsp_get_edge_vertices(const pxl8_bsp* bsp, i32 surfedge_idx, u32* out_v0_idx, u32* out_v1_idx) {
    if (surfedge_idx >= (i32)bsp->num_surfedges) return false;

    i32 edge_idx = bsp->surfedges[surfedge_idx];

    if (edge_idx >= 0) {
        if ((u32)edge_idx >= bsp->num_edges) return false;
        *out_v0_idx = bsp->edges[edge_idx].vertex[0];
        *out_v1_idx = bsp->edges[edge_idx].vertex[1];
    } else {
        edge_idx = -edge_idx;
        if ((u32)edge_idx >= bsp->num_edges) return false;
        *out_v0_idx = bsp->edges[edge_idx].vertex[1];
        *out_v1_idx = bsp->edges[edge_idx].vertex[0];
    }

    return *out_v0_idx < bsp->num_vertices && *out_v1_idx < bsp->num_vertices;
}

pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
    if (!path || !bsp) return PXL8_ERROR_INVALID_ARGUMENT;

    memset(bsp, 0, sizeof(*bsp));

    u8* file_data = NULL;
    size_t file_size = 0;
    pxl8_result result = pxl8_io_read_binary_file(path, &file_data, &file_size);
    if (result != PXL8_OK) {
        pxl8_error("Failed to load BSP file: %s", path);
        return result;
    }

    if (file_size < sizeof(pxl8_bsp_header)) {
        pxl8_error("BSP file too small: %s", path);
        free(file_data);
        return PXL8_ERROR_INVALID_FORMAT;
    }

    pxl8_stream stream = pxl8_stream_create(file_data, (u32)file_size);

    pxl8_bsp_header header;
    header.version = pxl8_read_u32(&stream);

    if (header.version != BSP_VERSION) {
        pxl8_error("Invalid BSP version: %u (expected %d)", header.version, BSP_VERSION);
        free(file_data);
        return PXL8_ERROR_INVALID_FORMAT;
    }

    for (i32 i = 0; i < CHUNK_COUNT; i++) {
        header.chunks[i].offset = pxl8_read_u32(&stream);
        header.chunks[i].size = pxl8_read_u32(&stream);
    }

    pxl8_bsp_chunk* chunk = &header.chunks[CHUNK_VERTICES];
    if (!validate_chunk(chunk, 12, file_size)) goto error_cleanup;
    bsp->num_vertices = chunk->size / 12;
    if (bsp->num_vertices > 0) {
        bsp->vertices = calloc(bsp->num_vertices, sizeof(pxl8_bsp_vertex));
        if (!bsp->vertices) goto error_cleanup;
        pxl8_stream_seek(&stream, chunk->offset);
        for (u32 i = 0; i < bsp->num_vertices; i++) {
            bsp->vertices[i].position = read_vec3(&stream);
        }
    }

    chunk = &header.chunks[CHUNK_EDGES];
    if (!validate_chunk(chunk, 4, file_size)) goto error_cleanup;
    bsp->num_edges = chunk->size / 4;
    if (bsp->num_edges > 0) {
        bsp->edges = calloc(bsp->num_edges, sizeof(pxl8_bsp_edge));
        pxl8_stream_seek(&stream, chunk->offset);
        for (u32 i = 0; i < bsp->num_edges; i++) {
            bsp->edges[i].vertex[0] = pxl8_read_u16(&stream);
            bsp->edges[i].vertex[1] = pxl8_read_u16(&stream);
        }
    }

    chunk = &header.chunks[CHUNK_SURFEDGES];
    if (!validate_chunk(chunk, 4, file_size)) goto error_cleanup;
    bsp->num_surfedges = chunk->size / 4;
    if (bsp->num_surfedges > 0) {
        bsp->surfedges = calloc(bsp->num_surfedges, sizeof(i32));
        pxl8_stream_seek(&stream, chunk->offset);
        for (u32 i = 0; i < bsp->num_surfedges; i++) {
            bsp->surfedges[i] = pxl8_read_i32(&stream);
        }
    }

    chunk = &header.chunks[CHUNK_PLANES];
    if (!validate_chunk(chunk, 20, file_size)) goto error_cleanup;
    bsp->num_planes = chunk->size / 20;
    if (bsp->num_planes > 0) {
        bsp->planes = calloc(bsp->num_planes, sizeof(pxl8_bsp_plane));
        pxl8_stream_seek(&stream, chunk->offset);
        for (u32 i = 0; i < bsp->num_planes; i++) {
            bsp->planes[i].normal = read_vec3(&stream);
            bsp->planes[i].dist = pxl8_read_f32(&stream);
            bsp->planes[i].type = pxl8_read_i32(&stream);
        }
    }

    chunk = &header.chunks[CHUNK_TEXINFO];
    if (!validate_chunk(chunk, 40, file_size)) goto error_cleanup;
    bsp->num_texinfo = chunk->size / 40;
    if (bsp->num_texinfo > 0) {
        bsp->texinfo = calloc(bsp->num_texinfo, sizeof(pxl8_bsp_texinfo));
        pxl8_stream_seek(&stream, chunk->offset);
        for (u32 i = 0; i < bsp->num_texinfo; i++) {
            bsp->texinfo[i].u_axis = read_vec3(&stream);
            bsp->texinfo[i].u_offset = pxl8_read_f32(&stream);
            bsp->texinfo[i].v_axis = read_vec3(&stream);
            bsp->texinfo[i].v_offset = pxl8_read_f32(&stream);
            bsp->texinfo[i].miptex = pxl8_read_u32(&stream);
        }
    }

    chunk = &header.chunks[CHUNK_FACES];
    if (!validate_chunk(chunk, 20, file_size)) goto error_cleanup;
    bsp->num_faces = chunk->size / 20;
    if (bsp->num_faces > 0) {
        bsp->faces = calloc(bsp->num_faces, sizeof(pxl8_bsp_face));
        pxl8_stream_seek(&stream, chunk->offset);
        for (u32 i = 0; i < bsp->num_faces; i++) {
            bsp->faces[i].plane_id = pxl8_read_u16(&stream);
            bsp->faces[i].side = pxl8_read_u16(&stream);
            bsp->faces[i].first_edge = pxl8_read_u32(&stream);
            bsp->faces[i].num_edges = pxl8_read_u16(&stream);
            bsp->faces[i].texinfo_id = pxl8_read_u16(&stream);
            bsp->faces[i].styles[0] = pxl8_read_u8(&stream);
            bsp->faces[i].styles[1] = pxl8_read_u8(&stream);
            bsp->faces[i].styles[2] = pxl8_read_u8(&stream);
            bsp->faces[i].styles[3] = pxl8_read_u8(&stream);
            bsp->faces[i].lightmap_offset = pxl8_read_u32(&stream);

            bsp->faces[i].aabb_min = (pxl8_vec3){1e30f, 1e30f, 1e30f};
            bsp->faces[i].aabb_max = (pxl8_vec3){-1e30f, -1e30f, -1e30f};
        }
    }

    chunk = &header.chunks[CHUNK_NODES];
    if (!validate_chunk(chunk, 24, file_size)) goto error_cleanup;
    bsp->num_nodes = chunk->size / 24;
    if (bsp->num_nodes > 0) {
        bsp->nodes = calloc(bsp->num_nodes, sizeof(pxl8_bsp_node));
        pxl8_stream_seek(&stream, chunk->offset);
        for (u32 i = 0; i < bsp->num_nodes; i++) {
            bsp->nodes[i].plane_id = pxl8_read_u32(&stream);
            bsp->nodes[i].children[0] = pxl8_read_i16(&stream);
            bsp->nodes[i].children[1] = pxl8_read_i16(&stream);
            for (u32 j = 0; j < 3; j++) bsp->nodes[i].mins[j] = pxl8_read_i16(&stream);
            for (u32 j = 0; j < 3; j++) bsp->nodes[i].maxs[j] = pxl8_read_i16(&stream);
            bsp->nodes[i].first_face = pxl8_read_u16(&stream);
            bsp->nodes[i].num_faces = pxl8_read_u16(&stream);
        }
    }

    chunk = &header.chunks[CHUNK_LEAFS];
    if (!validate_chunk(chunk, 28, file_size)) goto error_cleanup;
    bsp->num_leafs = chunk->size / 28;
    if (bsp->num_leafs > 0) {
        bsp->leafs = calloc(bsp->num_leafs, sizeof(pxl8_bsp_leaf));
        pxl8_stream_seek(&stream, chunk->offset);
        for (u32 i = 0; i < bsp->num_leafs; i++) {
            bsp->leafs[i].contents = pxl8_read_i32(&stream);
            bsp->leafs[i].visofs = pxl8_read_i32(&stream);
            for (u32 j = 0; j < 3; j++) bsp->leafs[i].mins[j] = pxl8_read_i16(&stream);
            for (u32 j = 0; j < 3; j++) bsp->leafs[i].maxs[j] = pxl8_read_i16(&stream);
            bsp->leafs[i].first_marksurface = pxl8_read_u16(&stream);
            bsp->leafs[i].num_marksurfaces = pxl8_read_u16(&stream);
            for (u32 j = 0; j < 4; j++) bsp->leafs[i].ambient_level[j] = pxl8_read_u8(&stream);
        }
    }

    chunk = &header.chunks[CHUNK_MARKSURFACES];
    if (!validate_chunk(chunk, 2, file_size)) goto error_cleanup;
    bsp->num_marksurfaces = chunk->size / 2;
    if (bsp->num_marksurfaces > 0) {
        bsp->marksurfaces = calloc(bsp->num_marksurfaces, sizeof(u16));
        pxl8_stream_seek(&stream, chunk->offset);
        for (u32 i = 0; i < bsp->num_marksurfaces; i++) {
            bsp->marksurfaces[i] = pxl8_read_u16(&stream);
        }
    }

    chunk = &header.chunks[CHUNK_MODELS];
    if (!validate_chunk(chunk, 64, file_size)) goto error_cleanup;
    bsp->num_models = chunk->size / 64;
    if (bsp->num_models > 0) {
        bsp->models = calloc(bsp->num_models, sizeof(pxl8_bsp_model));
        pxl8_stream_seek(&stream, chunk->offset);
        for (u32 i = 0; i < bsp->num_models; i++) {
            for (u32 j = 0; j < 3; j++) bsp->models[i].mins[j] = pxl8_read_f32(&stream);
            for (u32 j = 0; j < 3; j++) bsp->models[i].maxs[j] = pxl8_read_f32(&stream);
            bsp->models[i].origin = read_vec3(&stream);
            for (u32 j = 0; j < 4; j++) bsp->models[i].headnode[j] = pxl8_read_i32(&stream);
            bsp->models[i].visleafs = pxl8_read_i32(&stream);
            bsp->models[i].first_face = pxl8_read_i32(&stream);
            bsp->models[i].num_faces = pxl8_read_i32(&stream);
        }
    }

    chunk = &header.chunks[CHUNK_VISIBILITY];
    if (!validate_chunk(chunk, 1, file_size)) goto error_cleanup;
    bsp->visdata_size = chunk->size;
    if (bsp->visdata_size > 0) {
        bsp->visdata = malloc(bsp->visdata_size);
        memcpy(bsp->visdata, file_data + chunk->offset, bsp->visdata_size);
    }

    chunk = &header.chunks[CHUNK_LIGHTING];
    if (!validate_chunk(chunk, 1, file_size)) goto error_cleanup;
    bsp->lightdata_size = chunk->size;
    if (bsp->lightdata_size > 0) {
        bsp->lightdata = malloc(bsp->lightdata_size);
        memcpy(bsp->lightdata, file_data + chunk->offset, bsp->lightdata_size);
    }

    free(file_data);

    for (u32 i = 0; i < bsp->num_faces; i++) {
        pxl8_bsp_face* face = &bsp->faces[i];
        f32 min_x = 1e30f, min_y = 1e30f, min_z = 1e30f;
        f32 max_x = -1e30f, max_y = -1e30f, max_z = -1e30f;

        for (u32 j = 0; j < face->num_edges; j++) {
            i32 surfedge_idx = face->first_edge + j;
            u32 vert_idx;

            if (!pxl8_bsp_get_edge_vertex(bsp, surfedge_idx, &vert_idx)) continue;

            pxl8_vec3 v = bsp->vertices[vert_idx].position;

            if (v.x < min_x) min_x = v.x;
            if (v.x > max_x) max_x = v.x;
            if (v.y < min_y) min_y = v.y;
            if (v.y > max_y) max_y = v.y;
            if (v.z < min_z) min_z = v.z;
            if (v.z > max_z) max_z = v.z;
        }

        face->aabb_min = (pxl8_vec3){min_x, min_y, min_z};
        face->aabb_max = (pxl8_vec3){max_x, max_y, max_z};
    }

    pxl8_debug("Loaded BSP: %u verts, %u faces, %u nodes, %u leafs",
               bsp->num_vertices, bsp->num_faces, bsp->num_nodes, bsp->num_leafs);

    return PXL8_OK;

error_cleanup:
    pxl8_error("BSP chunk validation failed: %s", path);
    free(file_data);
    pxl8_bsp_destroy(bsp);
    return PXL8_ERROR_INVALID_FORMAT;
}

void pxl8_bsp_destroy(pxl8_bsp* bsp) {
    if (!bsp) return;

    free(bsp->edges);
    free(bsp->faces);
    free(bsp->leafs);
    free(bsp->lightdata);
    free(bsp->marksurfaces);
    free(bsp->models);
    free(bsp->nodes);
    free(bsp->planes);
    free(bsp->surfedges);
    free(bsp->texinfo);
    free(bsp->vertex_lights);
    free(bsp->vertices);
    free(bsp->visdata);

    memset(bsp, 0, sizeof(*bsp));
}

i32 pxl8_bsp_find_leaf(const pxl8_bsp* bsp, pxl8_vec3 pos) {
    if (!bsp || bsp->num_nodes == 0) return -1;

    i32 node_id = 0;

    while (node_id >= 0) {
        const pxl8_bsp_node* node = &bsp->nodes[node_id];
        const pxl8_bsp_plane* plane = &bsp->planes[node->plane_id];

        f32 dist = pxl8_vec3_dot(pos, plane->normal) - plane->dist;
        node_id = node->children[dist < 0 ? 1 : 0];
    }

    return -(node_id + 1);
}

bool pxl8_bsp_is_leaf_visible(const pxl8_bsp* bsp, i32 leaf_from, i32 leaf_to) {
    if (!bsp || !bsp->visdata || bsp->visdata_size == 0) return true;
    if (leaf_from < 0 || leaf_to < 0) return true;
    if ((u32)leaf_from >= bsp->num_leafs || (u32)leaf_to >= bsp->num_leafs) return true;

    i32 visofs = bsp->leafs[leaf_from].visofs;
    if (visofs < 0) return true;

    u32 target_byte = leaf_to >> 3;
    u32 target_bit = leaf_to & 7;

    u32 pos = (u32)visofs;
    u32 out_pos = 0;

    while (out_pos <= target_byte && pos < bsp->visdata_size) {
        u8 b = bsp->visdata[pos++];
        if (b != 0) {
            if (out_pos == target_byte) {
                return (b & (1 << target_bit)) != 0;
            }
            out_pos++;
        } else {
            if (pos >= bsp->visdata_size) break;
            u32 count = bsp->visdata[pos++];
            if (out_pos + count > target_byte) {
                return false;
            }
            out_pos += count;
        }
    }

    return false;
}

pxl8_bsp_pvs pxl8_bsp_decompress_pvs(const pxl8_bsp* bsp, i32 leaf) {
    pxl8_bsp_pvs pvs = {0};

    u32 pvs_size = (bsp->num_leafs + 7) / 8;
    pvs.data = calloc(pvs_size, 1);
    pvs.size = pvs_size;

    if (!pvs.data) return pvs;

    if (!bsp || leaf < 0 || (u32)leaf >= bsp->num_leafs) {
        memset(pvs.data, 0xFF, pvs_size);
        return pvs;
    }

    i32 visofs = bsp->leafs[leaf].visofs;
    if (visofs < 0 || !bsp->visdata || bsp->visdata_size == 0) {
        memset(pvs.data, 0xFF, pvs_size);
        return pvs;
    }

    u32 pos = (u32)visofs;
    u32 out = 0;

    while (out < pvs_size && pos < bsp->visdata_size) {
        u8 b = bsp->visdata[pos++];

        if (b != 0) {
            pvs.data[out++] = b;
        } else {
            if (pos >= bsp->visdata_size) break;
            u32 count = bsp->visdata[pos++];
            out += count;
        }
    }

    return pvs;
}

void pxl8_bsp_pvs_destroy(pxl8_bsp_pvs* pvs) {
    if (pvs) {
        free(pvs->data);
        pvs->data = NULL;
        pvs->size = 0;
    }
}

bool pxl8_bsp_pvs_is_visible(const pxl8_bsp_pvs* pvs, i32 leaf) {
    if (!pvs || !pvs->data || leaf < 0) return false;
    u32 byte_idx = leaf >> 3;
    u32 bit_idx = leaf & 7;
    if (byte_idx >= pvs->size) return false;
    return (pvs->data[byte_idx] & (1 << bit_idx)) != 0;
}

pxl8_bsp_lightmap pxl8_bsp_lightmap_uniform(u8 r, u8 g, u8 b) {
    return (pxl8_bsp_lightmap){
        .color = {r, g, b},
        .height = 0,
        .offset = 0,
        .width = 0,
    };
}

pxl8_bsp_lightmap pxl8_bsp_lightmap_mapped(u8 width, u8 height, u32 offset) {
    return (pxl8_bsp_lightmap){
        .color = {0, 0, 0},
        .height = height,
        .offset = offset,
        .width = width,
    };
}

pxl8_bsp_lightmap_sample pxl8_bsp_sample_lightmap(const pxl8_bsp* bsp, u32 face_idx, f32 u, f32 v) {
    pxl8_bsp_lightmap_sample white = {255, 255, 255};

    if (!bsp || !bsp->lightmaps || face_idx >= bsp->num_lightmaps) {
        return white;
    }

    const pxl8_bsp_lightmap* lm = &bsp->lightmaps[face_idx];

    if (lm->width == 0) {
        return (pxl8_bsp_lightmap_sample){lm->color[2], lm->color[1], lm->color[0]};
    }

    if (!bsp->lightdata || bsp->lightdata_size == 0) {
        return white;
    }

    f32 w = (f32)lm->width;
    f32 h = (f32)lm->height;
    f32 fx = u * w;
    f32 fy = v * h;

    if (fx < 0) fx = 0;
    if (fx > w - 1.001f) fx = w - 1.001f;
    if (fy < 0) fy = 0;
    if (fy > h - 1.001f) fy = h - 1.001f;

    u32 x0 = (u32)fx;
    u32 y0 = (u32)fy;
    u32 x1 = x0 + 1;
    u32 y1 = y0 + 1;
    if (x1 >= lm->width) x1 = lm->width - 1;
    if (y1 >= lm->height) y1 = lm->height - 1;

    f32 frac_x = fx - (f32)x0;
    f32 frac_y = fy - (f32)y0;

    u32 stride = lm->width;
    u32 base = lm->offset;

    u32 idx00 = base + y0 * stride + x0;
    u32 idx10 = base + y0 * stride + x1;
    u32 idx01 = base + y1 * stride + x0;
    u32 idx11 = base + y1 * stride + x1;

    u8 r00, g00, b00, r10, g10, b10, r01, g01, b01, r11, g11, b11;

    if (idx00 < bsp->lightdata_size) pxl8_rgb332_unpack(bsp->lightdata[idx00], &r00, &g00, &b00);
    else { r00 = g00 = b00 = 255; }
    if (idx10 < bsp->lightdata_size) pxl8_rgb332_unpack(bsp->lightdata[idx10], &r10, &g10, &b10);
    else { r10 = g10 = b10 = 255; }
    if (idx01 < bsp->lightdata_size) pxl8_rgb332_unpack(bsp->lightdata[idx01], &r01, &g01, &b01);
    else { r01 = g01 = b01 = 255; }
    if (idx11 < bsp->lightdata_size) pxl8_rgb332_unpack(bsp->lightdata[idx11], &r11, &g11, &b11);
    else { r11 = g11 = b11 = 255; }

    f32 inv_x = 1.0f - frac_x;
    f32 inv_y = 1.0f - frac_y;

    u8 r = (u8)(r00 * inv_x * inv_y + r10 * frac_x * inv_y + r01 * inv_x * frac_y + r11 * frac_x * frac_y);
    u8 g = (u8)(g00 * inv_x * inv_y + g10 * frac_x * inv_y + g01 * inv_x * frac_y + g11 * frac_x * frac_y);
    u8 b = (u8)(b00 * inv_x * inv_y + b10 * frac_x * inv_y + b01 * inv_x * frac_y + b11 * frac_x * frac_y);

    return (pxl8_bsp_lightmap_sample){b, g, r};
}

static inline bool face_in_frustum(const pxl8_bsp* bsp, u32 face_id, const pxl8_frustum* frustum) {
    const pxl8_bsp_face* face = &bsp->faces[face_id];
    return pxl8_frustum_test_aabb(frustum, face->aabb_min, face->aabb_max);
}

static inline bool leaf_in_frustum(const pxl8_bsp_leaf* leaf, const pxl8_frustum* frustum) {
    pxl8_vec3 mins = {(f32)leaf->mins[0], (f32)leaf->mins[1], (f32)leaf->mins[2]};
    pxl8_vec3 maxs = {(f32)leaf->maxs[0], (f32)leaf->maxs[1], (f32)leaf->maxs[2]};
    return pxl8_frustum_test_aabb(frustum, mins, maxs);
}

static void collect_face_to_mesh(
    const pxl8_bsp* bsp,
    u32 face_id,
    pxl8_mesh* mesh
) {
    const pxl8_bsp_face* face = &bsp->faces[face_id];
    if (face->num_edges < 3) return;

    pxl8_vec3 normal = {0, 1, 0};
    if (face->plane_id < bsp->num_planes) {
        normal = bsp->planes[face->plane_id].normal;
        if (face->side) {
            normal.x = -normal.x;
            normal.y = -normal.y;
            normal.z = -normal.z;
        }
    }

    const pxl8_bsp_texinfo* texinfo = NULL;
    f32 tex_scale = 64.0f;
    if (face->texinfo_id < bsp->num_texinfo) {
        texinfo = &bsp->texinfo[face->texinfo_id];
    }

    u16 base_idx = (u16)mesh->vertex_count;
    u32 num_verts = 0;

    for (u32 i = 0; i < face->num_edges && num_verts < 64; i++) {
        i32 surfedge_idx = face->first_edge + i;
        u32 vert_idx;

        if (!pxl8_bsp_get_edge_vertex(bsp, surfedge_idx, &vert_idx)) {
            continue;
        }

        pxl8_vec3 pos = bsp->vertices[vert_idx].position;

        f32 u = 0.0f, v = 0.0f;
        if (texinfo) {
            u = (pxl8_vec3_dot(pos, texinfo->u_axis) + texinfo->u_offset) / tex_scale;
            v = (pxl8_vec3_dot(pos, texinfo->v_axis) + texinfo->v_offset) / tex_scale;
        }

        u8 light = 255;
        if (bsp->vertex_lights && vert_idx < bsp->num_vertex_lights) {
            light = (bsp->vertex_lights[vert_idx] >> 24) & 0xFF;
        }

        pxl8_vertex vtx = {
            .position = pos,
            .normal = normal,
            .u = u,
            .v = v,
            .color = 15,
            .light = light,
        };
        pxl8_mesh_push_vertex(mesh, vtx);
        num_verts++;
    }

    if (num_verts < 3) return;

    for (u32 i = 1; i < num_verts - 1; i++) {
        pxl8_mesh_push_triangle(mesh, base_idx, base_idx + i, base_idx + i + 1);
    }
}

void pxl8_bsp_render_face(pxl8_gfx* gfx, const pxl8_bsp* bsp, u32 face_id, u32 texture_id) {
    if (!gfx || !bsp || face_id >= bsp->num_faces) return;

    pxl8_mesh* mesh = pxl8_mesh_create(64, 192);
    if (!mesh) return;

    collect_face_to_mesh(bsp, face_id, mesh);

    if (mesh->index_count > 0) {
        pxl8_mat4 identity = pxl8_mat4_identity();
        pxl8_gfx_material mat = {
            .texture_id = texture_id,
            .alpha = 255,
            .dither = true,
            .dynamic_lighting = true,
        };
        pxl8_3d_draw_mesh(gfx, mesh, &identity, &mat);
    }

    pxl8_mesh_destroy(mesh);
}

void pxl8_bsp_render_textured(pxl8_gfx* gfx, const pxl8_bsp* bsp, pxl8_vec3 camera_pos) {
    static int call_count = 0;
    if (!gfx || !bsp || bsp->num_faces == 0) {
        if (call_count++ < 5) {
            pxl8_debug("bsp_render_textured: early return - gfx=%p, bsp=%p, num_faces=%u",
                (void*)gfx, (void*)bsp, bsp ? bsp->num_faces : 0);
        }
        return;
    }

    const pxl8_frustum* frustum = pxl8_3d_get_frustum(gfx);
    if (!frustum) {
        if (call_count++ < 5) {
            pxl8_debug("bsp_render_textured: frustum is NULL!");
        }
        return;
    }

    i32 camera_leaf = pxl8_bsp_find_leaf(bsp, camera_pos);

    static u32 debug_counter = 0;
    bool do_debug = (debug_counter++ % 300 == 0);
    u32 visible_leafs = 0;
    u32 visible_faces = 0;
    u32 total_faces_in_visible_leafs = 0;
    static bool dumped_camera_leaf = false;
    if (do_debug) {
        bool self_visible = (camera_leaf < 0) || pxl8_bsp_is_leaf_visible(bsp, camera_leaf, camera_leaf);
        if (!self_visible) {
            pxl8_debug("WARNING: Camera leaf %d is NOT visible from itself!", camera_leaf);
        }
        if (!dumped_camera_leaf && camera_leaf >= 0) {
            const pxl8_bsp_leaf* cl = &bsp->leafs[camera_leaf];
            pxl8_debug("Camera leaf %d: contents=%d, marksurfaces=%u-%u (%u faces)",
                       camera_leaf, cl->contents, cl->first_marksurface,
                       cl->first_marksurface + cl->num_marksurfaces, cl->num_marksurfaces);
            dumped_camera_leaf = true;
        }
        for (u32 i = 0; i < bsp->num_leafs; i++) {
            if (camera_leaf < 0 || pxl8_bsp_is_leaf_visible(bsp, camera_leaf, i)) {
                visible_leafs++;
                total_faces_in_visible_leafs += bsp->leafs[i].num_marksurfaces;
            }
        }
    }

    static u8* rendered_faces = NULL;
    static u32 rendered_faces_capacity = 0;

    if (rendered_faces_capacity < bsp->num_faces) {
        u8* new_buffer = realloc(rendered_faces, bsp->num_faces);
        if (!new_buffer) return;
        rendered_faces = new_buffer;
        rendered_faces_capacity = bsp->num_faces;
    }

    memset(rendered_faces, 0, bsp->num_faces);

    pxl8_mesh* mesh = pxl8_mesh_create(8192, 16384);
    if (!mesh) return;

    u32 current_texture = 0xFFFFFFFF;

    for (u32 leaf_id = 0; leaf_id < bsp->num_leafs; leaf_id++) {
        if (camera_leaf >= 0 && !pxl8_bsp_is_leaf_visible(bsp, camera_leaf, leaf_id)) continue;

        const pxl8_bsp_leaf* leaf = &bsp->leafs[leaf_id];

        bool is_camera_leaf = ((i32)leaf_id == camera_leaf);

        f32 cx = ((f32)leaf->mins[0] + (f32)leaf->maxs[0]) * 0.5f;
        f32 cy = ((f32)leaf->mins[1] + (f32)leaf->maxs[1]) * 0.5f;
        f32 cz = ((f32)leaf->mins[2] + (f32)leaf->maxs[2]) * 0.5f;
        f32 dx = camera_pos.x - cx;
        f32 dy = camera_pos.y - cy;
        f32 dz = camera_pos.z - cz;
        f32 dist_sq = dx*dx + dy*dy + dz*dz;
        bool camera_near_leaf = dist_sq < (512.0f * 512.0f);

        bool skip_frustum = is_camera_leaf || camera_near_leaf;
        if (!skip_frustum && !leaf_in_frustum(leaf, frustum)) continue;

        for (u32 i = 0; i < leaf->num_marksurfaces; i++) {
            u32 surf_idx = leaf->first_marksurface + i;
            if (surf_idx >= bsp->num_marksurfaces) continue;

            u32 face_id = bsp->marksurfaces[surf_idx];
            if (face_id >= bsp->num_faces) continue;

            if (rendered_faces[face_id]) continue;
            rendered_faces[face_id] = 1;
            if (do_debug) visible_faces++;

            if (!skip_frustum && !face_in_frustum(bsp, face_id, frustum)) {
                continue;
            }

            const pxl8_bsp_face* face = &bsp->faces[face_id];
            u32 texture_id = 0;
            if (face->texinfo_id < bsp->num_texinfo) {
                texture_id = bsp->texinfo[face->texinfo_id].miptex;
            }

            if (texture_id != current_texture && mesh->index_count > 0) {
                pxl8_mat4 identity = pxl8_mat4_identity();
                pxl8_gfx_material mat = {
                    .texture_id = current_texture,
                    .alpha = 255,
                    .dither = true,
                    .dynamic_lighting = true,
                };
                pxl8_3d_draw_mesh(gfx, mesh, &identity, &mat);
                pxl8_mesh_clear(mesh);
            }

            current_texture = texture_id;
            collect_face_to_mesh(bsp, face_id, mesh);
        }
    }

    if (mesh->index_count > 0) {
        pxl8_mat4 identity = pxl8_mat4_identity();
        pxl8_gfx_material mat = {
            .texture_id = current_texture,
            .alpha = 255,
            .dither = true,
            .dynamic_lighting = true,
        };
        pxl8_3d_draw_mesh(gfx, mesh, &identity, &mat);
    }

    if (do_debug) {
        pxl8_debug("Camera at (%.1f, %.1f, %.1f) -> leaf %d, %u/%u leafs, %u/%u faces (expected %u)",
                   camera_pos.x, camera_pos.y, camera_pos.z,
                   camera_leaf, visible_leafs, bsp->num_leafs,
                   visible_faces, bsp->num_faces, total_faces_in_visible_leafs);
    }

    pxl8_mesh_destroy(mesh);
}

void pxl8_bsp_render_wireframe(pxl8_gfx* gfx, const pxl8_bsp* bsp, pxl8_vec3 camera_pos, u32 color) {
    if (!gfx || !bsp) return;

    const pxl8_frustum* frustum = pxl8_3d_get_frustum(gfx);
    if (!frustum) return;

    i32 camera_leaf = pxl8_bsp_find_leaf(bsp, camera_pos);
    u8 line_color = (u8)(color & 0xFF);

    for (u32 leaf_id = 0; leaf_id < bsp->num_leafs; leaf_id++) {
        if (camera_leaf >= 0 && !pxl8_bsp_is_leaf_visible(bsp, camera_leaf, leaf_id)) continue;

        const pxl8_bsp_leaf* leaf = &bsp->leafs[leaf_id];

        for (u32 i = 0; i < leaf->num_marksurfaces; i++) {
            u32 surf_idx = leaf->first_marksurface + i;
            if (surf_idx >= bsp->num_marksurfaces) continue;

            u32 face_id = bsp->marksurfaces[surf_idx];
            if (face_id >= bsp->num_faces) continue;

            if (!face_in_frustum(bsp, face_id, frustum)) continue;

            const pxl8_bsp_face* face = &bsp->faces[face_id];

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

                if (surfedge_idx >= (i32)bsp->num_surfedges) continue;

                u32 v0_idx, v1_idx;
                if (!pxl8_bsp_get_edge_vertices(bsp, surfedge_idx, &v0_idx, &v1_idx)) continue;

                pxl8_vec3 p0 = bsp->vertices[v0_idx].position;
                pxl8_vec3 p1 = bsp->vertices[v1_idx].position;

                pxl8_3d_draw_line(gfx, p0, p1, line_color);
            }
        }
    }
}
