import math, os, re

import pluginUtils as pU
log = pU.log.getLogger('V3D-MY')
import pluginUtils.gltf as gltf

import maya.cmds as cmds
import maya.mel as mel
import maya.OpenMaya as OpenMaya
import maya.OpenMayaAnim as OpenMayaAnim
import maya.api.OpenMaya as OpenMaya2

try:
    from PySide6.QtGui import QImage
except ImportError:
    # COMPAT: Maya < 2025
    from PySide2.QtGui import QImage

ANIM_ATTR_MAP = {
    'translation': 'translate',
    'rotation': 'rotate',
    'scale': 'scale'
}

ANIM_INTERP_MAP = {
    'auto': 'LINEAR',
    'spline': 'CUBICSPLINE',
    'linear': 'LINEAR',
    'clamped': 'CUBICSPLINE',
    'step': 'STEP',
    'stepnext': 'STEP',
    'flat': 'CUBICSPLINE',
    'fixed': 'CUBICSPLINE',
    'plateau': 'CUBICSPLINE',
    'fast': 'CUBICSPLINE', # TODO: check if we still need it
    'slow': 'CUBICSPLINE' # TODO: check if we still need it
}

ANIM_TIME_MAP = {
    'game': 15.0,
    'film': 24.0,
    'pal': 25.0,
    'ntsc': 30.0,
    'show': 48.0,
    'palf': 50.0,
    'ntscf': 60.0
}

SKYDOME_TEX_RES_MAP = {
    0: 64,
    1: 128,
    2: 256,
    3: 512,
    4: 1024
}

MAX_JOINTS_PER_VERT = 4
WEIGHT_EPSILON = 0.000001
CURVE_ANGLE_EPS = 1e-7 # the max angle between co-directional vectors

def getName(mNode):
    shortName = cmds.ls(mNode, shortNames=True)[0]
    # NOTE: ls returns long names for objects with a non-unique short name, e.g.
    # '|myObj' and '|myParent|myObj' instead of 'myObj' even if the shortName
    # parameter is True.
    shortName = shortName.split('|')[-1]
    return shortName

def isMesh(mNode):
    """Check if a node is a valid mesh object"""
    return (cmds.objectType(mNode) == 'mesh' and
            not cmds.getAttr(mNode + '.intermediateObject') and
            cmds.listConnections(mNode, type='shadingEngine'))

def isMeshType(mNode):
    # make additional check for 'type' to prevent warnings
    return isMesh(mNode) and 'type' in cmds.ls(nodeTypes=True) and cmds.ls(cmds.listHistory(mNode), type='type')

def isNurbsCurve(mNode):
    return (cmds.objectType(mNode) == 'nurbsCurve' and not cmds.getAttr(mNode + '.intermediateObject'))

def isBezierCurve(mNode):
    return (cmds.objectType(mNode) == 'bezierCurve' and not cmds.getAttr(mNode + '.intermediateObject'))

def isNurbsSurface(mNode):
    return (cmds.objectType(mNode) == 'nurbsSurface' and not cmds.getAttr(mNode + '.intermediateObject'))

def isCamera(mNode):
    return (cmds.objectType(mNode) == 'camera')

def isLight(mNode):
    return (cmds.objectType(mNode) in ('ambientLight', 'areaLight', 'directionalLight', 'pointLight', 'spotLight', 'aiAreaLight'))

def isLightProbe(mNode):
    return (cmds.objectType(mNode) in ('v3dReflectionCubemap', 'v3dReflectionPlane'))

def isClippingPlane(mNode):
    return (cmds.objectType(mNode) == 'v3dClippingPlane')

def isSkyDomeLight(mNode):
    return (cmds.objectType(mNode) == 'aiSkyDomeLight')

def isJoint(mNode):
    return (cmds.objectType(mNode) == 'joint')

def isTransformNode(mNode):
    return (cmds.objectType(mNode) == 'transform')

def isLocatorNode(mNode):
    return (cmds.objectType(mNode) == 'locator')

def getParent(mNode):
    rel = cmds.listRelatives(mNode, parent=True, fullPath=True)
    if rel:
        return rel[0]
    else:
        return None

def getTransformNode(mObj):
    return cmds.listRelatives(mObj, type='transform', parent=True, fullPath=True)[0]

def getFirstShape(mNode):
    shapes = cmds.listRelatives(mNode, shapes=True, fullPath=True)

    if shapes:
        return shapes[0]
    else:
        return None

def getMeshType(mMesh):
    return cmds.ls(cmds.listHistory(mMesh), type='type')[0]

def getUniqueShadingGroups(mMesh):
    """Retrieve unique shading groups / shading engines from the given mesh"""
    shadingGrps = cmds.listConnections(mMesh, type='shadingEngine')
    if shadingGrps:
        return list(set(shadingGrps))
    else:
        return []

