# coding=UTF-8
# ex:ts=4:sw=4:et=on
#  -------------------------------------------------------------------------
#  Copyright (C) 2014 by Mathijs Dumon <mathijs dot dumon at gmail dot com>
#  Copyright (C) 2005 by Roberto Cavada <roboogle@gmail.com>
#
#  mvc is a framework derived from the original pygtkmvc framework
#  hosted at: <http://sourceforge.net/projects/pygtkmvc/>
#
#  mvc is free software; you can redistribute it and/or
#  modify it under the terms of the GNU Lesser General Public
#  License as published by the Free Software Foundation; either
#  version 2 of the License, or (at your option) any later version.
#
#  mvc is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#  Lesser General Public License for more details.
#
#  You should have received a copy of the GNU Lesser General Public
#  License along with this library; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor,
#  Boston, MA 02110, USA.
#  -------------------------------------------------------------------------

import types
import logging
logger = logging.getLogger(__name__)

from collections import OrderedDict

from .properties import LabeledProperty
from .prop_intel import PropIntel
from .object_pool import ThreadedObjectPool
from .treenode import TreeNode

from pyxrd.generic.utils import get_unique_list, get_new_uuid # TODO

class ModelMeta(type):
    """
    This is a meta-class that provides support to quickly set up a model with
    observable properties. For the simplest attributes you can omit the usual
    setters and getters if nothing else needs to be done but setting and getting
    the property. You can also provide your own implementation of the setters 
    and getters (see below). Classes instantiated by this meta-class will be 
    also be registered in an object pool using a UUID attribute. You do not
    need to add this property to your inner 'Meta' class.
    Classes instantiated by this metaclass must provide a method named
    notify_property_value_change(self, prop_name, old, new), or notification of
    property changes using 'Observer' won't work.
    ModelMeta also provides multi-threading support for accessing properties, 
    through a (very basic) locking mechanism. It is assumed a lock is
    owned by the class that uses it. A Lock object called _prop_lock
    is assumed to be a member of the using class.
    
    How can you use this metaclass?
    First, '__metaclass__ = ModelMeta' must be class member of the class
    you want to make the automatic properties handling.
    
    Second, the model class needs to define an inner 'Meta' class in which
    you define meta-data which is used by this class (also see 'ModelMeta.Meta'
    class).
    This class defines several properties, the most important being 'properties'.
    This attribute is a list of 'PropIntel' objects, each of which describe
    a single property. Aside from the basics (name, default, etc.) it can
    contain additional information describing how/if other parts of the 
    framework can use this attribute (e.g. is it supposed to be stored?, does 
    it have a visual representation?). 
    That's all: after the instantiation, your class will contain all properties
    you named inside 'properties'. Each of them will be also associated
    to a couple of automatically-generated functions which get and set the
    property value inside a generated member variable.
    
    Custom setters and getters:
    Suppose the property is called 'x'.  The generated variable (which keeps
    the real value of the property x) is called _x. The getter is called 
    'get_x(self)', and the setter is called 'set_x(self, value)'. The base
    implementation of this getter is to return the value stored in the variable
    associated with the property. The setter simply sets its value.
    Programmers can override basic behavior for getters or setters simply by
    defining their getters and setters using the name convention above.
    The customized function can lie anywhere in the user classes hierarchy.
    Every overridden function will not be generated by the metaclass.

    For some properties it can be interesting to create a new PropIntel class, 
    and XXXXX TODO TODO

    """

    object_pool = ThreadedObjectPool()

    # ------------------------------------------------------------
    #      Type creation:
    # ------------------------------------------------------------
    def __new__(cls, name, bases, _dict):
        # find all data descriptors, auto-set their labels
        for n, v in _dict.items():
            if isinstance(v, LabeledProperty):
                v.label = n
        return super(ModelMeta, cls).__new__(cls, name, bases, _dict)

    def __init__(cls, name, bases, _dict):
        cls.process_properties(name, bases, _dict)
        return super(ModelMeta, cls).__init__(name, bases, _dict)

    def process_properties(cls, name, bases, _dict):  # @NoSelf
        """Processes the properties defined in the class's metadata class."""

        # Get the list of properties for this class type (excluding bases):
        try:
            meta = _dict["Meta"]
        except KeyError:
            if len(bases) == 1:
                meta = type("Meta", (bases[0].Meta,), dict(properties=[]))
                cls.set_attribute(_dict, "Meta", meta)
            else:
                raise TypeError("Class %s.%s has not defined an inner Meta class, and has multiple base classes!" % (cls.__module__, cls.__name__))

        # Get this types properties ('new' ones):
        properties = get_unique_list(meta.properties)
        # Check the list of observables is really an iterable:
        if not isinstance(properties, types.ListType):
            raise TypeError("In class %s.%s.Meta attribute 'properties' must be a list, not '%s'" %
                            (cls.__module__, cls.__name__, type(properties)))

        # Generates the list of _all_ properties available for this class's bases
        all_properties = OrderedDict()
        cls.Meta.all_properties = all_properties

        # Reverse order of the bases:
        for class_type in bases[::-1]:
            # Loop over properties, and update the dictionary:
            if hasattr(class_type, "Meta"):
                for prop in getattr(class_type.Meta, "all_properties", []):
                    all_properties[prop.name] = prop

        # Expand & add the properties:
        for prop in cls.expand_properties(properties, _dict):
            all_properties[prop.name] = prop

        # Set all_properties on the metadata class:
        cls.Meta.all_properties = all_properties.values()

        logger.debug("Class %s.%s has properties: %s" \
                     % (cls.__module__, cls.__name__, all_properties))

        pass # end of method

    def expand_properties(cls, properties, _dict):  # @NoSelf
        for prop in properties:
            # Check prop is really a PropIntel sub-class:
            if not isinstance(prop, PropIntel):
                raise TypeError("In class %s.%s.Meta attribute 'properties' must contain"\
                                    " only PropIntel instances (found %s)" %
                                (cls.__module__, cls.__name__, type(prop)))

            if prop.name in cls.Meta.all_properties:
                logger.warning("Cannot expand property '%s' of class '%s' twice, ignoring!" % (prop.name, cls))
                yield prop
                continue

            # Determine if the property is concrete or logical:
            # concrete = cls.is_concrete_attribute(prop)

            # Check to see if there's some plumbing to do:
            new_properties = cls.expand_property(_dict, prop)

            # Wrap property setters and getters:
            cls.wrap_accesors(_dict, prop)

            # If the expansion generated some new properties,
            # expand (& add any new generated props) as well:
            for new_prop in cls.expand_properties(new_properties, _dict):
                yield new_prop

            # Yield this property:
            yield prop

    def expand_property(cls, _dict, prop):  # @NoSelf
        """
            This method is called for each property before its
            setter and getter have been wrapped. This allows
            to 'expand' certain compact representations of common idioms, such
            as properties which can only have a value from a list of options,
            special business logic, ...
            
            This method can also return an iterable containing 'additional'
            PropIntel objects generated from the passed one.
        """

        if hasattr(prop, 'expand'):
            new_attrs, new_props = getattr(prop, 'expand')(cls)

            for attr_name, attr in new_attrs:
                cls.set_attribute(_dict, attr_name, attr)

            return new_props
        else:
            return []

    def wrap_accesors(cls, _dict, prop): # @NoSelf
        """
            Method that creates getter and setter, and the
            corresponding property.
            If an optional default_val is passed to attribute is considered
            concrete and not logical, and a private attribute (prepended
            with an underscore) will be generated.
            If this is not the case, attribute is considered logical, and no
            private attribute is set.
        """

        # Generate the getter wrapper:
        _getter = cls.wrap_getter(_dict, prop)

        # Generate the setter wrapper:
        _setter = cls.wrap_setter(_dict, prop, _getter)

        if hasattr(cls, prop.name):
            # If this is a LabeledProperty descriptor, make sure we have created
            # the private attribute as well:
            attr = getattr(cls, prop.name)
            if isinstance(attr, LabeledProperty) and not prop.get_private_name() in _dict:
                cls.set_attribute(_dict, prop.get_private_name(), attr.default)
            # Clear the property:
            cls.del_attribute(_dict, prop.name)

        # Create the property:
        _property = property(fget=_getter, fset=_setter)
        cls.set_attribute(_dict, prop.name, _property)

        # Wrap the underlying variable if needed
        # (e.g. if it's a list, tuple, dict, or other mutable class):
        default_found, default = cls.get_default_value(prop)
        if default_found:
            default = cls.create_value(prop, default)
            cls.set_attribute(_dict, prop.get_private_name(), default)

    def get_default_value(cls, prop):  # @NoSelf
        """This method returns a default value for the given PropIntel.
        There are three ways a default value can be defined, depending:
         - by setting a default value in the PropIntel
         - if it's a concrete property: the current (public)value
         - if it's a logical property: by getting the private attribute 
           '_%{prop_name}s' if this exists
        If no default value is defined anywhere, None is returned.
        """
        if hasattr(prop, "default"):
            return True, prop.default
        elif hasattr(cls, prop.get_private_name()):
            return True, getattr(cls, prop.get_private_name())
        else:
            return False, None

    def check_value_change(cls, old, new): # @NoSelf
        """Checks whether the value of the property changed in type
        or if the instance has been changed to a different instance.
        If true, a call to model._reset_property_notification should
        be called in order to re-register the new property instance
        or type"""
        from ..support import observables
        return  type(old) != type(new) or \
               isinstance(old, observables.ObsWrapperBase) and (old != new)

    def set_attribute(cls, _dict, name, value): # @NoSelf
        """Sets an attribute on the class and the dict"""
        _dict[name] = value
        setattr(cls, name, value)

    def del_attribute(cls, _dict, name): # @NoSelf
        """Deletes an attribute from the class and the dict"""
        del _dict[name]
        delattr(cls, name)

    def create_value(cls, prop, val, model=None): # @NoSelf
        """This is used to wrap a value to be assigned to a
        property. Depending on the type of the value, different values
        are created and returned. For example, for a list, a
        ListWrapper is created to wrap it, and returned for the
        assignment. model is different from None when the value is
        changed (a model exists). Otherwise, during property creation
        model is None"""
        from ..support import observables

        if isinstance(val, tuple):
            # this might be a class instance to be wrapped
            # (thanks to Tobias Weber for
            # providing a bug fix to avoid TypeError (in 1.99.1)
            if len(val) == 3:
                try:
                    wrap_instance = isinstance(val[1], val[0]) and \
                        (isinstance(val[2], tuple) or
                         isinstance(val[2], list))
                except TypeError:
                    pass # not recognized, it must be another type of tuple
                else:
                    if wrap_instance:
                        res = observables.ObsWrapper(val[1], val[2])
                        if model: res.__add_model__(model, prop.name)
                        return res
                    pass
                pass
            pass

        elif isinstance(val, list):
            res = observables.ObsListWrapper(val)
            if model: res.__add_model__(model, prop.name)
            return res

        elif isinstance(val, dict):
            res = observables.ObsMapWrapper(val)
            if model: res.__add_model__(model, prop.name)
            return res

        elif isinstance(val, TreeNode):
            res = observables.ObsTreeNodeWrapper(val)
            if model: res.__add_model__(model, prop.name)
            return res

        return val

    # ------------------------------------------------------------
    #               Services
    # ------------------------------------------------------------

    # Override these:
    def wrap_getter(cls, _dict, prop): # @NoSelf
        """
        Returns a (wrapped) getter function. This allows for metaclasses
        that need to catch get or set calls for e.g. notifying observers.
        """
        if not hasattr(cls, prop.name):
            _private_getter = getattr(cls, prop.get_getter_name(), None)
            if callable(_private_getter):
                def _getter(self):
                    with self._prop_lock:
                        return _private_getter(self)
            else:
                # Property needs to be created
                def _getter(self):
                    with self._prop_lock:
                        return getattr(self, prop.get_private_name())
        else:
            # Property exists
            # Get it:
            attr = getattr(cls, prop.name)

            if hasattr(attr, "__get__") and hasattr(attr, "__set__"):
                # Property is a data descriptor
                # Get the __get__ function & use it:
                _descr_get = getattr(attr, "__get__")
                def _getter(self):
                    with self._prop_lock:
                        return _descr_get(self, cls)
            else:
                # Property is not a data descriptor,
                # copy the value to a private attribute:
                setattr(cls, prop.get_private_name(), attr)
                _dict[prop.get_private_name()] = attr
                def _getter(self):
                    with self._prop_lock:
                        return getattr(self, prop.get_private_name())
        return _getter

    def wrap_setter(cls, _dict, prop, inner_getter): # @NoSelf
        """The setter follows the rules of the getter. First search
        for property variable, then logical custom setter. If no
        setter is found, None is returned (i.e. the property is
        read-only.)"""

        # 1. Get the setter
        if not hasattr(cls, prop.name):
            _private_setter = getattr(cls, prop.get_setter_name(), None)
            if callable(_private_setter):
                def _inner_setter(self, value):
                    with self._prop_lock:
                        return _private_setter(self, value)
            else:
                # Property does not exist
                def _inner_setter(self, value):
                    with self._prop_lock:
                        return setattr(self, prop.get_private_name(), value)
        else:
            # Property exists
            # Get it:
            attr = getattr(cls, prop.name)

            if hasattr(attr, "__get__") and hasattr(attr, "__set__"):
                # Property is a data descriptor
                # Get the __get__ function & use it:
                _descr_set = getattr(attr, "__set__")
                def _inner_setter(self, value):
                    with self._prop_lock:
                        return _descr_set(self, value)
            else:
                # Property is not a data descriptor
                def _inner_setter(self, value):
                    with self._prop_lock:
                        return setattr(self, prop.get_private_name(), value)

        # 2. Wrap the setter so we can have notifications
        def _setter(self, val):
            with self._prop_lock:
                # Get the old value
                old = inner_getter(self)
                # Wrap the new value
                new = type(self).create_value(prop, val, self)
                # Set the new value
                _inner_setter(self, new)
            if prop.observable:
                # Check if we've really changed it, and send notifications if so:
                if type(self).check_value_change(old, new):
                    self._reset_property_notification(prop, old)
                    pass

                # Notify any interested party we have set this property!
                if hasattr(self, 'notify_property_value_change'):
                    self.notify_property_value_change(prop.name, old, val)
            return

        return _setter

    # ------------------------------------------------------------
    #      Instance creation:
    # ------------------------------------------------------------
    def __call__(cls, *args, **kwargs): # @NoSelf
        """
        This method checks if the passed keyword args contained a "uuid" key, if
        so it is popped (the actual class's __init__ doesn't get it). If not
        a new UUID is created. 
        The class instance is then created and the UUID is set accordingly. 
        """
        # Check if uuid has been passed (e.g. when restored from disk)
        # if not, generate a new one
        try:
            uuid = kwargs.pop("uuid")
        except KeyError:
            uuid = get_new_uuid()

        # Create instance:
        instance = type.__call__(cls, *args, **kwargs)

        # Set the UUID (will invoke above setter)
        instance.uuid = uuid

        return instance

    pass # end of class
# ----------------------------------------------------------------------
