//
// Created by Tristan on 11/11/2025.
//
#include <stdint.h>
#include "../util.h"
#include "wmo.h"
#include "../logger/log.h"
#include "../renderer/texture.h"

void parse_mohd_chunk(const WMORootData *wmo_root_data) {
    SMOHeader *header = (SMOHeader *)wmo_root_data->mohd_data_ptr;
    log_info("\t%d groups found in root file", header->nGroups);
}

void get_wmo_group_names(ArchiveManager *archives, WMORootData *wmo_root_data, const char *wmo_file_path) {
    const SMOGroupInfo *groupInfo = (SMOGroupInfo* )wmo_root_data->mogi_data_ptr;
    size_t group_count = wmo_root_data->mogi_size / sizeof(SMOGroupInfo);

    wmo_root_data->groups = (WMOGroupData *)calloc(group_count, sizeof(WMOGroupData));
    if (wmo_root_data->groups == NULL) {
        log_error("Failed to allocate memory for WMO groups");
        return;
    }
    wmo_root_data->group_count = group_count;

    char wmo_base_name[MAX_PATH];
    if (get_wmo_base_name(wmo_base_name, MAX_PATH, wmo_file_path) == 0) {
        log_error("Could not determine WMO base path for group naming");
    }

    char mpq_path_base[MAX_PATH];
    strncpy(mpq_path_base, wmo_file_path, MAX_PATH);
    size_t path_len = strlen(mpq_path_base);
    if (path_len > 4 && strcmp(mpq_path_base + path_len - 4, ".wmo") == 0) {
        mpq_path_base[path_len - 4] = '\0'; // strips .wmo
    } else {
        log_error("WMO file path missing .wmo extension. Cannot construct group file paths");
    }

    log_info("Parsing %zu groups from MOGI chunk. Base path: %s", group_count, wmo_base_name);

    if (archives == NULL) {
        log_error("HANDLE is NULL. Cannot verify WMO group file existence");
        return;
    }

    int mpq_check_index = 0;
    size_t file_index_counter = 0;
    size_t file_index = 0;

    for (size_t i = 0; i < group_count; i++) {
        int32_t offset = groupInfo[i].name_offset;

        // Skip materials that dont reference a group
        if (offset == 0) {
            log_info("Group [%zu]: No Group referenced (Offset 0)", i);
            continue;
        }


        // -1 means no name wowdev.wiki/WMO#MOGI
        if (offset == -1) {
            log_info("Group [%zu]: No group name referenced (Offset -1)", i);

        }

        if (offset >= wmo_root_data->mogi_size && offset != -1) {
            log_error("Group [%zu]: Invalid group offset %lu (outside MOGI boundaries).", i, offset);
            continue;
        }

        if (archives != NULL) {
            bool file_found = false;
            int found_file_index = -1;

            while (mpq_check_index < 100) { // init_capacity is hardcoded, should dynamically check loaded mpqs...
                // need to pass struct pointer for that tho I think
                char file_to_check[MAX_PATH];
                snprintf(file_to_check, MAX_PATH, "%s_%03d.wmo", mpq_path_base, mpq_check_index);

                HANDLE hFile = get_file_in_archives(archives, file_to_check);

                if (hFile != NULL) {
                    SFileCloseFile(hFile);
                    found_file_index = mpq_check_index;
                    file_found = true;
                    mpq_check_index++;
                    break;
                }

                mpq_check_index++;
            }

            if (!file_found) {
                log_error("MOGI Group [%zu] found, but could not find a matching WMO group file in MPQ. Skipping.", i);
                continue;
            }


            char group_file_path[MAX_PATH];
            int result = snprintf(group_file_path, MAX_PATH, "%s_%03d.wmo", mpq_path_base, found_file_index);

            if (result >= MAX_PATH || result < 0) {
                log_error("Group path construction failed or truncated for index %zu", i);
                continue;
            }

            const char *mogn_name = (offset == -1) ? "No name referenced (Offset -1)" : (wmo_root_data->mogn_data_ptr + offset);

            log_info("Group [%zu] file path: %s (MOGN: %s) Mapped to actual file index %03d", i, group_file_path, mogn_name, found_file_index);
            // if (get_file_in_mpq())

            WMOGroupData group_data = parse_wmo_group_file(archives, group_file_path);

            wmo_root_data->groups[i] = group_data;

            if (wmo_root_data->groups[i].movt_data_ptr != NULL) {
                log_info("Successfully loaded group %zu: %lu vertices found!", i, wmo_root_data->groups[i].movt_size / sizeof(C3Vector));
            }
        }
    }
}

void get_mosb_skybox(const WMORootData *wmo_root_data) {
    if (wmo_root_data->mosb_data_ptr == NULL || wmo_root_data->mosb_size == 0) {
        log_info("MOSB chunk not found in WMO root");
    }

    const char *skybox_name = wmo_root_data->mosb_data_ptr;

    log_info("Parsing MOSB chunk...");

    if (*skybox_name != '\0') {
        log_info("  Skybox name found in MOSB chunk: %s", skybox_name);
    } else {
        log_info("  Skybox is not found for WMO. (first byte is 0)");
    }
}

void parse_mopv_chunk(const WMORootData *wmo_root_data) {
    if (wmo_root_data->mopv_data_ptr == NULL || wmo_root_data->mopv_size == 0) {
        log_info("MOPV chunk not found in WMO root");
    }
    // Portal vertices, one entry is a float[3], usually 4 * 3 * float per portal (actual number of vertices given in portal entry)
    // 4 bytes * float(x,y,z) * number of vertices
    C3Vector *portalVertexList = (C3Vector *)wmo_root_data->mopv_data_ptr;

    size_t portal_vertex_count = wmo_root_data->mopv_size / sizeof(C3Vector);

    log_info("Found MOPV data (portals) with %zu vertices.", portal_vertex_count);


    for (size_t i = 0; i < portal_vertex_count; i++) {
        C3Vector current_vertex = portalVertexList[i];
        log_info("Vertex %zu: X=%f, Y=%f, Z=%f", i, current_vertex.x, current_vertex.y, current_vertex.z);
    }
}

void parse_mopt_chunk(const WMORootData *wmo_root_data) {
    if (wmo_root_data->mopt_data_ptr == NULL || wmo_root_data->mopt_size == 0) {
        log_info("MOPT chunk not found in WMO root");
    }

    SMOPortal *portalList = (SMOPortal *)wmo_root_data->mopt_data_ptr;

    size_t portal_list_count = wmo_root_data->mopt_size / sizeof(SMOPortal);

    log_info("Found MOPT data (portal lists) with %zu portals", portal_list_count);

    for (size_t i = 0; i < portal_list_count; i++) {
        SMOPortal portal = portalList[i];
        log_info("Portal %zu: startVertex: %u, count: %u, plane: normal(X=%.2f, Y=%.2f, Z=%.2f), distance(%.2f)",
            i, portal.startVertex, portal.count, portal.plane.normal.x, portal.plane.normal.y, portal.plane.normal.z,
            portal.plane.distance);
    }
}

void parse_mopr_chunk(const WMORootData *wmo_root_data) {
    if (wmo_root_data->mopr_data_ptr == NULL || wmo_root_data->mopr_size == 0) {
        log_info("MOPR chunk not found in WMO root");
        return;
    }

    SMOPortalRef *portalRefList = (SMOPortalRef *)wmo_root_data->mopr_data_ptr;
    size_t portal_ref_list_count = wmo_root_data->mopr_size / sizeof(SMOPortalRef);

    log_info("Found MOPR data (portal ref lists) with %zu references", portal_ref_list_count);

    /* wmo_root_data->portal_references = (SMOPortalRef *)malloc(portal_ref_list_count * sizeof(SMOPortalRef));
    if (wmo_root_data->portal_references == NULL) {
        log_error("Failed to allocate memory for MOPR references.");
        return; // Handle allocation failure
    }*/
    //wmo_root_data->portal_ref_count = portal_ref_list_count;

    //memcpy(wmo_root_data->portal_references, wmo_root_data->mopr_data_ptr, wmo_root_data->mopr_size);

    for (size_t i = 0; i < portal_ref_list_count; i++) {
        SMOPortalRef portal_ref = portalRefList[i];
        log_info("Portal reference %zu: portalIndex: %u, groupIndex: %u, side: %d", i, portal_ref.portalIndex,
            portal_ref.groupIndex, portal_ref.side);
    }

    // TODO - DOES THE CALLER OR THE FUNCTION FREE MALLOC()?
}

WMOGroupData parse_wmo_group_file(ArchiveManager *archives, const char *group_file_path) {
    WMOGroupData group_data = {0};

    WMOData file_data = load_wmo_data(archives, group_file_path);
    if (file_data.data == NULL) {
        log_error("Failed to load WMO group file: %s", group_file_path);
        return group_data;
    }

    group_data.group_file_buffer = file_data.data;
    group_data.group_file_size = file_data.size;

    const char *current_ptr = file_data.data;
    const char *end_ptr = file_data.data + file_data.size;

    if (current_ptr + sizeof(WMOChunkHeader) > end_ptr) {
        log_error("Group file is too small to contain a chunk header");
        return group_data;
    }

    const WMOChunkHeader *mver_header = (const WMOChunkHeader *)current_ptr;


    if (mver_header->chunk_name != MVER) {
        log_error("Expected MVER chunk as first chunk, found: %c%c%c%c",
            (char)(mver_header->chunk_name >> 24), (char)(mver_header->chunk_name >> 16),
            (char)(mver_header->chunk_name >> 8), (char)(mver_header->chunk_name));
        return group_data;
    }

    current_ptr += sizeof(WMOChunkHeader) + mver_header->chunk_size;

    if (current_ptr + sizeof(WMOChunkHeader) > end_ptr) {
        log_error("Group file ended before MOGP chunk was found");
        return group_data;
    }

    const WMOChunkHeader *mogp_header = (const WMOChunkHeader *)current_ptr;
    const char *data_start = current_ptr + sizeof(WMOChunkHeader);

    if (mogp_header->chunk_name != MOGP) {
        log_error("Expected MVER chunk as first chunk, found: %c%c%c%c",
            (char)(mogp_header->chunk_name >> 24), (char)(mogp_header->chunk_name >> 16),
            (char)(mogp_header->chunk_name >> 8), (char)(mogp_header->chunk_name));
        return group_data;
    }

    log_info("Found MOGP chunk (size: %lu bytes) in group file", mogp_header->chunk_size);

    parse_mogp_sub_chunks(data_start, mogp_header->chunk_size, &group_data);

    /*
    if (file_data.data != NULL) {
        free(file_data.data);
    }*/

    return group_data;
}

void parse_moba_chunk(const WMOGroupData *wmo_group_data) {
    if (wmo_group_data->moba_data_ptr == NULL || wmo_group_data->moba_size == 0) {
        log_error("Could not find valid MOBA chunk inside WMO Group File");
        return;
    }

    const SMOBatch *batches = (const SMOBatch *)wmo_group_data->moba_data_ptr;
    size_t num_batches = wmo_group_data->moba_size / sizeof(SMOBatch);

    log_info("Found %zu batches in MOBA chunk (Total bytes: %lu)", num_batches, wmo_group_data->moba_size);

    for (size_t i = 0; i < num_batches; i++) {
        const SMOBatch *batch = &batches[i];
        log_info("Batch: %zu, Material ID: %u, MOVI Start Index: %u, MOVI Index Count: %u, MOVT Vertex Range: %u to %u",
            i, batch->material_id, batch->startIndex, batch->count, batch->minIndex, batch->maxIndex);
    }
}

void parse_motv_chunk(const WMOGroupData *wmo_group_data) {
    if (wmo_group_data->motv_data_ptr == NULL || wmo_group_data->motv_size == 0) {
        log_error("Could not find valid MOTV chunk inside WMO Group File");
        return;
    }

    const C2Vector *textureVertexList = (const C2Vector *)wmo_group_data->motv_data_ptr;
    size_t num_texture_vertexs = wmo_group_data->motv_size / sizeof(C2Vector);

    log_info("Found %zu texture vertexs in MOTV chunk (Total bytes: %lu)", num_texture_vertexs, wmo_group_data->motv_size);
    /*
    for (size_t i = 0; i < num_texture_vertexs; i++) {
        float x = textureVertexList[i].x;
        float y = textureVertexList[i].y;

        log_info("Texture Vertex %zu: (X: %.4f, Y: %.4f)", i, x, y);
    }*/
}

void parse_molr_chunk(const WMOGroupData *wmo_group_data) {
    if (wmo_group_data->molr_data_ptr == NULL || wmo_group_data->molr_size == 0) {
        log_error("Could not find valid MOLR chunk inside WMO Group File");
        return;
    }

    const uint16_t *overrideLightRefs = (const uint16_t *)wmo_group_data->molr_data_ptr;
    size_t num_light_refs = wmo_group_data->molr_size / sizeof(uint16_t);

    log_info("Found %zu light references in MOLR chunk (Total bytes: %lu", num_light_refs, wmo_group_data->molr_size);
    ///*
    for (size_t i = 0; i < num_light_refs; i++) {
        log_info("Light Reference %zu: %hu", i, overrideLightRefs[i]);
    } //*/

}

void parse_monr_chunk(const WMOGroupData *wmo_group_data) {
    if (wmo_group_data->monr_data_ptr == NULL || wmo_group_data->monr_size == 0) {
        log_error("Could not find valid MONR chunk inside WMO Group File");
        return;
    }

    const C3Vector *normalList = (const C3Vector *)wmo_group_data->monr_data_ptr;
    size_t num_normals = wmo_group_data->monr_size / sizeof(C3Vector);

    log_info("Found %zu normals in MONR chunk (Total bytes: %lu)", num_normals, wmo_group_data->monr_size);

    /*
    for (size_t i = 0; i < num_normals; i++) {
        float x = normalList[i].x;
        float y = normalList[i].y;
        float z = normalList[i].z;

        log_info("Normal %zu: (X: %.4f, Y: %.4f, Z: %.4f)", i, x, y, z);
    }*/
}

void parse_movt_chunk(const WMOGroupData *wmo_group_data) {
    if (wmo_group_data->movt_data_ptr == NULL || wmo_group_data->movt_size == 0) {
        log_error("Could not find valid MOVT chunk inside WMO Group File");
        return;
    }

    const C3Vector *vertexList = (const C3Vector *)wmo_group_data->movt_data_ptr;
    size_t num_vertexes = wmo_group_data->movt_size / sizeof(C3Vector);

    log_info("Found %zu vertices in MOVT chunk (Total bytes: %lu)", num_vertexes, wmo_group_data->movt_size);

    /*
    for (size_t i = 0; i < num_vertexs; i++) {
        float x = vertexList[i].x;
        float y = vertexList[i].y;
        float z = vertexList[i].z;

        log_info("Vertex %zu: (X: %.4f, Y: %.4f, Z: %.4f)", i, x, y, z);
    }*/
}

void parse_movi_chunk(const WMOGroupData *wmo_group_data) {
    if (wmo_group_data->movi_data_ptr == NULL || wmo_group_data->movi_size == 0) {
        log_error("Could not find valid MOVI chunk inside WMO Group File");
        return;
    }

    const uint16_t *indices = (const uint16_t *)wmo_group_data->movi_data_ptr;
    size_t num_indices = wmo_group_data->movi_size / sizeof(uint16_t);
    size_t num_triangles = num_indices / 3;

    log_info("Found %zu triangles from %zu indices", num_triangles, num_indices);

    /*
    for (size_t i = 0; i < num_indices; i += 3) {
        uint16_t v1_idx = indices[i + 0];
        uint16_t v2_idx = indices[i + 1];
        uint16_t v3_idx = indices[i + 2];

        log_info("Triangle %zu indices: %u, %u, %u", i/3, v1_idx, v2_idx, v3_idx);

        // TODO: export f line for obj file?
    }*/


}

void parse_mogp_sub_chunks(const char *mogp_data_buffer, DWORD mogp_data_size, WMOGroupData *out_group_data) {
    if (mogp_data_size < sizeof(SMOGroupHeader)) {
        log_error("MOGP chunk size is too small for SMOGroupHeader");
        return;
    }

    memcpy(&out_group_data->header, mogp_data_buffer, sizeof(SMOGroupHeader));
    log_info("SMOGroupHeader successfully extracted (size: %zu bytes)", sizeof(SMOGroupHeader));

    const char *current_ptr = mogp_data_buffer + sizeof(SMOGroupHeader);
    const char *end_ptr = mogp_data_buffer + mogp_data_size;

    log_info("Starting WMO Group file sub-chunk traversal...");

    while (current_ptr < end_ptr) {
        if (current_ptr + sizeof(WMOChunkHeader) > end_ptr) {
            log_warn("End of MOGP chunk reached unexpectedly");
            break;
        }

        const WMOChunkHeader *header = (const WMOChunkHeader *)current_ptr;
        const char *data_start = current_ptr + sizeof(WMOChunkHeader);

        switch (header->chunk_name) {
        case MOGX: // versions >10.0.0.46181
            out_group_data->mogx_data_ptr = data_start;
            out_group_data->mogx_size = header->chunk_size;
            log_info("  -> MOGX Chunk found at offset %td", out_group_data->mogx_data_ptr - mogp_data_buffer);
            break;
        case MOPY: // material info for triangles, two bytes per triangle. Size is twice number of triangles in WMO group
            out_group_data->mopy_data_ptr = data_start;
            out_group_data->mopy_size = header->chunk_size;
            log_info("  -> MOPY Chunk found at offset %td", out_group_data->mopy_data_ptr - mogp_data_buffer);
            break;
        case MPY2: // versions >10.0.0.46181
            out_group_data->mpy2_data_ptr = data_start;
            out_group_data->mpy2_size = header->chunk_size;
            log_info("  -> MPY2 Chunk found at offset %td", out_group_data->mpy2_data_ptr - mogp_data_buffer);
            break;
        case MOVI: // MapObject Vertex Indices
            out_group_data->movi_data_ptr = data_start;
            out_group_data->movi_size = header->chunk_size;
            log_info("  -> MOVI Chunk found at offset %td", out_group_data->movi_data_ptr - mogp_data_buffer);
            parse_movi_chunk(out_group_data);
            break;
        case MOVT:
            out_group_data->movt_data_ptr = data_start;
            out_group_data->movt_size = header->chunk_size;
            log_info("  -> MOVT Chunk found at offset %td", out_group_data->movt_data_ptr - mogp_data_buffer);
            parse_movt_chunk(out_group_data);
            break;
        case MONR:
            out_group_data->monr_data_ptr = data_start;
            out_group_data->monr_size = header->chunk_size;
            log_info("  -> MONR Chunk found at offset %td", out_group_data->monr_data_ptr - mogp_data_buffer);
            parse_monr_chunk(out_group_data);
            break;
        case MOTV:
            out_group_data->motv_data_ptr = data_start;
            out_group_data->motv_size = header->chunk_size;
            log_info("  -> MOTV Chunk found at offset %td", out_group_data->motv_data_ptr - mogp_data_buffer);
            parse_motv_chunk(out_group_data);
            break;
        case MOBA:
            out_group_data->moba_data_ptr = data_start;
            out_group_data->moba_size = header->chunk_size;
            log_info("  -> MOBA Chunk found at offset %td", out_group_data->moba_data_ptr - mogp_data_buffer);
            log_info("Sizeof SMOBatch: %zu", sizeof(SMOBatch));
            parse_moba_chunk(out_group_data);
            break;
        case MOQG: // versions >10.0.0.46181
            out_group_data->moqg_data_ptr = data_start;
            out_group_data->moqg_size = header->chunk_size;
            log_info("  -> MOQG Chunk found at offset %td", out_group_data->moqg_data_ptr - mogp_data_buffer);
            break;
        case MOLR:
            out_group_data->molr_data_ptr = data_start;
            out_group_data->molr_size = header->chunk_size;
            log_info("  -> MOLR Chunk found at offset %td", out_group_data->molr_data_ptr - mogp_data_buffer);
            parse_molr_chunk(out_group_data);
            break;
        case MODR:
        case MOBN:
        case MOBR:
        case MOCV:
        case MOC2:
        case MLIQ:
        case MORI:
        default:
            log_info("TODO! %c%c%c%c",
            (char)(header->chunk_name >> 24),
            (char)(header->chunk_name >> 16),
            (char)(header->chunk_name >> 8),
            (char)(header->chunk_name));
            break;
        }

        current_ptr = data_start + header->chunk_size;
    }
}

