# The MIT License
#
# Copyright (c) 2010 Jeffrey Jenkins
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

from mongoalchemy.exceptions import BadValueException, BadQueryException
import re

# No better way to do this
RE_TYPE = type(re.compile(''))

class FreeFormField(object):
    has_subfields = True
    no_real_attributes = True
    def __init__(self, name=None):
        self.__name = name
        self.db_field = name
    def __getattr__(self, name):
        return FreeFormField(name=name)
    def __getitem__(self, name):
        return getattr(self, name)
    @classmethod
    def wrap_value(self, value):
        return value
    def subfields(self):
        return FreeFormField(name=None)
    def is_valid_wrap(*args):
        return True
    is_valid_unwrap = is_valid_wrap
    __contains__ = is_valid_wrap

class FreeFormDoc(object):
    def __init__(self, name):
        self.__name = name
    def __getattr__(self, name):
        return QueryField(FreeFormField(name))
    @classmethod
    def unwrap(cls, value, *args, **kwargs):
        return value
    def get_collection_name(self):
        return self.__name
    def get_indexes(self):
        return []
    mongo_id = FreeFormField(name='_id')

Q = FreeFormDoc('')

class QueryField(object):
    def __init__(self, type, parent=None):
        self.__type = type
        self.__parent = parent
        self.__cached_id_value = None
        self.__matched_index = False

    @property
    def __cached_id(self):
        if self.__cached_id_value == None:
            self.__cached_id_value = str(self)
        return self.__cached_id_value

    def _get_parent(self):
        return self.__parent

    def get_type(self):
        ''' Returns the underlying :class:`mongoalchemy.fields.Field` '''
        return self.__type

    def matched_index(self):
        ''' Represents the matched array index on a query with objects inside
            of a list.  In the MongoDB docs, this is the ``$`` operator '''
        self.__matched_index = True
        return self

    def __getattr__(self, name):
        if not self.__type.no_real_attributes and hasattr(self.__type, name):
            return getattr(self.__type, name)

        if not self.__type.has_subfields:
            raise AttributeError(name)

        fields = self.__type.subfields()
        if name not in fields:
            raise BadQueryException('%s is not a field in %s' % (name, self.__type.sub_type()))
        return QueryField(fields[name], parent=self)

    def get_absolute_name(self):
        res = []
        current = self

        while type(current) != type(None):
            if current.__matched_index:
                res.append('$')
            res.append(current.get_type().db_field)
            current = current._get_parent()
        return '.'.join(reversed(res))

    def in_(self, *values):
        ''' A query to check if this query field is one of the values
            in ``values``.  Produces a MongoDB ``$in`` expression.
        '''
        return QueryExpression({
            self : { '$in' : [self.get_type().wrap_value(value) for value in values] }
        })

    def like(self, value):
        ''' Check to see that the value of ``qfield`` is LIKE ``value``

            :param value: Value should be a string containing at least one '%' operator 
        '''
        return QueryExpression({ self : re.compile(value.replace('%', '.*') ) } )

    def nin(self, *values):
        ''' A query to check if this query field is not one of the values
            in ``values``.  Produces a MongoDB ``$nin`` expression.
        '''
        return QueryExpression({
            self : { '$nin' : [self.get_type().wrap_value(value) for value in values] }
        })

    def __str__(self):
        return self.get_absolute_name()

    def __repr__(self):
        return 'QueryField(%s)' % str(self)

    def __hash__(self):
        return hash(self.__cached_id)

    def __eq__(self, value):
        return self.eq_(value)
    def eq_(self, value):
        ''' Creates a query expression where ``this field == value``

            .. note:: The prefered usage is via an operator: ``User.name == value``
        '''
        if isinstance(value, QueryField):
            return self.__cached_id == value.__cached_id
        return QueryExpression({ self : self.get_type().wrap_value(value) })

    def __lt__(self, value):
        return self.lt_(value)
    def lt_(self, value):
        ''' Creates a query expression where ``this field < value``

            .. note:: The prefered usage is via an operator: ``User.name < value``
        '''
        return self.__comparator('$lt', value)

    def __le__(self, value):
        return self.le_(value)
    def le_(self, value):
        ''' Creates a query expression where ``this field <= value``

            .. note:: The prefered usage is via an operator: ``User.name <= value``
        '''
        return self.__comparator('$lte', value)

    def __ne__(self, value):
        return self.ne_(value)
    def ne_(self, value):
        ''' Creates a query expression where ``this field != value``

            .. note:: The prefered usage is via an operator: ``User.name != value``
        '''
        if isinstance(value, QueryField):
            return self.__cached_id != value.__cached_id
        return self.__comparator('$ne', value)

    def __gt__(self, value):
        return self.gt_(value)
    def gt_(self, value):
        ''' Creates a query expression where ``this field > value``

            .. note:: The prefered usage is via an operator: ``User.name > value``
        '''
        return self.__comparator('$gt', value)

    def __ge__(self, value):
        return self.ge_(value)
    def ge_(self, value):
        ''' Creates a query expression where ``this field >= value``

            .. note:: The prefered usage is via an operator: ``User.name >= value``
        '''
        return self.__comparator('$gte', value)

    def __comparator(self, op, value):
        return QueryExpression({
            self : {
                op : self.get_type().wrap(value)
            }
        })


