"""
Custom manager for Objects.
"""
from itertools import chain
from django.db.models import Q
from django.conf import settings
from django.db.models.fields import exceptions
from evennia.typeclasses.managers import TypedObjectManager, TypeclassManager
from evennia.typeclasses.managers import returns_typeclass, returns_typeclass_list
from evennia.utils import utils
from evennia.utils.utils import to_unicode, is_iter, make_iter, string_partial_matching

__all__ = ("ObjectManager",)
_GA = object.__getattribute__

# delayed import
_ATTR = None


# Try to use a custom way to parse id-tagged multimatches.

_AT_MULTIMATCH_INPUT = utils.variable_from_module(*settings.SEARCH_AT_MULTIMATCH_INPUT.rsplit('.', 1))


class ObjectDBManager(TypedObjectManager):
    """
    This ObjectManager implements methods for searching
    and manipulating Objects directly from the database.

    Evennia-specific search methods (will return Typeclasses or
    lists of Typeclasses, whereas Django-general methods will return
    Querysets or database objects).

    dbref (converter)
    get_id (alias: dbref_search)
    get_dbref_range
    object_totals
    typeclass_search
    get_object_with_player
    get_objs_with_key_and_typeclass
    get_objs_with_attr
    get_objs_with_attr_match
    get_objs_with_db_property
    get_objs_with_db_property_match
    get_objs_with_key_or_alias
    get_contents
    object_search (interface to many of the above methods,
                   equivalent to evennia.search_object)
    copy_object

    """

    #
    # ObjectManager Get methods
    #

    # player related

    @returns_typeclass
    def get_object_with_player(self, ostring, exact=True, candidates=None):
        """
        Search for an object based on its player's name or dbref.

        Args:
            ostring (str or int): Search criterion or dbref. Searching
                for a player is sometimes initiated by appending an `*` to
                the beginning of the search criterion (e.g. in
                local_and_global_search). This is stripped here.
            exact (bool, optional): Require an exact player match.
            candidates (list, optional): Only search among this list of possible
                object candidates.

        Return:
            match (Object or list): One or more matching results.

        """
        ostring = to_unicode(ostring).lstrip('*')
        # simplest case - search by dbref
        dbref = self.dbref(ostring)
        if dbref:
            return dbref
        # not a dbref. Search by name.
        cand_restriction = candidates != None and Q(pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q()
        if exact:
            return self.filter(cand_restriction & Q(db_player__username__iexact=ostring))
        else: # fuzzy matching
            ply_cands = self.filter(cand_restriction & Q(playerdb__username__istartswith=ostring)).values_list("db_key", flat=True)
            if candidates:
                index_matches = string_partial_matching(ply_cands, ostring, ret_index=True)
                return [obj for ind, obj in enumerate(make_iter(candidates)) if ind in index_matches]
            else:
                return string_partial_matching(ply_cands, ostring, ret_index=False)

    @returns_typeclass_list
    def get_objs_with_key_and_typeclass(self, oname, otypeclass_path, candidates=None):
        """
        Returns objects based on simultaneous key and typeclass match.

        Args:
            oname (str): Object key to search for
            otypeclass_path (str): Full Python path to tyepclass to search for
            candidates (list, optional): Only match among the given list of candidates.

        Returns:
            matches (list): The matching objects.
        """
        cand_restriction = candidates != None and Q(pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q()
        return self.filter(cand_restriction & Q(db_key__iexact=oname, db_typeclass_path__exact=otypeclass_path))

    # attr/property related

    @returns_typeclass_list
    def get_objs_with_attr(self, attribute_name, candidates=None):
        """
        Get objects based on having a certain Attribute defined.

        Args:
            attribute_name (str): Attribute name to search for.
            candidates (list, optional): Only match among the given list of candidates.

        Returns:
            matches (list):  All objects having the given attribute_name defined at all.

        """
        cand_restriction = candidates != None and Q(db_attributes__db_obj__pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q()
        return list(self.filter(cand_restriction & Q(db_attributes__db_key=attribute_name)))

    @returns_typeclass_list
    def get_objs_with_attr_value(self, attribute_name, attribute_value, candidates=None, typeclasses=None):
        """
        Get all objects having the given attrname set to the given value.

        Args:
            attribute_name (str): Attribute key to search for.
            attribute_value (str):  Attribute value to search for.
            candidates (list, optional): Candidate objects to limit search to.
            typeclasses (list, optional): Python pats to restrict matches with.

        Returns:
            matches (list): Objects fullfilling both the `attribute_name` and `attribute_value` criterions.

        Notes:
            This uses the Attribute's PickledField to transparently search the database by matching
            the internal representation. This is reasonably effective but since Attribute values
            cannot be indexed, searching by Attribute key is to be preferred whenever possible.

        """
        cand_restriction = candidates != None and Q(pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q()
        type_restriction = typeclasses and Q(db_typeclass_path__in=make_iter(typeclasses)) or Q()

        ## This doesn't work if attribute_value is an object. Workaround below

        if isinstance(attribute_value, (basestring, int, float, bool, long)):
            return self.filter(cand_restriction & type_restriction & Q(db_attributes__db_key=attribute_name, db_attributes__db_value=attribute_value))
        else:
            # We have to loop for safety since the referenced lookup gives deepcopy error if attribute value is an object.
            global _ATTR
            if not _ATTR:
                from evennia.typeclasses.models import Attribute as _ATTR
            cands = list(self.filter(cand_restriction & type_restriction & Q(db_attributes__db_key=attribute_name)))