"""
mesh_converter.py — Roblox .mesh parser + GLB writer.

Roblox .mesh формат:
  v1.00, v1.01:  ASCII текстовый — лёгкий парсинг.
  v2.00, v3.00, v4.00, v5.00:  бинарный.

Все версии начинаются с ASCII строки "version X.YY\n" (12+ байт), затем
зависимо от версии — либо текст, либо binary.

Документация формата (community-reverse-engineered):
  https://devforum.roblox.com/t/roblox-mesh-format/326114
  https://github.com/MaximumADHD/Roblox-File-Format/blob/main/Plugins/MeshImporter.cs

Этот модуль:
  1. parse_roblox_mesh(blob) → MeshData (vertices, normals, uvs, faces)
  2. build_glb(mesh_data) → bytes (готовый .glb файл)
  3. convert(input_path, output_path) — высокоуровневая обёртка
"""
import struct
import io
import re
from dataclasses import dataclass, field
from typing import List, Tuple
import logging

logger = logging.getLogger(__name__)


@dataclass
class Vertex:
    x: float; y: float; z: float
    nx: float = 0.0; ny: float = 0.0; nz: float = 0.0
    u: float = 0.0; v: float = 0.0
    # Vertex color (RGBA в 0-1)
    r: float = 1.0; g: float = 1.0; b: float = 1.0; a: float = 1.0


@dataclass
class MeshData:
    version: str                          # 'v1.00', 'v2.00', ...
    vertices: List[Vertex] = field(default_factory=list)
    faces: List[Tuple[int, int, int]] = field(default_factory=list)  # индексы треугольников
    has_normals: bool = True
    has_uvs: bool = True


class MeshParseError(Exception):
    pass


# ──────────────────────────────────────────────────────────────────────
# Главный диспатчер
# ──────────────────────────────────────────────────────────────────────


def parse_roblox_mesh(blob: bytes) -> MeshData:
    """Парсит Roblox .mesh файл любой версии. Бросает MeshParseError."""
    # Все версии начинаются со строки "version X.YY\n"
    nl_pos = blob.find(b'\n')
    if nl_pos < 0:
        raise MeshParseError("no newline in first line — not a roblox mesh")
    first_line = blob[:nl_pos].decode('ascii', errors='replace').strip()
    m = re.match(r'version (\d+\.\d+)', first_line)
    if not m:
        raise MeshParseError(f"bad first line: {first_line!r}")
    version = 'v' + m.group(1)

    rest = blob[nl_pos+1:]

    if version in ('v1.00', 'v1.01'):
        return _parse_v1(rest, version)
    elif version == 'v2.00':
        return _parse_v2(rest, version)
    elif version == 'v3.00':
        return _parse_v3(rest, version)
    elif version in ('v4.00', 'v4.01'):
        return _parse_v4(rest, version)
    elif version == 'v5.00':
        return _parse_v5(rest, version)
    else:
        raise MeshParseError(f"unsupported mesh version: {version}")


# ──────────────────────────────────────────────────────────────────────
# v1.00 / v1.01 — ASCII текст
# ──────────────────────────────────────────────────────────────────────


def _parse_v1(rest: bytes, version: str) -> MeshData:
    """v1: ASCII text format.

    Структура:
        <num_faces>\n
        [x,y,z][nx,ny,nz][u,v,unused] [x,y,z][nx,ny,nz][u,v,unused] [x,y,z][nx,ny,nz][u,v,unused] ...

    Каждое лицо — 3 vertex'а подряд. Vertices не дедуплицируются (поэтому
    лиц = num_faces, vertices = num_faces*3). Для GLB сделаем dedup потом.

    В v1 vertices использовали диапазон [-0.5, 0.5] (нормализованный).
    """
    text = rest.decode('ascii', errors='replace').strip()
    parts = text.split('\n', 1)
    try:
        num_faces = int(parts[0].strip())
    except ValueError:
        raise MeshParseError(f"v1: bad face count: {parts[0]!r}")

    # Парсим все [x,y,z] tuples
    body = parts[1] if len(parts) > 1 else ''
    tuples = re.findall(r'\[([^\]]+)\]', body)
    # На каждый vertex — 3 tuple ([pos][normal][uv])
    if len(tuples) < num_faces * 9:
        raise MeshParseError(
            f"v1: expected {num_faces*9} bracketed tuples, got {len(tuples)}"
        )

    vertices = []
    for i in range(num_faces * 3):
        pos = list(map(float, tuples[i*3].split(',')))
        norm = list(map(float, tuples[i*3 + 1].split(',')))
        uv = list(map(float, tuples[i*3 + 2].split(',')))
        # v1 имеет позиции в [-0.5, 0.5]; для отображения масштабируем
        vertices.append(Vertex(
            x=pos[0] * 2.0, y=pos[1] * 2.0, z=pos[2] * 2.0,
            nx=norm[0], ny=norm[1], nz=norm[2],
            u=uv[0], v=1.0 - uv[1],  # GLTF V-flip
        ))

    faces = [(i*3, i*3 + 1, i*3 + 2) for i in range(num_faces)]
    return MeshData(version=version, vertices=vertices, faces=faces)


