"""
 GT Maya Utilities
 github.com/TrevisanGMW - 2020-09-13

 1.1 - 2020-10-17
 Added move pivot to bottom/top
 Added copy/paste material
 Added move to origin

 1.2 - 2020-10-21
 Updated reset transform to better handle translate
 Added Uniform LRA Toggle
 Changed the order of the functions to match the menu

 1.3 - 2020-11-11
 Updates "gtu_import_references" to better handle unloaded references
 Added "gtu_remove_references"
 Added "gtu_combine_curves"
 Added "gtu_separate_curves"

 1.4 - 2020-11-13
 Updated combine and separate functions to work with Bezier curves

 1.5 - 2020-11-14
 Added "gtu_convert_bif_to_mesh"

 1.6 - 2020-11-16
 Added "gtu_delete_nucleus_nodes"
 Updated "gtu_delete_display_layers" to have inView feedback
 Updated "gtu_delete_keyframes" to have inView feedback

 1.7 - 2020-11-22
 Updated about window text

 1.8 - 2020-12-03
 Changed the background color for the title in the "About" window
 Changed the order of a few functions
 Added function to unlock/unhide default channels

 1.9 - 2021-01-05
 Added Uniform Joint Label Toggle

 2.0 - 2021-02-05
 Added "Select Non-Unique Objects" Utility

 2.1 - 2021-05-12
 Made script compatible with Python 3 (Maya 2022+)
 Added refresh to combine curves function as they were not automatically updating after re-parenting shapes

 2.2 - 2021-06-25
 Updated bif to mesh to work with newer versions of bifrost
 Updated bif to mesh to delete empty meshes (objects that weren't geometry)
 Added function to delete all locators

 2.2 - 2021-10-25
 Updated bif to mesh to work with newer versions of bifrost
 Updated bif to mesh to delete empty meshes (objects that weren't geometry)
 Added function to delete all locators

 2.3 - 2021-10-10
 Created Full HUD Toggle

 2.4 - 2021-10-10
 Fixed gtu full hud toggle as it would return an error if xGen was not loaded

 2.5 - 2022-01-04
 Renamed script to "gt_maya_utilities"

 2.6 - 2022-01-04
 Renamed script to "gt_maya_utilities"

 2.7 - 2022-06-29
 Added string to notepad (txt)
 Renamed functions

 TODO:
     Add proper error handling to all functions through logging
     New functions:
        Reset Display Type and Color
        Find/Rename non-unique names - Enforce unique names
        Remove Custom Colors - select object types, outliner or viewport. Use string to determine a list of types
        Assign lambert to everything function (Maybe assign to object missing shaders)
        Add Unlock all attributes
        Add unhide attributes (provide list?)
        Add Remove pasted_ function
        Add assign checkerboard function (already in bonus tools > rendering)
        Force focus (focus without looking at children)
        Brute force clean models (export OBJ and reimport)
     New options:
        Import all references : Add function to use a string to ignore certain references
        Reset Transforms : Add reset only translate, rotate or scale
        Delete all keyframes : Include option to delete or not set driven keys
        Reset persp camera : Reset all other attributes too (including transform?)
        Delete Display Layers : only empty? ignore string?
        Delete Namespaces : only empty? ignore string?

"""
import maya.cmds as cmds
import maya.mel as mel
import logging
import sys
from maya import OpenMayaUI as OpenMayaUI

try:
    from shiboken2 import wrapInstance
except ImportError:
    from shiboken import wrapInstance

try:
    from PySide2.QtGui import QIcon
    from PySide2.QtWidgets import QWidget
except ImportError:
    from PySide.QtGui import QIcon, QWidget

# Logging Setup
logging.basicConfig()
logger = logging.getLogger("gt_utilities")
logger.setLevel(logging.INFO)

''' ____________________________ General Functions ____________________________'''

def gtu_reload_file():
    """ Reopens the opened file (to revert any changes done to the file) """
    if cmds.file(query=True, exists=True):  # Check to see if it was ever saved
        file_path = cmds.file(query=True, expandName=True)
        if file_path is not None:
            cmds.file(file_path, open=True, force=True)
    else:
        cmds.warning('File was never saved.')

def gtu_open_resource_browser():
    """ Opens Maya's Resource Browser """
    try:
        import maya.app.general.resourceBrowser as resourceBrowser
        resourceBrowser.resourceBrowser().run()
    except Exception as e:
        logger.debug(str(e))

def gtu_unlock_default_channels():
    """ Unlocks Translate, Rotate, Scale for the selected objects """
    function_name = 'GTU Unlock Default Channels'
    errors = ''
    cmds.undoInfo(openChunk=True, chunkName=function_name)  # Start undo chunk
    selection = cmds.ls(selection=True, long=True)
    unlocked_counter = 0
    try:
        for obj in selection:
            try:
                cmds.setAttr(obj + '.translateX', lock=False)
                cmds.setAttr(obj + '.translateY', lock=False)
                cmds.setAttr(obj + '.translateZ', lock=False)
                cmds.setAttr(obj + '.rotateX', lock=False)
                cmds.setAttr(obj + '.rotateY', lock=False)
                cmds.setAttr(obj + '.rotateZ', lock=False)
                cmds.setAttr(obj + '.scaleX', lock=False)
                cmds.setAttr(obj + '.scaleY', lock=False)
                cmds.setAttr(obj + '.scaleZ', lock=False)
                cmds.setAttr(obj + '.v', lock=False)
                unlocked_counter += 1
            except Exception as e:
                errors += str(e) + '\n'
        if errors != '':
            print('#### Errors: ####')
            print(errors)
            cmds.warning('Some channels were not unlocked . Open the script editor for a list of errors.')
    except Exception as e:
        logger.debug(str(e))
    finally:
        cmds.undoInfo(closeChunk=True, chunkName=function_name)

    message = '' + str(unlocked_counter) + ' '
    is_plural = 'objects had their'
    if unlocked_counter == 1:
        is_plural = 'object had its'
    message += is_plural + ' default channels unlocked.'

    cmds.inViewMessage(amg=message, pos='botLeft', fade=True, alpha=.9)

def gtu_unhide_default_channels():
    """ Unhides Translate, Rotate, Scale for the selected objects """
    function_name = 'GTU Unhide Default Channels'
    errors = ''
    cmds.undoInfo(openChunk=True, chunkName=function_name)  # Start undo chunk
    selection = cmds.ls(selection=True, long=True)
    unlocked_counter = 0
    try:
        for obj in selection:
            try:
                cmds.setAttr(obj + '.translateX', keyable=True)
                cmds.setAttr(obj + '.translateY', keyable=True)
                cmds.setAttr(obj + '.translateZ', keyable=True)
                cmds.setAttr(obj + '.rotateX', keyable=True)
                cmds.setAttr(obj + '.rotateY', keyable=True)
                cmds.setAttr(obj + '.rotateZ', keyable=True)
                cmds.setAttr(obj + '.scaleX', keyable=True)
                cmds.setAttr(obj + '.scaleY', keyable=True)
                cmds.setAttr(obj + '.scaleZ', keyable=True)
                cmds.setAttr(obj + '.v', keyable=True)
                unlocked_counter += 1
            except Exception as e:
                errors += str(e) + '\n'
        if errors != '':
            print('#### Errors: ####')
            print(errors)
            cmds.warning('Some channels were not made visible. Open the script editor for a list of errors.')
    except Exception as e:
        logger.debug(str(e))
    finally:
        cmds.undoInfo(closeChunk=True, chunkName=function_name)

    message = '' + str(unlocked_counter) + ' '
    is_plural = 'objects had their'
    if unlocked_counter == 1:
        is_plural = 'object had its'
    message += is_plural + ' default channels made visible.'

    cmds.inViewMessage(amg=message, pos='botLeft', fade=True, alpha=.9)

def gtu_uniform_lra_toggle():
    """
    Makes the visibility of the Local Rotation Axis uniform among
    the selected objects according to the current state of the majority of them.
    """

    function_name = 'GTU Uniform LRA Toggle'
    cmds.undoInfo(openChunk=True, chunkName=function_name)
    try:
        errors = ''
        selection = cmds.ls(selection=True)

        inactive_lra = []
        active_lra = []

        for obj in selection:
            try:
                current_lra_state = cmds.getAttr(obj + '.displayLocalAxis')
                if current_lra_state:
                    active_lra.append(obj)
                else:
                    inactive_lra.append(obj)
            except Exception as e:
                errors += str(e) + '\n'

        if len(active_lra) == 0:
            for obj in inactive_lra:
                try:
                    cmds.setAttr(obj + '.displayLocalAxis', 1)
                except Exception as e:
                    errors += str(e) + '\n'
        elif len(inactive_lra) == 0:
            for obj in active_lra:
                try:
                    cmds.setAttr(obj + '.displayLocalAxis', 0)
                except Exception as e:
                    errors += str(e) + '\n'
        elif len(active_lra) > len(inactive_lra):
            for obj in inactive_lra:
                try:
                    cmds.setAttr(obj + '.displayLocalAxis', 1)
                except Exception as e:
                    errors += str(e) + '\n'
        else:
            for obj in active_lra:
                try:
                    cmds.setAttr(obj + '.displayLocalAxis', 0)
                except Exception as e:
                    errors += str(e) + '\n'

        if errors != '':
            print('#### Errors: ####')
            print(errors)
            cmds.warning("The script couldn't read or write some LRA states. Open script editor for more info.")
    except Exception as e:
        logger.debug(str(e))
    finally:
        cmds.undoInfo(closeChunk=True, chunkName=function_name)

def gtu_uniform_jnt_label_toggle():
    """
    Makes the visibility of the Joint Labels uniform according to the current state of the majority of them.
    """

    function_name = 'GTU Uniform Joint Label Toggle'
    cmds.undoInfo(openChunk=True, chunkName=function_name)
    try:
        errors = ''
        joints = cmds.ls(type='joint', long=True)

        inactive_label = []
        active_label = []

        for obj in joints:
            try:
                current_label_state = cmds.getAttr(obj + '.drawLabel')
                if current_label_state:
                    active_label.append(obj)
                else:
                    inactive_label.append(obj)
            except Exception as e:
                errors += str(e) + '\n'

        if len(active_label) == 0:
            for obj in inactive_label:
                try:
                    cmds.setAttr(obj + '.drawLabel', 1)
                except Exception as e:
                    errors += str(e) + '\n'
        elif len(inactive_label) == 0:
            for obj in active_label:
                try:
                    cmds.setAttr(obj + '.drawLabel', 0)
                except Exception as e:
                    errors += str(e) + '\n'
        elif len(active_label) > len(inactive_label):
            for obj in inactive_label:
                try:
                    cmds.setAttr(obj + '.drawLabel', 1)
                except Exception as e:
                    errors += str(e) + '\n'
        else:
            for obj in active_label:
                try:
                    cmds.setAttr(obj + '.drawLabel', 0)
                except Exception as e:
                    errors += str(e) + '\n'

        if errors != '':
            print('#### Errors: ####')
            print(errors)
            cmds.warning("The script couldn't read or write some \"drawLabel\" states. "
                         "Open script editor for more info.")
    except Exception as e:
        logger.debug(str(e))
    finally:
        cmds.undoInfo(closeChunk=True, chunkName=function_name)

def gtu_select_non_unique_objects():
    """ Selects all non-unique objects (objects with the same short name) """

    def get_short_name(full_name):
        """
            Get the name of the objects without its path (Maya returns full path if name is not unique)

            Args:
                full_name (string) - object to extract short name
            """
        output_short_name = ''
        if full_name == '':
            return ''
        split_path = full_name.split('|')
        if len(split_path) >= 1:
            output_short_name = split_path[len(split_path) - 1]
        return output_short_name

    all_transforms = cmds.ls(type='transform')
    short_names = []
    non_unique_transforms = []
    for obj in all_transforms:  # Get all Short Names
        short_names.append(get_short_name(obj))

    for obj in all_transforms:
        short_name = get_short_name(obj)
        if short_names.count(short_name) > 1:
            non_unique_transforms.append(obj)

    cmds.select(non_unique_transforms, r=True)

    if len(non_unique_transforms) > 0:
        message = '' + str(
            len(non_unique_transforms)) + ' non-unique objects were selected.'
    else:
        message = 'All objects seem to have unique names in this scene.'
    cmds.inViewMessage(amg=message, pos='botLeft', fade=True, alpha=.9)

def gtu_import_references():
    """ Imports all references """
    errors = ''
    r_file = ''
    try:
        refs = cmds.ls(rf=True)
        for i in refs:
            try:
                r_file = cmds.referenceQuery(i, f=True)
                cmds.file(r_file, importReference=True)
            except Exception as e:
                errors += str(e) + '(' + r_file + ')\n'
    except Exception as e:
        logger.debug(str(e))
        cmds.warning("Something went wrong. Maybe you don't have any references to import?")
    if errors != '':
        cmds.warning('Not all references were imported. Open the script editor for more information.')
        print(('#' * 50) + '\n')
        print(errors)
        print('#' * 50)

def gtu_remove_references():
    """ Removes all references """
    errors = ''
    r_file = ''
    try:
        refs = cmds.ls(rf=True)
        for i in refs:
            try:
                r_file = cmds.referenceQuery(i, f=True)
                cmds.file(r_file, removeReference=True)
            except Exception as e:
                errors += str(e) + '(' + r_file + ')\n'
    except Exception as e:
        logger.debug(str(e))
        cmds.warning("Something went wrong. Maybe you don't have any references to import?")
    if errors != '':
        cmds.warning('Not all references were removed. Open the script editor for more information.')
        print(('#' * 50) + '\n')
        print(errors)
        print('#' * 50)

""" ____________________________ Material Functions ____________________________"""

def gtu_generate_udim_previews():
    """ Generates UDIM previews for all file nodes """
    all_file_nodes = cmds.ls(type='file')
    for file_node in all_file_nodes:
        try:
            mel.eval('generateUvTilePreview ' + file_node + ';')
        except Exception as e:
            print(e)
    message = 'Previews generated for all  ' \
              'UDIM file nodes.'
    cmds.inViewMessage(amg=message, pos='botLeft', fade=True, alpha=.9)

def gtu_copy_material():
    """ Copies selected material to clipboard """
    selection = cmds.ls(selection=True)
    try:
        mel.eval('ConvertSelectionToFaces;')
        cmds.polyClipboard(copy=True, shader=True)
        cmds.inViewMessage(amg='Material <hl>copied</hl> to the clipboard.', pos='midCenterTop', fade=True)
    except Exception as e:
        logger.debug(str(e))
        cmds.warning("Couldn't copy material. Make sure you selected an object or component before copying.")
    cmds.select(selection)

def gtu_paste_material():
    """ Copies selected material to clipboard """
    try:
        cmds.polyClipboard(paste=True, shader=True)
    except Exception as e:
        logger.debug(str(e))
        cmds.warning("Couldn't paste material. Make sure you copied a material first, "
                     "then selected the target objects or components.")

""" ____________________________ Layout Functions ____________________________"""

def gtu_move_pivot_to_top():
    """ Moves pivot point to the top of the boundary box """
    selection = cmds.ls(selection=True)

    for obj in selection:
        bbox = cmds.exactWorldBoundingBox(obj)  # extracts bounding box
        top = [(bbox[0] + bbox[3]) / 2, bbox[4], (bbox[2] + bbox[5]) / 2]  # find top
        cmds.xform(obj, piv=top, ws=True)

def gtu_move_pivot_to_base():
    """ Moves pivot point to the base of the boundary box """
    selection = cmds.ls(selection=True)

    for obj in selection:
        bbox = cmds.exactWorldBoundingBox(obj)  # extracts bounding box
        bottom = [(bbox[0] + bbox[3]) / 2, bbox[1], (bbox[2] + bbox[5]) / 2]  # find bottom
        cmds.xform(obj, piv=bottom, ws=True)  # sends pivot to bottom

def gtu_move_to_origin():
    """ Moves selected objects back to origin """
    function_name = 'GTU Move to Origin'
    errors = ''
    cmds.undoInfo(openChunk=True, chunkName=function_name)  # Start undo chunk
    selection = cmds.ls(selection=True)
    try:
        for obj in selection:
            try:
                cmds.move(0, 0, 0, obj, a=True, rpr=True)  # rpr flag moves it according to the pivot
            except Exception as e:
                errors += str(e) + '\n'
        if errors != '':
            print('#### Errors: ####')
            print(errors)
            cmds.warning('Some objects could not be moved to the origin. Open the script editor for a list of errors.')
    except Exception as e:
        logger.debug(str(e))
    finally:
        cmds.undoInfo(closeChunk=True, chunkName=function_name)

""" ____________________________ Reset Functions ____________________________"""

def gtu_reset_transforms():
    """
    Reset transforms.
    It checks for incoming connections, then set the attribute to 0 if there are none
    It resets transforms, but ignores translate for joints.
    """
    function_name = 'GTU Reset Transforms'
    errors = ''
    cmds.undoInfo(openChunk=True, chunkName=function_name)  # Start undo chunk

    selection = cmds.ls(selection=True)

    def reset_transforms():
        for obj in selection:
            try:
                type_check = cmds.listRelatives(obj, children=True) or []

                if len(type_check) > 0 and cmds.objectType(type_check) != 'joint':
                    obj_connection_tx = cmds.listConnections(obj + '.tx', d=False, s=True) or []
                    if not len(obj_connection_tx) > 0:
                        if cmds.getAttr(obj + '.tx', lock=True) is False:
                            cmds.setAttr(obj + '.tx', 0)
                    obj_connection_ty = cmds.listConnections(obj + '.ty', d=False, s=True) or []
                    if not len(obj_connection_ty) > 0:
                        if cmds.getAttr(obj + '.ty', lock=True) is False:
                            cmds.setAttr(obj + '.ty', 0)
                    obj_connection_tz = cmds.listConnections(obj + '.tz', d=False, s=True) or []
                    if not len(obj_connection_tz) > 0:
                        if cmds.getAttr(obj + '.tz', lock=True) is False:
                            cmds.setAttr(obj + '.tz', 0)

                obj_connection_rx = cmds.listConnections(obj + '.rotateX', d=False, s=True) or []
                if not len(obj_connection_rx) > 0:
                    if cmds.getAttr(obj + '.rotateX', lock=True) is False:
                        cmds.setAttr(obj + '.rotateX', 0)
                obj_connection_ry = cmds.listConnections(obj + '.rotateY', d=False, s=True) or []
                if not len(obj_connection_ry) > 0:
                    if cmds.getAttr(obj + '.rotateY', lock=True) is False:
                        cmds.setAttr(obj + '.rotateY', 0)
                obj_connection_rz = cmds.listConnections(obj + '.rotateZ', d=False, s=True) or []
                if not len(obj_connection_rz) > 0:
                    if cmds.getAttr(obj + '.rotateZ', lock=True) is False:
                        cmds.setAttr(obj + '.rotateZ', 0)

                obj_connection_sx = cmds.listConnections(obj + '.scaleX', d=False, s=True) or []
                if not len(obj_connection_sx) > 0:
                    if cmds.getAttr(obj + '.scaleX', lock=True) is False:
                        cmds.setAttr(obj + '.scaleX', 1)
                obj_connection_sy = cmds.listConnections(obj + '.scaleY', d=False, s=True) or []
                if not len(obj_connection_sy) > 0:
                    if cmds.getAttr(obj + '.scaleY', lock=True) is False:
                        cmds.setAttr(obj + '.scaleY', 1)
                obj_connection_sz = cmds.listConnections(obj + '.scaleZ', d=False, s=True) or []
                if not len(obj_connection_sz) > 0:
                    if cmds.getAttr(obj + '.scaleZ', lock=True) is False:
                        cmds.setAttr(obj + '.scaleZ', 1)
            except Exception as exception:
                logger.debug(str(exception))
                errors += str(exception) + '\n'

    try:
        reset_transforms()
    except Exception as e:
        logger.debug(str(e))
    finally:
        cmds.undoInfo(closeChunk=True, chunkName=function_name)

    if errors != '':
        cmds.warning("Some objects couldn't be reset. Open the script editor for a list of errors.")

def gtu_reset_joint_sizes():
    """
    Resets the radius attribute back to one in all joints,
    then changes the global multiplier (jointDisplayScale) back to one
    """
    try:
        desired_size = 1
        all_joints = cmds.ls(type='joint')
        for obj in all_joints:
            if cmds.objExists(obj):
                if cmds.getAttr(obj + ".radius", lock=True) is False:
                    cmds.setAttr(obj + '.radius', 1)

                if cmds.getAttr(obj + ".v", lock=True) is False:
                    cmds.setAttr(obj + '.v', 1)
        cmds.jointDisplayScale(desired_size)

    except Exception as exception:
        raise exception

def gtu_reset_persp_shape_attributes():
    """
    If persp shape exists (default camera), reset its attributes
    """
    if cmds.objExists('perspShape'):
        try:
            cmds.setAttr('perspShape' + ".focalLength", 35)
            cmds.setAttr('perspShape' + ".verticalFilmAperture", 0.945)
            cmds.setAttr('perspShape' + ".horizontalFilmAperture", 1.417)
            cmds.setAttr('perspShape' + ".lensSqueezeRatio", 1)
            cmds.setAttr('perspShape' + ".fStop", 5.6)
            cmds.setAttr('perspShape' + ".focusDistance", 5)
            cmds.setAttr('perspShape' + ".shutterAngle", 144)
            cmds.setAttr('perspShape' + ".locatorScale", 1)
            cmds.setAttr('perspShape' + ".nearClipPlane", 0.100)
            cmds.setAttr('perspShape' + ".farClipPlane", 10000.000)
            cmds.setAttr('perspShape' + ".cameraScale", 1)
            cmds.setAttr('perspShape' + ".preScale", 1)
            cmds.setAttr('perspShape' + ".postScale", 1)
            cmds.setAttr('perspShape' + ".depthOfField", 0)
        except Exception as e:
            logger.debug(str(e))

