import logging
from ..query import Query, WhereNode

from django.core.exceptions import FieldError
from django.db.models.fields import FieldDoesNotExist
from django.db import NotSupportedError
from django.db.models.expressions import Star
from django.db.models.sql.datastructures import EmptyResultSet
from django.db.models.query import QuerySet

try:
    from django.db.models.query import FlatValuesListIterable
except ImportError:
    # Django 1.8
    class FlatValuesListIterable(object):
        pass

try:
    from django.db.models.query import ValuesListQuerySet
except ImportError:
    # Django >= 1.9
    class ValuesListQuerySet(object):
        pass

from djangae import environment

from djangae.db.utils import get_top_concrete_parent


logger = logging.getLogger(__name__)


INVALID_ORDERING_FIELD_MESSAGE = (
    "Ordering on TextField or BinaryField is not supported on the datastore. "
    "You might consider using a ComputedCharField which stores the first "
    "_MAX_STRING_LENGTH (from google.appengine.api.datastore_types) bytes of the "
    "field and instead order on that."
)


def _get_concrete_fields_with_model(model):
    return [
        (f, f.model if f.model != model else None)
        for f in model._meta.get_fields()
        if f.concrete and (
            not f.is_relation
            or f.one_to_one
            or (f.many_to_one and f.related_model)
        )
    ]


def _walk_django_where(query, trunk_callback, leaf_callback, **kwargs):
    """
        Walks through a Django where tree. If a leaf node is encountered
        the leaf_callback is called, otherwise the trunk_callback is called
    """

    def walk_node(node, **kwargs):
        negated = kwargs["negated"]

        if node.negated:
            negated = not negated

        for child in node.children:
            new_kwargs = kwargs.copy()
            new_kwargs["negated"] = negated
            if not getattr(child, "children", []):
                leaf_callback(child, **new_kwargs)
            else:
                new_parent = trunk_callback(child, **new_kwargs)

                if new_parent:
                    new_kwargs["new_parent"] = new_parent

                walk_node(child, **new_kwargs)

    kwargs.setdefault("negated", False)
    walk_node(query.where, **kwargs)


class BaseParser(object):
    """
        Supports the most recent version of Django. Subclasses can
        override parts of this to support older versions.

        (NOTE: this goes against all my thoughts on good OO but I can't think
        of a cleaner way of doing things at the moment!)
    """

    def __init__(self, django_query, connection=None):
        self.django_query = django_query
        self.connection = connection
        self.model = self.django_query.model

    def _determine_query_kind(self):
        """ Basically returns SELECT or COUNT """
        query = self.django_query

        if query.annotations:
            if "__count" in query.annotations:
                field = query.annotations["__count"].source_expressions[0]
                if isinstance(field, Star) or field.value == "*":
                    return "COUNT"

        return "SELECT"

    def _prepare_for_transformation(self):
        from django.db.models.sql.where import NothingNode, WhereNode as DjangoWhereNode
        query = self.django_query
        # It could either be a NothingNode, or a WhereNode(AND NothingNode)
        if (
                isinstance(query.where, NothingNode) or (
                isinstance(query.where, DjangoWhereNode) and
                len(query.where.children) == 1 and
                isinstance(query.where.children[0], NothingNode)
                )):
            # Empty where means return nothing!
            raise EmptyResultSet()

    def _extract_projected_columns(self):
        result = []
        query = self.django_query

        if query.select:
            for x in query.select:
                column = x.target.column
                result.append(column)
            return result
        else:
            # If the query uses defer()/only() then we need to process deferred. We have to get all deferred columns
            # for all (concrete) inherited models and then only include columns if they appear in that list
            only_load = query.get_loaded_field_names()
            if only_load:
                for field, model in _get_concrete_fields_with_model(query.model):
                    model = model or query.model
                    try:
                        if field.column in only_load[model]: