#include "gfx/mesh_obj_load.h"

#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <sstream>
#include <string>

namespace fn::gfx {

namespace {

// Parse one face vertex token "v", "v/t", "v//n", or "v/t/n".
// Returns 0-based positive index for v_idx and n_idx (or -1 if not present).
// .obj indices are 1-based; negative indices are relative-to-end.
struct FaceVert { int v = -1; int n = -1; };

FaceVert parse_face_vert(const char* tok, int n_pos, int n_nrm) {
    FaceVert r;
    int v = 0, t = 0, n = 0;
    int slashes = 0;
    const char* p = tok;
    // crude split by '/'
    char buf[3][32];
    int bi = 0;
    int bj = 0;
    buf[0][0] = buf[1][0] = buf[2][0] = '\0';
    while (*p && *p != '\0' && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n') {
        if (*p == '/') { buf[bi][bj] = '\0'; bi++; bj = 0; if (bi > 2) break; slashes++; }
        else if (bj < 31) { buf[bi][bj++] = *p; }
        p++;
    }
    if (bi <= 2) buf[bi][bj] = '\0';

    if (buf[0][0]) v = std::atoi(buf[0]);
    if (slashes >= 1 && buf[1][0]) t = std::atoi(buf[1]);
    if (slashes == 2 && buf[2][0]) n = std::atoi(buf[2]);
    (void)t;

    // resolve 1-based / negative
    if (v > 0)        r.v = v - 1;
    else if (v < 0)   r.v = n_pos + v;
    if (n > 0)        r.n = n - 1;
    else if (n < 0)   r.n = n_nrm + n;
    return r;
}

void cross3(const float a[3], const float b[3], float out[3]) {
    out[0] = a[1]*b[2] - a[2]*b[1];
    out[1] = a[2]*b[0] - a[0]*b[2];
    out[2] = a[0]*b[1] - a[1]*b[0];
}
void sub3(const float a[3], const float b[3], float out[3]) {
    out[0] = a[0]-b[0]; out[1] = a[1]-b[1]; out[2] = a[2]-b[2];
}
void normalize3(float v[3]) {
    float l = std::sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
    if (l > 0) { v[0] /= l; v[1] /= l; v[2] /= l; }
}

} // namespace

Mesh mesh_obj_parse(const char* obj_text, size_t len) {
    Mesh m;
    if (!obj_text || len == 0) return m;

    // Raw arrays from the file.
    std::vector<float> raw_pos;   // stride 3
    std::vector<float> raw_nrm;   // stride 3

    // Per-output-vertex: dedup by (v_idx, n_idx) tuple. Map "v_n" -> out index.
    // For tiny meshes a linear scan of pairs suffices, but we use a vector +
    // simple hash map via std::string key (KISS, ~MB-scale meshes still ok).
    struct Key { int v; int n; };
    std::vector<Key> out_keys;

    auto find_or_add = [&](int v_idx, int n_idx) -> uint32_t {
        // linear scan — fine for inspection meshes (<<100k unique verts).
        for (uint32_t i = 0; i < out_keys.size(); ++i) {
            if (out_keys[i].v == v_idx && out_keys[i].n == n_idx) return i;
        }
        out_keys.push_back({v_idx, n_idx});
        return (uint32_t)(out_keys.size() - 1);
    };

    // Iterate lines.
    const char* p   = obj_text;
    const char* end = obj_text + len;

    // Stash face vertices for second pass (if normals are missing globally
    // we generate per face).
    struct FaceTri { FaceVert v[3]; };
    std::vector<FaceTri> faces;

    auto skip_ws = [](const char*& q, const char* qend) {
        while (q < qend && (*q == ' ' || *q == '\t')) q++;
    };

    while (p < end) {
        // skip leading whitespace
        skip_ws(p, end);
        if (p >= end) break;
        if (*p == '#') {
            while (p < end && *p != '\n') p++;
            if (p < end) p++;
            continue;
        }
        if (*p == '\n' || *p == '\r') { p++; continue; }

        // tag
        const char* tag_start = p;
        while (p < end && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++;
        size_t tag_len = (size_t)(p - tag_start);

        auto eat_line = [&](std::string& out) {
            skip_ws(p, end);
            const char* line_start = p;
            while (p < end && *p != '\n' && *p != '\r') p++;
            out.assign(line_start, p);
            while (p < end && (*p == '\n' || *p == '\r')) p++;
        };

        if (tag_len == 1 && tag_start[0] == 'v') {
            std::string body; eat_line(body);
            float x = 0, y = 0, z = 0;
            std::sscanf(body.c_str(), "%f %f %f", &x, &y, &z);
            raw_pos.push_back(x); raw_pos.push_back(y); raw_pos.push_back(z);
        } else if (tag_len == 2 && tag_start[0] == 'v' && tag_start[1] == 'n') {
            std::string body; eat_line(body);
            float x = 0, y = 0, z = 0;
            std::sscanf(body.c_str(), "%f %f %f", &x, &y, &z);
            raw_nrm.push_back(x); raw_nrm.push_back(y); raw_nrm.push_back(z);
        } else if (tag_len == 1 && tag_start[0] == 'f') {
            std::string body; eat_line(body);
            // Split body into space-delimited tokens.
            std::vector<FaceVert> fv;
            const char* bp = body.c_str();
            const char* be = bp + body.size();
            int npos = (int)(raw_pos.size() / 3);
            int nnrm = (int)(raw_nrm.size() / 3);
            while (bp < be) {
                while (bp < be && (*bp == ' ' || *bp == '\t')) bp++;
                if (bp >= be) break;
                fv.push_back(parse_face_vert(bp, npos, nnrm));
                while (bp < be && *bp != ' ' && *bp != '\t') bp++;
            }
            if (fv.size() == 3) {
                faces.push_back({fv[0], fv[1], fv[2]});
            } else if (fv.size() == 4) {
                faces.push_back({fv[0], fv[1], fv[2]});
                faces.push_back({fv[0], fv[2], fv[3]});
            }
            // n-gons silently dropped; documented in .md
        } else {
            // unknown tag — skip to EOL
            while (p < end && *p != '\n') p++;
            if (p < end) p++;
        }
    }

    if (raw_pos.empty() || faces.empty()) return Mesh{};

    // Build output vertices/normals/indices.
    // If any face vertex lacks a normal, we'll generate face normals.
    bool need_face_normals = raw_nrm.empty();
    if (!need_face_normals) {
        for (auto& f : faces) {
            for (int k = 0; k < 3; ++k) {
                if (f.v[k].n < 0) { need_face_normals = true; break; }
            }
            if (need_face_normals) break;
        }
    }

    // First pass: build deduped vertex list with explicit normals (when present).
    // If we need face normals, we'll emit unique vertices per face (no dedup
    // across faces for normals; positions can still be shared but we just
    // duplicate to keep code simple — this is flat shading by design).
    if (need_face_normals) {
        m.positions.reserve(faces.size() * 3 * 3);
        m.normals.reserve(faces.size() * 3 * 3);
        m.indices.reserve(faces.size() * 3);
        uint32_t next = 0;
        for (auto& f : faces) {
            const float* p0 = &raw_pos[f.v[0].v * 3];
            const float* p1 = &raw_pos[f.v[1].v * 3];
            const float* p2 = &raw_pos[f.v[2].v * 3];
            float e1[3], e2[3], n[3];
            sub3(p1, p0, e1);
            sub3(p2, p0, e2);
            cross3(e1, e2, n);
            normalize3(n);
            for (int k = 0; k < 3; ++k) {
                const float* pp = &raw_pos[f.v[k].v * 3];
                m.positions.push_back(pp[0]);
                m.positions.push_back(pp[1]);
                m.positions.push_back(pp[2]);
                m.normals.push_back(n[0]);
                m.normals.push_back(n[1]);
                m.normals.push_back(n[2]);
                m.indices.push_back(next++);
            }
        }
    } else {
        m.indices.reserve(faces.size() * 3);
        for (auto& f : faces) {
            for (int k = 0; k < 3; ++k) {
                uint32_t out = find_or_add(f.v[k].v, f.v[k].n);
                m.indices.push_back(out);
            }
        }
        m.positions.resize(out_keys.size() * 3);
        m.normals.resize(out_keys.size() * 3);
        for (size_t i = 0; i < out_keys.size(); ++i) {
            int vi = out_keys[i].v;
            int ni = out_keys[i].n;
            m.positions[i*3 + 0] = raw_pos[vi*3 + 0];
            m.positions[i*3 + 1] = raw_pos[vi*3 + 1];
            m.positions[i*3 + 2] = raw_pos[vi*3 + 2];
            m.normals[i*3 + 0]   = raw_nrm[ni*3 + 0];
            m.normals[i*3 + 1]   = raw_nrm[ni*3 + 1];
            m.normals[i*3 + 2]   = raw_nrm[ni*3 + 2];
        }
    }

    return m;
}

Mesh mesh_obj_load(const char* path) {
    if (!path || !*path) return Mesh{};
    std::ifstream f(path, std::ios::binary);
    if (!f) return Mesh{};
    std::ostringstream ss;
    ss << f.rdbuf();
    std::string s = ss.str();
    return mesh_obj_parse(s.data(), s.size());
}

} // namespace fn::gfx
