# -*- coding: UTF-8 -*-
# Copyright 2020 by Kurt Rathjen. All Rights Reserved.
#
# This library is free software: you can redistribute it and/or modify it 
# under the terms of the GNU Lesser General Public License as published by 
# the Free Software Foundation, either version 3 of the License, or 
# (at your option) any later version. This library is distributed in the 
# hope that it will be useful, but WITHOUT ANY WARRANTY; without even the 
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
# See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.

import logging
import platform
import traceback

from studiovendor.Qt import QtCore
from studiovendor.Qt import QtWidgets

try:
    import maya.mel
    import maya.cmds
except ImportError:
    traceback.print_exc()

import mutils


logger = logging.getLogger(__name__)


class MayaUtilsError(Exception):
    """Base class for exceptions in this module."""


class ObjectsError(MayaUtilsError):
    pass


class SelectionError(MayaUtilsError):
    pass


class NoMatchFoundError(MayaUtilsError):
    pass


class NoObjectFoundError(MayaUtilsError):
    pass


class MoreThanOneObjectFoundError(MayaUtilsError):
    pass


class ModelPanelNotInFocusError(MayaUtilsError):
    pass


def system():
    """
    Return the current platform in lowercase.
    
    :rtype: str
    """
    return platform.system().lower()


def isMac():
    """
    Return True if the current OS is Mac.
    
    :rtype: bool
    """
    return system().startswith("os") or \
           system().startswith("mac") or \
           system().startswith("darwin")


def isLinux():
    """    
    Return True if the current OS is linux.
    
    :rtype: bool
    """
    return system().lower().startswith("lin")


def isWindows():
    """
    Return True if the current OS is windows.
    
    :rtype: bool
    """
    return system().lower().startswith("win")


def isMaya():
    """
    Return True if the current python session is in Maya.
    
    :rtype: bool
    """
    try:
        import maya.cmds
        maya.cmds.about(batch=True)
        return True
    except ImportError:
        return False


def selectionModifiers():
    """
    Return a dictionary of the current key modifiers

    :rtype: dict
    """
    result = {"add": False, "deselect": False}
    modifiers = QtWidgets.QApplication.keyboardModifiers()

    if modifiers == QtCore.Qt.ShiftModifier:
        result["deselect"] = True
    elif modifiers == QtCore.Qt.ControlModifier:
        result["add"] = True

    return result


def ls(*args, **kwargs):
    """
    List all the node objects for the given options.

    :type args: list
    :type kwargs: dict
    """
    return [mutils.Node(name) for name in maya.cmds.ls(*args, **kwargs) or []]


def listAttr(name, **kwargs):
    """
    List all the attributes for the given object name.
    
    :type name: str
    :type kwargs: str
    :rtype: list[mutils.Attribute]
    """
    attrs = maya.cmds.listAttr(name, **kwargs) or []
    attrs = list(set(attrs))
    return [mutils.Attribute(name, attr) for attr in attrs]


def disconnectAll(name):
    """
    Disconnect all connections from the given object name.
    
    :type name: str
    """
    plugins = maya.cmds.listConnections(name, plugs=True, source=False) or []

    for plug in plugins:
        source, = maya.cmds.listConnections(plug, plugs=True)
        maya.cmds.disconnectAttr(source, plug)


def animCurve(fullname):
    """
    Return the animation curve name for the give attribute.

    :type fullname: str or None
    :rtype: str or None
    """
    attribute = mutils.Attribute(fullname)
    return attribute.animCurve()


def deleteUnknownNodes():
    """Delete all unknown nodes in the Maya scene."""
    nodes = maya.cmds.ls(type="unknown")
    if nodes:
        for node in nodes:
            if maya.cmds.objExists(node) and \
                    maya.cmds.referenceQuery(node, inr=True):
                maya.cmds.delete(node)


def currentModelPanel():
    """
    Get the current model panel name.
    
    :rtype: str or None
    """
    currentPanel = maya.cmds.getPanel(withFocus=True)
    currentPanelType = maya.cmds.getPanel(typeOf=currentPanel)

    if currentPanelType not in ['modelPanel']:
        return None

    return currentPanel


def getBakeAttrs(objects):
    """
    Get the attributes that are not connected to an animation curve.

    :type objects: list[str]
    :rtype: list[str]
    """
    result = []

    if not objects:
        raise Exception(u"没有指定对象")

    connections = maya.cmds.listConnections(
        objects,
        plugs=True,
        source=True,
        connections=True,
        destination=False
    ) or []

    for i in range(0, len(connections), 2):
        dstObj = connections[i]
        srcObj = connections[i + 1]

        nodeType = maya.cmds.nodeType(srcObj)

        if "animCurve" not in nodeType:
            result.append(dstObj)

    return result


