#!/usr/bin/env python3
"""
Convert a text-based static mesh file (.txt) to binary format (.txt.bin).

Usage:
    python convert_model_to_binary.py <input.txt> [<output.bin>]

If the output path is not given it is derived by appending ".bin" to the input path,
e.g. resources/w/firebox.txt  ->  resources/w/firebox.txt.bin

Binary format (BSMF v1) -- all values little-endian:

    HEADER
        4 bytes   magic "BSMF"
        uint32    version (1)
        uint32    numVertices
        uint32    numTriangles

    VERTICES (numVertices entries):
        3 x float   position x, y, z  -- engine coordinate space
        3 x float   normal   x, y, z  -- engine coordinate space
        2 x float   UV       u, v

    TRIANGLES (numTriangles entries):
        3 x uint32  vertex indices i0, i1, i2

The same Blender->engine axis swap applied by LoadFromTextFile02 is baked in here,
so the C++ binary loader can read coordinates directly without any post-processing:
    engine_x = blender_y
    engine_y = blender_z
    engine_z = blender_x
"""

import struct
import re
import sys
import os


def _parse_floats(text):
    return [float(x) for x in re.findall(r'[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?', text)]


def _parse_ints(text):
    return [int(x) for x in re.findall(r'[-]?\d+', text)]


def _swap_axes(x, y, z):
    """Blender Y-up -> engine coordinate system (mirrors LoadFromTextFile02)."""
    return y, z, x


def convert(input_path, output_path):
    with open(input_path, 'r', encoding='utf-8') as f:
        lines = [line.rstrip('\n') for line in f]

    idx = 0

    def next_line():
        nonlocal idx
        while idx < len(lines):
            line = lines[idx]
            idx += 1
            return line
        raise EOFError("Unexpected end of file while parsing: " + input_path)

    # --- Vertices ---
    while True:
        line = next_line()
        if '===Vertices' in line:
            break

    m = re.search(r'\d+', line)
    if not m:
        raise ValueError("Could not parse vertex count from: " + line)
    num_vertices = int(m.group())

    positions = []
    normals   = []
    uvs       = []

    for i in range(num_vertices):
        line = next_line()
        # V N: Pos(x, y, z) Norm(nx, ny, nz) UV(u, v)
        nums = _parse_floats(line)
        # nums[0] = vertex index (float-parsed), then 3 pos, 3 norm, 2 uv
        if len(nums) < 9:
            raise ValueError(f"Malformed vertex line {i}: {line}")
        px, py, pz = _swap_axes(nums[1], nums[2], nums[3])
        nx, ny, nz = _swap_axes(nums[4], nums[5], nums[6])
        positions.append((px, py, pz))
        normals.append((nx, ny, nz))
        uvs.append((nums[7], nums[8]))

    # --- Triangles ---
    while True:
        line = next_line()
        if '===Triangles' in line:
            break

    m = re.search(r'\d+', line)
    if not m:
        raise ValueError("Could not parse triangle count from: " + line)
    num_triangles = int(m.group())

    triangles = []
    for i in range(num_triangles):
        line = next_line()
        ints = _parse_ints(line)
        if len(ints) != 3:
            raise ValueError(f"Malformed triangle line {i}: {line}")
        triangles.append(tuple(ints))

    # --- Write binary ---
    with open(output_path, 'wb') as out:
        out.write(b'BSMF')
        out.write(struct.pack('<I', 1))
        out.write(struct.pack('<I', num_vertices))
        out.write(struct.pack('<I', num_triangles))

        for i in range(num_vertices):
            out.write(struct.pack('<3f', *positions[i]))
            out.write(struct.pack('<3f', *normals[i]))
            out.write(struct.pack('<2f', *uvs[i]))

        for tri in triangles:
            out.write(struct.pack('<3I', *tri))

    in_size  = os.path.getsize(input_path)
    out_size = os.path.getsize(output_path)
    print(f"Converted: {input_path} ({in_size:,} bytes text) -> {output_path} ({out_size:,} bytes binary)")
    print(f"  Vertices: {num_vertices}, Triangles: {num_triangles}")


if __name__ == '__main__':
    if len(sys.argv) < 2 or len(sys.argv) > 3:
        print(f"Usage: {sys.argv[0]} <input.txt> [<output.bin>]")
        sys.exit(1)

    input_path  = sys.argv[1]
    output_path = sys.argv[2] if len(sys.argv) == 3 else input_path + '.bin'
    convert(input_path, output_path)