def extractMeshData(mMesh, mShadingGroup, exportTangents):

    cmds.select(mShadingGroup)

    selList = OpenMaya.MSelectionList()
    OpenMaya.MGlobal.getActiveSelectionList(selList)

    if selList.isEmpty():
        cmds.select(deselect=True)
        return None

    mMeshOrig = None
    mDagPath = OpenMaya.MDagPath()
    mObjComp = OpenMaya.MObject()

    meshFound = False

    # search for the mesh inside the shader group / set selection
    for i in range(selList.length()):
        selList.getDagPath(i, mDagPath, mObjComp)
        if mDagPath.fullPathName() == mMesh:
            meshFound = True
            break

    if not meshFound:
        return None

    shapeTargets = getBlendShapeTargets(mMesh)
    if shapeTargets:
        numShapeTargets = len(shapeTargets)

        targets = []

        for i in range(numShapeTargets):
            targets.append({
                'name': shapeTargets[i]['name'],
                'weight': shapeTargets[i]['weight'],
                'positions': [],
                'normals': []
            })

        # use dag path to the original (undeformed) object, object component remains the same
        mMeshOrig = getMeshOriginal(mMesh)
    else:
        numShapeTargets = 0

    skinCluster = getSkinCluster(mMesh)
    if skinCluster:
        inflObjs = OpenMaya.MDagPathArray()
        skinCluster.influenceObjects(inflObjs)
        jointsMax = inflObjs.length()

        mWeights = OpenMaya.MDoubleArray()

        util = OpenMaya.MScriptUtil(0)
        intPtr = util.asUintPtr()

        # return all mesh weights
        mObjCompNull = OpenMaya.MObject()
        skinCluster.getWeights(mDagPath, mObjCompNull, mWeights, intPtr)

        mMeshOrig = getMeshOriginal(mMesh)

    # use dag path to the original (undeformed) object, object component remains the same
    if mMeshOrig:
        mDagPath = getDagPath(mMeshOrig)

    if mObjComp.isNull():
        mMeshIter = OpenMaya.MItMeshPolygon(mDagPath)
    else:
        mMeshIter = OpenMaya.MItMeshPolygon(mDagPath, mObjComp)

    meshFn = OpenMaya.MFnMesh(mDagPath)

    indices = []

    positions = []
    normals = []
    tangents = []
    colors = []

    joints = []
    weights = []

    mVertIds = OpenMaya.MIntArray()
    mFaceVertIds = OpenMaya.MIntArray()
    mVertPoints = OpenMaya.MPointArray()

    normal = OpenMaya.MVector()

    mNormals = OpenMaya.MFloatVectorArray()
    meshFn.getNormals(mNormals)

    uvs = []
    uvSetNames = []

    allUVSetNames = []
    meshFn.getUVSetNames(allUVSetNames)
    for name in allUVSetNames:
        if meshFn.numUVs(name) > 0:
            uvSetNames.append(name)

    numUVs = len(uvSetNames)
    if numUVs:
        uvUtil = OpenMaya.MScriptUtil()
        uvUtil.createFromList([0, 0], 2)
        uvPtr = uvUtil.asFloat2Ptr()
        uvs = [[] for i in range(numUVs)]
    else:
        exportTangents = False

    if exportTangents:
        mTangents = OpenMaya.MFloatVectorArray()
        meshFn.getTangents(mTangents, OpenMaya.MSpace.kObject, uvSetNames[0])

        mBinormals = OpenMaya.MFloatVectorArray()
        meshFn.getBinormals(mBinormals, OpenMaya.MSpace.kObject, uvSetNames[0])

    numColorLayers = meshFn.numColorSets()
    vColorLayers = []
    colorLayerNames = []
    if numColorLayers > 0:
        currentColorLayer = meshFn.currentColorSetName()
        meshFn.getColorSetNames(colorLayerNames)
        colors = [[] for i in range(numColorLayers)]
        defaultVColor = OpenMaya.MColor(0.0, 0.0, 0.0, 1.0)
        for i in range(numColorLayers):
            vertexColorList = OpenMaya.MColorArray()
            meshFn.getVertexColors(vertexColorList, colorLayerNames[i], defaultVColor)
            if colorLayerNames[i] == currentColorLayer:
                vColorLayers.insert(0, vertexColorList)
            else:
                vColorLayers.append(vertexColorList)

    vNewIdx = 0
    # [] * N does not work!
    vIndMap = [[] for i in range(meshFn.numVertices())]

    posScale = getScaleFactor()

    while not mMeshIter.isDone():
        mMeshIter.getTriangles(mVertPoints, mVertIds)
        mMeshIter.getVertices(mFaceVertIds)

        faceVertIds = list(mFaceVertIds)

        for vPoint, vIdx in zip(mVertPoints, mVertIds):

            position = (posScale * vPoint.x, posScale * vPoint.y, posScale * vPoint.z)

            faceVertIdx = faceVertIds.index(vIdx)
            normIdx = mMeshIter.normalIndex(faceVertIdx)
            mNorm = mNormals[normIdx]
            normal = (mNorm.x, mNorm.y, mNorm.z)

            uv = []

            for uvSetName in uvSetNames:
                if uvSetName is not None:
                    try:
                        mMeshIter.getUV(faceVertIdx, uvPtr, uvSetName)
                    except:
                        # NOTE: try ... except can be redundant, because completely
                        # empty uv sets are already filtered. Although, in case of
                        # partial uv sets (if it's a thing in Maya) all UVs will be
                        # still disabled.
                        log.warning('Not enough UV mapping info: {}'.format(getName(mMesh) + ' ' + uvSetName))

                        # NOTE: store zeroes in this UV
                        uvSetNames[uvSetNames.index(uvSetName)] = None

                        u = 0
                        v = 0
                    else:
                        u = uvUtil.getFloat2ArrayItem(uvPtr, 0, 0)
                        # NOTE: to comply with glTF spec [0,0] upper left angle
                        v = 1 - uvUtil.getFloat2ArrayItem(uvPtr, 0, 1)

                    if math.isinf(u) or math.isinf(v) or math.isnan(u) or math.isnan(v):
                        log.warning('Wrong UV coordinates: {}'.format(getName(mMesh)))

                        # NOTE: store zeroes in this UV
                        uvSetNames[uvSetNames.index(uvSetName)] = None

                        u = 0
                        v = 0
                else:
                    u = 0
                    v = 0

                uv.append((u, v))

            if exportTangents:
                tangIdx = mMeshIter.tangentIndex(faceVertIdx)

                mTan = mTangents[tangIdx]
                mBin = mBinormals[tangIdx]

                tangent = (mTan.x, mTan.y, mTan.z, 1 if ((mNorm ^ mTan) * mBin > 0) else -1)

            vColors = []
            for vertexColorList in vColorLayers:
                color = vertexColorList[vIdx]
                vColors.append((color.r, color.g, color.b, color.a))

            if shapeTargets:
                posIdx = mMeshIter.vertexIndex(faceVertIdx)

                for i in range(numShapeTargets):
                    mTargPos = shapeTargets[i]['mVertPoints'][posIdx]
                    mTargNorm = shapeTargets[i]['mNormals'][normIdx]

                    # store displacements

                    shapeTargets[i]['tmpPos'] = (posScale * mTargPos.x - position[0],
                                                 posScale * mTargPos.y - position[1],
                                                 posScale * mTargPos.z - position[2])

                    shapeTargets[i]['tmpNorm'] = (mTargNorm.x - normal[0],
                                                  mTargNorm.y - normal[1],
                                                  mTargNorm.z - normal[2])
            if skinCluster:
                vJoints = []
                vWeights = []

                for jointIdx in range(jointsMax):
                    weight = mWeights[jointsMax * vIdx + jointIdx]

                    if weight > WEIGHT_EPSILON:
                        vJoints.append(jointIdx)
                        vWeights.append(weight)

                    # enough
                    if len(vJoints) == MAX_JOINTS_PER_VERT:
                        break

                for fill in range(0, MAX_JOINTS_PER_VERT - len(vJoints)):
                    vJoints.append(0)
                    vWeights.append(0)

            indexNotFound = True

            for i in vIndMap[vIdx]:

                if (positions[3*i] != position[0] or
                    positions[3*i+1] != position[1] or
                    positions[3*i+2] != position[2] or
                    normals[3*i] != normal[0] or
                    normals[3*i+1] != normal[1] or
                    normals[3*i+2] != normal[2]):

                    continue

                uvNotFound = False

                for j in range(numUVs):
                    if uvs[j][2*i] != uv[j][0] or uvs[j][2*i+1] != uv[j][1]:
                        uvNotFound = True

                if uvNotFound:
                    continue

                colorNotFound = False

                for j in range(numColorLayers):
                    if (colors[j][3*i] != vColors[j][0] or colors[j][3*i+1] != vColors[j][1] or
                        colors[j][3*i+2] != vColors[j][2] or colors[j][3*i+2] != vColors[j][3]):

                        colorNotFound = True
                        break

                if colorNotFound:
                    continue

                targetNotFound = False

                for j in range(numShapeTargets):
                    targPos = shapeTargets[j]['tmpPos']
                    targNorm = shapeTargets[j]['tmpNorm']

                    if (targets[j]['positions'][3*i] != targPos[0] or
                        targets[j]['positions'][3*i+1] != targPos[1] or
                        targets[j]['positions'][3*i+2] != targPos[2]):
                        targetNotFound = True
                        break

                    if (targets[j]['normals'][3*i] != targNorm[0] or
                        targets[j]['normals'][3*i+1] != targNorm[1] or
                        targets[j]['normals'][3*i+2] != targNorm[2]):
                        targetNotFound = True
                        break

                if targetNotFound:
                    continue

                # vertex is the same

                indexNotFound = False
                indices.append(i)
                break

            if indexNotFound:
                indices.append(vNewIdx)
                vIndMap[vIdx].append(vNewIdx)
                vNewIdx += 1

                positions.append(position[0])
                positions.append(position[1])
                positions.append(position[2])

                normals.append(normal[0])
                normals.append(normal[1])
                normals.append(normal[2])

                for j in range(numUVs):
                    uvs[j].append(uv[j][0])
                    uvs[j].append(uv[j][1])

                if exportTangents:
                    tangents.append(tangent[0])
                    tangents.append(tangent[1])
                    tangents.append(tangent[2])
                    tangents.append(tangent[3])

                for j in range(numColorLayers):
                    colors[j].append(vColors[j][0])
                    colors[j].append(vColors[j][1])
                    colors[j].append(vColors[j][2])
                    colors[j].append(vColors[j][3])

                for j in range(numShapeTargets):
                    targPos = shapeTargets[j]['tmpPos']
                    targNorm = shapeTargets[j]['tmpNorm']

                    targets[j]['positions'].append(targPos[0])
                    targets[j]['positions'].append(targPos[1])
                    targets[j]['positions'].append(targPos[2])

                    targets[j]['normals'].append(targNorm[0])
                    targets[j]['normals'].append(targNorm[1])
                    targets[j]['normals'].append(targNorm[2])

                if skinCluster:
                    joints.append(vJoints[0])
                    joints.append(vJoints[1])
                    joints.append(vJoints[2])
                    joints.append(vJoints[3])

                    weights.append(vWeights[0])
                    weights.append(vWeights[1])
                    weights.append(vWeights[2])
                    weights.append(vWeights[3])

        mMeshIter.next()

    meshData = {
        'indices': indices,
        'positions': positions,
        'normals': normals
    }

    if numUVs:
        meshData['uvs'] = uvs

    if exportTangents:
        meshData['tangents'] = tangents

    if numColorLayers:
        meshData['colors'] = colors
        meshData['colorLayerNames'] = colorLayerNames

    if shapeTargets:
        meshData['targets'] = targets

    if skinCluster:
        meshData['joints'] = joints
        meshData['weights'] = weights

    cmds.select(deselect=True)
    return meshData

def extractMeshDataLine(mMesh, mShadingGroup):

    cmds.select(mShadingGroup)

    selList = OpenMaya.MSelectionList()
    OpenMaya.MGlobal.getActiveSelectionList(selList)

    if selList.isEmpty():
        cmds.select(deselect=True)
        return None

    mMeshOrig = None
    mDagPath = OpenMaya.MDagPath()
    mObjComp = OpenMaya.MObject()

    meshFound = False

    # search for the mesh inside the shader group / set selection
    for i in range(selList.length()):
        selList.getDagPath(i, mDagPath, mObjComp)
        if mDagPath.fullPathName() == mMesh:
            meshFound = True
            break

    if not meshFound:
        return None

    mMeshOrig = getMeshOriginal(mMesh)

    # use dag path to the original (undeformed) object, object component remains the same
    if mMeshOrig:
        mDagPath = getDagPath(mMeshOrig)

    if mObjComp.isNull():
        mMeshIter = OpenMaya.MItMeshEdge(mDagPath)
    else:
        mMeshIter = OpenMaya.MItMeshEdge(mDagPath, mObjComp)

    meshFn = OpenMaya.MFnMesh(mDagPath)

    indices = []

    positions = []

    vNewIdx = 0
    vIndMap = []
    vertex_index_to_new_index = {}

    posScale = getScaleFactor()

    while not mMeshIter.isDone():

        for i in range(2):
            vPoint = mMeshIter.point(i)
            vIdx = mMeshIter.index(i)

            if not vIdx in vIndMap:
                indices.append(vNewIdx)
                vIndMap.append(vIdx)
                vertex_index_to_new_index[vIdx] = vNewIdx
                vNewIdx += 1

                positions.append(posScale * vPoint.x)
                positions.append(posScale * vPoint.y)
                positions.append(posScale * vPoint.z)
            else:
                indices.append(vertex_index_to_new_index[vIdx])

        mMeshIter.next()

    meshData = {
        'indices': indices,
        'positions': positions,
    }

    cmds.select(deselect=True)
    return meshData

def extractNurbsCurveData(shape, steps):

    cmds.select(shape)
    selList = OpenMaya.MSelectionList()
    OpenMaya.MGlobal.getActiveSelectionList(selList)
    if selList.isEmpty():
        cmds.select(deselect=True)
        return node

    mDagPath = OpenMaya.MDagPath()
    mObjComp = OpenMaya.MObject()
    meshFound = False

    # search for the mesh inside the shader group / set selection
    for i in range(selList.length()):
        selList.getDagPath(i, mDagPath, mObjComp)
        if mDagPath.fullPathName() == shape:
            meshFound = True
            break

    if not meshFound:
        return

    curve = OpenMaya.MFnNurbsCurve(mDagPath)
    curveTmp = OpenMaya.MFnNurbsCurve()

    counter = 0; indx = 0; tmpInt = 0; pointsCount = 0

    numPieces = curve.numSpans()
    pointsCount = numPieces * (steps+1)

    positions = [0] * pointsCount * 3
    indices = [0] * pointsCount * 2
    nextPoint = OpenMaya.MPoint()
    pointTmp = OpenMaya.MPoint()
    pointTmp2 = OpenMaya.MPoint()
    vecTmp = OpenMaya.MVector()
    vecTmp2 = OpenMaya.MVector()
    posScale = getScaleFactor()

    def dotProduct(v1, v2):
        return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z
    def vSqrLen(v):
        return v.x*v.x + v.y*v.y + v.z*v.z
    def vLen(v):
        return math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z)

    for p in range(numPieces):
        deg = curve.degree()
        cvs = OpenMaya.MPointArray()
        knots = OpenMaya.MDoubleArray()
        pt = OpenMaya.MPoint()

        # must specify at least (curve's degree + 1) CVs to get a curve with a single span.
        for i in range(deg + 1):
            curve.getCV(p + i, pt)
            cvs.append(pt)
        for i in range(deg * 2): # numSpans + 2 * deg - 1, numSpans == 1
            knots.append(curve.knot(p + i))

        curveTmp.create(cvs, knots, deg, 1, False, False)
        curveTmp.updateCurve()

        pieceLen = curveTmp.length()
        currentLength = 0.0
        inc = pieceLen / steps
        if pieceLen > 0:
            for i in range(steps+1):
                param = curveTmp.findParamFromLength(currentLength)
                curveTmp.getPointAtParam(param, nextPoint)
                nextPoint = nextPoint * posScale

                if counter > 1:
                    pointTmp2.x = positions[tmpInt - 6]
                    pointTmp2.y = positions[tmpInt - 5]
                    pointTmp2.z = positions[tmpInt - 4]
                    pointTmp.x = positions[tmpInt - 3]
                    pointTmp.y = positions[tmpInt - 2]
                    pointTmp.z = positions[tmpInt - 1]

                    vecTmp = nextPoint - pointTmp
                    vecTmp2 = pointTmp - pointTmp2

                    if counter > 1:
                        if vSqrLen(vecTmp) * vSqrLen(vecTmp2) > 0.0:
                            if abs(dotProduct(vecTmp, vecTmp2) / (vLen(vecTmp) * vLen(vecTmp2)) - 1.0) < CURVE_ANGLE_EPS:
                                # replace the previous point by nextPoint
                                tmpInt -= 3; indx -= 2; counter -= 1
                        else:
                            currentLength += inc
                            continue

                positions[tmpInt] = nextPoint.x
                positions[tmpInt + 1] = nextPoint.y
                positions[tmpInt + 2] = nextPoint.z
                tmpInt += 3
                indices[indx] = counter
                indices[indx + 1] = counter
                indx += 2

                counter += 1
                currentLength += inc

        dagPath = curveTmp.fullPathName()
        cmds.delete(getParent(curveTmp.partialPathName()))

    indices = indices[1 : indx - 1]
    positions = positions[: (indx - 2) * 3]

    meshData = {
        'indices': indices,
        'positions': positions
    }
    cmds.select(deselect=True)
    return meshData

