"""
Defines arguments manipulation utilities, like checking if an argument is iterable, flattening a nested arguments list, etc.
These utility functions can be used by other util modules and are imported in util's main namespace for use by other pymel modules
"""

from collections import deque as _deque
import sys
import operator
import itertools

from utilitytypes import ProxyUnicode

# some functions used to need to make the difference between strings and non-string iterables when PyNode where unicode derived
# doing a hasattr(obj, '__iter__') test will fail for objects that implement __getitem__, but not __iter__, so try iter(obj)
def isIterable(obj):
    """
    Returns True if an object is iterable and not a string or ProxyUnicode type, otherwise returns False.

    :rtype: bool"""
    if isinstance(obj, basestring):
        return False
    elif isinstance(obj, ProxyUnicode):
        return False
    try:
        iter(obj)
    except TypeError:
        return False
    else:
        return True

# consider only ints and floats numeric
def isScalar(obj):
    """
    Returns True if an object is a number or complex type, otherwise returns False.

    :rtype: bool"""
    return operator.isNumberType(obj) and not isinstance(obj, complex)

# TODO : this is unneeded as operator provides it, can call directly to operator methods
def isNumeric(obj):
    """
    Returns True if an object is a number type, otherwise returns False.

    :rtype: bool
    """
    return operator.isNumberType(obj)

def isSequence(obj):
    """
    same as `operator.isSequenceType`

    :rtype: bool"""
    return operator.isSequenceType(obj)

def isMapping(obj):
    """
    Returns True if an object is a mapping (dictionary) type, otherwise returns False.

    same as `operator.isMappingType`

    :rtype: bool"""
    return operator.isMappingType(obj)

clsname = lambda x: type(x).__name__

def convertListArgs(args):
    if len(args) == 1 and isIterable(args[0]):
        return tuple(args[0])
    return args


def expandArgs(*args, **kwargs):
    """
    'Flattens' the arguments list: recursively replaces any iterable argument in *args by a tuple of its
    elements that will be inserted at its place in the returned arguments.

    By default will return elements depth first, from root to leaves.  Set postorder or breadth to control order.

    :Keywords:
        depth : int
            will specify the nested depth limit after which iterables are returned as they are

        type
            for type='list' will only expand lists, by default type='all' expands any iterable sequence

        postorder : bool
             will return elements depth first, from leaves to roots

        breadth : bool
            will return elements breadth first, roots, then first depth level, etc.

    For a nested list represent trees::

        a____b____c
        |    |____d
        e____f
        |____g

    preorder(default) :

        >>> expandArgs( 'a', ['b', ['c', 'd']], 'e', ['f', 'g'], limit=1 )
        ('a', 'b', ['c', 'd'], 'e', 'f', 'g')
        >>> expandArgs( 'a', ['b', ['c', 'd']], 'e', ['f', 'g'] )
        ('a', 'b', 'c', 'd', 'e', 'f', 'g')

    postorder :

        >>> expandArgs( 'a', ['b', ['c', 'd']], 'e', ['f', 'g'], postorder=True, limit=1)
        ('b', ['c', 'd'], 'a', 'f', 'g', 'e')
        >>> expandArgs( 'a', ['b', ['c', 'd']], 'e', ['f', 'g'], postorder=True)
        ('c', 'd', 'b', 'a', 'f', 'g', 'e')

    breadth :

        >>> expandArgs( 'a', ['b', ['c', 'd']], 'e', ['f', 'g'], limit=1, breadth=True)
        ('a', 'e', 'b', ['c', 'd'], 'f', 'g')
        >>> expandArgs( 'a', ['b', ['c', 'd']], 'e', ['f', 'g'], breadth=True)
        ('a', 'e', 'b', 'f', 'g', 'c', 'd')


    Note that with default depth (unlimited) and order (preorder), if passed a pymel Tree
    result will be the equivalent of doing a preorder traversal : [k for k in iter(theTree)] """

    tpe = kwargs.get('type', 'all')
    limit = kwargs.get('limit', sys.getrecursionlimit())
    postorder = kwargs.get('postorder', False)
    breadth = kwargs.get('breadth', False)
    if tpe == 'list' or tpe == list:
        def _expandArgsTest(arg):
            return type(arg) == list
    elif tpe == 'all':
        def _expandArgsTest(arg):
            return isIterable(arg)
    else:
        raise ValueError, "unknown expand type=%s" % str(tpe)

    if postorder:
        return postorderArgs(limit, _expandArgsTest, *args)
    elif breadth:
        return breadthArgs(limit, _expandArgsTest, *args)
    else:
        return preorderArgs(limit, _expandArgsTest, *args)