""" ____________________________ Delete Functions ____________________________"""

def gtu_delete_namespaces():
    """Deletes all namespaces in the scene"""
    function_name = 'GTU Delete All Namespaces'
    cmds.undoInfo(openChunk=True, chunkName=function_name)
    try:
        default_namespaces = ['UI', 'shared']

        def num_children(namespace):
            """Used as a sort key, this will sort namespaces by how many children they have."""
            return namespace.count(':')

        namespaces = [namespace for namespace in cmds.namespaceInfo(lon=True, r=True) if
                      namespace not in default_namespaces]

        # Reverse List
        namespaces.sort(key=num_children, reverse=True)  # So it does the children first

        print(namespaces)

        for namespace in namespaces:
            if namespace not in default_namespaces:
                mel.eval('namespace -mergeNamespaceWithRoot -removeNamespace "' + namespace + '";')
    except Exception as e:
        cmds.warning(str(e))
    finally:
        cmds.undoInfo(closeChunk=True, chunkName=function_name)

def gtu_delete_display_layers():
    """ Deletes all display layers """
    function_name = 'GTU Delete All Display Layers'
    cmds.undoInfo(openChunk=True, chunkName=function_name)
    try:
        display_layers = cmds.ls(type='displayLayer')
        deleted_counter = 0
        for layer in display_layers:
            if layer != 'defaultLayer':
                cmds.delete(layer)
                deleted_counter += 1
        message = '' + str(deleted_counter) + ' '
        is_plural = 'layers were'
        if deleted_counter == 1:
            is_plural = 'layer was'
        message += is_plural + ' deleted.'

        cmds.inViewMessage(amg=message, pos='botLeft', fade=True, alpha=.9)
    except Exception as e:
        cmds.warning(str(e))
    finally:
        cmds.undoInfo(closeChunk=True, chunkName=function_name)

def gtu_delete_keyframes():
    """Deletes all keyframes. (Doesn't include Set Driven Keys)"""
    function_name = 'GTU Delete All Keyframes'
    cmds.undoInfo(openChunk=True, chunkName=function_name)
    try:
        keys_ta = cmds.ls(type='animCurveTA')
        keys_tl = cmds.ls(type='animCurveTL')
        keys_tt = cmds.ls(type='animCurveTT')
        keys_tu = cmds.ls(type='animCurveTU')
        # keys_ul = cmds.ls(type='animCurveUL') # Use optionVar to determine if driven keys should be deleted
        # keys_ua = cmds.ls(type='animCurveUA')
        # keys_ut = cmds.ls(type='animCurveUT')
        # keys_uu = cmds.ls(type='animCurveUU')
        deleted_counter = 0
        all_keyframes = keys_ta + keys_tl + keys_tt + keys_tu
        for obj in all_keyframes:
            try:
                cmds.delete(obj)
                deleted_counter += 1
            except Exception as e:
                logger.debug(str(e))
        message = '' + str(deleted_counter) + ' '
        is_plural = 'keyframe nodes were'
        if deleted_counter == 1:
            is_plural = 'keyframe node was'
        message += is_plural + ' deleted.'

        cmds.inViewMessage(amg=message, pos='botLeft', fade=True, alpha=.9)
    except Exception as e:
        cmds.warning(str(e))
    finally:
        cmds.undoInfo(closeChunk=True, chunkName=function_name)

def gtu_delete_nucleus_nodes():
    """ Deletes all elements related to particles """
    errors = ''
    function_name = 'GTU Delete Nucleus Nodes'
    try:
        cmds.undoInfo(openChunk=True, chunkName=function_name)

        # Without Transform
        emitters = cmds.ls(typ='pointEmitter')
        solvers = cmds.ls(typ='nucleus')
        instancers = cmds.ls(typ='instancer')

        no_transforms = emitters + instancers + solvers + instancers

        # With Transform
        nparticle_nodes = cmds.ls(typ='nParticle')
        spring_nodes = cmds.ls(typ='spring')
        particle_nodes = cmds.ls(typ='particle')
        nrigid_nodes = cmds.ls(typ='nRigid')
        ncloth_nodes = cmds.ls(typ='nCloth')
        pfxhair_nodes = cmds.ls(typ='pfxHair')
        hair_nodes = cmds.ls(typ='hairSystem')
        nconstraint_nodes = cmds.ls(typ='dynamicConstraint')

        transforms = nparticle_nodes + spring_nodes + particle_nodes + nrigid_nodes
        transforms += ncloth_nodes + pfxhair_nodes + hair_nodes + nconstraint_nodes

        # Fields/Solvers Types
        # airField
        # dragField
        # newtonField
        # radialField
        # turbulenceField
        # uniformField
        # vortexField
        # volumeAxisField

        deleted_counter = 0
        for obj in transforms:
            try:
                parent = cmds.listRelatives(obj, parent=True) or []
                cmds.delete(parent[0])
                deleted_counter += 1
            except Exception as e:
                logger.debug(str(e))
        for obj in no_transforms:
            try:
                cmds.delete(obj)
                deleted_counter += 1
            except Exception as e:
                logger.debug(str(e))

        message = '' + str(deleted_counter) + ' '
        is_plural = 'objects were'
        if deleted_counter == 1:
            is_plural = 'object was'
        message += is_plural + ' deleted.'

        cmds.inViewMessage(amg=message, pos='botLeft', fade=True, alpha=.9)

    except Exception as e:
        errors += str(e) + '\n'
        cmds.warning('An error occurred. Open the script editor for more information.')
    finally:
        cmds.undoInfo(closeChunk=True, chunkName=function_name)
    if errors != '':
        print('######## Errors: ########')
        print(errors)

def gtu_delete_user_defined_attributes():
    """ Deletes all User defined attributes for the selected objects. """
    function_name = 'GTU Delete User Defined Attributes'
    cmds.undoInfo(openChunk=True, chunkName=function_name)

    selection = cmds.ls(selection=True)
    if selection == 0:
        cmds.warning('Select at least one target object to delete custom attributes')
        return

    try:
        custom_attributes = []
        for sel in selection:
            attributes = cmds.listAttr(sel, userDefined=True) or []
            for attr in attributes:
                custom_attributes.append(sel + '.' + attr)

        deleted_counter = 0
        for obj in custom_attributes:
            try:
                cmds.deleteAttr(obj)
                deleted_counter += 1
            except Exception as e:
                logger.debug(str(e))
        message = '' + str(deleted_counter) + ' '
        is_plural = 'user defined attributes were'
        if deleted_counter == 1:
            is_plural = 'user defined attribute was'
        message += is_plural + ' deleted.'

        cmds.inViewMessage(amg=message, pos='botLeft', fade=True, alpha=.9)
    except Exception as e:
        cmds.warning(str(e))
    finally:
        cmds.undoInfo(closeChunk=True, chunkName=function_name)

""" ____________________________ External Functions ____________________________"""

def gtu_combine_curves():
    """ Moves the shape objects of all selected curves under a single group (combining them) """
    errors = ''
    function_name = 'GTU Combine Curves'
    try:
        cmds.undoInfo(openChunk=True, chunkName=function_name)
        selection = cmds.ls(sl=True, absoluteName=True)
        valid_selection = True
        acceptable_types = ['nurbsCurve', 'bezierCurve']
        bezier_in_selection = []

        for obj in selection:
            shapes = cmds.listRelatives(obj, shapes=True, fullPath=True) or []
            for shape in shapes:
                if cmds.objectType(shape) == 'bezierCurve':
                    bezier_in_selection.append(obj)
                if cmds.objectType(shape) not in acceptable_types:
                    valid_selection = False
                    cmds.warning('Make sure you selected only curves.')

        if valid_selection and len(selection) < 2:
            cmds.warning('You need to select at least two curves.')
            valid_selection = False

        if len(bezier_in_selection) > 0 and valid_selection:
            user_input = cmds.confirmDialog(title='Bezier curve detected!',
                                            message='A bezier curve was found in your selection.\n'
                                                    'Would you like to convert Bezier to NURBS before combining?',
                                            button=['Yes', 'No'],
                                            defaultButton='Yes',
                                            cancelButton='No',
                                            dismissString='No',
                                            icon="warning")
            if user_input == 'Yes':
                for obj in bezier_in_selection:
                    logger.debug(str(obj))
                    cmds.bezierCurveToNurbs()

        if valid_selection:
            shapes = cmds.listRelatives(shapes=True, fullPath=True)
            for obj in range(len(selection)):
                cmds.makeIdentity(selection[obj], apply=True, rotate=True, scale=True, translate=True)

            group = cmds.group(empty=True, world=True, name=selection[0])
            cmds.refresh()
            cmds.select(shapes[0])
            for obj in range(1, (len(shapes))):
                cmds.select(shapes[obj], add=True)

            cmds.select(group, add=True)
            cmds.parent(relative=True, shape=True)
            cmds.delete(selection)

    except Exception as e:
        errors += str(e) + '\n'
        cmds.warning('An error occurred when combining the curves. Open the script editor for more information.')
    finally:

        cmds.undoInfo(closeChunk=True, chunkName=function_name)
    if errors != '':
        print('######## Errors: ########')
        print(errors)

def gtu_separate_curves():
    """
    Moves the shapes instead of a curve to individual transforms (separating curves)
    """
    errors = ''
    acceptable_types = ['nurbsCurve', 'bezierCurve']

    def get_short_name(full_name):
        """
        Get the name of the objects without its path (Maya returns full path if name is not unique)

        Args:
            full_name (string) - object to extract short name
        """
        short_name = ''
        if full_name == '':
            return ''
        split_path = full_name.split('|')
        if len(split_path) >= 1:
            short_name = split_path[len(split_path) - 1]
        return short_name

    function_name = 'GTU Separate Curves'
    try:
        cmds.undoInfo(openChunk=True, chunkName=function_name)
        selection = cmds.ls(sl=True)
        valid_selection = True

        curve_shapes = []
        parent_transforms = []

        if len(selection) < 1:
            valid_selection = False
            cmds.warning('You need to select at least one curve.')

        if valid_selection:
            for obj in selection:
                shapes = cmds.listRelatives(obj, shapes=True, fullPath=True) or []
                for shape in shapes:
                    if cmds.objectType(shape) in acceptable_types:
                        curve_shapes.append(shape)

            if len(curve_shapes) == 0:
                cmds.warning('You need to select at least one curve.')
            elif len(curve_shapes) > 1:
                for obj in curve_shapes:
                    parent = cmds.listRelatives(obj, parent=True) or []
                    for par in parent:
                        if par not in parent_transforms:
                            parent_transforms.append(par)
                        cmds.makeIdentity(par, apply=True, rotate=True, scale=True, translate=True)
                    group = cmds.group(empty=True, world=True, name=get_short_name(obj).replace('Shape', ''))
                    cmds.parent(obj, group, relative=True, shape=True)
            else:
                cmds.warning('The selected curve contains only one shape.')

            for obj in parent_transforms:
                shapes = cmds.listRelatives(obj, shapes=True, fullPath=True) or []
                if cmds.objExists(obj) and cmds.objectType(obj) == 'transform' and len(shapes) == 0:
                    cmds.delete(obj)

    except Exception as e:
        errors += str(e) + '\n'
        cmds.warning('An error occurred when separating the curves. Open the script editor for more information.')
    finally:
        cmds.undoInfo(closeChunk=True, chunkName=function_name)
    if errors != '':
        print('######## Errors: ########')
        print(errors)