def extractBezierCurveData(shape, steps):

    bezierName = getName(shape)

    duplicateShape = cmds.duplicate(shape, name = bezierName + 'Converted')[0]
    cmds.select(duplicateShape)

    log.info('Converting {} bezier to nurbs curve'.format(bezierName))
    cmds.bezierCurveToNurbs()
    cmds.select(deselect=True)

    nodeConv = cmds.ls(duplicateShape, long = True)[0]
    curveData = extractNurbsCurveData(nodeConv, steps)

    cmds.delete(nodeConv)

    return curveData

def getBlendShapeTargets(mMesh, extractGeometry=True):
    """
    NOTE: Use blendShape.weight attribute carefully, because it can be
    discontinuous if some blend shapes were removed from the mesh during
    modelling, e.g:

        blendShape.weight[0]: target
        blendShape.weight[1]: ---
        blendShape.weight[2]: target

    Moreover, when trying to access a non-existent weight as follows:

        cmds.getAttr('blendShape.weight[1]')

    a new weight attribute 'w[1]' without an actual target object will be
    created on the mesh.

    """

    shapeTargets = []

    blendShapes = cmds.ls(cmds.listHistory(mMesh), type='blendShape')
    if not blendShapes:
        return shapeTargets

    blendShape = blendShapes[0]

    weights = cmds.blendShape(blendShape, query=True, weight=True)
    if not weights:
        return shapeTargets

    targetObjs = cmds.blendShape(blendShape, query=True, target=True)

    # check for extractGeometry to report once
    if extractGeometry and len(weights) != len(targetObjs):
        log.warning('Blend shape targets missing for {}. Please rebuild target meshes in the Shape Editor'.format(blendShape))

    numVerts = cmds.polyEvaluate(mMesh, vertex=True)

    for i in range(len(targetObjs)):
        obj = targetObjs[i]

        if cmds.polyEvaluate(obj, vertex=True) != numVerts:
            log.error('Incorrect {} morph target: {}'.format(getName(mMesh), obj))
            continue

        target = {
            'name': getName(obj),
            'weight': weights[i],
            # used for animation
            'blendShape': blendShape
        }

        if extractGeometry:
            dagPath = getDagPath(obj)
            meshFn = OpenMaya.MFnMesh(dagPath)

            mVertPoints = OpenMaya.MPointArray()
            meshFn.getPoints(mVertPoints)

            mNormals = OpenMaya.MFloatVectorArray()
            meshFn.getNormals(mNormals)

            target['mVertPoints'] = mVertPoints
            target['mNormals'] = mNormals

        shapeTargets.append(target)

    return shapeTargets

def getInputNode(mNode, *attrNames):
    currentNode = mNode

    for attr in attrNames:
        if attr in cmds.listAttr(currentNode):
            colorConn = cmds.listConnections(currentNode + '.' + attr)
            if colorConn and colorConn[0]:
                currentNode = colorConn[0]
            else:
                return None
        else:
            return None

    return currentNode

def extractTexture(mNode, *attrNames):

    texNode = getInputNode(mNode, *attrNames)

    if texNode and isValidFileNode(texNode):
        return texNode
    else:
        return None

def isValidFileNode(node):
    if (cmds.objectType(node) == 'file' or cmds.objectType(node) == 'aiImage') and os.path.isfile(extractTexturePath(node)):
        return True
    else:
        return False

def extractTexturePath(mFileNode):
    if cmds.objectType(mFileNode) == 'aiImage':
        return cmds.getAttr(mFileNode + '.filename')
    else:
        return cmds.getAttr(mFileNode + '.fileTextureName')


def extractTexCoordShapeRefs(textures):
    """
    Extract shape names referenced by material textures and return as set()
    in case of different UV indices for the same texture.
    """

    shapes = set()
    isUvDiffer = False
    for tex in textures:
        if not cmds.objectType(tex) == 'aiImage':
            indices = set()
            uvSets = cmds.uvLink(query=True, texture=tex)
            for uvSet in uvSets:
                shapeNumMatch = re.search(r'(\w+)\.uvSet\[(\d+)\].', uvSet)
                if shapeNumMatch:
                    shapes.add(shapeNumMatch.group(1))
                    indices.add(int(shapeNumMatch.group(2)))
            if len(indices) > 1:
                isUvDiffer = True

    if isUvDiffer:
        return shapes
    else:
        return None

def extractTexCoordIndex(mFileNode, shape=None):
    uvSets = cmds.uvLink(query=True, texture=mFileNode)
    for uvSet in uvSets:
        shapeNumMatch = re.search(r'(\w+)\.uvSet\[(\d+)\].', uvSet)
        if shapeNumMatch:
            if shape:
                if shapeNumMatch.group(1) == shape:

                    # get full path to the first shape
                    # getUVSetNames does not work with shared shapes
                    shape = cmds.ls(shape, long=True)[0]

                    shapeUVSetNames = getExportableUVSetNames(shape)

                    uvSetName = cmds.getAttr(uvSet)
                    # returns array in case of shared shapes
                    if type(uvSetName) == list:
                        uvSetName = uvSetName[0]

                    if uvSetName in shapeUVSetNames:
                        return shapeUVSetNames.index(uvSetName)
                    else:
                        log.error('UV set name not found: {}'.format(uvSetName))
            else:
                # if shape is not given, return index from the first UV set
                num = int(shapeNumMatch.group(2))
                return num

    # index not found, e.g inside aiSkyDomeLight graph
    return 0

