#  ___________________________________________________________________________
#
#  Pyomo: Python Optimization Modeling Objects
#  Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
#  Under the terms of Contract DE-NA0003525 with National Technology and
#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
#  rights in this software.
#  This software is distributed under the 3-clause BSD License.
#  ___________________________________________________________________________

import weakref
import logging

from pyomo.common.log import is_debug_set
from pyomo.common.timing import ConstructionTimer
from pyomo.core.base.component import Component, ComponentData

logger = logging.getLogger('pyomo.core')

__all__ = ['Alias']

#
# Key Points (for now - subject to change):
#   - Alias mimics the behavior of weakref.proxy (mostly)
#     using a weakref.ref under the hood. Aliasing an
#     object will not prevent it from being garbage collected.
#   - Alias will forward all function calls and member lookups
#     through to the underlying proxy, EXCEPT for those on
#     the component base class.
#   - Alias prevents prevents __deepcopy__ on ComponentData
#     from being called. All clones/copies are shallow,
#     that is, unless an Alias is pickled completely separate
#     from what it is referencing.
#   - Updating the referenced object is disabled for now,
#     but would be easy to update.
#   - Many magic methods do not go through __getattr__,
#     so we manually forward them.
#   - aliased_object (property), provides direct access to
#     the aliased component (unless the weakref has become
#     invalid), so that Component base class methods can
#     be called if needed.
#   - Alias inherits the ctype of the object it proxies (for now). Not
#     sure if this is good or bad.  This could be bad for solver
#     plugins (e.g., extra variables), but I think when an expression
#     is generated due to forwarding NumValue base class methods, this
#     results in the Alias (e.g., for a Var) getting replaced with its
#     aliased object in the expression. So problem writers
#     could possibly never encounter Aliases (if we're
#     careful).

class Alias(Component):

    __slots__ = ('_aliased_object')

    # It would be nice if Component declared these as slots, but for
    # now we need to hard code them so they can be set during
    # Component.__init__ (and properly redirected back to the base
    # class from the overloaded __setattr__ method defined here
    _component_slots = ['_type','doc','name','_active','_constructed','_parent']

    def __init__(self, obj):
        self._aliased_object = None
        if isinstance(obj, Alias):
            obj = obj._aliased_object()
        if obj is not None:
            self._aliased_object = weakref.ref(obj)
        ctype = Alias
        if isinstance(obj, Component):
            ctype = obj.ctype
        else:
            if not isinstance(obj, ComponentData):
                raise TypeError("Aliased object must be an "
                                "instance of Component or ComponentData")
            ctype = obj.parent_component().ctype
        Component.__init__(self, ctype=ctype)

    @property
    def aliased_object(self):
        obj = Alias.__dict__['_aliased_object'].__get__(self)
        if obj is not None:
            obj = obj()
        return obj
    @aliased_object.setter
    def aliased_object(self, value):
        raise AttributeError("Assignment not allowed.")

    #
    # Overload methods on Component
    #

    def construct(self, data=None):
        if is_debug_set(logger):   #pragma:nocover
            try:
                name = str(self.name)
            except:
                name = type(self)
            logger.debug("Constructing Alias, name=%s, "
                         "from data=%s", name, str(data))
        if self._constructed:
            return
        timer = ConstructionTimer(self)
        self._constructed = True
        timer.report()

    def _pprint(self):
        if self.aliased_object is None:
            return [("Proxy","None")], None, None, None
        else:
            return [("Proxy", self.aliased_object.name)], None, None, None

    # Dereference so we can point to a constructed object
    # after model cloning / construction
    def __getstate__(self):
        result = super(Alias, self).__getstate__()
        result['_aliased_object'] = self.aliased_object
        return result

    def __setstate__(self, state):
        super(Alias, self).__setstate__(state)
        if self._aliased_object is not None:
            self._aliased_object = weakref.ref(self._aliased_object)

    #
    # Note: __getattr__ will only be invoked when a
    #       a method is not defined here or on base class
    #       or when an member name is not defined
    #       on this class (base class not included).
    #       Any members on the Component
    #       base class do not get redirected
    #       to the proxy, so we check there first
    #       before calling a proxy method (so that
    #       __init__ on Component works for this
    #       objects base class). This might be
    #       different if Component is slot-ized.
    #
    def __getattr__(self, name):

        # if we are proxy to a ComponentData
        # object, we need to make sure NOT to
        # invoke its deepcopy method
        if name == "__deepcopy__":
            raise AttributeError

        if name in Alias.__slots__:
            # should never happen
            assert False

        if name in Alias._component_slots:
            return super(Alias, self).__dict__[name].__get__(self)

        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return getattr(self.aliased_object, name)

    #
    # __setattr__ and __delattr__ will always be invoked
    #

    def __setattr__(self, name, attr):
        # avoid infinite recursion
        if name in Alias.__slots__:
            # this is how you access a slot while bypassing __setattr__
            return Alias.__dict__[name].__set__(self, attr)

        if name in Alias._component_slots:
            super(Alias, self).__dict__[name] = attr
            return

        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is not longer valid")
        return setattr(self.aliased_object, name, attr)

    def __delattr__(self, name):
        # avoid infinite recursion
        if name in Alias.__slots__:
            raise TypeError("'dictproxy' object does not support item deletion")

        if name in Alias._component_slots:
            del super(Alias, self).__dict__[name]

        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is not longer valid")
        return delattr(self.aliased_object, name)

    #
    # In addition to overloading __getattr__, the following magic
    # methods need to be redirected as Python does not access them
    # through __getitem__. If the aliased object does not implement
    # them an exception will be thrown
    #

    # E.g., IndexedComponent

    def __len__(self):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__len__()

    def __contains__(self, key):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__contains__(key)

    def __iter__(self):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__iter__()

    def __getitem__(self, key):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__getitem__(key)

    def __setitem__(self, key, value):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__setitem__(key, value)

    # E.g., NumValue

    def __float__(self):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__float__()

    def __int__(self):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__int__()

    def __lt__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__lt__(other)

    def __gt__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__gt__(other)

    def __le__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__le__(other)

    def __ge__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__ge__(other)

    def __eq__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__eq__(other)

    def __add__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__add__(other)

    def __sub__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__sub__(other)

    def __mul__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__mul__(other)

    def __div__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__div__(other)

    def __truediv__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__truediv__(other)

    def __pow__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__pow__(other)

    def __radd__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__radd__(other)

    def __rsub__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__rsub__(other)

    def __rmul__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__rmul__(other)

    def __rdiv__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__rdiv__(other)

    def __rtruediv__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__rtruediv__(other)

    def __rpow__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__rpow__(other)

    def __iadd__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__iadd__(other)

    def __isub__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__isub__(other)

    def __imul__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__imul__(other)

    def __idiv__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__idiv__(other)

    def __itruediv__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__itruediv__(other)

    def __ipow__(self, other):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__ipow__(other)

    def __neg__(self):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__neg__()

    def __pos__(self):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__pos__()

    def __abs__(self):
        if self.aliased_object is None:
            raise weakref.ReferenceError("Proxy is no longer valid")
        return self.aliased_object.__abs__()