def preorderArgs(limit=sys.getrecursionlimit(), testFn=isIterable, *args):
    """ returns a list of a preorder expansion of args """
    stack = [(x, 0) for x in args]
    result = _deque()
    while stack:
        arg, level = stack.pop()
        if testFn(arg) and level < limit:
            stack += [(x, level + 1) for x in arg]
        else:
            result.appendleft(arg)

    return tuple(result)

def postorderArgs(limit=sys.getrecursionlimit(), testFn=isIterable, *args):
    """ returns a list of  a postorder expansion of args """
    if len(args) == 1:
        return (args[0],)
    else:
        deq = _deque((x, 0) for x in args)
        stack = []
        result = []
        while deq:
            arg, level = deq.popleft()
            if testFn(arg) and level < limit:
                deq = _deque([(x, level + 1) for x in arg] + list(deq))
            else:
                if stack:
                    while stack and level <= stack[-1][1]:
                        result.append(stack.pop()[0])
                    stack.append((arg, level))
                else:
                    stack.append((arg, level))
        while stack:
            result.append(stack.pop()[0])

        return tuple(result)

def breadthArgs(limit=sys.getrecursionlimit(), testFn=isIterable, *args):
    """ returns a list of a breadth first expansion of args """
    deq = _deque((x, 0) for x in args)
    result = []
    while deq:
        arg, level = deq.popleft()
        if testFn(arg) and level < limit:
            for a in arg:
                deq.append((a, level + 1))
        else:
            result.append(arg)

    return tuple(result)

# Same behavior as expandListArg but implemented as an Python iterator, the recursieve approach
# will be more memory efficient, but slower
def iterateArgs(*args, **kwargs):
    """ Iterates through all arguments list: recursively replaces any iterable argument in *args by a tuple of its
    elements that will be inserted at its place in the returned arguments.

    By default will return elements depth first, from root to leaves.  Set postorder or breadth to control order.

    :Keywords:
        depth : int
            will specify the nested depth limit after which iterables are returned as they are

        type
            for type='list' will only expand lists, by default type='all' expands any iterable sequence

        postorder : bool
             will return elements depth first, from leaves to roots

        breadth : bool
            will return elements breadth first, roots, then first depth level, etc.

    For a nested list represent trees::

        a____b____c
        |    |____d
        e____f
        |____g

    preorder(default) :

        >>> tuple(k for k in iterateArgs( 'a', ['b', ['c', 'd']], 'e', ['f', 'g'], limit=1 ))
        ('a', 'b', ['c', 'd'], 'e', 'f', 'g')
        >>> tuple(k for k in iterateArgs( 'a', ['b', ['c', 'd']], 'e', ['f', 'g'] ))
        ('a', 'b', 'c', 'd', 'e', 'f', 'g')

    postorder :

        >>> tuple(k for k in iterateArgs( 'a', ['b', ['c', 'd']], 'e', ['f', 'g'], postorder=True, limit=1 ))
        ('b', ['c', 'd'], 'a', 'f', 'g', 'e')
        >>> tuple(k for k in iterateArgs( 'a', ['b', ['c', 'd']], 'e', ['f', 'g'], postorder=True))
        ('c', 'd', 'b', 'a', 'f', 'g', 'e')

    breadth :

        >>> tuple(k for k in iterateArgs( 'a', ['b', ['c', 'd']], 'e', ['f', 'g'], limit=1, breadth=True))
        ('a', 'e', 'b', ['c', 'd'], 'f', 'g')
        >>> tuple(k for k in iterateArgs( 'a', ['b', ['c', 'd']], 'e', ['f', 'g'], breadth=True))
        ('a', 'e', 'b', 'f', 'g', 'c', 'd')

    Note that with default depth (-1 for unlimited) and order (preorder), if passed a pymel Tree
    result will be the equivalent of using a preorder iterator : iter(theTree) """

    tpe = kwargs.get('type', 'all')
    limit = kwargs.get('limit', sys.getrecursionlimit())
    postorder = kwargs.get('postorder', False)
    breadth = kwargs.get('breadth', False)
    if tpe == 'list' or tpe == list:
        def _iterateArgsTest(arg):
            return type(arg) == list
    elif tpe == 'all':
        def _iterateArgsTest(arg):
            return isIterable(arg)
    else:
        raise ValueError, "unknown expand type=%s" % str(tpe)

    if postorder:
        for arg in postorderIterArgs(limit, _iterateArgsTest, *args):
            yield arg
    elif breadth:
        for arg in breadthIterArgs(limit, _iterateArgsTest, *args):
            yield arg
    else:
        for arg in preorderIterArgs(limit, _iterateArgsTest, *args):
            yield arg

