# Princeton University licenses this file to You under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.  You may obtain a copy of the License at:
#     http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
#
#
# *************************************************  Utilities *********************************************************

"""Utilities that must be accessible to all PsyNeuLink modules, but are not PsyNeuLink-specific

   That is:
       do not require any information about PsyNeuLink objects
       are not constrained to be used by PsyNeuLink objects

************************************************* UTILITIES ************************************************************


CONTENTS
--------

*TYPE CHECKING VALUE COMPARISON*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. note::
   PsyNeuLink-specific typechecking functions are in the `Component <Component>` module

* `parameter_spec`
* `optional_parameter_spec`
* `all_within_range`
* `is_matrix
* `is_matrix_spec`
* `is_numeric`
* `is_numeric_or_none`
* `is_iterable`
* `iscompatible`
* `is_value_spec`
* `is_unit_interval`
* `is_same_function_spec`
* `is_component`

*ENUM*
~~~~~~

* `Autonumber`
* `Modulation`
* `get_modulationOperation_name`

*KVO*
~~~~~

.. note::
   This is for potential future use;  not currently used by PsyNeuLink objects

* observe_value_at_keypath

*MATHEMATICAL*
~~~~~~~~~~~~~~

* norm
* sinusoid
* scalar_distance
* powerset
* tensor_power


*OTHER*
~~~~~~~

* `get_args`
* `recursive_update`
* `merge_param_dicts`
* `multi_getattr`
* `np_array_less_that_2d`
* `convert_to_np_array`
* `type_match`
* `get_value_from_array`
* `is_matrix`
* `underscore_to_camelCase`
* `append_type_to_name`
* `ReadOnlyOrderedDict`
* `ContentAddressableList`
* `make_readonly_property`
* `get_class_attributes`
* `insert_list`
* `flatten_list`
* `convert_to_list`
* `get_global_seed`
* `set_global_seed`

"""

import copy
import inspect
import logging
import numbers
import time
import warnings
import weakref

from enum import Enum, EnumMeta, IntEnum

import collections
import numpy as np

from psyneulink.core.globals.keywords import DISTANCE_METRICS, EXPONENTIAL, GAUSSIAN, LINEAR, MATRIX_KEYWORD_VALUES, NAME, SINUSOID, VALUE

__all__ = [
    'append_type_to_name', 'AutoNumber', 'ContentAddressableList', 'convert_to_list', 'convert_to_np_array',
    'convert_all_elements_to_np_array', 'NodeRole', 'get_class_attributes', 'flatten_list',
    'get_modulationOperation_name', 'get_value_from_array', 'is_component',
    'is_distance_metric', 'is_matrix',
    'insert_list', 'is_matrix_spec', 'all_within_range', 'is_iterable',
    'is_modulation_operation', 'is_numeric', 'is_numeric_or_none', 'is_same_function_spec', 'is_unit_interval',
    'is_value_spec', 'iscompatible', 'kwCompatibilityLength', 'kwCompatibilityNumeric', 'kwCompatibilityType',
    'make_readonly_property', 'merge_param_dicts',
    'Modulation', 'MODULATION_ADD', 'MODULATION_MULTIPLY','MODULATION_OVERRIDE',
    'multi_getattr', 'np_array_less_than_2d', 'object_has_single_value', 'optional_parameter_spec', 'normpdf',
    'parameter_spec', 'powerset', 'random_matrix', 'ReadOnlyOrderedDict', 'safe_len', 'scalar_distance', 'sinusoid',
    'tensor_power', 'TEST_CONDTION', 'type_match',
    'underscore_to_camelCase', 'UtilitiesError', 'unproxy_weakproxy'
]

logger = logging.getLogger(__name__)


class UtilitiesError(Exception):
    def __init__(self, error_value):
        self.error_value = error_value

    def __str__(self):
        return repr(self.error_value)


MODULATION_OVERRIDE = 'Modulation.OVERRIDE'
MODULATION_MULTIPLY = 'Modulation.MULTIPLY'
MODULATION_ADD = 'Modulation.ADD'

class Modulation(Enum):
    MULTIPLY = lambda runtime, default : runtime * default
    ADD = lambda runtime, default : runtime + default
    OVERRIDE = lambda runtime, default : runtime
    DISABLE = 0

def is_modulation_operation(val):
    # try:
    #     val(0,0)
    #     return True
    # except:
    #     return False
    return get_modulationOperation_name(val)

def get_modulationOperation_name(operation):
        x = operation(1,2)
        if x is 1:
            return MODULATION_OVERRIDE
        elif x is 2:
            return MODULATION_MULTIPLY
        elif x is 3:
            return MODULATION_ADD
        else:
            return False



class AutoNumber(IntEnum):
    """Autonumbers IntEnum type

    First item in list of declarations is numbered 0;
    Others incremented by 1

    Sample:

        >>> class NumberedList(AutoNumber):
        ...    FIRST_ITEM = ()
        ...    SECOND_ITEM = ()

        >>> NumberedList.FIRST_ITEM.value
        0
        >>> NumberedList.SECOND_ITEM.value
        1

    Adapted from AutoNumber example for Enum at https://docs.python.org/3/library/enum.html#enum.IntEnum:
    Notes:
    * Start of numbering changed to 0 (from 1 in example)
    * obj based on int rather than object
    """
    def __new__(component_type):
        # Original example:
        # value = len(component_type.__members__) + 1
        # obj = object.__new__(component_type)
        value = len(component_type.__members__)
        obj = int.__new__(component_type)
        obj._value_ = value
        return obj

# ******************************** GLOBAL STRUCTURES, CONSTANTS AND METHODS  *******************************************

TEST_CONDTION = False


def optional_parameter_spec(param):
    """Test whether param is a legal PsyNeuLink parameter specification or `None`

    Calls parameter_spec if param is not `None`
    Used with typecheck

    Returns
    -------
    `True` if it is a legal parameter or `None`.
    `False` if it is neither.


    """
    if not param:
        return True
    return parameter_spec(param)