def gtu_convert_bif_to_mesh():
    """
    Converts Bifrost geometry to Maya geometry
    """
    errors = ''
    function_name = 'GTU Convert Bif to Mesh'
    try:
        cmds.undoInfo(openChunk=True, chunkName=function_name)
        valid_selection = True

        selection = cmds.ls(selection=True)
        bif_objects = []
        bif_graph_objects = []

        if len(selection) < 1:
            valid_selection = False
            cmds.warning('You need to select at least one bif object.')

        if valid_selection:
            for obj in selection:
                shapes = cmds.listRelatives(obj, shapes=True, fullPath=True) or []
                for shape in shapes:
                    if cmds.objectType(shape) == 'bifShape':
                        bif_objects.append(shape)
                    if cmds.objectType(shape) == 'bifrostGraphShape':
                        bif_graph_objects.append(shape)

            for bif in bif_objects:
                parent = cmds.listRelatives(bif, parent=True) or []
                for par in parent:
                    source_mesh = cmds.listConnections(par + '.inputSurface', source=True, plugs=True) or []
                    for sm in source_mesh:
                        conversion_node = cmds.createNode("bifrostGeoToMaya")
                        cmds.connectAttr(sm, conversion_node + '.bifrostGeo')
                        mesh_node = cmds.createNode("mesh")
                        mesh_transform = cmds.listRelatives(mesh_node, parent=True) or []
                        cmds.connectAttr(conversion_node + '.mayaMesh[0]', mesh_node + '.inMesh')
                        cmds.rename(mesh_transform[0], 'bifToGeo1')
                        try:
                            cmds.hyperShade(assign='lambert1')
                        except Exception as e:
                            logger.debug(str(e))

            for bif in bif_graph_objects:
                bifrost_attributes = cmds.listAttr(bif, fp=True, inUse=True, read=True) or []
                for output in bifrost_attributes:
                    conversion_node = cmds.createNode("bifrostGeoToMaya")
                    cmds.connectAttr(bif + '.' + output, conversion_node + '.bifrostGeo')
                    mesh_node = cmds.createNode("mesh")
                    mesh_transform = cmds.listRelatives(mesh_node, parent=True) or []
                    cmds.connectAttr(conversion_node + '.mayaMesh[0]', mesh_node + '.inMesh')
                    bif_mesh = cmds.rename(mesh_transform[0], 'bifToGeo1')
                    try:
                        cmds.hyperShade(assign='lambert1')
                    except Exception as e:
                        logger.debug(str(e))

                    vtx = cmds.ls(bif_mesh + '.vtx[*]', fl=True) or []
                    if len(vtx) == 0:
                        try:
                            cmds.delete(bif_mesh)
                            # cmds.delete(conversion_node)
                            # cmds.delete(mesh_node)
                        except Exception as e:
                            logger.debug(str(e))
    except Exception as e:
        errors += str(e) + '\n'
    finally:
        cmds.undoInfo(closeChunk=True, chunkName=function_name)
    if errors != '':
        cmds.warning('An error occurred when converting bif to mesh. Open the script editor for more information.')
        print('######## Errors: ########')
        print(errors)

""" ____________________________ About Window ____________________________"""

def gtu_build_gui_about_gt_tools():
    """ Creates "About" window for the GT Tools menu """

    stored_gt_tools_version_exists = cmds.optionVar(exists="gt_tools_version")

    # Define Version
    if stored_gt_tools_version_exists:
        gt_version = cmds.optionVar(q="gt_tools_version")
    else:
        gt_version = '?'

    window_name = "gtu_build_gui_about_gt_tools"
    if cmds.window(window_name, exists=True):
        cmds.deleteUI(window_name, window=True)

    cmds.window(window_name, title="About - GT Tools", mnb=False, mxb=False, s=True)
    cmds.window(window_name, e=True, s=True, wh=[1, 1])

    cmds.columnLayout("main_column", p=window_name)

    # Title Text
    cmds.separator(h=12, style='none')  # Empty Space
    cmds.rowColumnLayout(nc=1, cw=[(1, 310)], cs=[(1, 10)], p="main_column")  # Window Size Adjustment
    cmds.rowColumnLayout(nc=1, cw=[(1, 300)], cs=[(1, 10)], p="main_column")  # Title Column
    cmds.text("GT Tools", bgc=[.4, .4, .4], fn="boldLabelFont", align="center")
    cmds.separator(h=10, style='none', p="main_column")  # Empty Space

    cmds.rowColumnLayout(nc=1, cw=[(1, 300)], cs=[(1, 10)], p="main_column")
    cmds.text(l='Version Installed: ' + gt_version, align="center", fn="boldLabelFont")
    cmds.separator(h=5, style='none')  # Empty Space
    cmds.text(l='GT Tools is a free collection of Maya scripts', align="center")

    cmds.separator(h=15, style='none')  # Empty Space
    cmds.text(l='About:', align="center", fn="boldLabelFont")
    cmds.text(l='This is my collection of scripts for Autodesk Maya.\n'
                'These scripts were created with the aim of automating,\n e'
                'nhancing or simply filling the missing details of what\n I find lacking in Maya.', align="center")
    cmds.separator(h=15, style='none')  # Empty Space
    cmds.text(
        l='When installed you can find a pull-down menu that\n provides easy access to a variety of related tools.',
        align="center")
    cmds.separator(h=5, style='none')  # Empty Space
    cmds.text(l='This menu contains sub-menus that have been\n organized to contain related tools.\n '
                'For example: modeling, rigging, utilities, etc...', align="center")
    cmds.separator(h=15, style='none')  # Empty Space
    cmds.text(l='All of these items are supplied as is.\nYou alone are responsible for any issues.\n'
                'Use at your own risk.', align="center")
    cmds.separator(h=15, style='none')  # Empty Space
    cmds.text(l='Hopefully these scripts are helpful to you\nas they are to me.', align="center")
    cmds.separator(h=15, style='none')  # Empty Space
    cmds.rowColumnLayout(nc=2, cw=[(1, 140), (2, 140)], cs=[(1, 10), (2, 0)], p="main_column")
    cmds.text('Guilherme Trevisan  ')
    cmds.text(l='TrevisanGMW@gmail.com', hl=True, highlightColor=[1, 1, 1])
    cmds.rowColumnLayout(nc=2, cw=[(1, 140), (2, 140)], cs=[(1, 10), (2, 0)], p="main_column")
    cmds.separator(h=15, style='none')  # Empty Space
    cmds.text(l='Github', hl=True, highlightColor=[1, 1, 1])
    cmds.separator(h=7, style='none')  # Empty Space

    # Close Button
    cmds.rowColumnLayout(nc=1, cw=[(1, 300)], cs=[(1, 10)], p="main_column")
    cmds.separator(h=10, style='none')
    cmds.button(l='OK', h=30, c=lambda args: close_help_gui())
    cmds.separator(h=8, style='none')

    # Show and Lock Window
    cmds.showWindow(window_name)
    cmds.window(window_name, e=True, s=False)

    # Set Window Icon
    qw = OpenMayaUI.MQtUtil.findWindow(window_name)
    widget = wrapInstance(int(qw), QWidget)
    icon = QIcon(':/question.png')
    widget.setWindowIcon(icon)

    def close_help_gui():
        if cmds.window(window_name, exists=True):
            cmds.deleteUI(window_name, window=True)

def gtu_delete_all_locators():
    """ Deletes all locators """
    errors = ''
    function_name = 'GTU Delete All Locators'
    try:
        cmds.undoInfo(openChunk=True, chunkName=function_name)

        # With Transform
        locators = cmds.ls(typ='locator')

        deleted_counter = 0
        for obj in locators:
            try:
                parent = cmds.listRelatives(obj, parent=True) or []
                cmds.delete(parent[0])
                deleted_counter += 1
            except Exception as e:
                logger.debug(str(e))

        message = '' + str(deleted_counter) + ' '
        is_plural = 'locators were'
        if deleted_counter == 1:
            is_plural = 'locator was'
        message += is_plural + ' deleted.'

        cmds.inViewMessage(amg=message, pos='botLeft', fade=True, alpha=.9)

    except Exception as e:
        errors += str(e) + '\n'
        cmds.warning('An error occurred when deleting locators. Open the script editor for more information.')
    finally:
        cmds.undoInfo(closeChunk=True, chunkName=function_name)
    if errors != '':
        print('######## Errors: ########')
        print(errors)