# ──────────────────────────────────────────────────────────────────────
# v2.00 — binary
# ──────────────────────────────────────────────────────────────────────


def _parse_v2(rest: bytes, version: str) -> MeshData:
    """v2: бинарный формат.

    Структура:
        header_size       (uint16, обычно 12)
        vertex_size       (uint8,  обычно 40 — 9 float + 4 byte color)
        face_size         (uint8,  обычно 12 — 3 uint32)
        num_vertices      (uint32)
        num_faces         (uint32)
        vertices...       (num_vertices * vertex_size байт)
        faces...          (num_faces * face_size байт)

    vertex (40 байт):
        pos     (3 * float32)
        normal  (3 * float32)
        uv      (2 * float32)
        color   (4 * uint8) — RGBA
    """
    r = io.BytesIO(rest)
    header_size = struct.unpack('<H', r.read(2))[0]
    vertex_size = struct.unpack('<B', r.read(1))[0]
    face_size = struct.unpack('<B', r.read(1))[0]
    num_vertices = struct.unpack('<I', r.read(4))[0]
    num_faces = struct.unpack('<I', r.read(4))[0]
    # пропускаем оставшиеся байты header'а если он больше 12
    if header_size > 12:
        r.read(header_size - 12)

    vertices = []
    for _ in range(num_vertices):
        v_data = r.read(vertex_size)
        # pos + normal + uv
        x, y, z = struct.unpack('<3f', v_data[0:12])
        nx, ny, nz = struct.unpack('<3f', v_data[12:24])
        u, vv = struct.unpack('<2f', v_data[24:32])
        # color если есть
        if vertex_size >= 40:
            cr, cg, cb, ca = v_data[32:36]
            vert = Vertex(x, y, z, nx, ny, nz, u, 1.0 - vv,
                          cr/255.0, cg/255.0, cb/255.0, ca/255.0)
        else:
            vert = Vertex(x, y, z, nx, ny, nz, u, 1.0 - vv)
        vertices.append(vert)

    faces = []
    for _ in range(num_faces):
        f_data = r.read(face_size)
        a, b, c = struct.unpack('<3I', f_data[0:12])
        faces.append((a, b, c))

    return MeshData(version=version, vertices=vertices, faces=faces)


# ──────────────────────────────────────────────────────────────────────
# v3, v4 — расширенные форматы с LOD и skinning
# ──────────────────────────────────────────────────────────────────────


def _parse_v3(rest: bytes, version: str) -> MeshData:
    """v3: v2 + LOD support.

    Header:
        header_size  (uint16) = 16
        vertex_size  (uint8)
        face_size    (uint8)
        lod_type     (uint16)
        num_vertices (uint32)
        num_faces    (uint32)
        num_lods     (uint16)
        ...

    Для импорта нам нужен только LOD 0 (highest quality), остальные пропускаем.
    """
    r = io.BytesIO(rest)
    header_size = struct.unpack('<H', r.read(2))[0]
    vertex_size = struct.unpack('<B', r.read(1))[0]
    face_size = struct.unpack('<B', r.read(1))[0]
    _lod_type = struct.unpack('<H', r.read(2))[0]
    num_vertices = struct.unpack('<I', r.read(4))[0]
    num_faces = struct.unpack('<I', r.read(4))[0]
    num_lods = struct.unpack('<H', r.read(2))[0]
    # если header больше — дочитаем
    consumed = 16
    if header_size > consumed:
        r.read(header_size - consumed)

    # vertices
    vertices = []
    for _ in range(num_vertices):
        v_data = r.read(vertex_size)
        x, y, z = struct.unpack('<3f', v_data[0:12])
        nx, ny, nz = struct.unpack('<3f', v_data[12:24])
        u, vv = struct.unpack('<2f', v_data[24:32])
        if vertex_size >= 40:
            cr, cg, cb, ca = v_data[32:36]
            vert = Vertex(x, y, z, nx, ny, nz, u, 1.0 - vv,
                          cr/255.0, cg/255.0, cb/255.0, ca/255.0)
        else:
            vert = Vertex(x, y, z, nx, ny, nz, u, 1.0 - vv)
        vertices.append(vert)

    faces = []
    for _ in range(num_faces):
        a, b, c = struct.unpack('<3I', r.read(12))
        faces.append((a, b, c))

    # LOD info (пропускаем — нам нужен только полный mesh)
    # LOD-таблица: num_lods+1 uint32 (face offsets)
    for _ in range(num_lods + 1):
        r.read(4)

    return MeshData(version=version, vertices=vertices, faces=faces)