def extractTextureColorSpace(mFileNode):
    colorSpace = cmds.getAttr(mFileNode + '.colorSpace')
    if colorSpace in ['Raw', 'ACEScg', 'ACES2065-1'] or 'scene-linear' in colorSpace:
        return 'linear'
    else:
        return 'srgb'

def getCompressionMethod(mFileNode, exportSettings, texSettings):
    texPath = extractTexturePath(mFileNode)

    method = texSettings['compressionMethod']

    if cmds.objectType(mFileNode) == 'aiImage':
        # calculate via QImage since there is no width/height params on aiImage
        img = QImage(texPath)
        width = img.width()
        height = img.height()
    else:
        width = int(cmds.getAttr(mFileNode + '.outSizeX'))
        height = int(cmds.getAttr(mFileNode + '.outSizeY'))

    # only JPEG/PNG (ktx2) or HDR (xz/ktx2) compression supported
    if (exportSettings['compressTextures'] and exportSettings['format'] != 'HTML' and
            method != 'DISABLE' and gltf.isCompatibleImagePath(texPath) and
            pU.math.isPowerOfTwo(width) and pU.math.isPowerOfTwo(height)):
        return method
    else:
        return None

def getMotionPath(mNode, name=False):
    motionPaths = cmds.ls(type='motionPath', long=True)

    mNodeDagPath = getDagPath(mNode)
    for mPath in motionPaths:
        mFnMotionPath = OpenMayaAnim.MFnMotionPath(getMObject(mPath))
        mTargets = OpenMaya.MDagPathArray()
        mFnMotionPath.getAnimatedObjects(mTargets)

        for i in range(mTargets.length()):
            if mTargets[i] == mNodeDagPath:
                return mPath if name else mFnMotionPath

    return None

def getNURBSCurveKnotDomain(curve):
    kUtilStart = OpenMaya.MScriptUtil()
    startPtr = kUtilStart.asDoublePtr()
    kUtilEnd = OpenMaya.MScriptUtil()
    endPtr = kUtilEnd.asDoublePtr()
    curve.getKnotDomain(startPtr, endPtr)
    return kUtilStart.getDouble(startPtr), kUtilEnd.getDouble(endPtr)

def getSkinCluster(mNode):
    skinClusters = cmds.ls(cmds.listHistory(mNode), type='skinCluster')

    if len(skinClusters) > 0:
        mObj = getMObject(skinClusters[0])
        mFnSkinCluster = OpenMayaAnim.MFnSkinCluster(mObj)
        return mFnSkinCluster
    else:
        return None

def getMeshOriginal(mMesh):
    """Works for skins and blend shape deformers"""
    meshes = cmds.ls(cmds.listHistory(mMesh), type='mesh', long=True)
    for mesh in meshes:
        if mesh != mMesh:
            return mesh

def getNurbsSurfaceOriginal(mMesh):
    """Get original nurbs from which the mesh was converted"""
    nurbs = cmds.ls(cmds.listHistory(mMesh), type='nurbsSurface', long=True)
    for nurb in nurbs:
        if nurb != mMesh:
            return nurb

def getSkinJoints(skinCluster):
    """Get paths to MFnSkinCluster influence objects"""

    inflObjs = OpenMaya.MDagPathArray()
    skinCluster.influenceObjects(inflObjs)

    jointsMax = inflObjs.length()

    joints = []

    for i in range(jointsMax):
        inflObj = inflObjs[i]
        joints.append(inflObj.fullPathName())

    return joints

def getFPS():
    timeUnit = cmds.currentUnit(query=True, time=True)
    fps = ANIM_TIME_MAP.get(timeUnit)
    if fps is None:
        return float(timeUnit.replace('fps', ''))
    else:
        return fps

def getScaleFactor():
    """Convert internal linear value in cm to current units"""
    return float(cmds.convertUnit(1, fromUnit='cm', toUnit=cmds.currentUnit(query=True, linear=True)))

def getAnimInterpolation(mNode, attributes, key):
    for attr in attributes:
        outTangent = cmds.keyTangent(mNode, attribute=attr, time=(key, key), query=True, outTangentType=True)
        if outTangent:
            return ANIM_INTERP_MAP[outTangent[0]]

def getDagPath(mNode):
    selList = OpenMaya.MSelectionList()
    selList.add(mNode)

    dagPath = OpenMaya.MDagPath()
    selList.getDagPath(0, dagPath)

    return dagPath

def getMObject(mNode):
    selectList = OpenMaya.MSelectionList()
    selectList.add(mNode)

    obj = OpenMaya.MObject()
    selectList.getDependNode(0, obj)

    return obj

def getExportableUVSetNames(mNode):
    """Low-level method to get "real" UV map names"""

    dagPath = getDagPath(mNode)
    meshFn = OpenMaya.MFnMesh(dagPath)

    uvSetNames = []
    allUVSetNames = []

    meshFn.getUVSetNames(allUVSetNames)
    for name in allUVSetNames:
        if meshFn.numUVs(name) > 0:
            uvSetNames.append(name)

    return uvSetNames


def getRotationQuat(mNode):

    obj = getMObject(mNode)

    if obj.hasFn(OpenMaya.MFn.kTransform):
        quat = OpenMaya.MQuaternion()
        trans = OpenMaya.MFnTransform(obj)
        trans.getRotation(quat)
        quat.normalizeIt()

    dest = [quat[x] for x in range(4)]
    return dest

def eulerToQuat(euler, order, eulerOffset=None):
    euler = OpenMaya.MEulerRotation(math.radians(euler[0]), math.radians(euler[1]), math.radians(euler[2]), order)
    quat = euler.asQuaternion()

    if eulerOffset:
        quatOffset = OpenMaya.MEulerRotation(math.radians(eulerOffset[0]),
                                             math.radians(eulerOffset[1]),
                                             math.radians(eulerOffset[2]), order).asQuaternion()
        quat *= quatOffset

    dest = [quat[x] for x in range(4)]
    return dest

def angleAxisToQuat(angle, axis):
    quat = OpenMaya.MQuaternion(angle, OpenMaya.MVector(axis[0], axis[1], axis[2]))
    dest = [quat[x] for x in range(4)]
    return dest