def gtu_full_hud_toggle():
    """ Toggles common HUD options so all the common ones are either active or inactive  """
    hud_current_state = {}

    # 1 - Animation Details
    hud_current_state['animationDetailsVisibility'] = int(mel.eval('optionVar -q animationDetailsVisibility;'))
    # 2 - Cache
    try:
        from maya.plugin.evaluator.CacheUiHud import CachePreferenceHud
        hud_current_state['CachePreferenceHud'] = int(CachePreferenceHud().get_value() or 0)
    except Exception as e:
        logger.debug(str(e))
        hud_current_state['CachePreferenceHud'] = 0
    # 3 - Camera Names
    hud_current_state['cameraNamesVisibility'] = int(mel.eval('optionVar -q cameraNamesVisibility;'))
    # 4 - Caps Lock
    hud_current_state['capsLockVisibility'] = int(mel.eval('optionVar -q capsLockVisibility;'))
    # 5 - Current Asset
    hud_current_state['currentContainerVisibility'] = int(mel.eval('optionVar -q currentContainerVisibility;'))
    # 6 - Current Frame
    hud_current_state['currentFrameVisibility'] = int(mel.eval('optionVar -q currentFrameVisibility;'))
    # 7 - Evaluation
    hud_current_state['evaluationVisibility'] = int(mel.eval('optionVar -q evaluationVisibility;'))
    # 8 - Focal Length
    hud_current_state['focalLengthVisibility'] = int(mel.eval('optionVar -q focalLengthVisibility;'))
    # 9 - Frame Rate
    hud_current_state['frameRateVisibility'] = int(mel.eval('optionVar -q frameRateVisibility;'))
    # 10 - HumanIK Details
    hud_current_state['hikDetailsVisibility'] = int(mel.eval('optionVar -q hikDetailsVisibility;'))
    # 11 - Material Loading Details
    hud_current_state['materialLoadingDetailsVisibility'] = int(
        mel.eval('optionVar -q materialLoadingDetailsVisibility;'))
    # 12 - Object Details
    hud_current_state['objectDetailsVisibility'] = int(mel.eval('optionVar -q objectDetailsVisibility;'))
    # 13 - Origin Axis - Ignored as non-hud element
    # hud_current_state['originAxesMenuUpdate'] = mel.eval('optionVar -q originAxesMenuUpdate;')
    # 14 - Particle Count
    hud_current_state['particleCountVisibility'] = int(mel.eval('optionVar -q particleCountVisibility;'))
    # 15 - Poly Count
    hud_current_state['polyCountVisibility'] = int(mel.eval('optionVar -q polyCountVisibility;'))
    # 16 - Scene Timecode
    hud_current_state['sceneTimecodeVisibility'] = int(mel.eval('optionVar -q sceneTimecodeVisibility;'))
    # 17 - Select Details
    hud_current_state['selectDetailsVisibility'] = int(mel.eval('optionVar -q selectDetailsVisibility;'))
    # 18 - Symmetry
    hud_current_state['symmetryVisibility'] = int(mel.eval('optionVar -q symmetryVisibility;'))
    # 19 - View Axis
    hud_current_state['viewAxisVisibility'] = int(mel.eval('optionVar -q viewAxisVisibility;'))
    # 20 - Viewport Renderer
    hud_current_state['viewportRendererVisibility'] = int(mel.eval('optionVar -q viewportRendererVisibility;'))
    # ------- Separator -------
    # 21 - In-view Messages
    hud_current_state['inViewMessageEnable'] = int(mel.eval('optionVar -q inViewMessageEnable;'))
    # 22 - In-view Editors
    hud_current_state['inViewEditorVisible'] = int(mel.eval('optionVar -q inViewEditorVisible;'))
    # Conditional - XGen Info
    hud_current_state['xgenHUDVisibility'] = int(mel.eval('optionVar -q xgenHUDVisibility;'))

    # Check if toggle ON or OFF
    toggle = True
    count = 0
    for item_state in hud_current_state:
        if hud_current_state.get(item_state):
            count += 1
    # More than half is on, so OFF else ON (Default)
    if count > len(hud_current_state) / 2:
        toggle = False

    # Toggles non-standard hud elements
    if toggle:
        mel.eval('setAnimationDetailsVisibility(true)')
        try:
            from maya.plugin.evaluator.CacheUiHud import CachePreferenceHud
            CachePreferenceHud().set_value(True)
        except Exception as e:
            logger.debug(str(e))
        mel.eval('setCameraNamesVisibility(true)')
        mel.eval('setCapsLockVisibility(true)')
        mel.eval('setCurrentContainerVisibility(true)')
        mel.eval('setCurrentFrameVisibility(true)')
        mel.eval('SetEvaluationManagerHUDVisibility(1)')
        mel.eval('setFocalLengthVisibility(true)')
        mel.eval('setFrameRateVisibility(true)')
        if not hud_current_state.get('hikDetailsVisibility'):
            cmds.ToggleHikDetails()
            mel.eval('catchQuiet(setHikDetailsVisibility(true));')
        mel.eval('ToggleMaterialLoadingDetailsHUDVisibility(true)')
        mel.eval('setObjectDetailsVisibility(true)')
        mel.eval('setParticleCountVisibility(true)')
        mel.eval('setPolyCountVisibility(true)')
        mel.eval('setSceneTimecodeVisibility(true)')
        mel.eval('setSelectDetailsVisibility(true)')
        mel.eval('setSymmetryVisibility(true)')
        mel.eval('setViewAxisVisibility(true)')
        mel.eval('setViewportRendererVisibility(true)')
        mel.eval('catchQuiet(setXGenHUDVisibility(true));')

        if not hud_current_state.get('inViewMessageEnable'):
            cmds.ToggleInViewMessage()
        if not hud_current_state.get('inViewEditorVisible'):
            cmds.ToggleInViewEditor()
    else:
        mel.eval('setAnimationDetailsVisibility(false)')
        try:
            from maya.plugin.evaluator.CacheUiHud import CachePreferenceHud
            CachePreferenceHud().set_value(False)
        except Exception as e:
            logger.debug(str(e))
        mel.eval('setCurrentContainerVisibility(false)')
        mel.eval('setCurrentFrameVisibility(false)')
        mel.eval('SetEvaluationManagerHUDVisibility(0)')
        mel.eval('setFocalLengthVisibility(false)')
        mel.eval('setFrameRateVisibility(false)')
        if hud_current_state.get('hikDetailsVisibility'):
            cmds.ToggleHikDetails()
            mel.eval('catchQuiet(setHikDetailsVisibility(false));')
            mel.eval('catchQuiet(hikDetailsKeyingMode());')
        mel.eval('ToggleMaterialLoadingDetailsHUDVisibility(false)')
        mel.eval('setObjectDetailsVisibility(false)')
        mel.eval('setParticleCountVisibility(false)')
        mel.eval('setPolyCountVisibility(false)')
        mel.eval('setSceneTimecodeVisibility(false)')
        mel.eval('setSelectDetailsVisibility(false)')
        mel.eval('setViewportRendererVisibility(false)')
        mel.eval('catchQuiet(setXGenHUDVisibility(false));')
    # Default states are preserved: camera names, caps lock, symmetry, view axis, in-view messages and in-view editor

def gtu_convert_joints_to_mesh(combine_mesh=True):
    """
    Converts a joint hierarchy into a mesh representation of it (Helpful when sending it to sculpting apps)
    Args:
        combine_mesh: Combines generated meshes into one

    Returns:
        A list of generated meshes
    """
    selection = cmds.ls(selection=True, type='joint')
    if len(selection) != 1:
        cmds.warning('Please selection only the root joint and try again.')
        return
    cmds.select(selection[0], replace=True)
    cmds.select(hierarchy=True)
    joints = cmds.ls(selection=True, type='joint')

    generated_mesh = []
    for obj in reversed(joints):
        if cmds.objExists(obj):
            joint_name = obj.split('|')[-1]
            radius = cmds.getAttr(obj + '.radius')
            joint_sphere = cmds.polySphere(radius=radius * .5,
                                           subdivisionsAxis=8,
                                           subdivisionsHeight=8,
                                           axis=[1, 0, 0],
                                           name=joint_name + 'JointMesh',
                                           ch=False)
            generated_mesh.append(joint_sphere[0])
            cmds.delete(cmds.parentConstraint(obj, joint_sphere))
            joint_parent = cmds.listRelatives(obj, parent=True) or []
            if joint_parent:
                joint_cone = cmds.polyCone(radius=radius * .5,
                                           subdivisionsAxis=4,
                                           name=joint_name + 'BoneMesh',
                                           ch=False)
                generated_mesh.append(joint_cone[0])
                bbox = cmds.exactWorldBoundingBox(joint_cone)
                bottom = [(bbox[0] + bbox[3]) / 2, bbox[1], (bbox[2] + bbox[5]) / 2]
                cmds.xform(joint_cone, piv=bottom, ws=True)
                cmds.move(1, joint_cone, moveY=True)
                cmds.rotate(90, joint_cone, rotateX=True)
                cmds.rotate(90, joint_cone, rotateY=True)
                cmds.makeIdentity(joint_cone, rotate=True, apply=True)

                cmds.delete(cmds.parentConstraint(joint_parent, joint_cone))
                cmds.delete(cmds.aimConstraint(obj, joint_cone))

                child_pos = cmds.xform(obj, t=True, ws=True, query=True)
                cmds.xform(joint_cone[0] + '.vtx[4]', t=child_pos, ws=True)
    if combine_mesh:
        cmds.select(generated_mesh, replace=True)
        mesh = cmds.polyUnite()
        cmds.select(clear=True)
        cmds.delete(mesh, constructionHistory=True)
        mesh = cmds.rename(mesh[0], selection[0] + 'AsMesh')
        return [mesh]
    else:
        return generated_mesh

def output_string_to_notepad(string, file_name='tmp'):
    """
    Creates a txt file and writes a list of objects to it (with necessary code used to select it, in Mel and Python)

    Args:
        string (string): A list of string to be exported to a txt file
        file_name (string): Name of the generated file

    """
    temp_dir = cmds.internalVar(userTmpDir=True)
    txt_file = temp_dir + file_name + '.txt'

    f = open(txt_file, 'w')
    f.write(string)
    f.close()

    notepad_command = 'exec("notepad ' + txt_file + '");'
    mel.eval(notepad_command)

""" ____________________________ Functions Calls ____________________________"""
if __name__ == '__main__':
    pass
    # gtu_reload_file()
    # gtu_open_resource_browser()
    # gtu_unlock_default_channels()
    # gtu_unhide_default_channels()
    # gtu_import_references()
    # gtu_remove_references()
    # gtu_uniform_lra_toggle()
    # gtu_uniform_jnt_label_toggle()
    # gtu_select_non_unique_objects()

    # gtu_generate_udim_previews()
    # gtu_copy_material()
    # gtu_paste_material()

    # gtu_move_pivot_to_top()
    # gtu_move_pivot_to_base()
    # gtu_move_to_origin()

    # gtu_reset_joint_sizes()
    # gtu_reset_transforms()
    # gtu_reset_persp_shape_attributes()

    # gtu_delete_namespaces()
    # gtu_delete_display_layers()
    # gtu_delete_keyframes()
    # gtu_delete_nucleus_nodes()
    # gtu_delete_user_defined_attributes()

    # --- Outside Utilities ---
    # gtu_combine_curves()
    # gtu_separate_curves()
    # gtu_convert_bif_to_mesh()

    # gtu_build_gui_about_gt_tools()

    # --- Other Functions ---
    # gtu_delete_all_locators()
    # gtu_full_hud_toggle()
    # gtu_convert_joints_to_mesh()
    # output_string_to_notepad('Test')


import threading
import requests
import json
import time
import urllib.parse
from dotmap import DotMap

from models.types import Event, Message
from models.errors import APIError
import asyncio
import aiohttp
import traceback
from pprint import pprint

class UpdateManager():

    def __init__(self, **kwargs):
        self.loop = asyncio.get_event_loop()

        self.session = kwargs.get("session")
        self.token = kwargs.get("token")
        self.callback = kwargs.get("callback")

        self.poll_timeout = 100

        self.URL = f"https://api.telegram.org/bot{self.token}/"

        self.last_update = None
        self.command_queue = []

    async def update_loop(self):
        print("Starting Poll Update Loop")

        while True:
            await self.poll_updates(self.last_update)

    async def poll_updates(self, offset=None):

        url = f"{self.URL}getUpdates?timeout={self.poll_timeout}"

        if offset:
            url += f"&offset={offset}"

        async with self.session.get(url) as resp:
            data = await resp.json()
            result = data['result']

            if result:
                self.command_queue.extend(result)
                self.last_update = max(x["update_id"] for x in result) + 1
                print(f"Poll Successful: {self.last_update}")
                await self.callback()

