"""
This implements the common managers that are used by the
abstract models in dbobjects.py (and which are thus shared by
all Attributes and TypedObjects).
"""
from functools import update_wrapper
from django.db.models import Q
from evennia.utils import idmapper
from evennia.utils.utils import make_iter, variable_from_module

__all__ = ("TypedObjectManager", )
_GA = object.__getattribute__
_Tag = None

#
# Decorators
#

def returns_typeclass_list(method):
    """
    Decorator: Always returns a list, even
    if it is empty.
    """
    def func(self, *args, **kwargs):
        self.__doc__ = method.__doc__
        return list(method(self, *args, **kwargs))
    return update_wrapper(func, method)


def returns_typeclass(method):
    """
    Decorator: Returns a single match or None
    """
    def func(self, *args, **kwargs):
        self.__doc__ = method.__doc__
        query = method(self, *args, **kwargs)
        return query
    return update_wrapper(func, method)

# Managers

class TypedObjectManager(idmapper.manager.SharedMemoryManager):
    """
    Common ObjectManager for all dbobjects.
    """
    # common methods for all typed managers. These are used
    # in other methods. Returns querysets.


    # Attribute manager methods
    def get_attribute(self, key=None, category=None, value=None, strvalue=None, obj=None, attrtype=None):
        """
        Return Attribute objects by key, by category, by value, by
        strvalue, by object (it is stored on) or with a combination of
        those criteria.

        attrtype - one of None (normal Attributes) or "nick"
        """
        query = [("attribute__db_attrtype", attrtype)]
        if obj:
            query.append(("%s__id" % self.model.__name__.lower(), obj.id))
        if key:
            query.append(("attribute__db_key", key))
        if category:
            query.append(("attribute__db_category", category))
        if strvalue:
            query.append(("attribute__db_strvalue", value))
        elif value:
            # strvalue and value are mutually exclusive
            query.append(("attribute__db_value", value))
        return [th.attribute for th in self.model.db_attributes.through.objects.filter(**dict(query))]

    def get_nick(self, key=None, category=None, value=None, strvalue=None, obj=None):
        return self.get_attribute(key=key, category=category, value=value, strvalue=strvalue, obj=obj)

    @returns_typeclass_list
    def get_by_attribute(self, key=None, category=None, value=None, strvalue=None, attrtype=None):
        """
        Return objects having attributes with the given key, category, value,
        strvalue or combination of those criteria.
        """
        query = [("db_attributes__db_attrtype", attrtype)]
        if key:
            query.append(("db_attributes__db_key", key))
        if category:
            query.append(("db_attributes__db_category", category))
        if strvalue:
            query.append(("db_attributes__db_strvalue", value))
        elif value:
            # strvalue and value are mutually exclusive
            query.append(("db_attributes__db_value", value))
        return self.filter(**dict(query))

    def get_by_nick(self, key=None, nick=None, category="inputline"):
        "Get object based on its key or nick."
        return self.get_by_attribute(key=key, category=category, strvalue=nick, attrtype="nick")

    # Tag manager methods

    def get_tag(self, key=None, category=None, obj=None, tagtype=None):
        """
        Return Tag objects by key, by category, by object (it is
        stored on) or with a combination of those criteria.

        tagtype - one of None (normal tags), "alias" or "permission"
        """
        query = [("tag__db_tagtype", tagtype)]
        if obj:
            query.append(("%s__id" % self.model.__name__.lower(), obj.id))
        if key:
            query.append(("tag__db_key", key))
        if category:
            query.append(("tag__db_category", category))
        return [th.tag for th in self.model.db_tags.through.objects.filter(**dict(query))]

    def get_permission(self, key=None, category=None, obj=None):
        return self.get_tag(key=key, category=category, obj=obj, tagtype="permission")

    def get_alias(self, key=None, category=None, obj=None):
        return self.get_tag(key=key, category=category, obj=obj, tagtype="alias")

    @returns_typeclass_list
    def get_by_tag(self, key=None, category=None, tagtype=None):
        """
        Return objects having tags with a given key or category or
        combination of the two.

        tagtype = None, alias or permission
        """
        query = [("db_tags__db_tagtype", tagtype)]
        if key:
            query.append(("db_tags__db_key", key))
        if category:
            query.append(("db_tags__db_category", category))
        return self.filter(**dict(query))

    def get_by_permission(self, key=None, category=None):
        return self.get_by_tag(key=key, category=category, tagtype="permission")

    def get_by_alias(self, key=None, category=None):
        return self.get_by_tag(key=key, category=category, tagtype="alias")

    def create_tag(self, key=None, category=None, data=None, tagtype=None):
        """
        Create a new Tag of the base type associated with this typedobject.
        This makes sure to create case-insensitive tags. If the exact same
        tag configuration (key+category+tagtype) exists on the model, a
        new tag will not be created, but an old one returned.  A data
        keyword is not part of the uniqueness of the tag and setting one
        on an existing tag will overwrite the old data field.
        """
        data = str(data) if data is not None else None
        # try to get old tag
        tag = self.get_tag(key=key, category=category, tagtype=tagtype)
        if tag and data is not None:
            # overload data on tag
            tag.db_data = data
            tag.save()
        elif not tag:
            # create a new tag
            global _Tag
            if not _Tag:
                from evennia.typeclasses.models import Tag as _Tag
            tag = _Tag.objects.create(
                db_key=key.strip().lower() if key is not None else None,
                db_category=category.strip().lower() if category and key is not None else None,
                db_data=data,
                db_tagtype=tagtype.strip().lower() if tagtype is not None else None)
            tag.save()
        return make_iter(tag)[0]

    # object-manager methods

    def dbref(self, dbref, reqhash=True):
        """
        Valid forms of dbref (database reference number)
        are either a string '#N' or an integer N.
        Output is the integer part.
        reqhash - require input to be on form "#N" to be
        identified as a dbref
        """
        if reqhash and not (isinstance(dbref, basestring) and dbref.startswith("#")):
            return None
        if isinstance(dbref, basestring):
            dbref = dbref.lstrip('#')
        try:
            if int(dbref) < 0:
                return None
        except Exception:
            return None
        return dbref

    @returns_typeclass
    def get_id(self, dbref):
        """
        Find object with given dbref
        """
        dbref = self.dbref(dbref, reqhash=False)
        try:
            return self.get(id=dbref)
        except self.model.DoesNotExist:
            pass
        return None

    def dbref_search(self, dbref):
        """
        Alias to get_id
        """
        return self.get_id(dbref)

    @returns_typeclass_list
    def get_dbref_range(self, min_dbref=None, max_dbref=None):
        """
        Return all objects inside and including the
        given boundaries.
        """
        retval = super(TypedObjectManager, self).all()
        if min_dbref is not None:
            retval = retval.filter(id__gte=self.dbref(min_dbref, reqhash=False))
        if max_dbref is not None:
            retval = retval.filter(id__lte=self.dbref(max_dbref, reqhash=False))
        return retval

    def object_totals(self):
        """
        Returns a dictionary with all the typeclasses active in-game
        as well as the number of such objects defined (i.e. the number
        of database object having that typeclass set on themselves).
        """
        dbtotals = {}
        typeclass_paths = set(self.values_list('db_typeclass_path', flat=True))
        for typeclass_path in typeclass_paths:
            dbtotals[typeclass_path] = \
               self.filter(db_typeclass_path=typeclass_path).count()
        return dbtotals

    @returns_typeclass_list
    def typeclass_search(self, typeclass, include_children=False, include_parents=False):
        """
        Searches through all objects returning those which has a
        certain typeclass. If location is set, limit search to objects
        in that location.

        typeclass - a typeclass class or a python path to a typeclass
        include_children - return objects with given typeclass and all
                       children inheriting from this typeclass.
        include_parents - return objects with given typeclass and all
                       parents to this typeclass
        The include_children/parents keywords are mutually exclusive.
        """

        if callable(typeclass):
            cls = typeclass.__class__
            typeclass = "%s.%s" % (cls.__module__, cls.__name__)
        elif not isinstance(typeclass, basestring) and hasattr(typeclass, "path"):
            typeclass = typeclass.path

        # query objects of exact typeclass
        query = Q(db_typeclass_path__exact=typeclass)

        if include_children:
            # build requests for child typeclass objects
            clsmodule, clsname = typeclass.rsplit(".", 1)
            cls = variable_from_module(clsmodule, clsname)
            subclasses = cls.__subclasses__()
            if subclasses:
                for child in (child for child in subclasses if hasattr(child, "path")):
                    query = query | Q(db_typeclass_path__exact=child.path)
        elif include_parents:
            # build requests for parent typeclass objects
            clsmodule, clsname = typeclass.rsplit(".", 1)
            cls = variable_from_module(clsmodule, clsname)
            parents = cls.__mro__
            if parents:
                for parent in (parent for parent in parents if hasattr(parent, "path")):
                    query = query | Q(db_typeclass_path__exact=parent.path)
        # actually query the database
        return self.filter(query)


