# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#

import operator
import types

from sqlalchemy import and_
from sqlalchemy import asc
from sqlalchemy import desc
from sqlalchemy import not_
from sqlalchemy import or_
from sqlalchemy.orm import aliased

import ceilometer
from ceilometer.storage.sqlalchemy import models


META_TYPE_MAP = {bool: models.MetaBool,
                 str: models.MetaText,
                 unicode: models.MetaText,
                 types.NoneType: models.MetaText,
                 int: models.MetaBigInt,
                 long: models.MetaBigInt,
                 float: models.MetaFloat}


class QueryTransformer(object):
    operators = {"=": operator.eq,
                 "<": operator.lt,
                 ">": operator.gt,
                 "<=": operator.le,
                 "=<": operator.le,
                 ">=": operator.ge,
                 "=>": operator.ge,
                 "!=": operator.ne,
                 "in": lambda field_name, values: field_name.in_(values),
                 "=~": lambda field, value: field.op("regexp")(value)}

    # operators which are differs for different dialects
    dialect_operators = {'postgresql': {'=~': (lambda field, value:
                                               field.op("~")(value))}}

    complex_operators = {"or": or_,
                         "and": and_,
                         "not": not_}

    ordering_functions = {"asc": asc,
                          "desc": desc}

    def __init__(self, table, query, dialect='mysql'):
        self.table = table
        self.query = query
        self.dialect_name = dialect

    def _get_operator(self, op):
        return (self.dialect_operators.get(self.dialect_name, {}).get(op)
                or self.operators[op])

    def _handle_complex_op(self, complex_op, nodes):
        op = self.complex_operators[complex_op]
        if op == not_:
            nodes = [nodes]
        element_list = []
        for node in nodes:
            element = self._transform(node)
            element_list.append(element)
        return op(*element_list)

    def _handle_simple_op(self, simple_op, nodes):
        op = self._get_operator(simple_op)
        field_name = nodes.keys()[0]
        value = nodes.values()[0]
        if field_name.startswith('resource_metadata.'):
            return self._handle_metadata(op, field_name, value)
        else:
            return op(getattr(self.table, field_name), value)

    def _handle_metadata(self, op, field_name, value):
        if op == self.operators["in"]:
            raise ceilometer.NotImplementedError('Metadata query with in '
                                                 'operator is not implemented')
        field_name = field_name[len('resource_metadata.'):]
        meta_table = META_TYPE_MAP[type(value)]
        meta_alias = aliased(meta_table)
        on_clause = and_(self.table.internal_id == meta_alias.id,
                         meta_alias.meta_key == field_name)
        # outer join is needed to support metaquery
        # with or operator on non existent metadata field
        # see: test_query_non_existing_metadata_with_result
        # test case.
        self.query = self.query.outerjoin(meta_alias, on_clause)
        return op(meta_alias.value, value)

    def _transform(self, sub_tree):
        operator = sub_tree.keys()[0]
        nodes = sub_tree.values()[0]
        if operator in self.complex_operators:
            return self._handle_complex_op(operator, nodes)
        else:
            return self._handle_simple_op(operator, nodes)

    def apply_filter(self, expression_tree):
        condition = self._transform(expression_tree)
        self.query = self.query.filter(condition)

    def apply_options(self, orderby, limit):
        self._apply_order_by(orderby)
        if limit is not None:
            self.query = self.query.limit(limit)

    def _apply_order_by(self, orderby):
        if orderby is not None:
            for field in orderby:
                ordering_function = self.ordering_functions[field.values()[0]]
                self.query = self.query.order_by(ordering_function(
                    getattr(self.table, field.keys()[0])))
        else:
            self.query = self.query.order_by(desc(self.table.timestamp))

    def get_query(self):
        return self.query