def parameter_spec(param, numeric_only=None):
    """Test whether param is a legal PsyNeuLink parameter specification

    Used with typecheck

    Returns
    -------
    `True` if it is a legal parameter.
    `False` if it is not.
    """
    # if isinstance(param, property):
    #     param = ??
    # if is_numeric(param):
    from psyneulink.core.components.functions.function import function_type
    from psyneulink.core.components.shellclasses import Projection
    from psyneulink.core.components.component import parameter_keywords
    from psyneulink.core.globals.keywords import MODULATORY_SPEC_KEYWORDS
    from psyneulink.core.components.component import Component

    if inspect.isclass(param):
        param = param.__name__
    elif isinstance(param, Component):
        param = param.__class__.__name__
    if (isinstance(param, (numbers.Number,
                           np.ndarray,
                           list,
                           tuple,
                           dict,
                           function_type,
                           Projection))
        or param in MODULATORY_SPEC_KEYWORDS
        or param in parameter_keywords):
        if numeric_only:
            if not is_numeric(param):
                return False
        return True
    return False


def all_within_range(x, min, max):
    x = np.array(x)
    try:
        if min is not None and (x<min).all():
            return False
        if max is not None and (x>max).all():
            return False
        return True
    except (ValueError, TypeError):
        for i in x:
            if not all_within_range(i, min, max):
                return False
        return True

def is_numeric_or_none(x):
    if x is None:
        return True
    return is_numeric(x)


def is_numeric(x):
    return iscompatible(x, **{kwCompatibilityNumeric:True, kwCompatibilityLength:0})


def is_matrix_spec(m):
    return isinstance(m, str) and m in MATRIX_KEYWORD_VALUES


def is_matrix(m):
    from psyneulink.core.components.component import Component

    if is_matrix_spec(m):
        return True
    if isinstance(m, (list, np.ndarray, np.matrix)):
        return True
    if m is None or isinstance(m, (Component, dict, set)) or (inspect.isclass(m) and issubclass(m, Component)):
        return False
    try:
        m2 = np.matrix(m)
        return is_matrix(m2)
    except:
        pass
    if callable(m):
        try:
            return is_matrix(m())
        except:
            return False
    return False


def is_distance_metric(s):
    if s in DISTANCE_METRICS:
        return True
    else:
        return False


def is_iterable(x):
    """
    Returns
    -------
        True - if **x** can be iterated on
        False - otherwise
    """
    if isinstance(x, np.ndarray) and x.ndim == 0:
        return False
    else:
        return isinstance(x, collections.abc.Iterable)


kwCompatibilityType = "type"
kwCompatibilityLength = "length"
kwCompatibilityNumeric = "numeric"

