# Scene scanning. Finds polygon mesh transforms and exportable cameras.

import math

from maya import cmds


def get_mesh_shape(transform):
    if not cmds.objExists(transform):
        return None
    shapes = cmds.listRelatives(transform, shapes=True, fullPath=True, type="mesh") or []
    for shape in shapes:
        try:
            if cmds.getAttr("{0}.intermediateObject".format(shape)):
                continue
        except Exception:
            pass
        return shape
    return None


def is_valid_polygon_mesh(transform):
    shape = get_mesh_shape(transform)
    if not shape:
        return False
    try:
        verts = cmds.polyEvaluate(shape, vertex=True)
        faces = cmds.polyEvaluate(shape, face=True)
    except Exception:
        return False
    return bool(verts) and bool(faces)


def _expand_to_mesh_transforms(nodes):
    result = []
    seen = set()
    for node in nodes or []:
        try:
            if not cmds.objExists(node):
                continue
            otype = cmds.objectType(node)
        except Exception:
            continue
        if otype == "mesh":
            candidates = cmds.listRelatives(node, parent=True, fullPath=True) or []
        else:
            candidates = [node]
            descendants = cmds.listRelatives(
                node, allDescendents=True, fullPath=True, type="mesh"
            ) or []
            for shape in descendants:
                parents = cmds.listRelatives(
                    shape, parent=True, fullPath=True
                ) or []
                candidates.extend(parents)
        for cand in candidates:
            try:
                full = cmds.ls(cand, long=True) or []
            except Exception:
                full = []
            for fpath in full:
                if fpath in seen:
                    continue
                if is_valid_polygon_mesh(fpath):
                    seen.add(fpath)
                    result.append(fpath)
    return result


def get_selected_mesh_transforms():
    sel = cmds.ls(selection=True, long=True) or []
    return _expand_to_mesh_transforms(sel)


def _is_visible(dag_path):
    if not cmds.objExists(dag_path):
        return False
    try:
        if not cmds.getAttr("{0}.visibility".format(dag_path)):
            return False
    except Exception:
        return False
    parents = cmds.listRelatives(dag_path, parent=True, fullPath=True) or []
    for parent in parents:
        if not _is_visible(parent):
            return False
    return True


def get_visible_mesh_transforms():
    all_meshes = cmds.ls(type="mesh", long=True, noIntermediate=True) or []
    transforms = []
    seen = set()
    for shape in all_meshes:
        parents = cmds.listRelatives(shape, parent=True, fullPath=True) or []
        for parent in parents:
            if parent in seen:
                continue
            if not _is_visible(parent):
                continue
            if is_valid_polygon_mesh(parent):
                seen.add(parent)
                transforms.append(parent)
    return transforms


def get_all_ancestors(transform):
    # Returns the chain from world to transform (excluding transform itself).
    chain = []
    cur = transform
    while True:
        parents = cmds.listRelatives(cur, parent=True, fullPath=True) or []
        if not parents:
            break
        cur = parents[0]
        chain.insert(0, cur)
    return chain


def _short(name):
    return name.split("|")[-1].split(":")[-1]


def _camera_scalar(shape, attr, default):
    try:
        if cmds.attributeQuery(attr, node=shape, exists=True):
            return float(cmds.getAttr("{0}.{1}".format(shape, attr)))
    except Exception:
        pass
    return float(default)


def _camera_world_matrix(transform):
    try:
        return [float(v) for v in cmds.getAttr(
            "{0}.worldMatrix[0]".format(transform)
        )]
    except Exception:
        return None


def extract_cameras(opts):
    # glTF cameras look down local -Z with +Y up, matching Maya cameras.
    out = []
    if not opts.get("include_cameras", True):
        return out

    for shape in (cmds.ls(type="camera", long=True) or []):
        try:
            if cmds.camera(shape, query=True, startupCamera=True):
                continue
        except Exception:
            if _short(shape) in ("perspShape", "frontShape", "sideShape", "topShape"):
                continue

        parents = cmds.listRelatives(
            shape, parent=True, fullPath=True
        ) or []
        if not parents:
            continue
        transform = parents[0]
        matrix = _camera_world_matrix(transform)
        if not matrix:
            continue

        znear = max(1.0e-6, _camera_scalar(shape, "nearClipPlane", 0.1))
        zfar = _camera_scalar(shape, "farClipPlane", 10000.0)
        zfar = max(znear + 1.0e-6, zfar)
        aspect = 1.0
        try:
            aspect = float(cmds.camera(shape, query=True, aspectRatio=True))
        except Exception:
            aspect = 1.0
        aspect = max(1.0e-6, aspect)

        is_ortho = bool(_camera_scalar(shape, "orthographic", 0.0))
        if is_ortho:
            width = max(1.0e-6, _camera_scalar(
                shape, "orthographicWidth", 30.0
            ))
            camera = {
                "name": _short(transform),
                "type": "orthographic",
                "orthographic": {
                    "xmag": width * 0.5,
                    "ymag": width / (2.0 * aspect),
                    "znear": znear,
                    "zfar": zfar,
                },
            }
        else:
            try:
                yfov_degrees = float(cmds.camera(
                    shape, query=True, verticalFieldOfView=True
                ))
            except Exception:
                focal = max(1.0e-6, _camera_scalar(shape, "focalLength", 35.0))
                aperture_mm = _camera_scalar(
                    shape, "verticalFilmAperture", 0.94488
                ) * 25.4
                yfov_degrees = math.degrees(
                    2.0 * math.atan(aperture_mm / (2.0 * focal))
                )
            camera = {
                "name": _short(transform),
                "type": "perspective",
                "perspective": {
                    "aspectRatio": aspect,
                    "yfov": math.radians(yfov_degrees),
                    "znear": znear,
                    "zfar": zfar,
                },
            }

        out.append({
            "name": _short(transform),
            "matrix": matrix,
            "camera": camera,
        })
    return out