def extractConstraints(glTF, mNode, advRenderSettings):
    dest = []

    # joint contstraints are not supported
    if isJoint(mNode):
        return dest

    mConstraints = cmds.ls(type='constraint', long=True)
    for mCons in mConstraints:

        mNodeParent = cmds.listRelatives(mCons, parent=True, fullPath=True)
        if mNodeParent and mNodeParent[0] == mNode:
            mTargets = cmds.listConnections(mCons + '.target')
            if not mTargets:
                log.error('Constraint target is empty: {}'.format(getName(mCons)))
                continue

            # make unique
            mTargets = list(set(mTargets))
            # full paths
            mTargets = cmds.ls(mTargets, long=True)

            for mTarget in mTargets:
                # ignore constraint itself
                if mTarget != mCons:
                    target = gltf.getNodeIndex(glTF, mTarget)
                    if target == -1:
                        log.error('Constraint target not found: {}'.format(getName(mCons)))
                        continue

                    type = cmds.objectType(mCons)

                    cons = { 'name': getName(mCons), 'mute': False }

                    if type == 'aimConstraint':
                        aimX = cmds.getAttr(mCons + '.aimVectorX')
                        aimY = cmds.getAttr(mCons + '.aimVectorY')
                        aimZ = cmds.getAttr(mCons + '.aimVectorZ')

                        if abs(aimX) > abs(aimY) and abs(aimX) > abs(aimZ):
                            trackAxis = 'X'
                            if (aimX) < 0:
                                trackAxis = '-X'
                        elif abs(aimY) > abs(aimX) and abs(aimY) > abs(aimZ):
                            trackAxis = 'Y'
                            if (aimY) < 0:
                                trackAxis = '-Y'
                        else:
                            trackAxis = 'Z'
                            if (aimY) < 0:
                                trackAxis = '-Z'

                        upX = cmds.getAttr(mCons + '.upVectorX')
                        upY = cmds.getAttr(mCons + '.upVectorY')
                        upZ = cmds.getAttr(mCons + '.upVectorZ')

                        if abs(upX) > abs(upY) and abs(upX) > abs(upZ):
                            upAxis = 'X'
                            if (upX) < 0:
                                upAxis = '-X'
                        elif abs(upY) > abs(upX) and abs(upY) > abs(upZ):
                            upAxis = 'Y'
                            if (upY) < 0:
                                upAxis = '-Y'
                        else:
                            upAxis = 'Z'
                            if (upZ) < 0:
                                upAxis = '-Z'

                        cons.update({
                            'type': 'trackTo',
                            'target': target,
                            'trackAxis': trackAxis,
                            'upAxis': upAxis
                        })
                        dest.append(cons)
                    elif type == 'pointConstraint':
                        cons.update({
                            'type': 'copyLocation',
                            'target': target
                        })
                        dest.append(cons)
                    elif type == 'orientConstraint':
                        cons.update({
                            'type': 'copyRotation',
                            'target': target
                        })
                        dest.append(cons)
                    elif type == 'scaleConstraint':
                        cons.update({
                            'type': 'copyScale',
                            'target': target
                        })
                        dest.append(cons)
                    elif type == 'parentConstraint':
                        # NOTE: multiple targets are not supported
                        targIdx = 0

                        transOffset = cmds.getAttr(mCons + '.target[' + str(targIdx) + '].targetOffsetTranslate')[0]
                        rotOffset = cmds.getAttr(mCons + '.target['  + str(targIdx) + '].targetOffsetRotate')[0]

                        cons.update({
                            'type': 'childOf',
                            'target': target,
                            'offsetMatrix': composeMMatrix(transOffset, rotOffset)
                        })
                        dest.append(cons)


    parent = getParent(mNode)
    if parent:
        parentShape = getFirstShape(parent)
        if parentShape and isCamera(parentShape):

            if advRenderSettings['fixOrthoZoom'] and cmds.camera(parentShape, query=True, orthographic=True):
                dest.append({
                    'name': 'Fit Ortho Zoom',
                    'mute': False,
                    'type': 'fixOrthoZoom',
                    'target': gltf.getNodeIndex(glTF, parent)
                })

            if advRenderSettings['canvasFitX'] != 'NONE' or advRenderSettings['canvasFitY'] != 'NONE':
                dest.append({
                    'name': 'Canvas Fit',
                    'mute': False,
                    'type': 'canvasFit',
                    'target': gltf.getNodeIndex(glTF, parent),
                    'edgeH': advRenderSettings['canvasFitX'],
                    'edgeV': advRenderSettings['canvasFitY'],
                    'fitShape': advRenderSettings['canvasFitShape'],
                    'offset': advRenderSettings['canvasFitOffset']
                })

    if advRenderSettings['canvasBreakEnabled']:
        dest.append({
            'name': 'Canvas Visibility Breakpoints',
            'mute': False,
            'type': 'canvasBreakpoints',
            'minWidth': advRenderSettings['canvasBreakMinWidth'],
            'maxWidth': advRenderSettings['canvasBreakMaxWidth'],
            'minHeight': advRenderSettings['canvasBreakMinHeight'],
            'maxHeight': advRenderSettings['canvasBreakMaxHeight'],
            'orientation': advRenderSettings['canvasBreakOrientation']
        })

    motionPath = getMotionPath(mNode)
    if motionPath:

        pathCurveDagPath = motionPath.pathObject()
        mPath = motionPath.name()
        curve = OpenMaya.MFnNurbsCurve(pathCurveDagPath)
        curveCVs = []
        curveKnots = []
        curveWeights = []
        degree = curve.degree()
        posScale = getScaleFactor()
        pTmp = OpenMaya.MPoint()

        for i in range(curve.numCVs()):
            curve.getCV(i, pTmp, 4)
            curveCVs.extend([pTmp.x * posScale, pTmp.y  * posScale, pTmp.z  * posScale])
            curveWeights.append(pTmp.w)

        for i in range(curve.numKnots()):
            curveKnots.append(curve.knot(i))

        if len(curveKnots) > 0:
            # number of knots required for a curve in Maya is M+2N-1, but we need M+2N+1
            curveKnots.insert(0, curveKnots[0])
            curveKnots.append(curveKnots[-1])

        isInvFront = cmds.getAttr(mPath + '.inverseFront')
        isInvUp = cmds.getAttr(mPath + '.inverseUp')
        axisDirs = ('X', 'Y', 'Z', '-X', '-Y', '-Z')

        worldUpVector = [0.0, 1.0, 0.0]
        if cmds.getAttr(mPath + '.worldUpType') == 3: # i.e. 'Vector'
            worldUpVector = [cmds.getAttr(mPath + '.worldUpVectorX'),
                             cmds.getAttr(mPath + '.worldUpVectorY'),
                             cmds.getAttr(mPath + '.worldUpVectorZ')]

        uValue = cmds.getAttr(mPath + '.uValue')
        if not cmds.getAttr(mPath + '.fractionMode'):
            startK, endK = getNURBSCurveKnotDomain(curve)
            uValue = (uValue - startK) / (endK - startK)

        dest.append({
            'name': mPath,
            'mute': False,
            'type': 'motionPath',
            'degree': degree,
            'cvs': curveCVs,
            'knots': curveKnots,
            'weights': curveWeights,
            'value': uValue,
            'follow': cmds.getAttr(mPath + '.follow'),
            'frontAxis': axisDirs[cmds.getAttr(mPath + '.frontAxis') + 3 * int(isInvFront)],
            'upAxis': axisDirs[cmds.getAttr(mPath + '.upAxis') + 3 * int(isInvUp)],
            'bank': cmds.getAttr(mPath + '.bank'),
            'bankScale': cmds.getAttr(mPath + '.bankScale') * math.radians(cmds.getAttr(mPath + '.bankLimit')),
            'bankLimit': math.radians(cmds.getAttr(mPath + '.bankLimit')),
            'worldUpVector': worldUpVector,
            'useChordLength': cmds.getAttr(mPath + '.fractionMode')
        })

    return dest