class TypeclassManager(TypedObjectManager):

    def get(self, **kwargs):
        """
        Overload the standard get. This will limit itself to only
        return the current typeclass.
        """
        kwargs.update({"db_typeclass_path":self.model.path})
        return super(TypedObjectManager, self).get(**kwargs)

    def filter(self, **kwargs):
        """
        Overload of the standard filter function. This filter will
        limit itself to only the current typeclass.
        """
        kwargs.update({"db_typeclass_path":self.model.path})
        return super(TypedObjectManager, self).filter(**kwargs)

    def all(self, **kwargs):
        """
        Overload method to return all matches, filtering for typeclass
        """
        return super(TypedObjectManager, self).all(**kwargs).filter(db_typeclass_path=self.model.path)

    def _get_subclasses(self, cls):
        """
        Recursively get all subclasses to a class
        """
        all_subclasses = cls.__subclasses__()
        for subclass in all_subclasses:
            all_subclasses.extend(self._get_subclasses(subclass))
        return all_subclasses

    def get_family(self, **kwargs):
        """
        Variation of get that not only returns the current
        typeclass but also all subclasses of that typeclass.
        """
        paths = [self.model.path] + ["%s.%s" % (cls.__module__, cls.__name__)
                         for cls in self._get_subclasses(self.model)]
        kwargs.update({"db_typeclass_path__in":paths})
        return super(TypedObjectManager, self).get(**kwargs)

    def filter_family(self, **kwargs):
        """
        Variation of filter that allows results both from typeclass
        and from subclasses of typeclass
        """
        # query, including all subclasses
        paths = [self.model.path] + ["%s.%s" % (cls.__module__, cls.__name__)
                         for cls in self._get_subclasses(self.model)]
        kwargs.update({"db_typeclass_path__in":paths})
        return super(TypedObjectManager, self).filter(**kwargs)

    def all_family(self, **kwargs):
        """
        Return all matches, allowing matches from all subclasses of
        the typeclass.
        """
        paths = [self.model.path] + ["%s.%s" % (cls.__module__, cls.__name__)
                         for cls in self._get_subclasses(self.model)]
        return super(TypedObjectManager, self).all(**kwargs).filter(db_typeclass_path__in=paths)


