"""
Convert 3D model from .obj format to .wrl with colors
"""

from __future__ import annotations

# Global imports
import logging
import re
import textwrap
from pathlib import Path

# Local imports
from ..easyeda.parameters_easyeda import Ee3dModel
from .parameters_kicad_footprint import Ki3dModel, Ki3dModelBase

VRML_HEADER = """#VRML V2.0 utf8
# 3D model generated by easyeda2kicad.py (https://github.com/uPesy/easyeda2kicad.py)
"""


def get_materials(obj_data: str) -> dict[str, dict[str, str | list[str]]]:
    material_regex = "newmtl .*?endmtl"
    matchs = re.findall(pattern=material_regex, string=obj_data, flags=re.DOTALL)

    materials = {}
    for match in matchs:
        material = {}
        material_id = None
        for value in match.splitlines():
            if value.startswith("newmtl"):
                material_id = value.split()[1]
            elif value.startswith("Ka"):
                material["ambient_color"] = value.split()[1:]
            elif value.startswith("Kd"):
                material["diffuse_color"] = value.split()[1:]
            elif value.startswith("Ks"):
                material["specular_color"] = value.split()[1:]
            elif value.startswith("d "):
                # EasyEDA uses d as transparency directly (0.0 = opaque, 1.0 = transparent),
                # which matches VRML transparency semantics — use value as-is.
                try:
                    material["transparency"] = str(round(float(value.split()[1]), 4))
                except (ValueError, IndexError):
                    material["transparency"] = "0"

        if material_id is not None:
            materials[material_id] = material
    return materials


def get_vertices(
    obj_data: str,
    offset_x: float = 0.0,
    offset_y: float = 0.0,
    offset_z: float = 0.0,
) -> list[str]:
    result = []
    for line in obj_data.splitlines():
        parts = line.split()
        if len(parts) < 4 or parts[0] != "v":
            continue
        x, y, z = float(parts[1]), float(parts[2]), float(parts[3])
        result.append(
            " ".join(
                [
                    str(round((x + offset_x) / 2.54, 4)),
                    str(round((y + offset_y) / 2.54, 4)),
                    str(round((z + offset_z) / 2.54, 4)),
                ]
            )
        )
    return result


def _get_obj_bbox(
    raw_obj: str,
) -> tuple[tuple[float, float], tuple[float, float], tuple[float, float]] | None:
    """Returns ((x_min, x_max), (y_min, y_max), (z_min, z_max)) or None if no vertices."""
    x_vals, y_vals, z_vals = [], [], []
    for line in raw_obj.splitlines():
        parts = line.split()
        if len(parts) < 4 or parts[0] != "v":
            continue
        x_vals.append(float(parts[1]))
        y_vals.append(float(parts[2]))
        z_vals.append(float(parts[3]))

    if not x_vals:
        return None

    return (
        (min(x_vals), max(x_vals)),
        (min(y_vals), max(y_vals)),
        (min(z_vals), max(z_vals)),
    )


