from __future__ import annotations

from pathlib import Path
from typing import Any, Literal

from gradio.components.base import Component
from gradio.data_classes import GradioModel
from pydantic import field_validator, model_validator


class AssetItem(GradioModel):
    name: str | None = None
    path: str
    url: str | None = None
    type: Literal["ply", "glb"] | None = None
    visible: bool = True
    color: list[int] | None = None
    metadata: dict[str, Any] | None = None

    @field_validator("color")
    @classmethod
    def validate_color(cls, v: list[int] | None) -> list[int] | None:
        if v is not None and len(v) != 3:
            raise ValueError("color must be a list of 3 integers [R, G, B]")
        return v


class Sync3DCompareData(GradioModel):
    assets: list[AssetItem]
    render_mode: Literal["points", "native"] = "points"
    sync_camera: bool = True
    point_size: float = 2.0
    default_zoom: float = 2.0
    min_zoom: float = 0.5
    max_zoom: float = 16.0
    height: int = 500
    num_views: int = 2


class Sync3DCompare(Component):
    """
    A Gradio custom component for synchronized comparison of 3D reconstruction outputs.
    Supports PLY and GLB files, displayed side-by-side with synchronized camera movement.
    Files can be uploaded interactively or set programmatically.

    Demos: demo
    """

    EVENTS = ["change"]
    data_model = Sync3DCompareData

    def __init__(
        self,
        value: list[dict] | None = None,
        *,
        label: str | None = "3D Comparison",
        render_mode: Literal["points", "native"] = "points",
        sync_camera: bool = True,
        point_size: float = 2.0,
        default_zoom: float = 2.0,
        min_zoom: float = 0.5,
        max_zoom: float = 16.0,
        height: int = 500,
        num_views: int = 2,
        interactive: bool = True,
        visible: bool = True,
        elem_id: str | None = None,
        elem_classes: list[str] | str | None = None,
        render: bool = True,
        **kwargs,
    ):
        self.render_mode = render_mode
        self.sync_camera = sync_camera
        self.point_size = point_size
        self.default_zoom = default_zoom
        self.min_zoom = min_zoom
        self.max_zoom = max_zoom
        self.height = height
        self.num_views = num_views

        super().__init__(
            value=value,
            label=label,
            interactive=interactive,
            visible=visible,
            elem_id=elem_id,
            elem_classes=elem_classes,
            render=render,
            **kwargs,
        )

    @staticmethod
    def _detect_type(path: str) -> str | None:
        """Auto-detect asset type from file extension."""
        ext = Path(path).suffix.lower()
        if ext == ".ply":
            return "ply"
        elif ext == ".glb":
            return "glb"
        return None

    def _validate_assets(self, value: list[dict]) -> list[dict]:
        if len(value) > self.num_views:
            raise ValueError(
                f"Received {len(value)} assets, but num_views={self.num_views}."
            )

        validated = []
        for i, item in enumerate(value):
            if not isinstance(item, dict):
                raise ValueError(f"Asset at index {i} must be a dict.")

            if "path" not in item or not item["path"]:
                raise ValueError(f"Asset at index {i} is missing required field 'path'.")

            # Auto-detect type from extension if not specified
            asset_type = item.get("type")
            if not asset_type:
                asset_type = self._detect_type(item["path"])
            if asset_type not in ("ply", "glb"):
                raise ValueError(
                    f"Unsupported or undetectable asset type at index {i}. "
                    f"Supported types are: ply, glb."
                )

            color = item.get("color")
            if color is not None and len(color) != 3:
                raise ValueError(
                    f"Asset at index {i} has invalid color. Expected [R, G, B]."
                )

            normalized = {
                "name": item.get("name") or f"Asset {i + 1}",
                "path": item["path"],
                "type": asset_type,
                "visible": item.get("visible", True),
                "color": color,
                "metadata": item.get("metadata"),
            }
            validated.append(normalized)

        return validated

    def preprocess(self, payload):
        if payload is None:
            return None
        if isinstance(payload, dict) and "assets" in payload:
            return payload["assets"]
        return payload

    def postprocess(self, value: list[dict] | None):
        if value is None:
            return None
        validated = self._validate_assets(value)
        min_zoom = max(self.min_zoom, 0.05)
        max_zoom = max(self.max_zoom, min_zoom)
        default_zoom = min(max(self.default_zoom, min_zoom), max_zoom)

        for asset in validated:
            served = self.serve_static_file(asset["path"])
            if served is not None:
                asset["url"] = served.get("url")
                asset["path"] = served.get("path", asset["path"])
            else:
                asset["url"] = asset["path"]

        return {
            "assets": validated,
            "render_mode": self.render_mode,
            "sync_camera": self.sync_camera,
            "point_size": self.point_size,
            "default_zoom": default_zoom,
            "min_zoom": min_zoom,
            "max_zoom": max_zoom,
            "height": self.height,
            "num_views": self.num_views,
        }

    def example_payload(self):
        return [
            {"name": "Example PLY", "path": "example.ply", "type": "ply"},
            {"name": "Example GLB", "path": "example.glb", "type": "glb"},
        ]

    def example_value(self):
        return [
            {"name": "Example PLY", "path": "example.ply", "type": "ply"},
            {"name": "Example GLB", "path": "example.glb", "type": "glb"},
        ]