def extractAnimData(mNode, path, startWithZero, customRange=None):

    if path == 'weights':
        shapeTargets = getBlendShapeTargets(mNode, False)

        # no need to verify, we already know we have shapes and animation
        blendShape = shapeTargets[0]['blendShape']

        allAnimAttrs = []
        keyframes = []

        for targ in shapeTargets:
            attr = targ['name']
            keys = cmds.keyframe(blendShape, attribute=attr, query=True, timeChange=True)
            if keys:
                keyframes += keys

            allAnimAttrs.append(attr)

        animNode = blendShape

    elif path in ANIM_ATTR_MAP:
        attr = ANIM_ATTR_MAP[path]
        keyframes = cmds.keyframe(mNode, attribute=attr, query=True, timeChange=True)
        if keyframes is None:
            keyframes = []

        allAnimAttrs = [attr + axis for axis in ['X', 'Y', 'Z']]

        animNode = mNode

    elif path == 'motionPath':
        motionPath = getMotionPath(mNode, name=True)
        keyframes = cmds.keyframe(motionPath, attribute='uValue', query=True, timeChange=True)
        if keyframes is None:
            keyframes = []

        allAnimAttrs = ['uValue']
        animNode = motionPath

    elif path == '':    # material animation
        keyframes = cmds.keyframe(mNode, query=True, timeChange=True)
        if keyframes is None:
            keyframes = []
        allAnimAttrs = [None]
        animNode = mNode

    # make unique and sort
    keyframes = sorted(list(set(keyframes)))

    if keyframes:
        interpolation = getAnimInterpolation(animNode, allAnimAttrs, keyframes[0])

        # NOTE: spline interpolation is currently not supported
        if not (interpolation in ['STEP', 'LINEAR']):
            interpolation = 'LINEAR'
    else:
        interpolation = 'LINEAR'

    if customRange:
        keyframes = [i for i in range(int(customRange[0]), int(customRange[1])+1)]

    values = []

    # e.g. when driven (float) keys and no custom range specified
    if not keyframes:
        return (keyframes, values, interpolation)

    if path in ['translation', 'scale']:
        for keyframe in keyframes:
            values.append(cmds.getAttr(animNode + '.' + ANIM_ATTR_MAP[path], time=keyframe)[0])

    elif path == 'rotation':
        order = cmds.getAttr(animNode + '.rotateOrder')

        eulerOffset = cmds.getAttr(animNode + '.jointOrient')[0] if isJoint(animNode) else None

        first = keyframes[0]
        last = keyframes[-1]
        # NOTE: iterating each frame to fix issues with quat interpolation
        # e.g if you need to animate 0-360 degree range
        # TODO: do it smarter
        keyframes = [i for i in range(int(first), int(last)+1)]
        for keyframe in keyframes:
            euler = cmds.getAttr(animNode + '.' + ANIM_ATTR_MAP[path], time=keyframe)[0]
            values.append(eulerToQuat(euler, order, eulerOffset))

    elif path == 'weights':
        for keyframe in keyframes:
            for attr in allAnimAttrs:
                values.append(cmds.getAttr(animNode + '.' + attr, time=keyframe))

    elif path == '':    # material animation
        for keyframe in keyframes:
            values.append(cmds.getAttr(animNode + '.output', time=keyframe))

    elif path == 'motionPath':
        for keyframe in keyframes:
            values.append(cmds.getAttr(animNode + '.uValue', time=keyframe))
        motionPath = getMotionPath(mNode)
        curve = OpenMaya.MFnNurbsCurve(motionPath.pathObject())
        if not cmds.getAttr(animNode + '.fractionMode'):
            startK, endK = getNURBSCurveKnotDomain(curve)
            knotInterval = endK - startK
            for i in range(len(values)):
                values[i] = (values[i] - startK) / (knotInterval)

    else:
        log.error('Wrong animation path: {}'.format(path))

    fps = getFPS()

    keyShift = keyframes[0] if startWithZero else 0

    keyframes = [(key - keyShift)/fps for key in keyframes]
    values = gltf.flatten(values)

    return (keyframes, values, interpolation)

def extractGroupNames(mNode):

    out = []

    currNode = mNode

    while True:
        parentList = cmds.listRelatives(currNode, parent=True, fullPath=True)
        if parentList:
            currNode = parentList[0]
            # only pure transform nodes
            if not cmds.listRelatives(currNode, shapes=True):
                out.append(getName(currNode))
        else:
            break

    return out

def extractObjectSetNames(mNode):
    sets = cmds.listSets(object=mNode)
    return sets if sets else []

def extractTransMatrix(mMat):
    """Convert matrix to column-major list with proper units scaling"""

    # to transform matrix from internal units
    scale = getScaleFactor()

    mat = list(mMat)

    mat[12] *= scale
    mat[13] *= scale
    mat[14] *= scale

    return mat

def transformBoundingBox(boxArr, mNode, resetNodeScale=True):

    # to transform box to internal units and back
    scale = getScaleFactor()

    transMat = OpenMaya2.MTransformationMatrix(OpenMaya2.MMatrix(cmds.getAttr(mNode + '.worldMatrix')))

    if resetNodeScale:
        transMat.setScale(OpenMaya2.MVector([1,1,1]), OpenMaya2.MSpace.kWorld)

    mat = transMat.asMatrixInverse()

    corner1 = OpenMaya2.MPoint(boxArr[0:3]) * (1.0 / scale)
    corner2 = OpenMaya2.MPoint(boxArr[3:6]) * (1.0 / scale)

    box = OpenMaya2.MBoundingBox(corner1, corner2)
    box.transformUsing(mat)

    corner1 = box.min * scale
    corner2 = box.max * scale

    return [corner1[0], corner1[1], corner1[2], corner2[0], corner2[1], corner2[2]]

def getUvWrapInfo(mTex):

    info = {
        'mirrorU': False,
        'mirrorV': False,
        'wrapU': True,
        'wrapV': True
    }

    p2dTex = getInputNode(mTex, 'uvCoord')
    if p2dTex and cmds.objectType(p2dTex) == 'place2dTexture':
        info['mirrorU'] = cmds.getAttr(p2dTex + '.mirrorU')
        info['mirrorV'] = cmds.getAttr(p2dTex + '.mirrorV')
        info['wrapU'] = cmds.getAttr(p2dTex + '.wrapU')
        info['wrapV'] = cmds.getAttr(p2dTex + '.wrapV')
    elif cmds.objectType(mTex) == 'aiImage':
        info['mirrorU'] = cmds.getAttr(mTex + '.swrap') == 3
        info['mirrorV'] = cmds.getAttr(mTex + '.twrap') == 3
        info['wrapU'] = not (cmds.getAttr(mTex + '.swrap') == 2)
        info['wrapV'] = not (cmds.getAttr(mTex + '.twrap') == 2)
    return info

def queryAreaLight(mLight, query=True, rgb=False, intensity=False, decayRate=False):
    if rgb == True:
        return cmds.getAttr(mLight + '.color')[0]
    elif intensity == True:
        return cmds.getAttr(mLight + '.intensity')
    elif decayRate == True:
        return cmds.getAttr(mLight + '.decayRate')
    else:
        return None