class QueryExpression(object):
    ''' A QueryExpression wraps a dictionary representing a query to perform
        on a mongo collection.  The

        .. note:: Multiple expressions can be specified to a single 
            call of :func:`Query.filter`
    '''
    def __init__(self, obj):
        self.obj = obj
    def not_(self):
        ''' Negates this instance's query expression using MongoDB's ``$not``
            operator

            **Example**: ``(User.name == 'Jeff').not_()``

            .. note:: Another usage is via an operator, but parens are needed
                to get past precedence issues: ``~ (User.name == 'Jeff')``
            '''
        ret_obj = {}
        for k, v in self.obj.iteritems():
            if not isinstance(v, dict):
                if type(v) == RE_TYPE:
                    ret_obj[k] = {'$not' : v } # $ne
                else:
                    ret_obj[k] = {'$ne' : v }
                continue
            num_ops = len([x for x in v if x[0] == '$'])
            if num_ops != len(v) and num_ops != 0:
                raise BadQueryException('$ operator used in field name')

            if num_ops == 0:
                ret_obj[k] = {'$ne' : v }
                continue

            for op, value in v.iteritems():
                k_dict = ret_obj.setdefault(k, {})
                not_dict = k_dict.setdefault('$not', {})
                not_dict[op] = value


        return QueryExpression(ret_obj)

    def __invert__(self):
        return self.not_()

    def __and__(self, expression):
        return self.and_(expression)

    def and_(self, expression):
        ''' Adds the given expression to this instance's MongoDB ``$and``
            expression, starting a new one if one does not exst

            **Example**: ``(User.name == 'Jeff').and_(User.active == 1)``

            .. note:: The prefered usageis via an operator: ``User.name == 'Jeff' & User.active == 1``

            '''

        if '$and' in self.obj:
            self.obj['$and'].append(expression.obj)
            return self
        self.obj = {
            '$and' : [self.obj, expression.obj]
        }
        return self

    def __or__(self, expression):
        return self.or_(expression)

    def or_(self, expression):
        ''' Adds the given expression to this instance's MongoDB ``$or``
            expression, starting a new one if one does not exst

            **Example**: ``(User.name == 'Jeff').or_(User.name == 'Jack')``

            .. note:: The prefered usageis via an operator: ``User.name == 'Jeff' | User.name == 'Jack'``

            '''

        if '$or' in self.obj:
            self.obj['$or'].append(expression.obj)
            return self
        self.obj = {
            '$or' : [self.obj, expression.obj]
        }
        return self


def flatten(obj):
    if not isinstance(obj, dict):
        return obj
    ret = {}
    for k, v in obj.iteritems():
        if not isinstance(k, basestring):
            k = str(k)
        if isinstance(v, dict):
            v = flatten(v)
        if isinstance(v, list):
            v = [flatten(x) for x in v]
        ret[k] = v
    return ret