def generate_wrl_model(model_3d: Ee3dModel) -> Ki3dModel:
    if not model_3d.raw_obj:
        return Ki3dModel(
            translation=Ki3dModelBase(),
            rotation=Ki3dModelBase(),
            name=model_3d.name,
            raw_wrl=None,
        )

    materials = get_materials(obj_data=model_3d.raw_obj)

    # Step 1: Center XY on (0,0); shift Z so bottom sits at z=0 (always).
    # Step 2: Apply EE metadata offset (c_origin - canvas_origin) in mm.
    # translate(-bbox_cx, -bbox_cy, -z_min) then apply EE offset,
    # applied unconditionally for both SMD and THT.
    offset_x, offset_y, offset_z = 0.0, 0.0, 0.0
    bbox = _get_obj_bbox(model_3d.raw_obj)
    if bbox:
        (x_min, x_max), (y_min, y_max), (z_min, _) = bbox
        offset_x = -(x_min + x_max) / 2.0
        offset_y = -(y_min + y_max) / 2.0
        offset_z = -z_min
    # Add EE placement offset (already in mm, same unit as OBJ vertices)
    offset_x += model_3d.translation.x
    offset_y += model_3d.translation.y
    offset_z += model_3d.translation.z
    logging.debug(
        f"3D centering offset: X={offset_x:.2f} Y={offset_y:.2f} Z={offset_z:.2f}"
    )

    vertices = get_vertices(
        obj_data=model_3d.raw_obj,
        offset_x=offset_x,
        offset_y=offset_y,
        offset_z=offset_z,
    )

    raw_wrl = VRML_HEADER
    shapes = model_3d.raw_obj.split("usemtl")[1:]
    if not shapes:
        logging.warning(
            f"3D model '{model_3d.name}': OBJ has no 'usemtl' sections; no geometry exported"
        )
        return Ki3dModel(
            translation=Ki3dModelBase(),
            rotation=Ki3dModelBase(),
            name=model_3d.name,
            raw_wrl=None,
        )
    for shape in shapes:
        lines = shape.splitlines()
        material_name = lines[0].replace(" ", "")
        material = materials.get(material_name)
        if material is None:
            logging.warning(
                f"3D model material '{material_name}' not found, skipping shape"
            )
            continue
        index_counter = 0
        link_dict = {}
        coord_index = []
        points = []
        for line in lines[1:]:
            if line.strip() and line.split()[0] == "f":
                face = [int(tok.split("/")[0]) for tok in line.split()[1:]]
                face_index = []
                for index in face:
                    if index not in link_dict:
                        link_dict[index] = index_counter
                        face_index.append(str(index_counter))
                        points.append(vertices[index - 1])
                        index_counter += 1
                    else:
                        face_index.append(str(link_dict[index]))
                face_index.append("-1")
                coord_index.append(",".join(face_index) + ",")

        # ambientIntensity: VRML expects a scalar (0-1), not an RGB triplet.
        # Compute luminance from Ka using Rec.601 weights.
        try:
            ka = material.get("ambient_color", ["0.2", "0.2", "0.2"])
            ambient_intensity = round(
                0.299 * float(ka[0]) + 0.587 * float(ka[1]) + 0.114 * float(ka[2]), 4
            )
        except (ValueError, IndexError):
            ambient_intensity = 0.2

        transparency = material.get("transparency", "0")

        shape_str = textwrap.dedent(
            f"""
            Shape{{
                appearance Appearance {{
                    material  Material 	{{
                        diffuseColor {" ".join(material["diffuse_color"])}
                        specularColor {" ".join(material["specular_color"])}
                        ambientIntensity {ambient_intensity}
                        transparency {transparency}
                        shininess 0.5
                    }}
                }}
                geometry IndexedFaceSet {{
                    ccw TRUE
                    solid FALSE
                    coord DEF co Coordinate {{
                        point [
                            {(", ").join(points)}
                        ]
                    }}
                    coordIndex [
                        {"".join(coord_index)}
                    ]
                }}
            }}"""
        )

        raw_wrl += shape_str

    return Ki3dModel(
        translation=Ki3dModelBase(),
        rotation=Ki3dModelBase(),
        name=model_3d.name,
        raw_wrl=raw_wrl,
    )


def _log_obj_bbox(raw_obj: str) -> None:
    """Log the OBJ vertex bounding box at DEBUG level."""
    bbox = _get_obj_bbox(raw_obj)
    if not bbox:
        logging.debug("3D OBJ: no vertices found")
        return

    (x_min, x_max), (y_min, y_max), (z_min, z_max) = bbox
    logging.debug(
        "3D OBJ bbox: "
        f"X [{x_min:.6g} .. {x_max:.6g}] "
        f"Y [{y_min:.6g} .. {y_max:.6g}] "
        f"Z [{z_min:.6g} .. {z_max:.6g}]"
    )


class Exporter3dModelKicad:
    def __init__(self, model_3d: Ee3dModel | None):
        self.input = model_3d
        if model_3d and model_3d.raw_obj:
            _log_obj_bbox(model_3d.raw_obj)
        self.output = (
            generate_wrl_model(model_3d=model_3d)
            if model_3d and model_3d.raw_obj
            else None
        )
        self.output_step = model_3d.step if model_3d else None

    def export(self, output_dir: str, overwrite: bool = True) -> bool:
        """Write WRL and STEP files into *output_dir* (the .3dshapes folder).

        EasyEDA always provides both OBJ (→WRL) and STEP for the same UUID.
        Returns False if files already exist and overwrite is False, True otherwise.
        """
        if not self.output:
            return False

        model_name = self.output.name
        output_path = Path(output_dir)
        wrl_path = output_path / f"{model_name}.wrl"
        step_path = output_path / f"{model_name}.step"

        if not overwrite and (wrl_path.exists() or step_path.exists()):
            logging.warning(f"3D model files already exist, skipping: {model_name}")
            return False

        output_path.mkdir(parents=True, exist_ok=True)

        if self.output.raw_wrl:
            wrl_path.write_text(self.output.raw_wrl, encoding="utf-8")

        if self.output_step:
            # TODO: STEP is copied as-is without offset baking (unlike WRL).
            # KiCad offset stays (0,0,0), so STEP is misplaced when
            # model_3d.translation != (0,0,0) (i.e. c_origin != canvas_origin).
            # Fix options: (a) write translation into .kicad_mod and remove WRL baking
            # to avoid double-offset; (b) transform STEP geometry (needs opencascade).
            step_path.write_bytes(self.output_step)

        return True
