# 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 .object_pool import ThreadedObjectPool

from ..support.utils import get_unique_list, get_new_uuid

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 descriptor 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 descriptor class, 
    and XXXXX TODO TODO

    """

    object_pool = ThreadedObjectPool()

    # ------------------------------------------------------------
    #      Type creation:
    # ------------------------------------------------------------
    def __new__(cls, name, bases, _dict):
        # find all data descriptors, auto-set their labels
        properties = {}
        for label, _property in list(_dict.items()):
            if isinstance(_property, LabeledProperty):
                _property.label = label
                properties[label] = _property

        # Create the class type:
        new_class = super(ModelMeta, cls).__new__(cls, name, bases, _dict)
        # Get the meta class:
        meta = cls.get_meta(new_class, name, bases, _dict)
        # Set the properties for future reference:
        meta.properties = sorted(properties.values(), key=lambda prop: getattr(prop, 'declaration_index', 0))
        # Return our new class:
        return new_class

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

    # ------------------------------------------------------------
    #      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
        instance.uuid = uuid

        return instance

    # ------------------------------------------------------------
    #               Services
    # ------------------------------------------------------------
    def get_meta(cls, name, bases, _dict): #@NoSelf
        """ Extracts or creates the meta class for this new model """
        try:
            meta = _dict["Meta"]
        except KeyError:
            if len(bases) == 1:
                meta = type("Meta", (bases[0].Meta,), dict(properties=[]))
                cls.set_attribute(_dict, "Meta", meta)
                _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__))
        return meta

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

        # Get the meta class:
        meta = cls.get_meta(name, bases, _dict)

        # Get the list of properties for this class type (excluding bases):
        properties = get_unique_list(meta.properties)

        # Check the list of observables is really an iterable:
        if not isinstance(properties, list):
            raise TypeError("%s.%s.Meta '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()

        # Loop over bases in reverse order:
        for class_type in bases[::-1]:
            # Loop over their properties, and update the dictionary:
            if hasattr(class_type, "Meta"):
                for attr in getattr(class_type.Meta, "all_properties", []):
                    all_properties[attr.label] = attr

        # Add new/Override old attributes:
        for attr in properties:
            all_properties[attr.label] = attr

        # Set all_properties on the metadata class:
        meta.all_properties = list(all_properties.values())

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

        pass # end of method

    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)

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