# IMPLEMENT SUPPORT OF *LIST* OF TYPES IN kwCompatibilityType (see Function.__init__ FOR EXAMPLE)
# IMPLEMENT: IF REFERENCE IS np.ndarray, try converting candidate to array and comparing
def iscompatible(candidate, reference=None, **kargs):
    """Check if candidate matches reference or, if that is omitted, it matches type, length and/or numeric specification

    If kargs is omitted, candidate and reference must match in type and length
    If reference and kargs are omitted, candidate must be a list of numbers (of any length)
    If reference is omitted, candidate must match specs in kargs

    kargs is an optional dictionary with the following entries:
        kwCompatibilityType ("type"):<type> (default: list):  (spec local_variable: match_type)
            - if reference is provided, candidate's type must match or be subclass of reference,
                irrespective of whether kwCompatibilityType is specified or absent;  however, if:
                + kwCompatibilityType is absent, enums are treated as numbers (of which they are a subclass)
                + kwCompatibilityType = enum, then candidate must be an enum if reference is one
            - if reference is absent:
                if kwCompatibilityType is also absent:
                    if kwCompatibilityNumeric is `True`, all elements of candidate must be numbers
                    if kwCompatibilityNumeric is :keyword:`False`, candidate can contain any type
                if kwCompatibilityType is specified, candidate's type must match or be subclass of specified type
            - for iterables, if kwNumeric is :keyword:`False`, candidate can have multiple types but
                if a reference is provided, then the corresponding items must have the same type
        kwCompatibilityLength ("length"):<int>  (default: 0):    (spec local_variable: match_length)
            - if kwCompatibilityLength is absent:
                if reference is absent, candidate can be of any length
                if reference is provided, candidate must match reference length
            - if kwCompatibilityLength = 0:
                candidate can be any length, irrespective of the reference or its length
            - if kwCompatibilityLength > 0:
                if reference is provided, candidate must be same length as reference
                if reference is omitted, length of candidate must equal value of kwLength
            Note: kwCompatibility < 0 is illegal;  it will generate a warning and be set to 0
        kwCompatibilityNumeric ("number": <bool> (default: `True`)  (spec local_variable: number_only)
            If kwCompatibilityNumeric is `True`, candidate must be either numeric or a list or tuple of
                numeric types
            If kwCompatibilityNumberic is :keyword:`False`, candidate can be strings, lists or tuples of strings,
                or dicts
                Note: if the candidate is a dict, the number of entries (lengths) are compared, but not their contents

    :param candidate: (value)
    :param reference:  (value)
    :param args: (dict)
    :return:
    """

    # If the two are equal, can settle it right here
    # IMPLEMENTATION NOTE: remove the duck typing when numpy supports a direct comparison of iterables
    try:
        with warnings.catch_warnings():
            warnings.simplefilter(action='ignore', category=FutureWarning)
            if reference is not None and (candidate == reference):
                return True
    except ValueError:
        # raise UtilitiesError("Could not compare {0} and {1}".format(candidate, reference))
        # IMPLEMENTATION NOTE: np.array generates the following error:
        # ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
        pass

    # If args not provided, assign to default values
    # if not specified in args, use these:
    #     args[kwCompatibilityType] = list
    #     args[kwCompatibilityLength] = 1
    #     args[kwCompatibilityNumeric] = True
    try:
        kargs[kwCompatibilityType]
    except KeyError:
        kargs[kwCompatibilityType] = list
    try:
        kargs[kwCompatibilityLength]
    except KeyError:
        kargs[kwCompatibilityLength] = 1
    try:
        # number_only = kargs[kwCompatibilityNumeric]
        kargs[kwCompatibilityNumeric]
    except KeyError:
        kargs[kwCompatibilityNumeric] = True
        # number_only = True


    # If reference is not provided, assign local_variables to arg values (provided or default)
    if reference is None:
        match_type = kargs[kwCompatibilityType]
        match_length = kargs[kwCompatibilityLength]
        number_only = kargs[kwCompatibilityNumeric]
    # If reference is provided, assign specification local_variables to reference-based values
    else:
        match_type = type(reference)
        # If length specification is non-zero (i.e., use length) and reference is an object for which len is defined:
        if (kargs[kwCompatibilityLength] and
                (isinstance(reference, (list, tuple, dict)) or
                         isinstance(reference, np.ndarray) and reference.ndim)
            ):
            match_length = len(reference)
        else:
            match_length = 0
        # If reference is not a number, then don't require the candidate to be one
        if not isinstance(reference, numbers.Number):
            number_only = False
        else:
            number_only = kargs[kwCompatibilityNumeric]

    if match_length < 0:
        # if settings & Settings.VERBOSE:
        print("\niscompatible({0}, {1}): length argument must be non-negative; it has been set to 0\n".
              format(candidate, kargs, match_length))
        match_length = 0

    # # FIX??
    # # Reference is a matrix or a keyword specification for one
    if is_matrix_spec(reference):
        return is_matrix(candidate)

    # MODIFIED 10/29/17 NEW:
    # IMPLEMENTATION NOTE: This allows a number in an ndarray to match a float or int
    # If both the candidate and reference are either a number or an ndarray of dim 0, consider it a match
    if ((isinstance(candidate, numbers.Number) or (isinstance(candidate, np.ndarray) and candidate.ndim == 0)) or
            (isinstance(reference, numbers.Number) or (isinstance(reference, np.ndarray) and reference.ndim == 0))):
        return True
    # MODIFIED 10/29/17 END

    # IMPLEMENTATION NOTE:
    #   modified to allow numeric type mismatches (e.g., int and float;
    #   should be added as option in future (i.e., to disallow it)
    if (isinstance(candidate, match_type) or
            (isinstance(candidate, (list, np.ndarray)) and (issubclass(match_type, (list, np.ndarray)))) or
            (isinstance(candidate, numbers.Number) and issubclass(match_type,numbers.Number)) or
            # IMPLEMENTATION NOTE: Allow UserDict types to match dict (make this an option in the future)
            (isinstance(candidate, UserDict) and match_type is dict) or
            # IMPLEMENTATION NOTE: Allow UserList types to match list (make this an option in the future)
            (isinstance(candidate, UserList) and match_type is list) or
            # IMPLEMENTATION NOTE: This is needed when kwCompatiblityType is not specified
            #                      and so match_type==list as default
            (isinstance(candidate, numbers.Number) and issubclass(match_type,list)) or
                (isinstance(candidate, np.ndarray) and issubclass(match_type,list))
        ):

        # Check compatibility of enum's
        # IMPLEMENTATION NOTE: THE FIRST VERSION BELOW SOUGHT TO CHECK COMPATIBILTY OF ENUM VALUE;  NEEDS WORK
        # if (kargs[kwCompatibilityType] is Enum and
        #         (isinstance(candidate, Enum) or isinstance(match_type, (Enum, IntEnum, EnumMeta)))):
        #     # If either the candidate enum's label is not in the reference's Enum class
        #     #    or its value is different, then return with fail
        #     try:
        #         match_type.__dict__['_member_names_']
        #     except:
        #         pass
        #     if not (candidate.name in match_type.__dict__['_member_names_'] and
        #                 candidate.value is match_type.__dict__['_member_map_'][candidate.name].value):
        #         return False
        # This version simply enforces the constraint that, if either is an enum of some sort, then both must be
        if (kargs[kwCompatibilityType] is Enum and
                (isinstance(candidate, Enum) != isinstance(match_type, (Enum, IntEnum, EnumMeta)))
            ):
            return False

        if isinstance(candidate, numbers.Number):
            return True
        if number_only:
            if isinstance(candidate, np.ndarray) and candidate.ndim ==0 and np.isreal(candidate):
                return True
            if not isinstance(candidate, (list, tuple, np.ndarray, np.matrix)):
                return False
            def recursively_check_elements_for_numeric(value):
                # Matrices can't be checked recursively, so convert to array
                if isinstance(value, np.matrix):
                    value = value.A
                if isinstance(value, (list, np.ndarray)):
                    for item in value:
                        if not recursively_check_elements_for_numeric(item):
                            return False
                        else:
                            return True
                else:
                    if not isinstance(value, numbers.Number):
                        try:
                            # True for autograd ArrayBox (and maybe other types?)
                            # if isinstance(value._value, numbers.Number):
                            from autograd.numpy.numpy_boxes import ArrayBox
                            if isinstance(value, ArrayBox):
                                return True
                        except:
                            return False
                    else:
                        return True
            # Test copy since may need to convert matrix to array (see above)
            if not recursively_check_elements_for_numeric(candidate.copy()):
                return False

        if isinstance(candidate, (list, tuple, dict, np.ndarray)):
            if not match_length:
                return True
            else:
                if len(candidate) == match_length:
                    # No reference, so item by item comparison is not relevant
                    if reference is None:
                        return True
                    # Otherwise, carry out recursive elementwise comparison
                    if isinstance(candidate, np.matrix):
                        candidate = np.asarray(candidate)
                    if isinstance(reference, np.matrix):
                        reference = np.asarray(reference)
                    cr = zip(candidate, reference)
                    if all(iscompatible(c, r, **kargs) for c, r in cr):
                        return True
                    # IMPLEMENTATION NOTE:  ??No longer needed given recursive call above
                    # Deal with ints in one and floats in the other
                    # # elif all((isinstance(c, numbers.Number) and isinstance(r, numbers.Number))
                    # #          for c, r in cr):
                    # #     return True
                else:
                    return False
        else:
            return True
    else:
        return False


# MATHEMATICAL  ********************************************************************************************************

def normpdf(x, mu=0, sigma=1):
    u = float((x-mu) / abs(sigma))
    y = np.exp(-u*u/2) / (np.sqrt(2*np.pi) * abs(sigma))
    return y

def sinusoid(x, amplitude=1, frequency=1, phase=0):
    return amplitude * np.sin(2 * np.pi * frequency * x + phase)

def scalar_distance(measure, value, scale=1, offset=0):
    if measure is GAUSSIAN:
        return normpdf(value, offset, scale)
    if measure is LINEAR:
        return scale*value+offset
    if measure is EXPONENTIAL:
        return np.exp(scale*value+offset)
    if measure is SINUSOID:
        return sinusoid(value, frequency=scale, phase=offset)

from itertools import chain, combinations
def powerset(iterable):
    """powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)"""
    s = list(iterable)
    return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))

