"""
macOS GLB export pipeline.

Replaces cumesh + nvdiffrast + flex_gemm with:
- fast_simplification / trimesh for mesh decimation
- xatlas for UV unwrapping
- PyTorch MPS-accelerated UV rasterization + texture baking
- OpenCV cv2.inpaint for texture inpainting
"""
from typing import *
from tqdm import tqdm
import numpy as np
import torch
import torch.nn.functional as F
import cv2
from PIL import Image
import trimesh
import trimesh.visual
import xatlas

try:
    import fast_simplification
    HAS_FAST_SIMPLIFICATION = True
except ImportError:
    HAS_FAST_SIMPLIFICATION = False


def _get_device():
    """Get best available device: MPS > CPU."""
    if torch.backends.mps.is_available():
        return torch.device('mps')
    return torch.device('cpu')


def _rasterize_uv_gpu(vertices, faces, uvs, texture_size, device=None):
    """
    GPU-accelerated UV-space rasterization via vectorized PyTorch.

    For each face, computes barycentric coords for all texels in its bounding box
    in parallel on MPS/CPU. Replaces the slow Python double-loop.

    Args:
        vertices: (V, 3) vertex positions
        faces: (F, 3) face indices (int64)
        uvs: (V, 2) per-vertex UV coords in [0, 1]
        texture_size: int
        device: torch device (defaults to MPS if available)

    Returns:
        pos: (H, W, 3) interpolated 3D positions
        mask: (H, W) bool - valid texels
    """
    if device is None:
        device = _get_device()

    H = W = texture_size

    # Move to device
    verts = vertices.float().to(device)
    faces_t = faces.long().to(device)
    uv = uvs.float().to(device)

    # Gather per-face data: (F, 3, 2) UVs and (F, 3, 3) positions
    face_uvs = uv[faces_t]      # (F, 3, 2)
    face_verts = verts[faces_t]  # (F, 3, 3)

    # Scale UVs to pixel coords
    face_uvs_px = face_uvs.clone()
    face_uvs_px[..., 0] *= (W - 1)
    face_uvs_px[..., 1] *= (H - 1)

    # Output buffers
    pos_buf = torch.zeros(H, W, 3, device=device)
    mask_buf = torch.zeros(H, W, dtype=torch.bool, device=device)
    # Use a depth buffer (face index) to handle overlaps — last writer wins like nvdiffrast

    # Process in chunks to avoid OOM
    num_faces = faces_t.shape[0]
    chunk_size = 50000

    for start in range(0, num_faces, chunk_size):
        end = min(start + chunk_size, num_faces)
        chunk_uvs = face_uvs_px[start:end]   # (C, 3, 2)
        chunk_verts = face_verts[start:end]   # (C, 3, 3)
        C = chunk_uvs.shape[0]

        # Bounding boxes per face: (C,)
        bb_min_x = chunk_uvs[..., 0].min(dim=1).values.floor().clamp(min=0).int()
        bb_max_x = chunk_uvs[..., 0].max(dim=1).values.ceil().clamp(max=W - 1).int()
        bb_min_y = chunk_uvs[..., 1].min(dim=1).values.floor().clamp(min=0).int()
        bb_max_y = chunk_uvs[..., 1].max(dim=1).values.ceil().clamp(max=H - 1).int()

        # Compute max bbox size in this chunk to allocate grid
        max_w = (bb_max_x - bb_min_x + 1).max().item()
        max_h = (bb_max_y - bb_min_y + 1).max().item()

        if max_w <= 0 or max_h <= 0:
            continue

        # Create local pixel grids for each face: (C, max_h, max_w, 2)
        # Use offsets from bb_min
        local_y = torch.arange(max_h, device=device).float()
        local_x = torch.arange(max_w, device=device).float()
        gy, gx = torch.meshgrid(local_y, local_x, indexing='ij')  # (max_h, max_w)
        grid = torch.stack([gx, gy], dim=-1)  # (max_h, max_w, 2)
        grid = grid.unsqueeze(0).expand(C, -1, -1, -1).clone()  # (C, max_h, max_w, 2)

        # Offset to absolute pixel coords
        grid[..., 0] += bb_min_x.float().view(C, 1, 1)
        grid[..., 1] += bb_min_y.float().view(C, 1, 1)

        # Validity mask: within bounding box
        valid = (
            (grid[..., 0] <= bb_max_x.float().view(C, 1, 1)) &
            (grid[..., 1] <= bb_max_y.float().view(C, 1, 1)) &
            (grid[..., 0] >= 0) & (grid[..., 0] < W) &
            (grid[..., 1] >= 0) & (grid[..., 1] < H)
        )  # (C, max_h, max_w)

        # Compute barycentric coordinates for each texel relative to each face
        # v0 = uv1 - uv0, v1 = uv2 - uv0, v2 = p - uv0
        uv0 = chunk_uvs[:, 0, :]  # (C, 2)
        v0 = chunk_uvs[:, 1, :] - uv0  # (C, 2)
        v1 = chunk_uvs[:, 2, :] - uv0  # (C, 2)

        # (C, max_h, max_w, 2)
        v2 = grid - uv0.view(C, 1, 1, 2)

        # Barycentric via dot products — all vectorized
        dot00 = (v0 * v0).sum(dim=1)  # (C,)
        dot01 = (v0 * v1).sum(dim=1)  # (C,)
        dot11 = (v1 * v1).sum(dim=1)  # (C,)

        dot02 = (v2 * v0.view(C, 1, 1, 2)).sum(dim=-1)  # (C, max_h, max_w)
        dot12 = (v2 * v1.view(C, 1, 1, 2)).sum(dim=-1)  # (C, max_h, max_w)

        inv_denom = dot00 * dot11 - dot01 * dot01  # (C,)
        # Skip degenerate triangles
        non_degen = inv_denom.abs() > 1e-10
        inv_denom = torch.where(non_degen, 1.0 / inv_denom.clamp(min=1e-10), torch.zeros_like(inv_denom))

        u = (dot11.view(C, 1, 1) * dot02 - dot01.view(C, 1, 1) * dot12) * inv_denom.view(C, 1, 1)
        v = (dot00.view(C, 1, 1) * dot12 - dot01.view(C, 1, 1) * dot02) * inv_denom.view(C, 1, 1)
        w = 1.0 - u - v

        # Inside triangle test (with small epsilon for edge cases)
        eps = -1e-4
        inside = (u >= eps) & (v >= eps) & ((u + v) <= 1.0 - eps) & valid & non_degen.view(C, 1, 1)

        # Interpolate 3D positions: w*v0 + u*v1 + v*v2
        # chunk_verts: (C, 3, 3) — positions of 3 verts per face
        interp_pos = (
            w.unsqueeze(-1) * chunk_verts[:, 0:1, :].unsqueeze(2) +  # (C, 1, 1, 3)
            u.unsqueeze(-1) * chunk_verts[:, 1:2, :].unsqueeze(2) +
            v.unsqueeze(-1) * chunk_verts[:, 2:3, :].unsqueeze(2)
        )  # (C, max_h, max_w, 3)

        # Write to output buffer
        # Get absolute pixel coords for valid texels
        abs_x = grid[..., 0].long()
        abs_y = grid[..., 1].long()

        # Flatten and filter
        inside_flat = inside.reshape(-1)
        abs_x_flat = abs_x.reshape(-1)[inside_flat]
        abs_y_flat = abs_y.reshape(-1)[inside_flat]
        pos_flat = interp_pos.reshape(-1, 3)[inside_flat]

        if abs_x_flat.numel() > 0:
            pos_buf[abs_y_flat, abs_x_flat] = pos_flat
            mask_buf[abs_y_flat, abs_x_flat] = True

    return pos_buf.cpu().numpy(), mask_buf.cpu().numpy()