void extract_wmo_geometry(const WMORootData *wmo_root_data, FILE *output_file) {
    if (wmo_root_data->groups == NULL || wmo_root_data->group_count == 0) {
        log_error("No WMO group data found to extract geometry from");
        return;
    }

    if (output_file == NULL) {
        log_error("Output file handle is NULL. Cannot log geometry details.");
        return;
    }

    fprintf(output_file, "# WMO Geometry Data Extraction Log\n");
    fprintf(output_file, "# Root WMO contains %zu groups\n", wmo_root_data->group_count);

    log_info("Starting geometry extraction for %zu WMO groups...", wmo_root_data->group_count);

    if (wmo_root_data->mohd_data_ptr == NULL) {
        log_error("MOHD data is missing. Cannot calculate global center");
        return;
    }

    const SMOHeader *header = (const SMOHeader *)wmo_root_data->mohd_data_ptr;

    float center_x = (header->bounding_box.min[0] + header->bounding_box.max[0]) / 2.0f;
    float center_y = (header->bounding_box.min[1] + header->bounding_box.max[1]) / 2.0f;
    float center_z = (header->bounding_box.min[2] + header->bounding_box.max[2]) / 2.0f;

    log_info("Global WMO Center calculated: (%.2f, %.2f, %.2f)", center_x, center_y, center_z);

    size_t cumulative_vertex_count = 0;

    for (size_t i = 0; i < wmo_root_data->group_count; i++) {
        const WMOGroupData *group = &wmo_root_data->groups[i];

        if (group->movt_data_ptr == NULL) {
            fprintf(output_file, "g Group_%zu_SKIPPED\n", i);
            log_info("Group %zu skipped (no geometry data found)", i);
            continue;
        }

        fprintf(output_file, "\n# -------------------\n");
        fprintf(output_file, "g Group_%zu\n", i);

        /* -----------------------------------------------------------------
           1. SETUP ALL CHUNKS AND OFFSETS
           ----------------------------------------------------------------- */
        size_t num_vertices = group->movt_size / sizeof(C3Vector);
        const C3Vector *vertices = (const C3Vector *)group->movt_data_ptr;

        // Pointers for new chunks
        const C3Vector *normals = (const C3Vector *)group->monr_data_ptr;
        const C2Vector *tex_coords = (const C2Vector *)group->motv_data_ptr;

        // Offset (already calculated)
        float offset_x = group->header.boundingBox.min[0];
        float offset_y = group->header.boundingBox.min[1];
        float offset_z = group->header.boundingBox.min[2];

        fprintf(output_file, "# Found %zu vertices (MOVT), normals (MONR), and texcoords (MOTV)\n", num_vertices);

        /* -----------------------------------------------------------------
           2. VERTICES, NORMALS, AND TEXCOORDS (MOVT, MONR, MOTV) - ONE LOOP
           ----------------------------------------------------------------- */
        for (size_t j = 0; j < num_vertices; j++) {
            // A. Vertices (v) - Apply offset and coordinate swap
            float x_wmo = vertices[j].x + offset_x;
            float y_wmo = vertices[j].y + offset_y;
            float z_wmo = vertices[j].z + offset_z;

            // WMO -> OBJ Transformation
            float x_obj = x_wmo;
            float y_obj = z_wmo;
            float z_obj = -y_wmo;

            x_obj -= center_x;
            y_obj -= center_z;
            z_obj -= center_y;

            fprintf(output_file, "v %.6f %.6f %.6f\n", x_obj, y_obj, z_obj);

            // B. Texture Coordinates (vt) - Apply V-flip
            float u_wmo = tex_coords[j].x;
            float v_wmo = tex_coords[j].y;
            float v_obj = 1.0f - v_wmo; // V-Flip is commonly required

            fprintf(output_file, "vt %.6f %.6f\n", u_wmo, v_obj);

            // C. Normals (vn) - Apply the SAME coordinate swap (NO offset for normals)
            // Normals are unit vectors and should NOT be translated.
            float nx_wmo = normals[j].x;
            float ny_wmo = normals[j].y;
            float nz_wmo = normals[j].z;

            float nx_obj = nx_wmo;
            float ny_obj = nz_wmo;
            float nz_obj = -ny_wmo;

            fprintf(output_file, "vn %.6f %.6f %.6f\n", nx_obj, ny_obj, nz_obj);
        }

        /* -----------------------------------------------------------------
           3. FACES (MOVI & MOBA) - Second Loop
           ----------------------------------------------------------------- */
        const uint16_t *all_indices = (const uint16_t *)group->movi_data_ptr;
        const SMOBatch *batches = (const SMOBatch *)group->moba_data_ptr;
        size_t num_batches = group->moba_size / sizeof(SMOBatch);

        fprintf(output_file, "# Found %zu batches (MOBA) for face definitions.\n", num_batches);

        for (size_t b = 0; b < num_batches; b++) {
            const SMOBatch *batch = &batches[b];

            // Output material information (requires MTL file, but helps organize the OBJ)
            fprintf(output_file, "\n# Material Group ID: %u\n", batch->material_id);
            // In a full export, you would write: fprintf(output_file, "usemtl material_%u\n", batch->material_id);

            uint32_t start_index = batch->startIndex;
            uint32_t index_count = batch->count;

            // Iterate through the indices for this batch, three at a time (triangles)
            for (uint32_t k = start_index; k < start_index + index_count; k += 3) {

                // Get the local (0-based) vertex indices
                uint16_t v1_local = all_indices[k + 0];
                uint16_t v2_local = all_indices[k + 1];
                uint16_t v3_local = all_indices[k + 2];

                // Apply the GLOBAL offset and the 1-based OBJ requirement
                size_t v1_obj = (size_t)v1_local + 1 + cumulative_vertex_count;
                size_t v2_obj = (size_t)v2_local + 1 + cumulative_vertex_count;
                size_t v3_obj = (size_t)v3_local + 1 + cumulative_vertex_count;

                // Write face line: f v/vt/vn v/vt/vn v/vt/vn
                // Since MOVT, MOTV, and MONR indices are 1:1, all three indices are the same.
                fprintf(output_file, "f %zu/%zu/%zu %zu/%zu/%zu %zu/%zu/%zu\n",
                    v1_obj, v1_obj, v1_obj,
                    v2_obj, v2_obj, v2_obj,
                    v3_obj, v3_obj, v3_obj
                );
            }
        }

        // ----------------------------------------------------
        // C. Update Cumulative Count for the NEXT Group
        // ----------------------------------------------------
        cumulative_vertex_count += num_vertices;
    }

    log_info("Geometry data written to output file");
}