import typecheck as tc
@tc.typecheck
def tensor_power(items, levels:tc.optional(range)=None, flat=False):
    """return tensor product for all members of powerset of items

    levels specifies a range of set levels to return;  1=first order terms, 2=2nd order terms, etc.
    if None, all terms will be returned

    if flat=False, returns list of 1d arrays with tensor product for each member of the powerset
    if flat=True, returns 1d array of values
    """

    ps = list(powerset(items))
    max_levels = max([len(s) for s in ps])
    levels = levels or range(1,max_levels)
    max_spec = max(list(levels))
    min_spec = min(list(levels))
    if  max_spec > max_levels:
        raise UtilitiesError("range ({},{}) specified for {} arg of tensor_power() "
                             "exceeds max for items specified ({})".
                             format(min_spec, max_spec+1, repr('levels'), max_levels+1))

    pp = []
    for s in ps:
        order = len(s)
        if not order in list(levels):
            continue
        if order==1:
            pp.append(np.array(s[0]))
        else:
            i = 0
            tp = np.tensordot(s[i],s[i+1],axes=0)
            i+=2
            while i < order:
                tp = np.tensordot(tp, s[i], axes=0)
                i+=1
            if flat is True:
                pp.extend(tp.reshape(-1))
            else:
                pp.append(tp.reshape(-1))
    return pp



# OTHER ****************************************************************************************************************

def get_args(frame):
    """Gets dictionary of arguments and their values for a function
    Frame should be assigned as follows in the function itself:  frame = inspect.currentframe()
    """
    args, _, _, values = inspect.getargvalues(frame)
    return dict((key, value) for key, value in values.items() if key in args)


from collections.abc import Mapping
def recursive_update(d, u, non_destructive=False):
    """Recursively update entries of dictionary d with dictionary u
    From: https://stackoverflow.com/questions/3232943/update-value-of-a-nested-dictionary-of-varying-depth
    """
    for k, v in u.items():
        if isinstance(v, Mapping):
            r = recursive_update(d.get(k, {}), v)
            d[k] = r
        else:
            if non_destructive and k in d and d[k] is not None:
                continue
            d[k] = u[k]
    return d


def merge_param_dicts(source, specific, general):
    """Search source dict for specific and general dicts, merge specific with general, and return merged

    Description:
        - used to merge only a subset of dicts in param set (that may have several dicts
        - allows dicts to be referenced by name (e.g., paramName) rather than by object
        - searches source dict for specific and general dicts
        - if both are found, merges them, with entries from specific overwriting any duplicates in general
        - if only one is found, returns just that dict
        - if neither are found, returns empty dict

    Arguments:
        - source (dict): container dict (entries are dicts); search entries for specific and general dicts
        - specific (dict or str): if str, use as key to look for specific dict in source, and check that it is a dict
        - general (dict or str): if str, use as key to look for general dict in source, and check that it is a dict


    :param source: (dict)
    :param specific: (dict or str)
    :param general: (dict or str)
    :return merged: (dict)
    """

    # Validate source as dict
    if not source:
        return
    if not isinstance(source, dict):
        raise UtilitiesError("merge_param_dicts: source {0} must be a dict".format(source))

    # Get specific and make sure it is a dict
    if isinstance(specific, str):
        try:
            specific = source[specific]
        except (KeyError, TypeError):
            specific = {}
    if not isinstance(specific, dict):
        raise UtilitiesError("merge_param_dicts: specific {0} must be dict or the name of one in {1}".
                        format(specific, source))

    # Get general and make sure it is a dict
    if isinstance(general, str):
        try:
            general = source[general]
        except (KeyError, TypeError):
            general = {}
    if not isinstance(general, dict):
        raise UtilitiesError("merge_param_dicts: general {0} must be dict or the name of one in {1}".
                        format(general, source))

# FIX: SHOULD THIS BE specific, NOT source???
#     # MODIFIED 7/16/16 OLD:
#     return general.update(source)
    # MODIFIED 7/16/16 NEW:
    return general.update(specific)


def multi_getattr(obj, attr, default = None):
    """
    Get a named attribute from an object; multi_getattr(x, 'a.b.c.d') is
    equivalent to x.a.b.c.d. When a default argument is given, it is
    returned when any attribute in the chain doesn't exist; without
    it, an exception is raised when a missing attribute is encountered.

    """
    attributes = attr.split(".")
    for i in attributes:
        try:
            obj = getattr(obj, i)
        except AttributeError:
            if default:
                return default
            else:
                raise
    return obj

# # Example usage
# obj  = [1,2,3]
# attr = "append.__doc__.capitalize.__doc__"

# http://pythoncentral.io/how-to-get-an-attribute-from-an-object-in-python/
# getattr also accepts modules as arguments.

# # http://code.activestate.com/recipes/577346-getattr-with-arbitrary-depth/
# multi_getattr(obj, attr) #Will return the docstring for the
#                          #capitalize method of the builtin string
#                          #object


# based off the answer here https://stackoverflow.com/a/15774013/3131666
def get_deepcopy_with_shared(shared_keys=None, shared_types=None):
    """
        Arguments
        ---------
            shared_keys
                an Iterable containing strings that should be shallow copied

            shared_types
                an Iterable containing types that when objects of that type are encountered
                will be shallow copied

        Returns
        -------
            a __deepcopy__ function
    """
    try:
        shared_types = tuple(shared_types)
    except TypeError:
        shared_types = ()

    if shared_keys is None:
        shared_keys = []

    def __deepcopy__(self, memo):
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result

        for k in shared_keys:
            if k in self.__dict__:
                setattr(result, k, self.__dict__[k])

        for k, v in self.__dict__.items():
            if isinstance(self.__dict__[k], shared_types):
                res_val = self.__dict__[k]
                setattr(result, k, res_val)
            elif k not in shared_keys:
                res_val = copy.deepcopy(v, memo)
                setattr(result, k, res_val)
        return result

    return __deepcopy__