def _grid_sample_3d_gpu(attr_volume, coords, grid_size, query_pts, voxel_size, aabb, device=None):
    """
    GPU-accelerated trilinear sampling from sparse attribute volume.
    Uses F.grid_sample on MPS.
    """
    if device is None:
        device = _get_device()

    C = attr_volume.shape[1]
    D, H, W = int(grid_size[0]), int(grid_size[1]), int(grid_size[2])

    # Build dense volume on device
    attr_vol = attr_volume.float().to(device)
    coords_d = coords.long().to(device)

    dense_vol = torch.zeros(1, C, D, H, W, dtype=torch.float32, device=device)
    cx, cy, cz = coords_d[:, 0], coords_d[:, 1], coords_d[:, 2]
    dense_vol[0, :, cx, cy, cz] = attr_vol.T

    # Normalize query pts to [-1, 1] for grid_sample
    query = query_pts.float().to(device)
    aabb_d = aabb.float().to(device)
    vs_d = voxel_size.float().to(device)

    grid_pts = (query - aabb_d[0]) / vs_d
    grid_pts_norm = torch.stack([
        grid_pts[:, 2] / (W - 1) * 2 - 1,
        grid_pts[:, 1] / (H - 1) * 2 - 1,
        grid_pts[:, 0] / (D - 1) * 2 - 1,
    ], dim=-1)
    grid_pts_norm = grid_pts_norm.reshape(1, 1, 1, -1, 3)

    sampled = F.grid_sample(dense_vol, grid_pts_norm, mode='bilinear', align_corners=True, padding_mode='border')
    return sampled.reshape(C, -1).T.cpu()