def preorderIterArgs(limit=sys.getrecursionlimit(), testFn=isIterable, *args):
    """ iterator doing a preorder expansion of args """
    if limit:
        for arg in args:
            if testFn(arg):
                for a in preorderIterArgs(limit - 1, testFn, *arg):
                    yield a
            else:
                yield arg
    else:
        for arg in args:
            yield arg

def postorderIterArgs(limit=sys.getrecursionlimit(), testFn=isIterable, *args):
    """ iterator doing a postorder expansion of args """
    if limit:
        last = None
        for arg in args:
            if testFn(arg):
                for a in postorderIterArgs(limit - 1, testFn, *arg):
                    yield a
            else:
                if last:
                    yield last
                last = arg
        if last:
            yield last
    else:
        for arg in args:
            yield arg

def breadthIterArgs(limit=sys.getrecursionlimit(), testFn=isIterable, *args):
    """ iterator doing a breadth first expansion of args """
    deq = _deque((x, 0) for x in args)
    while deq:
        arg, level = deq.popleft()
        if testFn(arg) and level < limit:
            for a in arg:
                deq.append((a, level + 1))
        else:
            yield arg

def preorder(iterable, testFn=isIterable, limit=sys.getrecursionlimit()):
    """ iterator doing a preorder expansion of args """
    if limit:
        for arg in iterable:
            if testFn(arg):
                for a in preorderIterArgs(limit - 1, testFn, *arg):
                    yield a
            else:
                yield arg
    else:
        for arg in iterable:
            yield arg

def postorder(iterable, testFn=isIterable, limit=sys.getrecursionlimit()):
    """ iterator doing a postorder expansion of args """
    if limit:
        last = None
        for arg in iterable:
            if testFn(arg):
                for a in postorderIterArgs(limit - 1, testFn, *arg):
                    yield a
            else:
                if last:
                    yield last
                last = arg
        if last:
            yield last
    else:
        for arg in iterable:
            yield arg

def breadth(iterable, testFn=isIterable, limit=sys.getrecursionlimit()):
    """ iterator doing a breadth first expansion of args """
    deq = _deque((x, 0) for x in iterable)
    while deq:
        arg, level = deq.popleft()
        if testFn(arg) and level < limit:
            for a in arg:
                deq.append((a, level + 1))
        else:
            yield arg

def listForNone(res):
    "returns an empty list when the result is None"
    if res is None:
        return []
    return res

# for discussion of implementation,
# see http://mail.python.org/pipermail/python-list/2008-January/474369.html for discussion...
def pairIter(sequence):
    '''
    Returns an iterator over every 2 items of sequence.

    ie, [x for x in pairIter([1,2,3,4])] == [(1,2), (3,4)]

    If sequence has an odd number of items, the last item will not be returned in a pair.
    '''
    theIter = iter(sequence)
    return itertools.izip(theIter, theIter)

def reorder(x, indexList=[], indexDict={}):
    """
    Reorder a list based upon a list of positional indices and/or a dictionary of fromIndex:toIndex.

        >>> l = ['zero', 'one', 'two', 'three', 'four', 'five', 'six']
        >>> reorder( l, [1, 4] ) # based on positional indices: 0-->1, 1-->4
        ['one', 'four', 'zero', 'two', 'three', 'five', 'six']
        >>> reorder( l, [1, None, 4] ) # None can be used as a place-holder
        ['one', 'zero', 'four', 'two', 'three', 'five', 'six']
        >>> reorder( l, [1, 4], {5:6} )  # remapping via dictionary: move the value at index 5 to index 6
        ['one', 'four', 'zero', 'two', 'three', 'six', 'five']
    """

    x = list(x)
    num = len(x)
    popCount = 0
    indexValDict = {}

    for i, index in enumerate(indexList):
        if index is not None:
            val = x.pop(index - popCount)
            assert index not in indexDict, indexDict
            indexValDict[i] = val
            popCount += 1
    for k, v in indexDict.items():
        indexValDict[v] = x.pop(k - popCount)
        popCount += 1

    newlist = []
    for i in range(num):
        try:
            val = indexValDict[i]
        except KeyError:
            val = x.pop(0)
        newlist.append(val)
    return newlist

