"""Minimal Wavefront OBJ reader/writer.

Tailored to the project's face.obj:
- Latin-1 / GB-encoded comments allowed (we tolerate undecodable bytes).
- Vertices written with 4 decimal places (matches existing file).
- Preserves comment/mtllib/group lines if requested.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Sequence


@dataclass
class ObjMesh:
    """1-indexed in OBJ files; stored 0-indexed internally."""
    positions: list[tuple[float, float, float]] = field(default_factory=list)
    texcoords: list[tuple[float, float]]        = field(default_factory=list)
    normals:   list[tuple[float, float, float]] = field(default_factory=list)
    # Each face is a list of 3 (pos_idx, uv_idx, normal_idx) tuples, 0-indexed.
    # -1 means "absent".
    faces:     list[list[tuple[int, int, int]]] = field(default_factory=list)
    header_lines: list[str] = field(default_factory=list)  # comments / mtllib

    def n_v(self) -> int:  return len(self.positions)
    def n_vt(self) -> int: return len(self.texcoords)
    def n_vn(self) -> int: return len(self.normals)
    def n_f(self) -> int:  return len(self.faces)


def read_obj(path: str) -> ObjMesh:
    """Read an OBJ file, ignoring undecodable bytes in comments."""
    with open(path, "rb") as f:
        raw = f.read()
    text = raw.decode("latin-1", errors="replace")
    mesh = ObjMesh()

    for line in text.splitlines():
        line = line.strip()
        if not line:
            continue
        if line.startswith("#") or line.startswith("mtllib") or line.startswith("o ") or line.startswith("g ") or line.startswith("s "):
            mesh.header_lines.append(line)
            continue
        parts = line.split()
        kind = parts[0]
        if kind == "v":
            mesh.positions.append((float(parts[1]), float(parts[2]), float(parts[3])))
        elif kind == "vt":
            mesh.texcoords.append((float(parts[1]), float(parts[2])))
        elif kind == "vn":
            mesh.normals.append((float(parts[1]), float(parts[2]), float(parts[3])))
        elif kind == "f":
            verts = []
            for spec in parts[1:]:
                seg = spec.split("/")
                pi = int(seg[0]) - 1 if seg[0] else -1
                ti = int(seg[1]) - 1 if len(seg) > 1 and seg[1] else -1
                ni = int(seg[2]) - 1 if len(seg) > 2 and seg[2] else -1
                verts.append((pi, ti, ni))
            # Triangulate fan if quad/n-gon (shouldn't happen for our mesh)
            for k in range(1, len(verts) - 1):
                mesh.faces.append([verts[0], verts[k], verts[k + 1]])
        # ignore everything else
    return mesh


def write_obj(path: str, mesh: ObjMesh, header: Sequence[str] | None = None) -> None:
    """Write an OBJ file with the project's conventions (1-indexed, 4dp)."""
    out: list[str] = []
    if header is not None:
        out.extend(header)
    else:
        out.append("# head3d extended face mesh")
        out.append(f"# vertices={mesh.n_v()} texcoords={mesh.n_vt()} normals={mesh.n_vn()} faces={mesh.n_f()}")

    out.append("")
    for x, y, z in mesh.positions:
        out.append(f"v  {x:.4f} {y:.4f} {z:.4f}")
    out.append("")
    for u, v in mesh.texcoords:
        out.append(f"vt {u:.4f} {v:.4f} 0.0000")
    out.append("")
    for nx, ny, nz in mesh.normals:
        out.append(f"vn {nx:.4f} {ny:.4f} {nz:.4f}")
    out.append("")
    for face in mesh.faces:
        toks = []
        for pi, ti, ni in face:
            a = str(pi + 1)
            b = str(ti + 1) if ti >= 0 else ""
            c = str(ni + 1) if ni >= 0 else ""
            toks.append(f"{a}/{b}/{c}")
        out.append("f " + " ".join(toks))

    with open(path, "w", encoding="utf-8", newline="\r\n") as f:
        f.write("\n".join(out))
        f.write("\n")