def to_glb(
    vertices: torch.Tensor,
    faces: torch.Tensor,
    attr_volume: torch.Tensor,
    coords: torch.Tensor,
    attr_layout: Dict[str, slice],
    aabb: Union[list, tuple, np.ndarray, torch.Tensor],
    voxel_size: Union[float, list, tuple, np.ndarray, torch.Tensor] = None,
    grid_size: Union[int, list, tuple, np.ndarray, torch.Tensor] = None,
    decimation_target: int = 1000000,
    texture_size: int = 2048,
    remesh: bool = False,
    remesh_band: float = 1,
    remesh_project: float = 0.9,
    verbose: bool = False,
    use_tqdm: bool = False,
    **kwargs,
):
    """
    macOS GLB export. Replaces the CUDA pipeline (cumesh + nvdiffrast + flex_gemm)
    with fast_simplification + xatlas + PyTorch MPS rasterization.
    """
    device = _get_device()
    if verbose:
        print(f"Using device: {device}")

    # --- Input Normalization ---
    if isinstance(aabb, (list, tuple)):
        aabb = np.array(aabb)
    if isinstance(aabb, np.ndarray):
        aabb = torch.tensor(aabb, dtype=torch.float32)
    aabb = aabb.cpu()

    if voxel_size is not None:
        if isinstance(voxel_size, (int, float)):
            voxel_size = [voxel_size] * 3
        if isinstance(voxel_size, (list, tuple)):
            voxel_size = np.array(voxel_size)
        if isinstance(voxel_size, np.ndarray):
            voxel_size = torch.tensor(voxel_size, dtype=torch.float32)
        grid_size = ((aabb[1] - aabb[0]) / voxel_size).round().int()
    else:
        assert grid_size is not None
        if isinstance(grid_size, int):
            grid_size = [grid_size] * 3
        if isinstance(grid_size, (list, tuple)):
            grid_size = np.array(grid_size)
        if isinstance(grid_size, np.ndarray):
            grid_size = torch.tensor(grid_size, dtype=torch.int32)
        voxel_size = (aabb[1] - aabb[0]) / grid_size.float()

    if use_tqdm:
        pbar = tqdm(total=5, desc="Extracting GLB")
    if verbose:
        print(f"Original mesh: {vertices.shape[0]} vertices, {faces.shape[0]} faces")

    vertices = vertices.cpu()
    faces = faces.cpu()

    # --- Step 1: Mesh Cleaning ---
    if use_tqdm:
        pbar.set_description("Cleaning mesh")
    tm = trimesh.Trimesh(
        vertices=vertices.numpy(),
        faces=faces.numpy(),
        process=False,
    )
    trimesh.repair.fill_holes(tm)
    trimesh.repair.fix_normals(tm)
    if verbose:
        print(f"After hole filling: {len(tm.vertices)} vertices, {len(tm.faces)} faces")

    # --- Step 2: Simplification ---
    if decimation_target < len(tm.faces):
        if HAS_FAST_SIMPLIFICATION:
            ratio = min(1.0, decimation_target / max(len(tm.faces), 1))
            new_verts, new_faces = fast_simplification.simplify(
                tm.vertices.astype(np.float64), tm.faces,
                target_reduction=(1.0 - ratio),
            )
            tm = trimesh.Trimesh(vertices=new_verts, faces=new_faces, process=False)
        elif hasattr(tm, 'simplify_quadric_decimation'):
            tm = tm.simplify_quadric_decimation(decimation_target)
        if verbose:
            print(f"After simplification: {len(tm.vertices)} vertices, {len(tm.faces)} faces")

    trimesh.repair.fill_holes(tm)
    trimesh.repair.fix_normals(tm)

    if use_tqdm:
        pbar.update(1)

    # --- Step 3: UV Unwrapping with xatlas ---
    if use_tqdm:
        pbar.set_description("UV unwrapping")
    if verbose:
        print("UV unwrapping with xatlas...")

    vmapping, out_faces_np, out_uvs_np = xatlas.parametrize(
        tm.vertices.astype(np.float32),
        tm.faces.astype(np.uint32),
    )
    out_vertices_np = tm.vertices[vmapping].astype(np.float32)
    out_normals_np = tm.vertex_normals[vmapping].astype(np.float32)

    if verbose:
        print(f"After UV: {out_vertices_np.shape[0]} vertices, {out_faces_np.shape[0]} faces")
    if use_tqdm:
        pbar.update(1)

    # --- Step 4: Texture Baking (GPU-accelerated) ---
    if use_tqdm:
        pbar.set_description("Baking textures (MPS)" if device.type == 'mps' else "Baking textures")
    if verbose:
        print("Baking textures...")

    out_vertices_t = torch.from_numpy(out_vertices_np)
    out_faces_t = torch.from_numpy(out_faces_np.astype(np.int64))
    out_uvs_t = torch.from_numpy(out_uvs_np)

    # GPU rasterization in UV space
    pos, mask = _rasterize_uv_gpu(out_vertices_t, out_faces_t, out_uvs_t, texture_size, device)

    valid_pos = torch.from_numpy(pos[mask]).float()

    # Sample attributes from volume (GPU-accelerated)
    attr_volume = attr_volume.cpu()
    coords = coords.cpu()
    attrs_full = torch.zeros(texture_size, texture_size, attr_volume.shape[1])
    if valid_pos.shape[0] > 0:
        sampled = _grid_sample_3d_gpu(attr_volume, coords, grid_size, valid_pos, voxel_size, aabb, device)
        attrs_full[mask] = sampled

    if use_tqdm:
        pbar.update(1)

    # --- Step 5: Texture Post-Processing ---
    if use_tqdm:
        pbar.set_description("Post-processing textures")
    if verbose:
        print("Post-processing textures...")

    mask_inv = (~mask).astype(np.uint8)

    base_color = np.clip(attrs_full[..., attr_layout['base_color']].numpy() * 255, 0, 255).astype(np.uint8)
    metallic = np.clip(attrs_full[..., attr_layout['metallic']].numpy() * 255, 0, 255).astype(np.uint8)
    roughness = np.clip(attrs_full[..., attr_layout['roughness']].numpy() * 255, 0, 255).astype(np.uint8)
    alpha = np.clip(attrs_full[..., attr_layout['alpha']].numpy() * 255, 0, 255).astype(np.uint8)

    # Auto-detect transparency from baked alpha values
    alpha_valid = alpha[mask]
    if alpha_valid.size > 0 and alpha_valid.min() < 250:
        alpha_mode = 'BLEND'
        if verbose:
            print(f"Detected transparency (alpha min={alpha_valid.min()}), using BLEND mode")
    else:
        alpha_mode = 'OPAQUE'

    # Inpainting to fill UV seams
    base_color = cv2.inpaint(base_color, mask_inv, 3, cv2.INPAINT_TELEA)
    metallic = cv2.inpaint(metallic, mask_inv, 1, cv2.INPAINT_TELEA)[..., None]
    roughness = cv2.inpaint(roughness, mask_inv, 1, cv2.INPAINT_TELEA)[..., None]
    alpha = cv2.inpaint(alpha, mask_inv, 1, cv2.INPAINT_TELEA)[..., None]

    if use_tqdm:
        pbar.update(1)

    # --- Step 6: Build PBR Material & Export ---
    if use_tqdm:
        pbar.set_description("Finalizing")
    if verbose:
        print("Building PBR material...")

    material = trimesh.visual.material.PBRMaterial(
        baseColorTexture=Image.fromarray(np.concatenate([base_color, alpha], axis=-1)),
        baseColorFactor=np.array([255, 255, 255, 255], dtype=np.uint8),
        metallicRoughnessTexture=Image.fromarray(
            np.concatenate([np.zeros_like(metallic), roughness, metallic], axis=-1)
        ),
        metallicFactor=1.0,
        roughnessFactor=1.0,
        alphaMode=alpha_mode,
        doubleSided=True,
    )

    # Coordinate system conversion (Y-up to Z-up for GLB)
    vertices_out = out_vertices_np.copy()
    normals_out = out_normals_np.copy()
    vertices_out[:, 1], vertices_out[:, 2] = out_vertices_np[:, 2].copy(), -out_vertices_np[:, 1].copy()
    normals_out[:, 1], normals_out[:, 2] = out_normals_np[:, 2].copy(), -out_normals_np[:, 1].copy()
    uvs_out = out_uvs_np.copy()
    uvs_out[:, 1] = 1 - uvs_out[:, 1]

    textured_mesh = trimesh.Trimesh(
        vertices=vertices_out,
        faces=out_faces_np,
        vertex_normals=normals_out,
        process=False,
        visual=trimesh.visual.TextureVisuals(uv=uvs_out, material=material),
    )

    if use_tqdm:
        pbar.update(1)
        pbar.close()
    if verbose:
        print("Done")

    return textured_mesh