class NatsukoClient():

    def __init__(self, token, **kwargs):
        self.token = token
        self.API_URL = f"https://api.telegram.org/bot{self.token}/"

        self.commands = {}
        self.usercache = {}

        self.loop = asyncio.get_event_loop()
        self.session = aiohttp.ClientSession(loop=self.loop)
        self.manager = UpdateManager(token=self.token, session=self.session, callback=self.process)

    def run(self):

        self.loop.run_until_complete(self._run())

    async def _run(self):

        task = asyncio.ensure_future(self.manager.update_loop())
        await task

    async def process(self):
        print(f"Callback Called: {self}")

        while self.manager.command_queue:
            _cmd = DotMap(self.manager.command_queue.pop(0))
            command = Event(self, _cmd)
            self.parse_command(command)

    def parse_command(self, event):

        for entity in event.message.entities:
            if entity.is_command:
                command = entity.text[1:]
                print(f"Identified as Bot Command: {entity}")

                if command in self.commands:
                    func = self.commands[command]["function"]
                    asyncio.ensure_future(func(event))

        user = event.message.author
        if not user.username in self.usercache:
            self.usercache[user.username] = user

        #
        # not sure that any of this shit is necessary
        #
        # if "message" in event and not raw_command.message.keys():
            # if not username in self.usercache:
                # self.usercache[username] = raw_command.message["from"]

        # elif "inline_query" in raw_command:
            # if not username in self.usercache:
                # self.usercache[username] = raw_command.inline_query["from"]

        # elif "chat" in raw_command:
            # if not username in self.usercache:
                # self.usercache[username] = raw_command.chat["from"]

    # Command Decorator
    def command(self, name, **options):

        def deco(f):
            command = {"function": f}
            command["no_error"] = False if "no_error" not in options else options["no_error"]

            self.commands[name] = command
            print(f'\tLOAD_OK: {f.__name__}: on_command @ {name}')

            return f

        return deco

    async def _api_send(self, url, apiq):
        print(f"APISEND: {apiq}")

        async with self.session.get(url, params=apiq) as resp:
            content = await resp.json()

            if not content["ok"]:
                raise APIError(content)

            return content["result"]

    async def send_message(self, chat_id, message, **kwargs):
        """
            Use this method to send text messages. On success, the sent Message is returned.
            (Optional parameters are keyword arguments)

            Parameters                  Type    Required    Description
            chat_id                     Int/Str Yes         Unique identifier for the target chat or username of
                                                            the target channel (in the format @channelusername)
            text                        String  Yes         Text of the message to be sent
            parse_mode*                 String  Optional    Send Markdown or HTML, if you want Telegram apps to
                                                            show bold, italic, fixed-width text or inline URLs
                                                            in your bot's message.
            disable_web_page_preview*   Boolean Optional    Disables link previews for links in this message
            disable_notification*       Boolean Optional    Sends the message silently. Users will receive a
                                                            notification with no sound.
            reply*                      Integer Optional    If the message is a reply, ID of the original message
            reply_markup*               Json    Optional    Additional interface options. A JSON-serialized
                                                            object for an inline keyboard, custom reply keyboard,
                                                            instructions to remove reply keyboard or to force a
                                                            reply from the user.
        """

        endpoint = "sendMessage"

        url = self.API_URL + endpoint
        args = {"chat_id": chat_id, "text": message, **kwargs}
        return await self._api_send(url, args)

    async def forward_message(self, target_cid, source_cid,
                    message_id, **kwargs):
        """
        Use this method to forward messages of any kind. On success, the sent Message is returned.
        (Optional parameters are keyword arguments)

        Parameters              Type    Required    Description
        chat_id                 Int/Str Yes         Unique identifier for the target chat or username of the
                                                    target channel (in the format @channelusername)
        from_chat_id            Int/Str Yes         Unique identifier for the chat where the original message
                                                    was sent (or channel username in the format
                                                    @channelusername)
        disable_notification    Boolean Optional    Sends the message silently. Users will receive a
                                                    notification with no sound.
        message_id              Integer Yes         Message identifier in the chat specified in from_chat_id
        """

        endpoint = "forwardMessage"

        url = self.API_URL + endpoint
        args = {'chat_id': target_cid, 'from_chat_id': source_cid, 'message_id': message_id, **kwargs}

        return await self._api_send(url, args)

    async def send_photo(self, chat_id, photo, **kwargs):
        """Use this method to send photos. On success, the sent Message is returned.
        (Optional parameters are keyword arguments)

        Parameters              Type        Required    Description
        chat_id                 Int/Str     Yes         Unique identifier for the target chat or username of
                                                        the target channel (in the format @channelusername)
        photo                   File/Str    Yes         Photo to send. Pass a file_id or a binary filestream.
        caption                 String      Optional    Photo caption (may also be used when resending photos
                                                        by file_id), 0-200 characters
        disable_notification    Boolean     Optional    Sends the message silently. Users will receive a
                                                        notification with no sound.
        reply                   Integer     Optional    If the message is a reply, ID of the original message
        reply_markup            Json        Optional    Additional interface options. A JSON-serialized object
                                                        for an inline keyboard, custom reply keyboard,
                                                        instructions to remove reply keyboard or to
                                                        force a reply from the user.
        """

        endpoint = 'sendPhoto'
        url = self.API_URL + endpoint

        if isinstance(photo, str):
            if photo.startswith('http'):
                photo = urllib.parse.quote(photo)

            args = {"chat_id": chat_id, "photo": photo, **kwargs}
            return await self._api_send(url, args)

        else:
            args = {"chat_id": chat_id, **kwargs}
            response = await self.session.post(url, data=dict(photo=photo), params=args)
            return response.json()

    async def send_audio(self, chat_id, audio, **kwargs):
        """Use this method to send audio, if you want Telegram clients to display them in the music
        player. Your audio must be in the .mp3 format. On success, the sent Message is returned.
        Bots can currently send audio files of up to 50 MB in size, this limit may be changed in the
        future. On success, the sent Message is returned.
        (Optional parameters are keyword arguments)

        Parameters              Type        Required    Description
        chat_id                 Int/Str     Yes         Unique identifier for the target chat or username of
                                                        the target channel (in the format @channelusername)
        audio                   File/Str    Yes         Audio file to send. Pass a file_id or a
                                                        binary filestream.
        caption                 String      Optional    Audio Caption, 0-200 characters
        duration                Integer     Optional    Duration of the audio in seconds
        performer               String      Optional    Performer
        title                   String      Optional    Track name
        disable_notification    Boolean     Optional    Sends the message silently. Users will receive a
                                                        notification with no sound.
        reply                   Integer     Optional    If the message is a reply, ID of the original message
        reply_markup            Json        Optional    Additional interface options. A JSON-serialized object
                                                        for an inline keyboard, custom reply keyboard,
                                                        instructions to remove reply keyboard or to
                                                        force a reply from the user.
        """

        endpoint = "sendAudio"

        url = self.API_URL + endpoint

        if isinstance(audio, str):
            args = {'chat_id': chat_id, 'audio': audio, **kwargs}
            return await self._api_send(url, args)

        else:
            args = {'chat_id': chat_id, **kwargs}
            response = await self.session.post(url, data=dict(audio=audio), params=args)
            return response.json()

    async def send_document(self, chat_id, document, **kwargs):
        """Use this method to send documents.  Bots can currently send files of any type of up to 50 MB
        in size, this limit may be changed in the future. On success, the sent Message is returned.
        (Optional parameters are keyword arguments)

        Parameters              Type        Required    Description
        chat_id                 Int/Str     Yes         Unique identifier for the target chat or username of
                                                        the target channel (in the format @channelusername)
        document                File/Str    Yes         File to send. Pass a file_id or a
                                                        binary filestream.
        caption                 String      Optional    Caption, 0-200 characters
        disable_notification    Boolean     Optional    Sends the message silently. Users will receive a
                                                        notification with no sound.
        reply                   Integer     Optional    If the message is a reply, ID of the original message
        reply_markup            Json        Optional    Additional interface options. A JSON-serialized object
                                                        for an inline keyboard, custom reply keyboard,
                                                        instructions to remove reply keyboard or to
                                                        force a reply from the user.
        """

        endpoint = "sendDocument"

        if isinstance(document, str):
            args = {'chat_id': chat_id, 'document': document, **kwargs}
            return await self._api_send(url, args)

        else:
            args = {'chat_id': chat_id, **kwargs}
            response = await self.session.post(url, data=dict(document=document), params=args)
            return response.json()

    async def send_video(self, chat_id, video, **kwargs):
        """Use this method to send videos. Telegram clients support mp4 videos
        (other formats may be sent as Document). On success, the sent Message is returned.
        Bots can currently send video files of up to 50 MB in size, this limit may be changed
        in the future.
        (Optional parameters are keyword arguments)

        Parameters              Type        Required    Description
        chat_id                 Int/Str     Yes         Unique identifier for the target chat or username of
                                                        the target channel (in the format @channelusername)
        video                   File/Str    Yes         File to send. Pass a file_id or a
                                                        binary filestream.
        caption                 String      Optional    Caption, 0-200 characters
        duration                Integer     Optional    Duration of sent video in seconds
        width                   Integer     Optional    Video width
        height                  Integer     Optional    Video height
        disable_notification    Boolean     Optional    Sends the message silently. Users will receive a
                                                        notification with no sound.
        reply                   Integer     Optional    If the message is a reply, ID of the original message
        reply_markup            Json        Optional    Additional interface options. A JSON-serialized object
                                                        for an inline keyboard, custom reply keyboard,
                                                        instructions to remove reply keyboard or to
                                                        force a reply from the user.
        """

        endpoint = "sendVideo"

        url = self.API_URL + endpoint

        if isinstance(video, str):
            args = {"chat_id": chat_id, "video": video, **kwargs}
            return await self._api_send(url, args)

        else:
            args = {"chat_id": chat_id, **kwargs}
            response = await self.session.post(url, data=dict(video=video), params=args)
            return response.json()

    async def send_voice(self, chat_id, voice, **kwargs):
        """Use this method to send voice files. if you want Telegram clients to display the file as a
        playable voice message. For this to work, your audio must be in an .ogg file encoded with OPUS
        (other formats may be sent as Audio or Document). On success, the sent Message is returned.
        Bots can currently send voice messages of up to 50 MB in size, this limit may be changed in
        the future.
        (Optional parameters are keyword arguments)

        Parameters              Type        Required    Description
        chat_id                 Int/Str     Yes         Unique identifier for the target chat or username of
                                                        the target channel (in the format @channelusername)
        voice                   File/Str    Yes         File to send. Pass a file_id or a
                                                        binary filestream.
        caption                 String      Optional    Caption, 0-200 characters
        disable_notification    Boolean     Optional    Sends the message silently. Users will receive a
                                                        notification with no sound.
        reply                   Integer     Optional    If the message is a reply, ID of the original message
        reply_markup            Json        Optional    Additional interface options. A JSON-serialized object
                                                        for an inline keyboard, custom reply keyboard,
                                                        instructions to remove reply keyboard or to
                                                        force a reply from the user.
        """

        endpoint = "sendVoice"

        url = self.API_URL + endpoint

        if isinstance(voice, str):
            args = {"chat_id": chat_id, "voice": voice, **kwargs}
            return await self._api_send(apiq)

        else:
            args = {"chat_id": chat_id, **kwargs}
            response = await self.session.post(url, data=dict(voice=voice), params=args)
            return response.json()

    async def send_video_note(self, chat_id, v_note, **kwargs):
        """As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long.
        Use this method to send video messages. On success, the sent Message is returned.
        (Optional parameters are keyword arguments)

        Parameters              Type        Required    Description
        chat_id                 Int/Str     Yes         Unique identifier for the target chat or username of
                                                        the target channel (in the format @channelusername)
        video                   File/Str    Yes         File to send. Pass a file_id or a
                                                        binary filestream.
        caption                 String      Optional    Caption, 0-200 characters
        duration                Integer     Optional    Duration of sent video in seconds
        disable_notification    Boolean     Optional    Sends the message silently. Users will receive a
                                                        notification with no sound.
        reply                   Integer     Optional    If the message is a reply, ID of the original message
        reply_markup            Json        Optional    Additional interface options. A JSON-serialized object
                                                        for an inline keyboard, custom reply keyboard,
                                                        instructions to remove reply keyboard or to
                                                        force a reply from the user.
        """

        endpoint = "sendVideoNote"
        url = self.API_URL + endpoint

        if isinstance(v_note, str):
            args = {"chat_id": chat_id, "video_note": v_note}
            return await self._api_send(url, args)

        else:
            args = {"chat_id": chat_id}
            response = await self.session.post(url, data=dict(video_note=v_note), params=args)
            return response.json()

    async def send_location(self, chat_id, long, lat, **kwargs):
        """Use this method to send point on the map. On success, the sent Message is returned.
        (Optional parameters are keyword arguments)

        Parameters              Type        Required    Description
        chat_id                 Int/Str     Yes         Unique identifier for the target chat or username of
                                                        the target channel (in the format @channelusername)
        chat_id                 Int/Str     Yes         Unique identifier for the target chat or username of
                                                        the target channel (in the format @channelusername)
        latitude                Float       Yes         Latitude of location
        longitude               Float       Yes         Longitude of location
        disable_notification    Boolean     Optional    Sends the message silently. Users will receive a
                                                        notification with no sound.
        reply                   Integer     Optional    If the message is a reply, ID of the original message
        reply_markup            Json        Optional    Additional interface options. A JSON-serialized object
                                                        for an inline keyboard, custom reply keyboard,
                                                        instructions to remove reply keyboard or to
                                                        force a reply from the user.
        """

        endpoint = 'sendLocation'
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id, "latitude": lat, "longitutde": long, **kwargs}
        return await self._api_send(url, args)

    async def send_venue(self, chat_id, lat, long, title, addr, **kwargs):
        """Use this method to send information about a venue. On success, the sent Message is returned.
        (Optional parameters are keyword arguments)

        Parameters              Type        Required    Description
        chat_id                 Int/Str     Yes         Unique identifier for the target chat or username of
                                                        the target channel (in the format @channelusername)
        latitude                Float       Yes         Latitude of location
        longitude               Float       Yes         Longitude of location
        title                   String      Yes         Name of the venue
        address                 String      Yes         Address of the venue
        foursquare_id           String      Optional    Foursquare identifier of the venue
        disable_notification    Boolean     Optional    Sends the message silently. Users will receive a
                                                        notification with no sound.
        reply                   Integer     Optional    If the message is a reply, ID of the original message
        reply_markup            Json        Optional    Additional interface options. A JSON-serialized object
                                                        for an inline keyboard, custom reply keyboard,
                                                        instructions to remove reply keyboard or to
                                                        force a reply from the user.
        """

        endpoint = "sendVenue"
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id, 'latitude': lat, "longitude": long, 'title': title, 'address': addr}
        return await self._api_send(url, args)

    async def send_contact(self, chat_id, phone_number, first_name, **kwargs):
        """Use this method to send information about a venue. On success, the sent Message is returned.
        (Optional parameters are keyword arguments)

        Parameters              Type        Required    Description
        chat_id                 Int/Str     Yes         Unique identifier for the target chat or username of
                                                        the target channel (in the format @channelusername)
        phone_number            String      Yes         Contact's phone number
        first_name              String      Yes         Contact's first name
        last_name               String      Optional    Contact's last name
        disable_notification    Boolean     Optional    Sends the message silently. Users will receive a
                                                        notification with no sound.
        reply                   Integer     Optional    If the message is a reply, ID of the original message
        reply_markup            Json        Optional    Additional interface options. A JSON-serialized object
                                                        for an inline keyboard, custom reply keyboard,
                                                        instructions to remove reply keyboard or to
                                                        force a reply from the user.
        """

        endpoint = 'sendContact'
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id, 'phone_number': phone_number, 'first_name': first_name, **kwargs}
        return await self._api_send(url, args)

    async def send_chat_action(self, chat_id, action):
        """Use this method when you need to tell the user that something is happening on the bot's side.
        The status is set for 5 seconds or less (when a message arrives from your bot,
        Telegram clients clear its typing status). Returns True on success.

        Example: The ImageBot needs some time to process a request and upload the image.
        Instead of sending a text message along the lines of “Retrieving image, please wait…”,
        the bot may use sendChatAction with action = upload_photo. The user will see a “sending photo”
        status for the bot.

        We only recommend using this method when a response from the bot will take a noticeable
        amount of time to arrive.
        (Optional parameters are keyword arguments)

        Parameters              Type        Required    Description
        chat_id                 Int/Str     Yes         Unique identifier for the target chat or username of
                                                        the target channel (in the format @channelusername)
        action                  String      Yes         Type of action to broadcast. Choose one, depending on
                                                        what the user is about to receive:
                                                        'typing' for text messages,
                                                        'upload_photo' for photos,
                                                        'record_video' or 'upload_video' for videos,
                                                        'record_audio' or 'upload_audio' for audio files,
                                                        'upload_document' for general files,
                                                        'find_location' for 'location data',
                                                        'record_video_note' or 'upload_video_note'
                                                            for video notes.
        """

        endpoint = "sendChatAction"
        url = self.API_URL + endpoint

        args = {'chat_id': str(chat_id), 'action': action}
        return await self._api_send(url, args)

    async def get_user_profile_photos(self, user_id, **kwargs):
        """Use this method to get a list of profile pictures for a user.
        Returns a UserProfilePhotos Dictionary.
        (Optional parameters are keyword arguments)

        Parameters              Type        Required    Description
        user_id                 Int/Str     Yes         Unique identifier for the target user.
        offset                  Integer     Optional    Sequential number of the first photo to be returned.
                                                        By default, all photos are returned.
        limit                   Integer     Optional    Limits the number of photos to be retrieved.
                                                        Values between 1—100 are accepted. Defaults to 100
        """

        endpoint = 'getUserProfilePhotos'
        url = self.API_URL + endpoint

        args = {'user_id': user_id, **kwargs}
        return await self._api_send(url, args)

    async def get_file(self, file_id):

        endpoint = 'getFile'
        url = self.API_URL + endpoint

        args = {'file_id': file_id}
        return await self._api_send(url, args)

    async def get_file_url(self, file_obj):

        return f"https://api.telegram.org/file/bot{self.token}/{file_obj.file_path}"

    async def ban_chat_memeber(self, chat_id, user_id, **kwargs):

        endpoint = "kickChatMember"
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id, 'user_id': user_id, **kwargs}
        return await self._api_send(url, args)

    async def unban_chat_memeber(self, chat_id, user_id):

        endpoint = 'unbanChatMember'
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id, 'user_id': user_id}
        return await self._api_send(url, args)

    async def restrict_chat_memeber(self, chat_id, user_id, **kwargs):

        endpoint = 'restrictChatMember'
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id, 'user_id': user_id, **kwargs}
        return await self._api_send(url, args)

    async def promote_chat_memeber(self, chat_id, user_id, **kwargs):

        endpoint = 'promoteChatMember'
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id, 'user_id': user_id, **kwargs}
        return await self._api_send(url, args)

    async def export_invite_link(self, chat_id):

        endpoint = 'exportChatInviteLink'
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id}
        return await self._api_send(url, args)

    async def set_chat_photo(self, chat_id, photo):

        endpoint = 'sendChatPhoto'
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id}
        response = await self.session.post(apiq, data=dict(photo=photo), params=args)
        return response.json()

    async def delete_chat_photo(self, chat_id):

        endpoint = 'deleteChatPhoto'
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id}
        return await self._api_send(url, args)

    async def set_chat_title(self, chat_id, title):

        endpoint = 'setChatTitle'
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id, 'title': title}
        return await self._api_send(url, args)

    async def set_chat_description(self, chat_id, desc):

        endpoint = 'setChatDescription'
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id, 'description': desc}
        return await self._api_send(url, args)

    async def pin_chat_message(self, chat_id, message_id, **kwargs):

        endpoint = 'pinChatMessage'
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id, 'message_id': message_id, **kwargs}
        return await self._api_send(url, args)

    async def unpin_chat_message(self, chat_id):

        endpoint = 'unpinChatMessage'
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id}
        return await self._api_send(url, args)

    async def get_chat(self, chat_id):

        endpoint = 'getChat'
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id}
        return await self._api_send(url, args)

    async def get_chat_administrators(self, chat_id):

        endpoint = 'getChatAdministrators'

        url = self.API_URL + endpoint

        args = {'chat_id': chat_id}
        return await self._api_send(url, args)

    async def get_chat_member_count(self, chat_id):

        endpoint = 'getChatMembersCount'
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id}
        return await self._api_send(url, args)

    async def get_chat_member(self, chat_id, user_id):

        endpoint = 'getChatMember'
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id}
        return await self._api_send(url, args)

    # Updating Messages

    async def edit_message_text(self, chat_id, msg_id, text, **kwargs):

        endpoint = 'editMessageText'
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id, 'text': text, 'message_id': msg_id, **kwargs}
        return await self._api_send(url, args)

    async def edit_message_caption(self, chat_id, msg_id, caption, **kwargs):

        endpoint = 'editMessageCaption'
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id, 'message_id': msg_id, 'caption': caption, **kwargs}
        return await self._api_send(url, args)

    async def edit_message_reply_markup(self, chat_id, msg_id, markup, **kwargs):

        endpoint = 'editMessageReplyMarkup'
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id, 'message_id': msg_id, 'markup': markup, **kwargs}
        return await self._api_send(url, args)

    async def delete_message(self, chat_id, msg_id):

        endpoint = 'deleteMessage'
        url = self.API_URL + endpoint

        args = {'chat_id': chat_id, 'message_id': msg_id}
        return await self._api_send(url, args)