def copy_dict_or_list_with_shared(obj, shared_types=None):
    try:
        shared_types = tuple(shared_types)
    except TypeError:
        shared_types = ()

    dict_types = (dict, collections.UserDict)
    list_types = (list, collections.UserList, collections.deque)

    if isinstance(obj, dict_types):
        result = obj.__class__()
        for (k, v) in obj.items():
            # key can never be unhashable dict or list
            new_k = k if isinstance(k, shared_types) else copy.deepcopy(k)

            if isinstance(v, dict_types + list_types):
                new_v = copy_dict_or_list_with_shared(v, shared_types)
            elif isinstance(v, shared_types):
                new_v = v
            else:
                new_v = copy.deepcopy(v)

            try:
                result[new_k] = new_v
            except UtilitiesError:
                # handle ReadOnlyOrderedDict
                result.__additem__(new_k, new_v)
    elif isinstance(obj, list_types):
        result = obj.__class__()
        for item in obj:
            if isinstance(item, dict_types + list_types):
                new_item = copy_dict_or_list_with_shared(item, shared_types)
            elif isinstance(item, shared_types):
                new_item = item
            else:
                new_item = copy.deepcopy(item)
            result.append(new_item)
    else:
        raise TypeError

    return result


def get_alias_property_getter(name, attr=None):
    """
        Arguments
        ---------
            name : str

            attr : str : default None

        Returns
        -------
            a property getter method that

            if **attr** is None, returns the **name** attribute of an object

            if **attr** is not None, returns the **name** attribute of the
            **attr** attribute of an object
    """
    if attr is not None:
        def getter(obj):
            return getattr(getattr(obj, attr), name)
    else:
        def getter(obj):
            return getattr(obj, name)

    return getter


def get_alias_property_setter(name, attr=None):
    """
        Arguments
        ---------
            name : str

            attr : str : default None

        Returns
        -------
            a property setter method that

            if **attr** is None, sets the **name** attribute of an object

            if **attr** is not None, sets the **name** attribute of the
            **attr** attribute of an object
    """
    if attr is not None:
        def setter(obj, value):
            setattr(getattr(obj, attr), name, value)
    else:
        def setter(obj, value):
            setattr(obj, name, value)

    return setter


#region NUMPY ARRAY METHODS ******************************************************************************************

def np_array_less_than_2d(array):
    if not isinstance(array, np.ndarray):
        raise UtilitiesError("Argument to np_array_less_than_2d() ({0}) must be an np.ndarray".format(array))
    if array.ndim <= 1:
        return True
    else:
        return False

def convert_to_np_array(value, dimension):
    """Convert value to np.array if it is not already

    Check whether value is already an np.ndarray and whether it has non-numeric elements

    Return np.array of specified dimension

    :param value:
    :return:
    """
    if value is None:
        return None

    if dimension is 1:
        # KAM 6/28/18: added for cases when even np does not recognize the shape/dtype
        # Needed this specifically for the following shape: variable = [[0.0], [0.0], np.array([[0.0, 0.0]])]
        # Which is due to a custom output state variable that includes an owner value, owner param, and owner input
        # state variable. FIX: This branch of code may erroneously catch other shapes that could be handled by np

        try:
            value = np.atleast_1d(value)

        # KAM 6/28/18: added exception for cases when even np does not recognize the shape/dtype
        # Needed this specifically for the following shape: variable = [[0.0], [0.0], np.array([[0.0, 0.0]])]
        # Due to a custom OutputState variable (variable = [owner value[0], owner param, owner InputState variable])
        # FIX: (1) is this exception specific enough? (2) this is not actually converting to an np.array but in this
        # case (as far as I know) we cannot convert to np -- should we warn other methods that this value is "not np"?
        except ValueError:
            return value

    elif dimension is 2:
        from numpy import ndarray
        # if isinstance(value, ndarray) and value.dtype==object and len(value) == 2:
        value = np.array(value)
        # if value.dtype==object and len(value) == 2:
        # Array is made up of non-uniform elements, so treat as 2d array and pass
        if value.dtype==object:
            pass
        else:
            value = np.atleast_2d(value)
    else:
        raise UtilitiesError("dimensions param ({0}) must be 1 or 2".format(dimension))
    if 'U' in repr(value.dtype):
        raise UtilitiesError("{0} has non-numeric entries".format(value))
    return value


def object_has_single_value(obj):
    """
        Returns
        -------
            True : if **obj** contains only one value, in any dimension
            False : otherwise

            **obj** will be cast to a numpy array if it is not already one
    """
    if not isinstance(obj, np.ndarray):
        obj = np.asarray(obj)

    for s in obj.shape:
        if s > 1:
            return False

    return True


def type_match(value, value_type):
    if isinstance(value, value_type):
        return value
    if value_type in {int, np.integer, np.int64, np.int32}:
        return int(value)
    if value_type in {float, np.float, np.float64, np.float32}:
        return float(value)
    if value_type is np.ndarray:
        return np.array(value)
    if value_type is list:
        return list(value)
    if value_type is None:
        return None
    if value_type is type(None):
        return value
    raise UtilitiesError("Type of {} not recognized".format(value_type))

def get_value_from_array(array):
    """Extract numeric value from array, preserving numeric type
    :param array:
    :return:
    """

def random_matrix(sender, receiver, clip=1, offset=0):
    """Generate a random matrix

    Calls np.random.rand to generate a 2d np.array with random values.

    Arguments
    ----------
    sender : int
        specifies number of rows.

    receiver : int
        spcifies number of columns.

    range : int
        specifies upper limit (lower limit = 0).

    offset : int
        specifies amount added to each entry of the matrix.

    Returns
    -------
    2d np.array
    """
    return (clip * np.random.rand(sender, receiver)) + offset

def underscore_to_camelCase(item):
    item = item[1:]
    item = ''.join(x.capitalize() or '_' for x in item.split('_'))
    return item[0].lower() + item[1:]

def append_type_to_name(object, type=None):
    name = object.name
    # type = type or object.componentType
    # type = type or object.__class__.__base__.__base__.__base__.__name__
    type = type or object.__class__.__base__.__name__
    if any(token in name for token in [type.lower(), type.upper(), type.capitalize()]):
        string = name
    else:
        string = "\'" + name +  "\'" + ' ' + type.lower()
        # string = name + ' ' + type.lower()
    return string
#endregion