def bakeConnected(objects, time, sampleBy=1):
    """
    Bake the given objects.
    
    :type objects: list[str]
    :type time: (int, int)
    :type sampleBy: int
    """
    bakeAttrs = getBakeAttrs(objects)

    if bakeAttrs:
        maya.cmds.bakeResults(
            bakeAttrs,
            time=time,
            shape=False,
            simulation=True,
            sampleBy=sampleBy,
            controlPoints=False,
            minimizeRotation=True,
            bakeOnOverrideLayer=False,
            preserveOutsideKeys=False,
            sparseAnimCurveBake=False,
            disableImplicitControl=True,
            removeBakedAttributeFromLayer=False,
        )
    else:
        logger.error(u"找不到任何连接可以约束!")


def getSelectedObjects():
    """
    Get a list of the selected objects in Maya.    
    
    :rtype: list[str]
    :raises: mutils.SelectionError:
    """
    selection = maya.cmds.ls(selection=True)
    if not selection:
        raise mutils.SelectionError(u"没有选择对象!")
    return selection


def getSelectedAttrs():
    """
    Get the attributes that are selected in the channel box.
    
    :rtype: list[str]
    """
    attributes = maya.cmds.channelBox(
        "mainChannelBox",
        query=True,
        selectedMainAttributes=True
    )

    if attributes is not None:
        attributes = str(attributes)
        attributes = attributes.replace("tx", "translateX")
        attributes = attributes.replace("ty", "translateY")
        attributes = attributes.replace("tz", "translateZ")
        attributes = attributes.replace("rx", "rotateX")
        attributes = attributes.replace("ry", "rotateY")
        attributes = attributes.replace("rz", "rotateZ")
        attributes = eval(attributes)

    return attributes


def currentFrameRange():
    """
    Get the current first and last frame depending on the context.

    :rtype: (int, int)
    """
    start, end = selectedFrameRange()

    if end == start:
        start, end = selectedObjectsFrameRange()

        if start == end:
            start, end = playbackFrameRange()

    return start, end


def playbackFrameRange():
    """
    Get the first and last frame from the play back options.

    :rtype: (int, int)
    """
    start = maya.cmds.playbackOptions(query=True, min=True)
    end = maya.cmds.playbackOptions(query=True, max=True)
    return start, end


def selectedFrameRange():
    """
    Get the first and last frame from the play back slider.
 
    :rtype: (int, int)
    """
    result = maya.mel.eval("timeControl -q -range $gPlayBackSlider")
    start, end = result.replace('"', "").split(":")
    start, end = int(start), int(end)
    if end - start == 1:
        end = start
    return start, end


def selectedObjectsFrameRange(objects=None):
    """
    Get the first and last animation frame from the given objects.
    
    :type objects: list[str]
    :rtype: (int, int)
    """
    start = 0
    end = 0

    if not objects:
        objects = maya.cmds.ls(selection=True) or []

    if objects:
        start = int(maya.cmds.findKeyframe(objects, which='first'))
        end = int(maya.cmds.findKeyframe(objects, which='last'))

    return start, end


def getDurationFromNodes(objects, time=None):
    """
    Get the duration of the animation from the given object names.

    :type time: [str, str]
    :type objects: list[str]
    :rtype: float
    """
    if objects:

        first = maya.cmds.findKeyframe(objects, which='first')
        last = maya.cmds.findKeyframe(objects, which='last')

        if time:
            startKey = maya.cmds.findKeyframe(objects, time=(time[0], time[0]), which="next")
            if startKey > time[1] or startKey < time[0]:
                return 0

        if first == last:
            if maya.cmds.keyframe(objects, query=True, keyframeCount=True) > 0:
                return 1
            else:
                return 0

        return last - first
    else:
        return 0


def getReferencePaths(objects, withoutCopyNumber=False):
    """
    Get the reference paths for the given objects.

    :type objects: list[str]
    :type withoutCopyNumber: bool
    :rtype: list[str]
    """
    paths = []
    for obj in objects:
        if maya.cmds.referenceQuery(obj, isNodeReferenced=True):
            paths.append(maya.cmds.referenceQuery(obj, f=True, wcn=withoutCopyNumber))

    return list(set(paths))


def getReferenceData(objects):
    """
    Get the reference paths for the given objects.

    :type objects: list[str]
    :rtype: list[dict]
    """
    data = []
    paths = getReferencePaths(objects)

    for path in paths:
        data.append({
            "filename": path,
            "unresolved": maya.cmds.referenceQuery(path, filename=True, withoutCopyNumber=True),
            "namespace": maya.cmds.file(path, q=True, namespace=True),
            "node": maya.cmds.referenceQuery(path, referenceNode=True)
        })

    return data