def getSkyDomeLightTexRes(mNode):
    samplingId = cmds.getAttr(mNode + '.sampling')
    return SKYDOME_TEX_RES_MAP[samplingId]


def composeMMatrix(trans, rot=[0,0,0], scale=[1,1,1]):
    mTransformMatrix = OpenMaya2.MTransformationMatrix()

    unitsScale = getScaleFactor()

    mTrans = OpenMaya2.MVector(trans) * (1 / unitsScale)
    mRot = OpenMaya2.MEulerRotation(rot)
    mScale = OpenMaya2.MVector(scale)

    mTransformMatrix.setTranslation(mTrans, OpenMaya2.MSpace.kWorld)
    mTransformMatrix.setRotation(mRot)
    mTransformMatrix.setScale(mScale, OpenMaya2.MSpace.kWorld)

    return list(mTransformMatrix.asMatrix(1.0))


def decomposeMMatrix(mMatrix):
    mTransformMatrix = OpenMaya2.MTransformationMatrix(mMatrix)

    mTrans = mTransformMatrix.translation(OpenMaya2.MSpace.kWorld)
    mQuat = mTransformMatrix.rotation().asQuaternion()

    unitsScale = getScaleFactor()
    trans = [mTrans.x * unitsScale, mTrans.y * unitsScale, mTrans.z * unitsScale]
    rot = [mQuat.x, mQuat.y, mQuat.z, mQuat.w]
    scale = mTransformMatrix.scale(OpenMaya2.MSpace.kWorld)

    return trans, rot, scale

def getNodeWorldTranslation(mNode):
    mat = OpenMaya2.MMatrix(cmds.getAttr(mNode + '.worldMatrix'))
    return decomposeMMatrix(mat)[0]

def calcBoundBoxFarthestDistanceFromPoint(boxArr, point):
    dx = max(abs(point[0] - boxArr[0]), abs(point[0] - boxArr[3]))
    dy = max(abs(point[1] - boxArr[1]), abs(point[1] - boxArr[4]))
    dz = max(abs(point[2] - boxArr[2]), abs(point[2] - boxArr[5]))
    return math.sqrt(dx**2 + dy**2 + dz**2)

def attrNode(attr):
    return attr.split('.')[0]

def findSkeletonRoot(mNode):

    currNode = mNode

    while True:
        parentList = cmds.listRelatives(currNode, parent=True, fullPath=True)
        if parentList:
            currNode = parentList[0]
            if cmds.getAttr(currNode + '.v3d.animSkeletonRoot'):
                return currNode
        else:
            break

    return None

def extractCustomProps(mNode):

    if 'customProperties' in cmds.listAttr(mNode):
        customPropsList = cmds.getAttr(mNode + '.customProperties')
        if customPropsList and len(customPropsList):
            dest = {}
            customPropsList = customPropsList.split()
            for prop in customPropsList:
                dest[prop] = cmds.getAttr(mNode + '.' + prop)

            return dest

    return None

def extractAlphaMode(mMat, matSettings):

    alphaMode = matSettings['alphaMode']

    if alphaMode in ['OPAQUE', 'BLEND', 'MASK']:
        return alphaMode
    elif alphaMode == 'ADD' or alphaMode == 'COVERAGE':
        return 'BLEND'

    # handle AUTO mode

    matType = cmds.objectType(mMat)

    if matType in ['phong', 'phongE', 'lambert', 'blinn']:

        transparency = cmds.getAttr(mMat + '.transparency')[0]

        if (getInputNode(mMat, 'transparency') or
            getInputNode(mMat, 'transparencyR') or
            getInputNode(mMat, 'transparencyG') or
            getInputNode(mMat, 'transparencyB')):

            return 'BLEND'

        elif (transparency[0] > 0 or
                 transparency[1] > 0 or
                 transparency[2] > 0):

            return 'BLEND'

    elif matType in ['aiStandardSurface', 'standardSurface']:

        opacity = cmds.getAttr(mMat + '.opacity')[0]
        transmission = cmds.getAttr(mMat + '.transmission')

        if (getInputNode(mMat, 'opacity') or
            getInputNode(mMat, 'opacityR') or
            getInputNode(mMat, 'opacityG') or
            getInputNode(mMat, 'opacityB') or
            getInputNode(mMat, 'transmission')):

            return 'BLEND'

        elif (opacity[0] < 1 or
                 opacity[1] < 1 or
                 opacity[2] < 1 or
                 transmission > 0):

            return 'BLEND'

    elif matType == 'openPBRSurface':

        opacity = cmds.getAttr(mMat + '.geometryOpacity')
        transmission = cmds.getAttr(mMat + '.transmissionWeight')

        if getInputNode(mMat, 'geometryOpacity') or getInputNode(mMat, 'transmissionWeight'):
            return 'BLEND'

        elif opacity < 1 or transmission > 0:
            return 'BLEND'

    elif matType == 'usdPreviewSurface':
        return 'MASK'

    elif matType == 'aiLambert':

        opacity = cmds.getAttr(mMat + '.opacity')[0]

        if (getInputNode(mMat, 'opacity') or
            getInputNode(mMat, 'opacityR') or
            getInputNode(mMat, 'opacityG') or
            getInputNode(mMat, 'opacityB')):

            return 'BLEND'

        elif opacity[0] < 1 or opacity[1] < 1 or opacity[2] < 1:
            return 'BLEND'


    return 'OPAQUE'

def setFilterScript(name, quiet=False):
    """
    A wrapper around the Maya built-in MEL procedure "setFilterScript".
    Additionally supports suppressing some warning and error messages appearing
    in the original MEL procedure.
    """
    if quiet:
        if mel.eval("catchQuiet(`nodeType -api \"%s\"`)" % name) == 1:
            return False
        if mel.eval("catchQuiet(`nodeType \"%s\"`)" % name) == 1:
            return False
    return bool(mel.eval('setFilterScript("%s")' % name))

def getObjectSetsAbsolute():
    """
    Returns a list of object sets' absolute names. The names are invariant to
    the Relative Namespaces option and the current active namespace.
    """
    return [objectSetGetAbsoluteName(s) for s in cmds.ls(sets=True, absoluteName=True)]

def objectSetGetAbsoluteName(objectSet):
    """
    Constructs an absolute name for the given objectSet.
    NOTE: the given objectSet argument should be obtained right before calling
    this function via one of the Maya's API methods (e.g. cmds.ls,
    cmds.listConnections), since the result of such a method depends on the
    Relative Namespaces option and the current active namespace, which is what
    this function takes into account.
    """
    if isRelativeNameMode() and not objectSet.startswith(':'):
        objectSet = getCurrNamespace() + ':' + objectSet

    if not objectSet.startswith(':'):
        objectSet = ':' + objectSet

    return objectSet

def isRelativeNameMode():
    return cmds.namespace(query=True, relativeNames=True)

def getCurrNamespace():
    return cmds.namespaceInfo(fullName=True)
