"""``openposeCharacter`` node: per-character OpenPose_full joint mapping.

A pure MPxLocatorNode with one connectable message-attribute slot per
OpenPose_full keypoint (``targetJoint[0..136]``). The mapping lives entirely
as Maya connections (joint.message -> openposeCharacter.targetJoint[i]) so it
survives joint renames/reparenting and round-trips through the scene file
with no separate config to keep in sync. The node has no compute() output --
it is a passive connection target read directly by the draw override
(live viewport skeleton) and the render override (OpenPose image output).
"""

import maya.api.OpenMaya as om
import maya.api.OpenMayaUI as omui

from . import constants
from . import face_blendshapes

NODE_NAME = "openposeCharacter"
NODE_ID = om.MTypeId(0x0012F5C0)
DRAW_CLASSIFICATION = "drawdb/geometry/openposeCharacter"
DRAW_REGISTRANT_ID = "openposeRendererPlugin"

# Attribute handles, filled in by initialize()
aTargetJoint = None
aEnableDisplay = None
aJointRadiusScale = None
aLimbThicknessScale = None
aCharacterName = None
aBodyModel = None
aFaceSource = None
aBlendshapeWeight = None
aFaceScale = None

# aBodyModel enum field values, in registration order (index 0 = BODY_25).
BODY_MODEL_FIELDS = [constants.BODY_MODEL_BODY_25, constants.BODY_MODEL_COCO]

# aFaceSource enum field values, in registration order (index 0 = Joints).
FACE_SOURCE_JOINTS = "Joints"
FACE_SOURCE_ARKIT_BLENDSHAPES = "ARKitBlendshapes"
FACE_SOURCE_FIELDS = [FACE_SOURCE_JOINTS, FACE_SOURCE_ARKIT_BLENDSHAPES]

DEFAULT_FACE_SCALE = 15.0


class OpenposeCharacterNode(omui.MPxLocatorNode):

    kTypeName = NODE_NAME
    kTypeId = NODE_ID

    def __init__(self):
        omui.MPxLocatorNode.__init__(self)

    def compute(self, plug, data_block):
        # Pure passive mapping node: nothing to compute.
        return None

    def isBounded(self):
        return False

    @staticmethod
    def creator():
        return OpenposeCharacterNode()

    @staticmethod
    def initialize():
        global aTargetJoint, aEnableDisplay, aJointRadiusScale
        global aLimbThicknessScale, aCharacterName, aBodyModel
        global aFaceSource, aBlendshapeWeight, aFaceScale

        msg_attr = om.MFnMessageAttribute()
        aTargetJoint = msg_attr.create("targetJoint", "tj")
        msg_attr.array = True
        # indexMatters already defaults to True (sparse, index-preserving
        # array) -- explicitly assigning True here throws kInvalidParameter
        # in Maya's Python binding, so it is deliberately left untouched.
        msg_attr.storable = True
        msg_attr.readable = True
        msg_attr.writable = True
        om.MPxNode.addAttribute(aTargetJoint)

        num_attr = om.MFnNumericAttribute()

        aEnableDisplay = num_attr.create(
            "enableDisplay", "ed", om.MFnNumericData.kBoolean, True
        )
        num_attr.keyable = True
        om.MPxNode.addAttribute(aEnableDisplay)

        aJointRadiusScale = num_attr.create(
            "jointRadiusScale", "jrs", om.MFnNumericData.kFloat, 1.0
        )
        num_attr.keyable = True
        num_attr.setMin(0.0)
        om.MPxNode.addAttribute(aJointRadiusScale)

        aLimbThicknessScale = num_attr.create(
            "limbThicknessScale", "lts", om.MFnNumericData.kFloat, 1.0
        )
        num_attr.keyable = True
        num_attr.setMin(0.0)
        om.MPxNode.addAttribute(aLimbThicknessScale)

        str_data = om.MFnStringData()
        empty_string_obj = str_data.create("")
        typed_attr = om.MFnTypedAttribute()
        aCharacterName = typed_attr.create(
            "characterName", "cn", om.MFnData.kString, empty_string_obj
        )
        typed_attr.storable = True
        om.MPxNode.addAttribute(aCharacterName)

        enum_attr = om.MFnEnumAttribute()
        aBodyModel = enum_attr.create("bodyModel", "bmd", 0)
        for field_index, field_name in enumerate(BODY_MODEL_FIELDS):
            enum_attr.addField(field_name, field_index)
        enum_attr.keyable = True
        om.MPxNode.addAttribute(aBodyModel)

        face_source_attr = om.MFnEnumAttribute()
        aFaceSource = face_source_attr.create("faceSource", "fsrc", 0)
        for field_index, field_name in enumerate(FACE_SOURCE_FIELDS):
            face_source_attr.addField(field_name, field_index)
        face_source_attr.keyable = True
        om.MPxNode.addAttribute(aFaceSource)

        weight_attr = om.MFnNumericAttribute()
        aBlendshapeWeight = weight_attr.create("blendshapeWeight", "bsw", om.MFnNumericData.kFloat, 0.0)
        weight_attr.array = True
        weight_attr.keyable = True
        weight_attr.setMin(0.0)
        weight_attr.setMax(1.0)
        om.MPxNode.addAttribute(aBlendshapeWeight)

        face_scale_attr = om.MFnNumericAttribute()
        aFaceScale = face_scale_attr.create("faceScale", "fsc", om.MFnNumericData.kFloat, DEFAULT_FACE_SCALE)
        face_scale_attr.keyable = True
        face_scale_attr.setMin(0.0)
        om.MPxNode.addAttribute(aFaceScale)