def _parse_v4(rest: bytes, version: str) -> MeshData:
    """v4: v3 + bones + envelope (skinning).

    Header (v4.00):
        header_size  (uint16)
        lod_type     (uint16)
        num_vertices (uint32)
        num_faces    (uint32)
        num_lods     (uint16)
        num_bones    (uint16)
        bone_names_size (uint32)
        num_subsets  (uint16)
        num_high_quality_lods (uint8)
        unused (uint8)
        ...

    Для импорта в Rublox bones мы пока не поддерживаем — забираем только vertices+faces.
    """
    r = io.BytesIO(rest)
    header_size = struct.unpack('<H', r.read(2))[0]
    _lod_type = struct.unpack('<H', r.read(2))[0]
    num_vertices = struct.unpack('<I', r.read(4))[0]
    num_faces = struct.unpack('<I', r.read(4))[0]
    num_lods = struct.unpack('<H', r.read(2))[0]
    num_bones = struct.unpack('<H', r.read(2))[0]
    bone_names_size = struct.unpack('<I', r.read(4))[0]
    num_subsets = struct.unpack('<H', r.read(2))[0]
    _hq_lods = struct.unpack('<B', r.read(1))[0]
    _unused = struct.unpack('<B', r.read(1))[0]
    consumed = 22
    if header_size > consumed:
        r.read(header_size - consumed)

    # v4 vertex = 40 байт (как v2) + envelope в отдельном блоке
    vertex_size = 40
    vertices = []
    for _ in range(num_vertices):
        v_data = r.read(vertex_size)
        x, y, z = struct.unpack('<3f', v_data[0:12])
        nx, ny, nz = struct.unpack('<3f', v_data[12:24])
        u, vv = struct.unpack('<2f', v_data[24:32])
        cr, cg, cb, ca = v_data[32:36]
        vertices.append(Vertex(x, y, z, nx, ny, nz, u, 1.0 - vv,
                               cr/255.0, cg/255.0, cb/255.0, ca/255.0))

    # envelope (skinning weights) — пропускаем
    if num_bones > 0:
        # envelope = 8 байт на vertex (4 bone indexes + 4 weights)
        r.read(num_vertices * 8)

    faces = []
    for _ in range(num_faces):
        a, b, c = struct.unpack('<3I', r.read(12))
        faces.append((a, b, c))

    return MeshData(version=version, vertices=vertices, faces=faces)


def _parse_v5(rest: bytes, version: str) -> MeshData:
    """v5: v4 + facial animation + расширенный LOD.

    Header структура почти такая же как v4, плюс несколько полей в конце.
    """
    # Парсим как v4 — для базовой геометрии этого достаточно
    return _parse_v4(rest, version)


# ──────────────────────────────────────────────────────────────────────
# GLB writer — конвертер MeshData → GLB бинарь
# ──────────────────────────────────────────────────────────────────────