from collections import UserDict
class ReadOnlyOrderedDict(UserDict):
    def __init__(self, dict=None, name=None, **kwargs):
        self.name = name or self.__class__.__name__
        UserDict.__init__(self, dict, **kwargs)
        self._ordered_keys = []
    def __setitem__(self, key, item):
        raise UtilitiesError("{} is read-only".format(self.name))
    def __delitem__(self, key):
        raise UtilitiesError("{} is read-only".format(self.name))
    def clear(self):
        raise UtilitiesError("{} is read-only".format(self.name))
    def pop(self, key, *args):
        raise UtilitiesError("{} is read-only".format(self.name))
    def popitem(self):
        raise UtilitiesError("{} is read-only".format(self.name))
    def __additem__(self, key, value):
        self.data[key] = value
        if not key in self._ordered_keys:
            self._ordered_keys.append(key)
    def __deleteitem__(self, key):
        del self.data[key]
    def keys(self):
        return self._ordered_keys
    def copy(self):
        return self.data.copy()

from collections import UserList
class ContentAddressableList(UserList):
    """
    ContentAddressableList( component_type, key=None, list=None)

    Implements dict-like list, that can be keyed by a specified attribute of the `Compoments <Component>` in its
    entries.

    The key with which it is created is also assigned as a property of the class, that returns a list
    with the keyed attribute of its entries.  For example, the `output_states <Mechanism_Base.output_states>` attribute
    of a `Mechanism` is a ContentAddressableList of the Mechanism's `OutputStates <OutputState>`, keyed by their
    names.  Therefore, ``my_mech.output_states.names`` returns the names of all of the Mechanism's OutputStates::

        >>> import psyneulink as pnl
        >>> print(pnl.DDM().output_states.names)
        ['DECISION_VARIABLE', 'RESPONSE_TIME']

    The keyed attribute can also be used to access an item of the list.  For examples::

        >>> print(pnl.DDM().output_states['DECISION_VARIABLE'])
        (OutputState DECISION_VARIABLE)

    Supports:
      * getting and setting entries in the list using keys (string), in addition to numeric indices.
        the key to use is specified by the **key** arg of the constructor, and must be a string attribute;
        * for getting an entry:
          the key must match the keyed attribute of a Component in the list; otherwise an exception is raised;
        * for setting an entry:
            - the key must match the key of the component being assigned;
            - if there is already a component in the list the keyed vaue of which matches the key, it is replaced;
            - if there is no component in the list the keyed attribute of which matches the key,
              the component is appended to the list;
        * for getting lists of the names, values of the keyed attributes, and values of the `value <Component.value>`
            attributes of components in the list.

    IMPLEMENTATION NOTE:
        This class allows Components to be maintained in lists, while providing ordered storage
        and the convenience of access and assignment by name (e.g., akin to a dict).
        Lists are used (instead of a dict or OrderedDict) since:
            - ordering is in many instances convenient, and in some critical (e.g., for consistent mapping from
                collections of states to other variables, such as lists of their values);
            - they are most commonly accessed either exhaustively (e.g., in looping through them during execution),
                or by key (e.g., to get the first, "primary" one), which makes the efficiencies of a dict for
                accessing by key/name less critical;
            - the number of states in a collection for a given Mechanism is likely to be small so that, even when
                accessed by key/name, the inefficiencies of searching a list are likely to be inconsequential.

    Arguments
    ---------

    component_type : Class
        specifies the class of the items in the list.

    name : str : 'ContentAddressableList'
        name to use for ContentAddressableList

    key : str : default `name`
        specifies the attribute of **component_type** used to key items in the list by content;
        **component_type** must have this attribute or, if it is not provided, an attribute with the name 'name'.

    list : List : default None
        specifies a list used to initialize the list;
        all of the items must be of type **component_type** and have the **key** attribute.

    Attributes
    ----------

    component_type : Class
        the class of the items in the list.

    name: str
        name if provided as arg, else name of of ContentAddressableList class

    key : str
        the attribute of `component_type <ContentAddressableList.component_type>` used to key items in the list by content;

    data : List (property)
        the actual list of items.

    names : List (property)
        values of the `name <Component>` attribute of components in the list.

    key_values : List (property)
        values of the keyed attribute of each component in the list.

    values : List (property)
        values of the `value <Component>` attribute of components in the list.

    """

    def __init__(self, component_type, key=None, list=None, name=None, **kwargs):
        self.component_type = component_type
        self.key = key or 'name'
        self.component_type = component_type
        self.name = name or component_type.__name__
        if not isinstance(component_type, type):
            raise UtilitiesError("component_type arg for {} ({}) must be a class"
                                 .format(self.name, component_type))
        if not isinstance(self.key, str):
            raise UtilitiesError("key arg for {} ({}) must be a string".
                                 format(self.name, self.key))
        if not hasattr(component_type, self.key):
            raise UtilitiesError("key arg for {} (\'{}\') must be an attribute of {}".
                                 format(self.name,
                                        self.key,
                                        component_type.__name__))
        if list is not None:
            if not all(isinstance(obj, self.component_type) for obj in list):
                raise UtilitiesError("All of the items in the list arg for {} "
                                     "must be of the type specified in the component_type arg ({})"
                                     .format(self.name, self.component_type.__name__))
        UserList.__init__(self, list, **kwargs)

    # def __repr__(self):
    #     return '[\n\t{0}\n]'.format('\n\t'.join(['{0}\t{1}\t{2}'.format(i, self[i].name,
    #                                                                     repr(self[i].value))
    #                                              for i in range(len(self))]))

    def __getitem__(self, key):
        if key is None:
            raise KeyError("None is not a legal key for {}".format(self.name))
        try:
            return self.data[key]
        except TypeError:
            key_num = self._get_key_for_item(key)
            if key_num is None:
                # raise TypeError("\'{}\' is not a key in the {} being addressed".
                                # format(key, self.__class__.__name__))
                # raise KeyError("\'{}\' is not a key in {}".
                raise TypeError("\'{}\' is not a key in {}".
                                format(key, self.name))
            return self.data[key_num]


    def __setitem__(self, key, value):
        # For efficiency, first assume the key is numeric (duck typing in action!)
        try:
            self.data[key] = value
        # If it is not
        except TypeError:
            # It must be a string
            if not isinstance(key, str):
                raise UtilitiesError("Non-numeric key used for {} ({}) must be "
                                     "a string)".format(self.name, key))
            # The specified string must also match the value of the attribute of the class used for addressing
            if not key == value.name:
            # if not key == type(value).__name__:
                raise UtilitiesError("The key of the entry for {} {} ({}) "
                                     "must match the value of its {} attribute "
                                     "({})".format(self.name,
                                                   value.__class__.__name__,
                                                   key,
                                                   self.key,
                                                   getattr(value, self.key)))
            key_num = self._get_key_for_item(key)
            if key_num is not None:
                self.data[key_num] = value
            else:
                self.data.append(value)

    def __contains__(self, item):
        if super().__contains__(item):
            return True
        else:
            return any(item == obj.name for obj in self.data)

    def _get_key_for_item(self, key):
        if isinstance(key, str):
            obj = next((obj for obj in self.data if obj.name == key), None)
            if obj is None:
                return None
            else:
                return self.data.index(obj)
        elif isinstance(key, self.component_type):
            return self.data.index(key)
        else:
            raise UtilitiesError("{} is not a legal key for {} (must be "
                                 "number, string or State)".format(key,
                                                                   self.key))

    def __delitem__(self, key):
        if key is None:
            raise KeyError("None is not a legal key for {}".format(self.name))
        try:
            del self.data[key]
        except TypeError:
            key_num = self._get_key_for_item(key)
            del self.data[key_num]

    def clear(self):
        super().clear()

    # def pop(self, key, *args):
    #     raise UtilitiesError("{} is read-only".format(self.name))
    #
    # def popitem(self):
    #     raise UtilitiesError("{} is read-only".format(self.name))

    def __additem__(self, key, value):
        if key >= len(self.data):
            self.data.append(value)
        else:
            self.data[key] = value

    def __add__(self, item):
        try:
            if self.component_type != item.component_type:
                raise TypeError(f'Type mismatch {self.component_type} and {item.component_type}')

            if self.key != item.key:
                raise TypeError(f'Key mismatch {self.key} and {item.key}')

        except AttributeError:
            raise TypeError('ContentAddressableList can only be added to ContentAddressableList')

        return ContentAddressableList(self.component_type, self.key, self.data + item.data, self.name)

    def copy(self):
        return self.data.copy()

    @property
    def names(self):
        """Return list of `values <Component.value>` of the name attribute of components in the list.
        Returns
        -------
        names :  list
            list of the values of the `name <Component.name>` attributes of components in the list.
        """
        return [getattr(item, NAME) for item in self.data]

    @property
    def key_values(self):
        """Return list of `values <Component.value>` of the keyed attribute of components in the list.
        Returns
        -------
        key_values :  list
            list of the values of the `keyed <Component.name>` attributes of components in the list.
        """
        return [getattr(item, self.key) for item in self.data]

    @property
    def values(self):
        """Return list of values of components in the list.
        Returns
        -------
        values :  list
            list of the values of the `value <Component.value>` attributes of components in the list.
        """
        return [getattr(item, VALUE) for item in self.data]

    @property
    def values_as_lists(self):
        """Return list of values of components in the list, each converted to a list.
        Returns
        -------
        values :  list
            list of list values of the `value <Component.value>` attributes of components in the list,
        """
        return [np.ndarray.tolist(getattr(item, VALUE)) for item in self.data]

    def get_values_as_lists(self, context=None):
        """Return list of values of components in the list, each converted to a list.
        Returns
        -------
        values :  list
            list of list values of the `value <Component.value>` attributes of components in the list,
        """
        return [np.ndarray.tolist(item.parameters.value.get(context)) for item in self.data]