class RemovedKey(object):

    def __init__(self, oldVal):
        self.oldVal = oldVal

    def __eq__(self, other):
        return self.oldVal == other.oldVal

    def __ne__(self, other):
        return not self.oldVal == other.oldVal

    def __repr__(self):
        return '%s(%r)' % (type(self).__name__, self.oldVal)

class AddedKey(object):

    def __init__(self, newVal):
        self.newVal = newVal

    def __eq__(self, other):
        return self.newVal == other.newVal

    def __ne__(self, other):
        return not self.newVal == other.newVal

    def __repr__(self):
        return '%s(%r)' % (type(self).__name__, self.newVal)

class ChangedKey(object):

    def __init__(self, oldVal, newVal):
        self.oldVal = oldVal
        self.newVal = newVal

    def __eq__(self, other):
        return self.newVal == other.newVal and self.oldVal == other.oldVal

    def __ne__(self, other):
        return not self.newVal == other.newVal

    def __repr__(self):
        return '%s(%r, %r)' % (type(self).__name__, self.oldVal, self.newVal)

def compareCascadingDicts(dict1, dict2, encoding=None, useAddedKeys=False,
                          useChangedKeys=False):
    '''compares two cascading dicts

    Parameters
    ----------
    dict1 : dict, list, or tuple
        the first object to compare
    dict2 : dict, list, or tuple
        the second object to compare
    encoding : `str` or None or False
        controls how comparisons are made when one value is a str, and one is a
        unicode; if None, then comparisons are simply made with == (so ascii
        characters will compare equally); if the value False, then unicode and
        str are ALWAYS considered different - ie, u'foo' and 'foo' would not be
        considered equal; otherwise, it should be the name of a unicode
        encoding, which will be applied to the unicode string before comparing
    useAddedKeys : bool
        if True, then similarly to how 'RemovedKey' objects are used in the
        returned diferences object (see the Returns section), 'AddedKey' objects
        are used for keys which exist in dict2 but not in dict1; this allows
        a user to distinguish, purely by inspecting the differences dict, which
        keys are brand new, versus merely changed; mergeCascadingDicts will
        treat AddedKey objects exactly the same as though they were their
        contents - ie, useAddedKeys should make no difference to the behavior
        of mergeCascadingDicts
    useChangedKeys : bool
        if True, then similarly to how 'RemovedKey' objects are used in the
        returned diferences object (see the Returns section), 'ChangedKey'
        objects are used for keys which exist in both dict1 and dict2, but with
        different values

    Returns
    -------
    both : `set`
        keys that were present in both (non-recursively)
        (both, only1, and only2 should be discrete partitions of all the keys
        present in both dict1 and dict2)
    only1 : `set`
        keys that were present in only1 (non-recursively)
    only2 : `set`
        keys that were present in only2 (non-recursively)
    differences : `dict`
        recursive sparse dict containing information that was 'different' in
        dict2 - either not present in dict1, or having a different value in
        dict2, or removed in dict2 (in which case an instance of 'RemovedKey'
        will be set as the value in differences)
        Values that are different, and both dictionaries, will themselves have
        sparse entries, showing only what is different
        The return value should be such that if you do if you merge the
        differences with d1, you will get d2.
    '''
    areSets = False
    if isinstance(dict1, set) and isinstance(dict2, set):
        areSets = True
        v1 = dict1
        v2 = dict2
    else:
        if isinstance(dict1, (list, tuple)):
            dict1 = dict(enumerate(dict1))
        if isinstance(dict2, (list, tuple)):
            dict2 = dict(enumerate(dict2))
        v1 = set(dict1)
        v2 = set(dict2)
    both = v1 & v2
    only1 = v1 - both
    only2 = v2 - both

    if areSets:
        if useAddedKeys:
            differences = set(AddedKey(key) for key in only2)
        else:
            differences = set(only2)
        differences.update(RemovedKey(key) for key in only1)
    else:
        recurseTypes = (dict, list, tuple, set)
        strUnicode = set([str, unicode])