def build_glb(mesh: MeshData) -> bytes:
    """Собирает GLB (glTF 2.0 binary) из MeshData.

    GLB структура:
        magic 'glTF' (4 bytes)
        version (uint32) = 2
        length (uint32) — total file size
        JSON chunk:
            length (uint32)
            type 'JSON'
            JSON payload (UTF-8)
        BIN chunk:
            length (uint32)
            type 'BIN\0'
            binary payload (vertex data + index data)
    """
    import json

    n_verts = len(mesh.vertices)
    n_faces = len(mesh.faces)

    if n_verts == 0 or n_faces == 0:
        raise MeshParseError("empty mesh — cannot build GLB")

    # Бинарные данные: POSITION (3f) + NORMAL (3f) + TEXCOORD_0 (2f) + indices (uint32)
    pos_buf = bytearray()
    norm_buf = bytearray()
    uv_buf = bytearray()
    for v in mesh.vertices:
        pos_buf.extend(struct.pack('<3f', v.x, v.y, v.z))
        norm_buf.extend(struct.pack('<3f', v.nx, v.ny, v.nz))
        uv_buf.extend(struct.pack('<2f', v.u, v.v))

    idx_buf = bytearray()
    for a, b, c in mesh.faces:
        idx_buf.extend(struct.pack('<3I', a, b, c))

    # Все буферы конкатенируются в один большой
    # Каждый должен начинаться с 4-byte alignment
    def pad4(buf):
        while len(buf) % 4 != 0:
            buf.append(0)
        return buf

    pos_offset = 0
    pos_size = len(pos_buf)
    pad4(pos_buf)

    norm_offset = len(pos_buf)
    norm_size = len(norm_buf)
    pad4(norm_buf)

    uv_offset = norm_offset + len(norm_buf)
    uv_size = len(uv_buf)
    pad4(uv_buf)

    idx_offset = uv_offset + len(uv_buf)
    idx_size = len(idx_buf)
    pad4(idx_buf)

    bin_total = pos_buf + norm_buf + uv_buf + idx_buf

    # Mins/maxes — нужны GLTF спекой для POSITION
    xs = [v.x for v in mesh.vertices]
    ys = [v.y for v in mesh.vertices]
    zs = [v.z for v in mesh.vertices]
    pos_min = [min(xs), min(ys), min(zs)]
    pos_max = [max(xs), max(ys), max(zs)]

    gltf = {
        "asset": {"version": "2.0", "generator": "rublox-rbxl-importer"},
        "scene": 0,
        "scenes": [{"nodes": [0]}],
        "nodes": [{"mesh": 0}],
        "meshes": [{
            "primitives": [{
                "attributes": {
                    "POSITION": 0,
                    "NORMAL": 1,
                    "TEXCOORD_0": 2,
                },
                "indices": 3,
                "mode": 4,  # TRIANGLES
            }],
        }],
        "buffers": [{
            "byteLength": len(bin_total),
        }],
        "bufferViews": [
            {"buffer": 0, "byteOffset": pos_offset, "byteLength": pos_size, "target": 34962},  # ARRAY_BUFFER
            {"buffer": 0, "byteOffset": norm_offset, "byteLength": norm_size, "target": 34962},
            {"buffer": 0, "byteOffset": uv_offset, "byteLength": uv_size, "target": 34962},
            {"buffer": 0, "byteOffset": idx_offset, "byteLength": idx_size, "target": 34963},  # ELEMENT_ARRAY_BUFFER
        ],
        "accessors": [
            {"bufferView": 0, "componentType": 5126, "count": n_verts, "type": "VEC3", "min": pos_min, "max": pos_max},  # FLOAT
            {"bufferView": 1, "componentType": 5126, "count": n_verts, "type": "VEC3"},
            {"bufferView": 2, "componentType": 5126, "count": n_verts, "type": "VEC2"},
            {"bufferView": 3, "componentType": 5125, "count": n_faces * 3, "type": "SCALAR"},  # UNSIGNED_INT
        ],
    }

    json_bytes = json.dumps(gltf, separators=(',', ':')).encode('utf-8')
    while len(json_bytes) % 4 != 0:
        json_bytes += b' '

    # GLB header
    total_len = 12 + 8 + len(json_bytes) + 8 + len(bin_total)

    out = bytearray()
    out.extend(b'glTF')
    out.extend(struct.pack('<I', 2))            # version
    out.extend(struct.pack('<I', total_len))    # length

    # JSON chunk
    out.extend(struct.pack('<I', len(json_bytes)))
    out.extend(b'JSON')
    out.extend(json_bytes)

    # BIN chunk
    out.extend(struct.pack('<I', len(bin_total)))
    out.extend(b'BIN\x00')
    out.extend(bin_total)

    return bytes(out)


# ──────────────────────────────────────────────────────────────────────
# CLI
# ──────────────────────────────────────────────────────────────────────


def convert(input_path: str, output_path: str) -> dict:
    """Конвертит .mesh → .glb. Возвращает dict с metadata."""
    with open(input_path, 'rb') as f:
        blob = f.read()
    mesh = parse_roblox_mesh(blob)
    glb = build_glb(mesh)
    with open(output_path, 'wb') as f:
        f.write(glb)
    return {
        'version': mesh.version,
        'vertices': len(mesh.vertices),
        'faces': len(mesh.faces),
        'input_size': len(blob),
        'output_size': len(glb),
    }


if __name__ == '__main__':
    import sys
    if len(sys.argv) < 3:
        print("usage: mesh_converter.py input.mesh output.glb")
        sys.exit(1)
    info = convert(sys.argv[1], sys.argv[2])
    for k, v in info.items():
        print(f"  {k}: {v}")