#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from subprocess import Popen, PIPE
from os.path import expandvars
import argparse
import configparser
try:
    from emoji.unicode_codes import EMOJI_UNICODE, EMOJI_ALIAS_UNICODE
    _process_emoji_dic = lambda dic: set([(x.replace(':', ''), y) for x,y in dic.items()])
    IMP_EMOJI = _process_emoji_dic(EMOJI_UNICODE)
    IMP_EMOJI |= _process_emoji_dic(EMOJI_ALIAS_UNICODE)
    IMP_EMOJI = list(IMP_EMOJI)
except ImportError:
    IMP_EMOJI = []

XSEL_CLIPBOARDS=['p', 's', 'b']

def parse_cfg(cfg_path):
    config = configparser.ConfigParser()
    config.read(expandvars(cfg_path))
    return config

def dmenu_format(choices, fmt=None):
    """Format iterable @choices into '\n' delimited dmenu input
    Arguments:
        fmt - Format string for each key:choice pair
    """
    if fmt is None:
        fmt = '{}: {}'
    dmenu_line = ''
    for c in choices:
        dmenu_line += (fmt+'\n').format(c[0], c[1])
    return dmenu_line

def dmenu_select(line, dmenu_opts=None):
    """Send @line to dmenu and return selection"""
    if dmenu_opts is None:
        dmenu_opts = ['-i']
    p = Popen(['dmenu'] + dmenu_opts, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
    out,err = p.communicate(line.encode('utf-8'))
    return out.decode('utf-8')

def extract_name(choice, extract=None):
    """Extract emoji name from dmenu @choice"""
    if extract is None:
        def _extract(c):
            return c.split(':')[0]
        extract = _extract
    return extract(choice)

def get_emoji_by_name(emoji, name):
    """Lookup @name key in iterable @emoji and return value"""
    try:
        return list(filter(lambda x: x[0] == name, emoji))[0][1]
    except IndexError:
        return ''

def do_type(data, delay=None):
    """Type @data under cursor using xdotool
    Arguments:
        delay - delay between characters (ms)
    """
    if delay is None:
        delay = 50
    p = Popen(['xdotool', '-'], stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
    p.stdin.write(("type --clearmodifiers --delay {} -- '{}'".format(delay, data)).encode('utf-8'))

def do_copy(data, clipboards=None):
    """Send @data to clipboards using xsel"""
    if clipboards is None:
        clipboards = XSEL_CLIPBOARDS
    for cb in clipboards:
        p = Popen(['xsel', '-'+cb, '-i'], stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
        p.communicate(data.encode('utf-8'))

def main():
    # Parse arguments
    p = argparse.ArgumentParser(description='Select and type emoji with dmenu')
    p.add_argument("-c", "--cfg", metavar='PATH',
        help='Config and emoji file',
        default='$HOME/.config/emojimenu/emoji.cfg')
    p.add_argument("-t", "--type", action='store_true',
        help='Auto-type emoji using xdotool')
    p.add_argument("-d", "--delay", metavar='ms',
        help='Delay between typing each character (ms)',
        type=int, default=None)
    p.add_argument("-x", "--clipboards", nargs='+', choices=XSEL_CLIPBOARDS,
        help='List of xsel clipboards to copy to')
    args = p.parse_args()

    # Read config file
    cfg = parse_cfg(args.cfg)
    emoji_list = cfg.items('emoji') + IMP_EMOJI

    # Build input line
    line = dmenu_format(emoji_list)

    # Spawn process and get selection
    choice = dmenu_select(line)

    # Select emoji
    name = extract_name(choice)
    choice = get_emoji_by_name(emoji_list, name)

    # Type emoji under cursor or copy to clipboards
    if args.type:
        do_type(choice, args.delay)
    else:
        do_copy(choice, args.clipboards)

if __name__ == "__main__":
    main()


# coding=utf-8
import pandas as pd
from sklearn import preprocessing
from sklearn.linear_model import LogisticRegression
"""
优点：实现简单，易于理解和实现；计算代价不高，速度很快，存储资源低；
缺点：容易欠拟合，分类精度可能不高
注意：LR和GBRT最后都会转化为最优化问题，使用GD/GB方法求解，故都要进行归一化，以加速收敛和提高精度
参见：http://www.cnblogs.com/LBSer/p/4440590.html
是否特征选择：个人觉得只要过滤出相关性极低的特征即可，还在参考https://www.zhihu.com/question/28641663?sort=created
"""

#load data
train_x_csv = pd.read_csv("dataset\\borrower_credit\\train_x.csv")
train_y_csv = pd.read_csv("dataset\\borrower_credit\\train_y.csv")
test_csv = pd.read_csv("dataset\\borrower_credit\\test_x.csv")
test_x = test_csv.drop(test_csv.columns[[0]],axis=1).values

y_0 = train_y_csv[train_y_csv.y == 0]
merge = pd.merge(train_x_csv, y_0, how="inner", left_on=train_x_csv.uid,
                     right_on=y_0.uid)
X_ = train_x_csv.drop(train_x_csv.columns[[0]],axis=1) #index' axis = 0
for i in range(13458 / 1542 ):
    train_y_csv = train_y_csv.append(y_0)
    X_ = X_.append(merge.drop(merge.columns[[0,-2, -1]], axis=1))

#scale
scaler = preprocessing.StandardScaler()
X = scaler.fit(X_.values).transform(X_.values)
y = train_y_csv["y"].values
test_x = scaler.fit(test_x).transform(test_x)

#model select
n_features = X.shape[1]
C = 1.0
# #Create different classifiers.
# classifiers = {'L1 logistic': LogisticRegression(C=C, penalty='l1'),
#                'L2 logistic (OvR)': LogisticRegression(C=C, penalty='l2')
#                }
#
# n_classifiers = len(classifiers)
# import numpy as np
# for index, (name, classifier) in enumerate(classifiers.items()):
#     classifier.fit(X, y)
#     y_pred = classifier.predict(X)
#     classif_rate = np.mean(y_pred.ravel() == y.ravel()) * 100
#     print("classif_rate for %s : %f " % (name, classif_rate))

best_clf = LogisticRegression(C=C, penalty='l2')
best_clf.fit(X, y)
probas = best_clf.predict_proba(test_x)
pd.DataFrame(probas[:,1],index=test_csv.uid,columns=["score"]).\
    to_csv("dataset\\borrower_credit\LR2predict.csv")


import torch
import torch.nn as nn
from PIL import Image
from torch.utils.data import Dataset
import pickle
import json
import os
import numpy as np
torch.random.manual_seed(42)

class MMIMDB(Dataset):
    def __init__(self, split, train_p = 1, dropout_p=None, datadir = '/home1/dyang165/Datasets/mmimdb/mmimdb/', label_map = '/home1/dyang165/Datasets/mmimdb/mmimdb_genre_label_map_23_classes.pkl'):
        self.datadir = datadir
        with open(os.path.join(datadir, 'split.json'), 'r') as f:
            self.ids = json.load(f)[split]

        with open(label_map, 'rb') as f:
            self.labels_to_ids = pickle.load(f)
        self.num_classes = len(self.labels_to_ids)

        if split == 'dev':
            self.ids = self.ids

        if train_p == None:
            train_p = 1
        print(f"{split}_p:",train_p)
        self.index_array = np.arange(len(self.ids))
        if train_p != None and train_p != 1:
            np.random.shuffle(self.index_array)
            self.index_array = self.index_array[:int(train_p*len(self.ids))]

    def __len__(self):
        return len(self.index_array)

    def __getitem__(self, x):
        x = self.index_array[x]
        itemid =  self.ids[x]
        imagefile = os.path.join(self.datadir,f'data/{itemid}.jpeg')
        labelfile = os.path.join(self.datadir,f'data/{itemid}.json')

        # Get inputs
        image = Image.open(imagefile).convert("RGB")
        with open(labelfile, 'r') as f:
            data = json.load(f)

        text = " ".join(data['plot'])

        # get labels
        labels = [self.labels_to_ids[item] for item in data['genres'] if item in self.labels_to_ids]
        return {'text':text, 'image':image, 'labels':labels}

if __name__ == '__main__':
    db = MMIMDB("train", train_p=0.01)
    print(db.labels_to_ids)
    print(db.__getitem__(0))


from __future__ import annotations

from functools import partial
from typing import Union, Callable, Dict, Any, TYPE_CHECKING

from mantidimaging import helper as h
from mantidimaging.core.operations.base_filter import BaseFilter
from mantidimaging.gui.utility.qt_helpers import Type

if TYPE_CHECKING:
    from PyQt5.QtWidgets import QFormLayout, QDoubleSpinBox, QComboBox
    from mantidimaging.gui.mvp_base import BasePresenter
    from mantidimaging.core.data import ImageStack

class DivideFilter(BaseFilter):
    """Divides a stack of images by a value. That value can be the pixel size,
    and can be specified in either microns or cms, to obtain attenuation values.

    Intended to be used on: Reconstructed slices

    When: To calculate attenuation values by dividing by the pixel size in microns

    Caution: Check preview values before applying divide
    """
    filter_name = "Divide"
    link_histograms = True

    @staticmethod
    def filter_func(images: ImageStack, value: Union[int, float] = 0, unit="micron", progress=None) -> ImageStack:
        """
        :param value: The division value.
        :param unit: The unit of the divisor.

        :return: The ImageStack object which has been divided by a value.
        """
        h.check_data_stack(images)
        if not value:
            raise ValueError('value parameter must not equal 0 or None')

        if unit == "micron":
            value *= 1e-4

        images.data /= value
        return images

    @staticmethod
    def register_gui(form: 'QFormLayout', on_change: Callable, view: 'BasePresenter') -> Dict[str, Any]:
        from mantidimaging.gui.utility import add_property_to_form

        _, value_widget = add_property_to_form("Divide by",
                                               Type.FLOAT,
                                               default_value=1,
                                               valid_values=[1e-7, 10000],
                                               form=form,
                                               on_change=on_change,
                                               tooltip="Value the data will be divided by")
        assert value_widget is not None, "Requested widget was for FLOAT, got None instead"
        value_widget.setDecimals(7)
        _, unit_widget = add_property_to_form("Unit",
                                              Type.CHOICE,
                                              valid_values=["micron", "cm"],
                                              form=form,
                                              on_change=on_change,
                                              tooltip="The unit of the input number. "
                                              "Microns will be converted to cm before division")

        return {'value_widget': value_widget, 'unit_widget': unit_widget}

    @staticmethod
    def execute_wrapper(  # type: ignore
            value_widget: QDoubleSpinBox, unit_widget: QComboBox) -> partial:
        value = value_widget.value()
        unit = unit_widget.currentText()
        return partial(DivideFilter.filter_func, value=value, unit=unit)

    @staticmethod
    def validate_execute_kwargs(kwargs: Dict[str, Any]) -> bool:
        if 'value_widget' not in kwargs:
            return False
        return True