# -*- coding: utf-8 -*-

import re
import pymongo
import tornado
from tornado import gen
from bson.code import Code
from mongoengine.common import _import_class
from mongoengine.queryset import base
from mongoengine.errors import NotUniqueError, OperationError
from mongomotor import signals
from mongomotor.queryset import transform


class BaseQuerySet(base.BaseQuerySet):
    """
    BaseQuerySet that uses motor
    """

    @gen.coroutine
    def aggregate(self, *pipeline, **kwargs):
        """
        Perform a aggregate function based in your queryset params
        :param pipeline: list of aggregation commands,\
            see: http://docs.mongodb.org/manual/core/aggregation-pipeline/

        """
        initial_pipeline = []

        if self._query:
            initial_pipeline.append({'$match': (yield self._query)})

        if self._ordering:
            initial_pipeline.append({'$sort': dict(self._ordering)})

        if self._limit is not None:
            initial_pipeline.append({'$limit': self._limit})

        if self._skip is not None:
            initial_pipeline.append({'$skip': self._skip})

        pipeline = initial_pipeline + list(pipeline)

        collection = self._collection
        result = collection.aggregate(pipeline, **kwargs)

        return result

    @gen.coroutine
    def map_reduce(self, map_f, reduce_f, output, finalize_f=None, limit=None,
                   scope=None, get_out_docs=True):
        """map_reduce that uses motor

        Perform a map/reduce query using the current query spec
        and ordering. While ``map_reduce`` respects ``QuerySet`` chaining,
        it must be the last call made, as it does not return a maleable
        ``QuerySet``.

        See the :meth:`~mongoengine.tests.QuerySetTest.test_map_reduce`
        and :meth:`~mongoengine.tests.QuerySetTest.test_map_advanced`
        tests in ``tests.queryset.QuerySetTest`` for usage examples.

        :param map_f: map function, as :class:`~bson.code.Code` or string
        :param reduce_f: reduce function, as
                         :class:`~bson.code.Code` or string
        :param output: output collection name, if set to 'inline' will try to
           use :class:`~pymongo.collection.Collection.inline_map_reduce`
           This can also be a dictionary containing output options
           see: http://docs.mongodb.org/manual/reference/command/mapReduce/#dbcmd.mapReduce
        :param finalize_f: finalize function, an optional function that
                           performs any post-reduction processing.
        :param scope: values to insert into map/reduce global scope. Optional.
        :param limit: number of objects from current query to provide
                      to map/reduce method.
        :param get_out_docs: Indicates if should return the documents from
          the map reduce. Useful if your return is very big and you are
          setting the output to a collection.

        Returns a list of :class:`~mongoengine.document.MapReduceDocument`.

        """
        queryset = self.clone()

        MapReduceDocument = _import_class('MapReduceDocument')

        if not hasattr(self._collection, "map_reduce"):
            raise NotImplementedError("Requires MongoDB >= 1.7.1")

        map_f_scope = {}
        if isinstance(map_f, Code):
            map_f_scope = map_f.scope
            map_f = str(map_f)
        map_f = Code(queryset._sub_js_fields(map_f), map_f_scope)

        reduce_f_scope = {}
        if isinstance(reduce_f, Code):
            reduce_f_scope = reduce_f.scope
            reduce_f = str(reduce_f)
        reduce_f_code = queryset._sub_js_fields(reduce_f)
        reduce_f = Code(reduce_f_code, reduce_f_scope)

        mr_args = {'query': (yield queryset._query)}

        if finalize_f:
            finalize_f_scope = {}
            if isinstance(finalize_f, Code):
                finalize_f_scope = finalize_f.scope
                finalize_f = str(finalize_f)
            finalize_f_code = queryset._sub_js_fields(finalize_f)
            finalize_f = Code(finalize_f_code, finalize_f_scope)
            mr_args['finalize'] = finalize_f

        if scope:
            mr_args['scope'] = scope

        if limit:
            mr_args['limit'] = limit

        if output == 'inline' and not queryset._ordering:
            map_reduce_function = 'inline_map_reduce'
        else:
            map_reduce_function = 'map_reduce'
            mr_args['out'] = output

        results = getattr(queryset._collection, map_reduce_function)(
            map_f, reduce_f, **mr_args)

        results = yield results

        if not get_out_docs:
            return []

        if map_reduce_function == 'map_reduce':
            results = results.find()

        if queryset._ordering:
            results = results.sort(queryset._ordering)

        if map_reduce_function == 'map_reduce':
            rlist = []
            while (yield results.fetch_next):
                doc = results.next_object()
                rlist.append(doc)
            results = rlist

        if not get_out_docs:
            return []

        return [MapReduceDocument(queryset._document, queryset._collection,
                                  doc['_id'], doc['value']) for doc in results]
        # for doc in results:
        #     yield MapReduceDocument(queryset._document, queryset._collection,
        #                             doc['_id'], doc['value'])

    @gen.coroutine
    def exec_js(self, code, *fields, **options):
        """Execute a Javascript function on the server. A list of fields may be
        provided, which will be translated to their correct names and supplied
        as the arguments to the function. A few extra variables are added to
        the function's scope: ``collection``, which is the name of the
        collection in use; ``query``, which is an object representing the
        current query; and ``options``, which is an object containing any
        options specified as keyword arguments.

        As fields in MongoEngine may use different names in the database (set
        using the :attr:`db_field` keyword argument to a :class:`Field`
        constructor), a mechanism exists for replacing MongoEngine field names
        with the database field names in Javascript code. When accessing a
        field, use square-bracket notation, and prefix the MongoEngine field
        name with a tilde (~).

        :param code: a string of Javascript code to execute
        :param fields: fields that you will be using in your function, which
            will be passed in to your function as arguments
        :param options: options that you want available to the function
            (accessed in Javascript through the ``options`` object)
        """
        queryset = self.clone()

        code = queryset._sub_js_fields(code)

        fields = [queryset._document._translate_field_name(f) for f in fields]
        collection = queryset._document._get_collection_name()

        scope = {
            'collection': collection,
            'options': options or {},
        }

        query = yield queryset._query
        if queryset._where_clause:
            query['$where'] = queryset._where_clause

        scope['query'] = query
        code = Code(code, scope=scope)

        db = queryset._document._get_db()
        return (yield db.eval(code, *fields))

    @gen.coroutine
    def item_frequencies(self, field, normalize=False, map_reduce=True):
        """Returns a dictionary of all items present in a field across
        the whole queried set of documents, and their corresponding frequency.
        This is useful for generating tag clouds, or searching documents.

        .. note::

            Can only do direct simple mappings and cannot map across
            :class:`~mongoengine.fields.ReferenceField` or
            :class:`~mongoengine.fields.GenericReferenceField` for more complex
            counting a manual map reduce call would is required.

        If the field is a :class:`~mongoengine.fields.ListField`, the items within
        each list will be counted individually.

        :param field: the field to use
        :param normalize: normalize the results so they add to 1.0
        :param map_reduce: Use map_reduce over exec_js

        """
        if map_reduce:
            freq = yield self._item_frequencies_map_reduce(field,
                                                           normalize=normalize)
            return freq
        yield self._item_frequencies_exec_js(field, normalize=normalize)

    @gen.coroutine
    def in_bulk(self, object_ids):
        """Retrieve a set of documents by their ids.

        :param object_ids: a list or tuple of ``ObjectId``\ s
        :rtype: dict of ObjectIds as keys and collection-specific
                Document subclasses as values.

        """
        doc_map = {}

        docs = self._collection.find({'_id': {'$in': object_ids}},
                                     **self._cursor_args)
        if self._scalar:
            while (yield docs.fetch_next):
                doc = docs.next_object()
                doc_map[doc['_id']] = self._get_scalar(
                    self._document._from_son(doc))
        elif self._as_pymongo:
            while (yield docs.fetch_next):
                doc = docs.next_object()
                doc_map[doc['_id']] = self._get_as_pymongo(doc)
        else:
            while (yield docs.fetch_next):
                doc = docs.next_object()
                doc_map[doc['_id']] = self._document._from_son(doc)

        return doc_map

    @gen.coroutine
    def get(self, *q_objs, **query):
        """Retrieve the the matching object raising
        :class:`~mongoengine.queryset.MultipleObjectsReturned` or
        `DocumentName.MultipleObjectsReturned` exception if multiple results
        and :class:`~mongoengine.queryset.DoesNotExist` or
        `DocumentName.DoesNotExist` if no results are found.

        :param eager_on: Fields that must be dereferenced before returning
          the query result. Note that a new query is done to the database,
          it is only a convenience to not handle with futures while accessing
          references.
        """

        queryset = self.clone()
        queryset = yield queryset.limit(2)
        queryset = queryset.filter(*q_objs, **query)

        try:
            result = yield next(queryset)
        except StopIteration:
            msg = ("%s matching query does not exist."
                   % queryset._document._class_name)
            raise queryset._document.DoesNotExist(msg)

        if not result:
            msg = ("%s matching query does not exist."
                   % queryset._document._class_name)
            raise queryset._document.DoesNotExist(msg)

        try:
            n = yield next(queryset)
        except StopIteration:
            return result

        if not n:
            return result

        yield queryset.rewind()
        message = '%d items returned, instead of 1' % (yield queryset.count())
        raise queryset._document.MultipleObjectsReturned(message)

    @gen.coroutine
    def rewind(self):
        """Rewind the cursor to its unevaluated state.

        .. versionadded:: 0.3
        """
        self._iter = False
        cursor = yield self._cursor
        cursor.rewind()

    @gen.coroutine
    def delete(self, write_concern=None, _from_doc_delete=False):
        """Delete the documents matched by the query.

        :param write_concern: Extra keyword arguments are passed down which
            will be used as options for the resultant
            ``getLastError`` command.  For example,
            ``save(..., write_concern={w: 2, fsync: True}, ...)`` will
            wait until at least two servers have recorded the write and
            will force an fsync on the primary server.
        :param _from_doc_delete: True when called from document delete therefore
            signals will have been triggered so don't loop.
        """
        queryset = self.clone()
        doc = queryset._document

        if write_concern is None:
            write_concern = {}

        # Handle deletes where skips or limits have been applied or
        # there is an untriggered delete signal
        has_delete_signal = signals.signals_available and (
            signals.pre_delete.has_receivers_for(self._document) or
            signals.post_delete.has_receivers_for(self._document))

        call_document_delete = (queryset._skip or queryset._limit or
                                has_delete_signal) and not _from_doc_delete

        if call_document_delete:
            for d in queryset:
                doc = yield d
                if not doc:
                    continue

                yield doc.delete(write_concern=write_concern)
            return

        delete_rules = doc._meta.get('delete_rules') or {}
        # Check for DENY rules before actually deleting/nullifying any other
        # references
        for rule_entry in delete_rules:
            document_cls, field_name = rule_entry
            rule = doc._meta['delete_rules'][rule_entry]
            if rule == base.DENY and (yield document_cls.objects(
                    **{field_name + '__in': self}).count() > 0):
                msg = ("Could not delete document (%s.%s refers to it)"
                       % (document_cls.__name__, field_name))
                raise OperationError(msg)

        for rule_entry in delete_rules:
            document_cls, field_name = rule_entry
            rule = doc._meta['delete_rules'][rule_entry]
            if rule == base.CASCADE:
                ref_q = document_cls.objects(**{field_name + '__in': self})
                ref_q_count = yield ref_q.count()
                if (doc != document_cls and ref_q_count > 0
                        or (doc == document_cls and ref_q_count > 0)):
                    yield ref_q.delete(write_concern=write_concern)

            # Need to work on .update
            elif rule == base.NULLIFY:
                document_cls.objects(**{field_name + '__in': self}).update(
                    write_concern=write_concern, **{'unset__%s' % field_name: 1})
            elif rule == base.PULL:
                document_cls.objects(**{field_name + '__in': self}).update(
                    write_concern=write_concern,
                    **{'pull_all__%s' % field_name: self})

        yield queryset._collection.remove((yield queryset._query),
                                          write_concern=write_concern)

    @gen.coroutine
    def first(self):
        """Retrieve the first object matching the query.
        """
        queryset = self.clone()
        result = yield queryset[0]
        return result

    @gen.coroutine
    def insert(self, doc_or_docs, load_bulk=True, write_concern=None):
        """bulk insert documents

        :param docs_or_doc: a document or list of documents to be inserted
        :param load_bulk (optional): If True returns the list of document
            instances
        :param write_concern: Extra keyword arguments are passed down to
                :meth:`~pymongo.collection.Collection.insert`
                which will be used as options for the resultant
                ``getLastError`` command.  For example,
                ``insert(..., {w: 2, fsync: True})`` will wait until at least
                two servers have recorded the write and will force an fsync on
                each server being written to.

        By default returns document instances, set ``load_bulk`` to False to
        return just ``ObjectIds``

        .. versionadded:: 0.5
        """
        Document = _import_class('Document')

        if write_concern is None:
            write_concern = {}

        docs = doc_or_docs
        return_one = False
        if isinstance(docs, Document) or issubclass(docs.__class__, Document):
            return_one = True
            docs = [docs]

        raw = []
        for doc in docs:
            if not isinstance(doc, self._document):
                msg = ("Some documents inserted aren't instances of %s"
                       % str(self._document))
                raise OperationError(msg)
            if doc.pk and not doc._created:
                msg = "Some documents have ObjectIds use doc.update() instead"
                raise OperationError(msg)
            raw.append(doc.to_mongo())

        yield signals.pre_bulk_insert.send(self._document, documents=docs)
        try:
            ids = yield self._collection.insert(raw, **write_concern)
        except pymongo.errors.OperationFailure as err:
            message = 'Could not save document (%s)'
            if re.match('^E1100[01] duplicate key', str(err)):
                # E11000 - duplicate key error index
                # E11001 - duplicate key on update
                message = 'Tried to save duplicate unique keys (%s)'
                raise NotUniqueError(message % str(err))
            raise OperationError(message % str(err))

        if not load_bulk:
            yield signals.post_bulk_insert.send(
                self._document, documents=docs, loaded=False)
            return return_one and ids[0] or ids

        documents = yield self.in_bulk(ids)
        results = []
        for obj_id in ids:
            results.append(documents.get(obj_id))
        yield signals.post_bulk_insert.send(
            self._document, documents=results, loaded=True)
        return return_one and results[0] or results

    @gen.coroutine
    def update(self, upsert=False, multi=True, write_concern=None,
               full_result=False, **update):
        """Perform an atomic update on the fields matched by the query.

        :param upsert: Any existing document with that "_id" is overwritten.
        :param multi: Update multiple documents.
        :param write_concern: Extra keyword arguments are passed down which
            will be used as options for the resultant
            ``getLastError`` command.  For example,
            ``save(..., write_concern={w: 2, fsync: True}, ...)`` will
            wait until at least two servers have recorded the write and
            will force an fsync on the primary server.
        :param full_result: Return the full result rather than just the number
            updated.
        :param update: Django-style update keyword arguments

        .. versionadded:: 0.2
        """
        if not update and not upsert:
            raise OperationError("No update parameters, would remove data")

        if write_concern is None:
            write_concern = {}

        queryset = self.clone()
        query = yield queryset._query
        update = transform.update(queryset._document, **update)

        # If doing an atomic upsert on an inheritable class
        # then ensure we add _cls to the update operation
        if upsert and '_cls' in query:
            if '$set' in update:
                update["$set"]["_cls"] = queryset._document._class_name
            else:
                update["$set"] = {"_cls": queryset._document._class_name}
        try:
            result = yield queryset._collection.update(
                query, update, multi=multi, upsert=upsert, **write_concern)
            if full_result:
                return result
            elif result:
                return result['n']
        except pymongo.errors.DuplicateKeyError as err:
            raise NotUniqueError('Update failed (%s)' % str(err))
        except pymongo.errors.OperationFailure as err:
            if str(err) == 'multi not coded yet':
                message = 'update() method requires MongoDB 1.1.3+'
                raise OperationError(message)
            raise OperationError('Update failed (%s)' % str(err))

    @gen.coroutine
    def update_one(self, upsert=False, write_concern=None, **update):
        """Perform an atomic update on first field matched by the query.

        :param upsert: Any existing document with that "_id" is overwritten.
        :param write_concern: Extra keyword arguments are passed down which
            will be used as options for the resultant
            ``getLastError`` command.  For example,
            ``save(..., write_concern={w: 2, fsync: True}, ...)`` will
            wait until at least two servers have recorded the write and
            will force an fsync on the primary server.
        :param update: Django-style update keyword arguments

        .. versionadded:: 0.2
        """
        result = yield self.update(
            upsert=upsert, multi=False, write_concern=write_concern, **update)
        return result

    @gen.coroutine
    def modify(self, upsert=False, full_response=False, remove=False,
               new=False, **update):
        """Update and return the updated document.

        Returns either the document before or after modification based on `new`
        parameter. If no documents match the query and `upsert` is false,
        returns ``None``. If upserting and `new` is false, returns ``None``.

        If the full_response parameter is ``True``, the return value will be
        the entire response object from the server, including the 'ok' and
        'lastErrorObject' fields, rather than just the modified document.
        This is useful mainly because the 'lastErrorObject' document holds
        information about the command's execution.

        :param upsert: insert if document doesn't exist (default ``False``)
        :param full_response: return the entire response object from the
            server (default ``False``, not available for PyMongo 3+)
        :param remove: remove rather than updating (default ``False``)
        :param new: return updated rather than original document
            (default ``False``)
        :param update: Django-style update keyword arguments

        """

        if remove and new:
            raise OperationError("Conflicting parameters: remove and new")

        if not update and not upsert and not remove:
            raise OperationError(
                "No update parameters, must either update or remove")

        queryset = self.clone()
        query = yield queryset._query
        sort = queryset._ordering

        try:
            result = yield queryset._collection.find_and_modify(
                query, update, upsert=upsert, sort=sort, remove=remove,
                new=new, full_response=full_response, **self._cursor_args)
        except pymongo.errors.DuplicateKeyError as err:
            raise NotUniqueError("Update failed (%s)" % err)
        except pymongo.errors.OperationFailure as err:
            raise OperationError("Update failed (%s)" % err)

        if full_response:
            if result["value"] is not None:
                result["value"] = self._document._from_son(
                    result["value"], only_fields=self.only_fields)
        else:
            if result is not None:
                result = self._document._from_son(
                    result, only_fields=self.only_fields)

        return result

    @gen.coroutine
    def limit(self, n):
        """Limit the number of returned documents to `n`. This may also be
        achieved using array-slicing syntax (e.g. ``User.objects[:5]``).

        :param n: the maximum number of objects to return
        """
        queryset = self.clone()
        cursor = yield queryset._cursor
        if n == 0:
            cursor.limit(1)
        else:
            cursor.limit(n)
        queryset._limit = n
        # Return self to allow chaining
        return queryset

    @gen.coroutine
    def skip(self, n):
        """Skip `n` documents before returning the results. This may also be
        achieved using array-slicing syntax (e.g. ``User.objects[5:]``).

        :param n: the number of objects to skip before returning results
        """
        queryset = self.clone()
        cursor = yield queryset._cursor
        cursor.skip(n)
        queryset._skip = n
        return queryset

    @gen.coroutine
    def distinct(self, field):
        """Return a list of distinct values for a given field.

        :param field: the field to select distinct values from

        .. note:: This is a command and won't take ordering or limit into
           account.

        .. versionadded:: 0.4
        .. versionchanged:: 0.5 - Fixed handling references
        .. versionchanged:: 0.6 - Improved db_field refrence handling
        """
        queryset = self.clone()
        try:
            field = self._fields_to_dbfields([field]).pop()
        finally:
            cursor = yield queryset._cursor
            distinct = yield cursor.distinct(field)
            distinct = yield self._dereference(distinct, 1,
                                               name=field,
                                               instance=self._document)

            # We may need to cast to the correct type eg.
            # ListField(EmbeddedDocumentField)
            doc_field = getattr(
                self._document._fields.get(field), "field", None)
            instance = getattr(doc_field, "document_type", False)
            if instance:
                distinct = [instance(**doc) for doc in distinct]
            return distinct

    @gen.coroutine
    def __next__(self):
        """Wrap the result in a :class:`~mongoengine.Document` object.
        """

        if not hasattr(self, '_next_doc'):
            self._next_doc = yield self._get_next_doc()

        raw_doc = self._next_doc

        if self._limit == 0 or self._none or not raw_doc:
            raise StopIteration

        if self._as_pymongo:
            doc = self._get_as_pymongo(raw_doc)
            self._next_doc = yield self._get_next_doc()
            return doc

        doc = self._document._from_son(
            raw_doc, _auto_dereference=self._auto_dereference)

        if self._scalar:
            doc = self._get_scalar(doc)
            self._next_doc = yield self._get_next_doc()
            return doc

        self._next_doc = yield self._get_next_doc()
        # doc = yield self._consume_references_futures(doc)
        return doc

    @gen.coroutine
    def __getitem__(self, key):
        """Support skip and limit using getitem and slicing syntax.
        """
        queryset = self.clone()

        # Slice provided
        if isinstance(key, slice):
            try:
                cursor = yield queryset._cursor
                limit = key.stop
                if key.start and key.stop:
                    limit = key.stop - key.start
                queryset._cursor_obj = cursor.skip(key.start).limit(limit)
                queryset._skip, queryset._limit = key.start, limit
            except IndexError as err:
                # PyMongo raises an error if key.start == key.stop, catch it,
                # bin it, kill it.
                start = key.start or 0
                if start >= 0 and key.stop >= 0 and key.step is None:
                    if start == key.stop:
                        queryset.limit(0)
                        queryset._skip = key.start
                        queryset._limit = key.stop - start
                        return queryset
                raise err
            # Allow further QuerySet modifications to be performed
            return queryset
        # Integer index provided
        elif isinstance(key, int):
            new_cursor = yield queryset._cursor
            skip = queryset._skip or 0
            new_cursor = new_cursor.skip(skip + key).limit(-1)
            yield new_cursor.fetch_next
            raw_doc = new_cursor.next_object()
            if not raw_doc:
                return raw_doc
            doc = queryset._document._from_son(
                raw_doc, _auto_dereference=self._auto_dereference)

            # doc = yield self._consume_references_futures(doc)

            if queryset._scalar:
                return queryset._get_scalar(doc)
            if queryset._as_pymongo:
                n = yield next(queryset._cursor)
                return queryset._get_as_pymongo(n)

            # son = queryset._document._from_son(
            #     raw_doc,
            #     _auto_dereference=self._auto_dereference)

            return doc
        raise AttributeError

    @gen.coroutine
    def _consume_references_futures(self, doc, fields):

        for field in fields:
            attr = getattr(doc, field)
            if isinstance(attr, tornado.concurrent.Future):
                attr = yield attr
                setattr(doc, field, attr)

        return doc

    @gen.coroutine
    def _get_next_doc(self):
        cursor = yield self._cursor
        yield cursor.fetch_next
        n = cursor.next_object()
        return n

    @property
    def _cursor_args(self):
        # Motor does not support slave_okay
        r = super()._cursor_args
        try:
            del r['slave_okay']
        except KeyError:
            pass
        return r

    @property
    @gen.coroutine
    def _cursor(self):
        if self._cursor_obj is None:
            self._cursor_obj = self._collection.find((yield self._query),
                                                     **self._cursor_args)
            # Apply where clauses to cursor
            if self._where_clause:
                where_clause = self._sub_js_fields(self._where_clause)
                self._cursor_obj.where(where_clause)

            if self._ordering:
                # Apply query ordering
                self._cursor_obj.sort(self._ordering)
            elif self._document._meta['ordering']:
                # Otherwise, apply the ordering from the document model
                order = self._get_order_by(self._document._meta['ordering'])
                self._cursor_obj.sort(order)

            if self._limit is not None:
                self._cursor_obj.limit(self._limit)

            if self._skip is not None:
                self._cursor_obj.skip(self._skip)

            if self._hint != -1:
                self._cursor_obj.hint(self._hint)

        return self._cursor_obj

    @property
    @gen.coroutine
    def _query(self):
        if self._mongo_query is None:
            self._mongo_query = yield self._query_obj.to_query(self._document)
            if self._class_check:
                self._mongo_query.update(self._initial_query)
        return self._mongo_query

    @gen.coroutine
    def count(self, with_limit_and_skip=True):
        """Count the selected elements in the query.

        :param with_limit_and_skip (optional): take any :meth:`limit` or
        :meth:`skip` that has been applied to this cursor into account when
        getting the count
        """
        if self._limit == 0 and with_limit_and_skip or self._none:
            return 0
        cursor = yield self._cursor
        n = yield cursor.count(with_limit_and_skip=with_limit_and_skip)
        return n

    @gen.coroutine
    def average(self, field):
        """Average over the values of the specified field.

        :param field: the field to average over; use dot-notation to refer to
            embedded document fields

        """
        map_func = """
            function() {
                var path = '{{~%(field)s}}'.split('.'),
                field = this;

                for (p in path) {
                    if (typeof field != 'undefined')
                       field = field[path[p]];
                    else
                       break;
                }

                if (field && field.constructor == Array) {
                    field.forEach(function(item) {
                        emit(1, {t: item||0, c: 1});
                    });
                } else if (typeof field != 'undefined') {
                    emit(1, {t: field||0, c: 1});
                }
            }
        """ % dict(field=field)

        reduce_func = Code("""
            function(key, values) {
                var out = {t: 0, c: 0};
                for (var i in values) {
                    var value = values[i];
                    out.t += value.t;
                    out.c += value.c;
                }
                return out;
            }
        """)

        finalize_func = Code("""
            function(key, value) {
                return value.t / value.c;
            }
        """)

        results = yield self.map_reduce(
            map_func, reduce_func,
            finalize_f=finalize_func, output='inline')
        for result in results:
            return result.value
        else:
            return 0

    @gen.coroutine
    def aggregate_average(self, field):
        """Average over the values of the specified field.

        :param field: the field to average over; use dot-notation to refer to
            embedded document fields

        This method is more performant than the regular `average`, because it
        uses the aggregation framework instead of map-reduce.
        """
        result = self._document._get_collection().aggregate([
            {'$match': (yield self._query)},
            {'$group': {'_id': 'avg', 'total': {'$avg': '$' + field}}}
        ])
        result = yield result.to_list(1)
        if result:
            return result[0]['total']
        return 0

    @gen.coroutine
    def sum(self, field):
        """Sum over the values of the specified field.

        :param field: the field to sum over; use dot-notation to refer to
            embedded document fields

        .. versionchanged:: 0.5 - updated to map_reduce as db.eval doesnt work
            with sharding.
        """
        map_func = """
            function() {
                var path = '{{~%(field)s}}'.split('.'),
                field = this;

                for (p in path) {
                    if (typeof field != 'undefined')
                       field = field[path[p]];
                    else
                       break;
                }

                if (field && field.constructor == Array) {
                    field.forEach(function(item) {
                        emit(1, item||0);
                    });
                } else if (typeof field != 'undefined') {
                    emit(1, field||0);
                }
            }
        """ % dict(field=field)

        reduce_func = Code("""
            function(key, values) {
                var sum = 0;
                for (var i in values) {
                    sum += values[i];
                }
                return sum;
            }
        """)

        results = yield self.map_reduce(map_func, reduce_func, output='inline')
        for result in results:
            return result.value
        else:
            return 0

    @gen.coroutine
    def aggregate_sum(self, field):
        """Sum over the values of the specified field.

        :param field: the field to sum over; use dot-notation to refer to
            embedded document fields

        This method is more performant than the regular `sum`, because it uses
        the aggregation framework instead of map-reduce.
        """
        result = self._document._get_collection().aggregate([
            {'$match': (yield self._query)},
            {'$group': {'_id': 'sum', 'total': {'$sum': '$' + field}}}
        ])
        result = yield result.to_list(1)
        if result:
            return result[0]['total']
        return 0

    @gen.coroutine
    def _item_frequencies_map_reduce(self, field, normalize=False):
        map_func = """
            function() {
                var path = '{{~%(field)s}}'.split('.');
                var field = this;

                for (p in path) {
                    if (typeof field != 'undefined')
                       field = field[path[p]];
                    else
                       break;
                }
                if (field && field.constructor == Array) {
                    field.forEach(function(item) {
                        emit(item, 1);
                    });
                } else if (typeof field != 'undefined') {
                    emit(field, 1);
                } else {
                    emit(null, 1);
                }
            }
        """ % dict(field=field)
        reduce_func = """
            function(key, values) {
                var total = 0;
                var valuesSize = values.length;
                for (var i=0; i < valuesSize; i++) {
                    total += parseInt(values[i], 10);
                }
                return total;
            }
        """
        values = yield self.map_reduce(map_func, reduce_func, 'inline')
        frequencies = {}
        for f in values:
            key = f.key
            if isinstance(key, float):
                if int(key) == key:
                    key = int(key)
            frequencies[key] = int(f.value)

        if normalize:
            count = sum(frequencies.values())
            frequencies = dict([(k, float(v) / count)
                                for k, v in list(frequencies.items())])

        return frequencies