def is_value_spec(spec):
    if isinstance(spec, (numbers.Number, np.ndarray)):
        return True
    elif isinstance(spec, list) and is_numeric(spec):
        return True
    else:
        return False


def is_unit_interval(spec):
    if isinstance(spec, (int, float)) and 0 <= spec <= 1:
        return True
    else:
        return False


def is_same_function_spec(fct_spec_1, fct_spec_2):
    """Compare two function specs and return True if both are for the same class of Function

    Arguments can be either a class or instance of a PsyNeuLink Function, or any other callable method or function
    Return True only if both are instances or classes of a PsyNeuLink Function;  otherwise, return False
    """
    from psyneulink.core.components.functions.function import Function

    def _convert_to_type(fct):
        if isinstance(fct, Function):
            return type(fct)
        elif inspect.isclass(fct) and issubclass(fct, Function):
            return fct
        else:
            return None

    fct_1 = _convert_to_type(fct_spec_1)
    fct_2 = _convert_to_type(fct_spec_2)

    if fct_1 and fct_2 and fct_1 == fct_2:
        return True
    else:
        return False


def is_component(val):
    """This allows type-checking for Component definitions where Component module can't be imported
    """
    from psyneulink.core.components.component import Component
    return isinstance(val, Component)

def is_instance_or_subclass(candidate, spec):
    """
    Returns
    -------

    True if **candidate** is a subclass of **spec** or an instance thereof, False otherwise
    """
    return isinstance(candidate, spec) or (inspect.isclass(candidate) and issubclass(candidate, spec))


def make_readonly_property(val, name=None):
    """Return property that provides read-only access to its value
    """
    if name is None:
        name = val

    def getter(self):
        return val

    def setter(self, val):
        raise UtilitiesError("{} is read-only property of {}".format(name, self.__class__.__name__))

    # Create the property
    prop = property(getter).setter(setter)
    return prop


def get_class_attributes(cls):
    boring = dir(type('dummy', (object,), {}))
    return [item
            for item in inspect.getmembers(cls)
            if item[0] not in boring]


def convert_all_elements_to_np_array(arr, cast_from=None, cast_to=None):
    """
        Recursively converts all items in **arr** to numpy arrays, optionally casting
        items of type/dtype **cast_from** to type/dtype **cast_to**

        Arguments
        ---------
            cast_from - type, numpy.dtype - type when encountered to cast to **cast_to**
            cast_to - type, numpy.dtype - type to cast **cast_from** to

        Returns
        -------
        a numpy array containing the converted **arr**
    """
    if isinstance(arr, np.ndarray) and arr.ndim == 0:
        if cast_from is not None and isinstance(arr.item(0), cast_from):
            return np.asarray(arr, dtype=cast_to)
        else:
            return arr

    if cast_from is not None and isinstance(arr, cast_from):
        return np.asarray(arr, dtype=cast_to)

    if not isinstance(arr, collections.abc.Iterable) or isinstance(arr, str):
        return np.array(arr)

    if isinstance(arr, np.matrix):
        if arr.dtype == object:
            return np.asarray([convert_all_elements_to_np_array(arr.item(i), cast_from, cast_to) for i in range(arr.size)])
        else:
            return arr

    subarr = [convert_all_elements_to_np_array(x, cast_from, cast_to) for x in arr]

    if all([subarr[i].shape == subarr[0].shape for i in range(1, len(subarr))]):
        # the elements are all uniform in shape, so we can use numpy's standard behavior
        return np.asarray(subarr)
    else:
        # the elements are nonuniform, so create an array that just wraps them individually
        # numpy cannot easily create arrays with subarrays of certain dimensions, workaround here
        # https://stackoverflow.com/q/26885508/3131666
        len_subarr = len(subarr)
        elementwise_subarr = np.empty(len_subarr, dtype=np.ndarray)
        for i in range(len_subarr):
            elementwise_subarr[i] = subarr[i]

        return elementwise_subarr


