class Query(object):
    INEQ = ('<=', '>=') # TODO support < and >
    EQ = ('=',)
    OPS = INEQ + EQ

    MIN_DEFAULT = None
    MAX_DEFAULT = {}

    def __init__(self):
        self.preconditions = []
        self.key_props = [None]
        self.min_key = []
        self.max_key = []

    def require(self, precondition):
        self.preconditions.append(precondition)

    def filter(self, spec, value):
        prop, op = spec.split()
        if op == '<=':
            if self.key_props[-1] == prop and self.max_key[-1] is Query.MAX_DEFAULT:
                self.max_key[-1] = value
            else:
                self.key_props.append(prop)
                self.min_key.append(Query.MIN_DEFAULT)
                self.max_key.append(value)
        elif op == '>=' :
            if self.key_props[-1] == prop and self.min_key[-1] is Query.MIN_DEFAULT:
                self.min_key[-1] = value
            else:
                self.key_props.append(prop)
                self.min_key.append(value)
                self.max_key.append(Query.MAX_DEFAULT)
        elif op == '=':
            self.key_props.append(prop)
            self.min_key.append(value)
            self.max_key.append(value)
        return self

    def run(self, db):
        return self._run(db)[self.min_key:self.max_key]

    def _run(self, db):
        return db.query(self._gen_view())

    def _gen_view(self):
        return ('function(doc) {'
            + self._gen_precondition()
            + 'emit(['
            + ', '.join('doc.' + prop for prop in self.key_props[1:])
            + '], doc); }')

    def _gen_precondition(self):
        if not self.preconditions:
            return ''
        conditions = '&&'.join(('(%s)' % pc) for pc in self.preconditions)
        return 'if (!(%s)) return;' % conditions


class SchemaQuery(Query):
    def __init__(self, type_):
        super(SchemaQuery, self).__init__()
        self.type = type_
        self.require('doc.type == "%s"' % type_.__name__)

    def _run(self, db):
        return self.type.query(db, self._gen_view(), None)


class CachedQueryMixin(object):
    def store(self, db):
        doc = db.get(self._design_doc())
        if doc is None:
            doc = {'language': 'javascript', 'views': {}}
        doc['views'][self._view_func()] = {
            'language': 'javascript',
            'map': self._gen_view(),
        }
        db[self._design_doc()] = doc

    def exists_in(self, db):
        try:
            doc = db[self._design_doc()]
        except KeyError:
            return False
        return self._view_func() in doc['views']

    def _run(self, db):
        return db.view(self._view_doc())

    def _design_doc(self):
        return '_design/' + self._view_ns()

    def _view_doc(self):
        return '_view/' + self._view_name()

    def _view_name(self):
        return self._view_ns() + '/' + self._view_func()

    def _view_ns(self):
        return 'saved_queries'

    def _view_func(self):
        if len(self.key_props) == 1:
            return 'all'
        return 'by_' + '_'.join(self.key_props[1:])


class CachedSchemaQuery(SchemaQuery, CachedQueryMixin):
    def _run(self, db):
        return self.type.view(db, self._view_doc())

    def _view_ns(self):
        return self.type.__name__