def get_mapped_joint_paths(dep_node_fn):
    """Return {keypoint_index: MDagPath} for every connected targetJoint slot.

    ``dep_node_fn`` is an MFnDependencyNode wrapping an openposeCharacter.
    Unmapped keypoint indices are simply absent from the returned dict.
    """
    result = {}
    plug = dep_node_fn.findPlug(aTargetJoint, False)
    for logical_index in plug.getExistingArrayAttributeIndices():
        if logical_index < 0 or logical_index >= constants.TOTAL_KEYPOINTS:
            continue
        element_plug = plug.elementByLogicalIndex(logical_index)
        if not element_plug.isConnected:
            continue
        source_plug = element_plug.source()
        if source_plug.isNull:
            continue
        source_node = source_plug.node()
        if not source_node.hasFn(om.MFn.kDagNode):
            continue
        dag_fn = om.MFnDagNode(source_node)
        result[logical_index] = dag_fn.getPath()
    return result


def get_body_model(dep_node_fn):
    """Return constants.BODY_MODEL_BODY_25 / BODY_MODEL_COCO for this character."""
    try:
        field_index = dep_node_fn.findPlug(aBodyModel, False).asShort()
    except RuntimeError:
        field_index = 0
    if 0 <= field_index < len(BODY_MODEL_FIELDS):
        return BODY_MODEL_FIELDS[field_index]
    return constants.BODY_MODEL_BODY_25


def get_face_source(dep_node_fn):
    """Return FACE_SOURCE_JOINTS / FACE_SOURCE_ARKIT_BLENDSHAPES for this character."""
    try:
        field_index = dep_node_fn.findPlug(aFaceSource, False).asShort()
    except RuntimeError:
        field_index = 0
    if 0 <= field_index < len(FACE_SOURCE_FIELDS):
        return FACE_SOURCE_FIELDS[field_index]
    return FACE_SOURCE_JOINTS


def get_blendshape_weights(dep_node_fn):
    """Return {arkit_blendshape_name: weight} for all 52 slots (0.0 default)."""
    plug = dep_node_fn.findPlug(aBlendshapeWeight, False)
    weights = {}
    for index, name in enumerate(face_blendshapes.ARKIT_BLENDSHAPE_NAMES):
        element_plug = plug.elementByLogicalIndex(index)
        weights[name] = element_plug.asFloat()
    return weights


def get_face_scale(dep_node_fn):
    try:
        return dep_node_fn.findPlug(aFaceScale, False).asFloat()
    except RuntimeError:
        return DEFAULT_FACE_SCALE


def iter_character_nodes():
    """Yield an MFnDependencyNode for every openposeCharacter node in the scene."""
    it = om.MItDependencyNodes(om.MFn.kPluginLocatorNode)
    while not it.isDone():
        obj = it.thisNode()
        fn = om.MFnDependencyNode(obj)
        if fn.typeName == NODE_NAME:
            yield fn
        it.next()


def register(fn_plugin):
    fn_plugin.registerNode(
        NODE_NAME,
        NODE_ID,
        OpenposeCharacterNode.creator,
        OpenposeCharacterNode.initialize,
        om.MPxNode.kLocatorNode,
        DRAW_CLASSIFICATION,
    )


def deregister(fn_plugin):
    fn_plugin.deregisterNode(NODE_ID)