def insert_list(list1, position, list2):
    """Insert list2 into list1 at position"""
    return list1[:position] + list2 + list1[position:]

def convert_to_list(l):
    if isinstance(l, list):
        return l
    elif isinstance(l, set):
        return list(l)
    else:
        return [l]

def flatten_list(l):
    return [item for sublist in l for item in sublist]

_seed = int(time.monotonic())
def get_global_seed(offset=1):
    global _seed
    _seed += offset
    return _seed - offset

def set_global_seed(new_seed):
    global _seed
    _seed = new_seed


def safe_len(arr, fallback=1):
    """
    Returns
    -------
        len(**arr**) if possible, otherwise **fallback**
    """
    try:
        return len(arr)
    except TypeError:
        return fallback


import typecheck as tc
@tc.typecheck
def _get_arg_from_stack(arg_name:str):
    # Get arg from the stack

    import inspect

    curr_frame = inspect.currentframe()
    prev_frame = inspect.getouterframes(curr_frame, 2)
    i = 1
    # Search stack for first frame (most recent call) with a arg_val specification
    arg_val = None
    while arg_val is None:
        try:
            arg_val = inspect.getargvalues(prev_frame[i][0]).locals[arg_name]
        except KeyError:
            # Try earlier frame
            i += 1
        except IndexError:
            # Ran out of frames, so just set arg_val to empty string
            arg_val = ""
        else:
            break

    # No arg_val was specified in any frame
    if arg_val is None: # cxt-done
        raise UtilitiesError("PROGRAM ERROR: arg_name not found in any frame")

    return arg_val


_unused_args_sig_cache = weakref.WeakKeyDictionary()


def prune_unused_args(func, args=None, kwargs=None):
    """
        Arguments
        ---------
            func : function

            args : *args

            kwargs : **kwargs


        Returns
        -------
            a tuple such that the first item is the intersection of **args** and the
            positional arguments of **func**, and the second item is the intersection
            of **kwargs** and the keyword arguments of **func**

    """
    # use the func signature to filter out arguments that aren't compatible
    try:
        sig = _unused_args_sig_cache[func]
    except KeyError:
        sig = inspect.signature(func)
        _unused_args_sig_cache[func] = sig

    has_args_param = False
    has_kwargs_param = False
    count_positional = 0
    func_kwargs_names = set()

    for name, param in sig.parameters.items():
        if param.kind is inspect.Parameter.VAR_POSITIONAL:
            has_args_param = True
        elif param.kind is inspect.Parameter.VAR_KEYWORD:
            has_kwargs_param = True
        elif param.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD or param.kind is inspect.Parameter.KEYWORD_ONLY:
            if param.default is inspect.Parameter.empty:
                count_positional += 1
            func_kwargs_names.add(name)

    if args is not None:
        try:
            args = list(args)
        except TypeError:
            args = [args]

        if not has_args_param:
            num_extra_args = len(args) - count_positional
            if num_extra_args > 0:
                logger.debug('{1} extra arguments specified to function {0}, will be ignored (values: {2})'.format(func, num_extra_args, args[-num_extra_args:]))
            args = args[:count_positional]
    else:
        args = []

    if kwargs is not None:
        kwargs = dict(kwargs)

        if not has_kwargs_param:
            filtered = set()
            for kw in kwargs:
                if kw not in func_kwargs_names:
                    filtered.add(kw)
            if len(filtered) > 0:
                logger.debug('{1} extra keyword arguments specified to function {0}, will be ignored (values: {2})'.format(func, len(filtered), filtered))
            for kw in filtered:
                del kwargs[kw]
    else:
        kwargs = {}

    return args, kwargs


def call_with_pruned_args(func, *args, **kwargs):
    """
        Calls **func** with only the **args** and **kwargs** that
        exist in its signature
    """
    args, kwargs = prune_unused_args(func, args, kwargs)
    return func(*args, **kwargs)


class NodeRole(Enum):
    """
    COMMENT:
    Attributes
    ----------
    COMMENT

    ORIGIN
        A Node that does not receive any projections. A Composition may have many `ORIGIN` Nodes.

    INPUT
        A Node that receives external input. A Composition may have many `INPUT` Nodes.

    TERMINAL
        A Node that does not send any projections. A Composition may have many `TERMINAL` Nodes.

    OUTPUT
        A Node whose `output_values <Mechanism_Base.output_values>` are returned as output of the Composition. A
        Composition may have many `OUTPUT` Nodes.

    INTERNAL
        A Node that is neither `ORIGIN` nor `TERMINAL`

    CONTROLLER_OBJECTIVE
        A Node that is the ObjectiveMechanism of a controller.

    FEEDBACK_SENDER
        A Node with one or more outgoing projections marked as "feedback". This means that the Node is at the end of a
        pathway which would otherwise form a cycle.

    FEEDBACK_RECEIVER
        A Node with one or more incoming projections marked as "feedback". This means that the Node is at the start of a
        pathway which would otherwise form a cycle.

    CYCLE
        A Node that belongs to a cycle.

    LEARNING
        A Node that is only executed when learning is enabled.

    TARGET
        A Node that receives the target for a learning sequence
    """
    ORIGIN = 0
    INPUT = 1
    TERMINAL = 2
    OUTPUT = 3
    INTERNAL = 4
    CONTROLLER_OBJECTIVE = 5
    FEEDBACK_SENDER = 6
    FEEDBACK_RECEIVER = 7
    CYCLE = 8
    LEARNING = 9
    TARGET = 10

def unproxy_weakproxy(proxy):
    """
        Returns the actual object weak-referenced by a weakproxy.proxy object
        Much like
            >>> a = weakref.ref(b)
            >>> a() is b
            True
    """
    return proxy.__repr__.__self__