// isTransFace: triangles flagged as TRANSITION. They blend lighting from exterior to interior
char smopoly_is_trans_face(SMOPolyFlags flags) {
    return flags.f_unk && (flags.f_detail || flags.f_render);
}
// isColor: used to determine if the triangle has color/texture data (not just collision)
char smopoly_is_color(SMOPolyFlags flags) {
    return !flags.f_collision;
}
// isRenderFace: triangles that should be visible
char smopoly_is_render_face(SMOPolyFlags flags) {
    return flags.f_render && !flags.f_detail;
}
// isCollidable: Triangles that can be physically interacted with
char smopoly_is_collidable(SMOPolyFlags flags) {
    return flags.f_collision || smopoly_is_render_face(flags);
}


void wmo_load_textures_to_gpu(ArchiveManager* manager, WMORootData* root) {
    if (!root->momt_data_ptr || !root->motx_data_ptr)
        // TODO: error logging
        return;

    SMOMaterial *materials = (SMOMaterial *)root->momt_data_ptr;
    int num_materials = root->momt_size / sizeof(SMOMaterial);

    // allocate opengl ID array
    root->material_textures = (GLuint*)calloc(num_materials, sizeof(GLuint));

    log_info("Loading %d textures to GPU...", num_materials);

    for (int i = 0; i < num_materials; i++) {
        uint32_t name_offset = materials[i].texture_name_offset;

        /*
        if (name_offset == 0 || name_offset >= root->motx_size) {
            log_warn("Material %d has invalid texture offset", i);
            root->material_textures[i] = 0;
            continue;
        }*/

        const char *filename = root->motx_data_ptr + name_offset;

        HANDLE hFile = get_file_in_archives(manager, filename);

        if (hFile) {
            DWORD fileSize = SFileGetFileSize(hFile, NULL);
            if (fileSize > 0) {
                uint8_t* buffer = (uint8_t*)malloc(fileSize);

                DWORD bytesRead = 0;
                SFileReadFile(hFile, buffer, fileSize, &bytesRead, NULL);

                root->material_textures[i] = texture_load_from_blp_memory(buffer, fileSize);

                free(buffer);
            }
            SFileCloseFile(hFile);
        } else {
            log_warn("Failed to find texture: %s", filename);
            root->material_textures[i] = 0;
        }
    }
}