import collections
import functools

from ..error import GraphQLError
from ..language import ast
from ..language.parser import parse
from ..language.source import Source
from ..pyutils.default_ordered_dict import DefaultOrderedDict
from ..pyutils.defer import (Deferred, DeferredDict, DeferredList, defer,
                             succeed)
from ..type import (GraphQLEnumType, GraphQLInterfaceType, GraphQLList,
                    GraphQLNonNull, GraphQLObjectType, GraphQLScalarType,
                    GraphQLUnionType)
from ..validation import validate
from .base import (ExecutionContext, ExecutionResult, ResolveInfo, Undefined,
                   collect_fields, default_resolve_fn, get_field_def,
                   get_operation_root_type)


class Executor(object):

    def __init__(self, execution_middlewares=None, default_resolver=default_resolve_fn, map_type=dict):
        assert issubclass(map_type, collections.MutableMapping)

        self._execution_middlewares = execution_middlewares or []
        self._default_resolve_fn = default_resolver
        self._map_type = map_type
        self._enforce_strict_ordering = issubclass(map_type, collections.OrderedDict)

    @property
    def enforce_strict_ordering(self):
        return self._enforce_strict_ordering

    @property
    def map_type(self):
        return self._map_type

    def execute(self, schema, request='', root=None, args=None, operation_name=None, request_context=None,
                execute_serially=False, validate_ast=True):

        curried_execution_function = functools.partial(
            self._execute,
            schema,
            request,
            root,
            args,
            operation_name,
            request_context,
            execute_serially,
            validate_ast
        )

        for middleware in self._execution_middlewares:
            if hasattr(middleware, 'execution_result'):
                curried_execution_function = functools.partial(middleware.execution_result, curried_execution_function)

        return curried_execution_function()

    def _execute(self, schema, request, root, args, operation_name, request_context, execute_serially, validate_ast):
        if not isinstance(request, ast.Document):
            if not isinstance(request, Source):
                request = Source(request, 'GraphQL request')

            request = parse(request)

        if validate_ast:
            validation_errors = validate(schema, request)
            if validation_errors:
                return succeed(ExecutionResult(
                    errors=validation_errors,
                    invalid=True,
                ))

        return self._execute_graphql_query(
            schema,
            root or object(),
            request,
            operation_name,
            args or {},
            request_context or {},
            execute_serially)

    def _execute_graphql_query(self, schema, root, ast, operation_name, args, request_context, execute_serially=False):
        ctx = ExecutionContext(schema, root, ast, operation_name, args, request_context)

        return defer(self._execute_operation, ctx, root, ctx.operation, execute_serially) \
            .add_errback(
            lambda error: ctx.errors.append(error)
        ) \
            .add_callback(
            lambda data: ExecutionResult(data, ctx.errors),
        )

    def _execute_operation(self, ctx, root, operation, execute_serially):
        type = get_operation_root_type(ctx.schema, operation)

        if operation.operation == 'mutation' or execute_serially:
            execute_serially = True

        fields = DefaultOrderedDict(list) \
            if (execute_serially or self._enforce_strict_ordering) \
            else collections.defaultdict(list)

        fields = collect_fields(ctx, type, operation.selection_set, fields, set())

        if execute_serially:
            return self._execute_fields_serially(ctx, type, root, fields)

        return self._execute_fields(ctx, type, root, fields)

    def _execute_fields_serially(self, execution_context, parent_type, source_value, fields):
        def execute_field_callback(results, response_name):
            field_asts = fields[response_name]
            result = self._resolve_field(execution_context, parent_type, source_value, field_asts)
            if result is Undefined:
                return results

            def collect_result(resolved_result):
                results[response_name] = resolved_result
                return results

            if isinstance(result, Deferred):
                return succeed(result).add_callback(collect_result)

            else:
                return collect_result(result)

        def execute_field(prev_deferred, response_name):
            return prev_deferred.add_callback(execute_field_callback, response_name)

        return functools.reduce(execute_field, fields.keys(), succeed(self._map_type()))

    def _execute_fields(self, execution_context, parent_type, source_value, fields):
        contains_deferred = False

        results = self._map_type()
        for response_name, field_asts in fields.items():
            result = self._resolve_field(execution_context, parent_type, source_value, field_asts)
            if result is Undefined:
                continue

            results[response_name] = result
            if isinstance(result, Deferred):
                contains_deferred = True

        if not contains_deferred:
            return results

        return DeferredDict(results)

    def _resolve_field(self, execution_context, parent_type, source, field_asts):
        field_ast = field_asts[0]
        field_name = field_ast.name.value

        field_def = get_field_def(execution_context.schema, parent_type, field_name)
        if not field_def:
            return Undefined

        return_type = field_def.type
        resolve_fn = field_def.resolver or self._default_resolve_fn

        # Build a dict of arguments from the field.arguments AST, using the variables scope to
        # fulfill any variable references.
        args = execution_context.get_argument_values(field_def, field_ast)

        # The resolve function's optional third argument is a collection of
        # information about the current execution state.
        info = ResolveInfo(
            field_name,
            field_asts,
            return_type,
            parent_type,
            execution_context
        )

        result = self.resolve_or_error(resolve_fn, source, args, info)
        return self.complete_value_catching_error(
            execution_context, return_type, field_asts, info, result
        )

    def complete_value_catching_error(self, ctx, return_type, field_asts, info, result):
        # If the field type is non-nullable, then it is resolved without any
        # protection from errors.
        if isinstance(return_type, GraphQLNonNull):
            return self.complete_value(ctx, return_type, field_asts, info, result)

        # Otherwise, error protection is applied, logging the error and
        # resolving a null value for this field if one is encountered.
        try:
            completed = self.complete_value(ctx, return_type, field_asts, info, result)
            if isinstance(completed, Deferred):
                def handle_error(error):
                    ctx.errors.append(error)
                    return None

                return completed.add_errback(handle_error)

            return completed
        except Exception as e:
            ctx.errors.append(e)
            return None

    def complete_value(self, ctx, return_type, field_asts, info, result):
        """
        Implements the instructions for completeValue as defined in the
        "Field entries" section of the spec.

        If the field type is Non-Null, then this recursively completes the value for the inner type. It throws a field
        error if that completion returns null, as per the "Nullability" section of the spec.

        If the field type is a List, then this recursively completes the value for the inner type on each item in the
        list.

        If the field type is a Scalar or Enum, ensures the completed value is a legal value of the type by calling the
        `serialize` method of GraphQL type definition.

        Otherwise, the field type expects a sub-selection set, and will complete the value by evaluating all
        sub-selections.
        """
        # If field type is NonNull, complete for inner type, and throw field error if result is null.
        if isinstance(result, Deferred):
            return result.add_callbacks(
                lambda resolved: self.complete_value(
                    ctx,
                    return_type,
                    field_asts,
                    info,
                    resolved
                ),
                lambda error: GraphQLError(error.value and str(error.value), field_asts, error)
            )

        if isinstance(result, Exception):
            raise GraphQLError(str(result), field_asts, result)

        if isinstance(return_type, GraphQLNonNull):
            completed = self.complete_value(
                ctx, return_type.of_type, field_asts, info, result
            )
            if completed is None:
                raise GraphQLError(
                    'Cannot return null for non-nullable field {}.{}.'.format(info.parent_type, info.field_name),
                    field_asts
                )

            return completed

        # If result is null-like, return null.
        if result is None:
            return None

        # If field type is List, complete each item in the list with the inner type
        if isinstance(return_type, GraphQLList):
            assert isinstance(result, collections.Iterable), \
                ('User Error: expected iterable, but did not find one' +
                 'for field {}.{}').format(info.parent_type, info.field_name)

            item_type = return_type.of_type
            completed_results = []
            contains_deferred = False
            for item in result:
                completed_item = self.complete_value_catching_error(ctx, item_type, field_asts, info, item)
                if not contains_deferred and isinstance(completed_item, Deferred):
                    contains_deferred = True

                completed_results.append(completed_item)

            return DeferredList(completed_results) if contains_deferred else completed_results

        # If field type is Scalar or Enum, serialize to a valid value, returning null if coercion is not possible.
        if isinstance(return_type, (GraphQLScalarType, GraphQLEnumType)):
            serialized_result = return_type.serialize(result)

            if serialized_result is None:
                return None

            return serialized_result

        runtime_type = None

        # Field type must be Object, Interface or Union and expect sub-selections.
        if isinstance(return_type, GraphQLObjectType):
            runtime_type = return_type

        elif isinstance(return_type, (GraphQLInterfaceType, GraphQLUnionType)):
            runtime_type = return_type.resolve_type(result, info)
            if runtime_type and not return_type.is_possible_type(runtime_type):
                raise GraphQLError(
                    u'Runtime Object type "{}" is not a possible type for "{}".'.format(runtime_type, return_type),
                    field_asts
                )

        if not runtime_type:
            return None

        if runtime_type.is_type_of and not runtime_type.is_type_of(result, info):
            raise GraphQLError(
                u'Expected value of type "{}" but got {}.'.format(return_type, type(result).__name__),
                field_asts
            )

        # Collect sub-fields to execute to complete this value.
        subfield_asts = DefaultOrderedDict(list) if self._enforce_strict_ordering else collections.defaultdict(list)
        visited_fragment_names = set()
        for field_ast in field_asts:
            selection_set = field_ast.selection_set
            if selection_set:
                subfield_asts = collect_fields(
                    ctx, runtime_type, selection_set,
                    subfield_asts, visited_fragment_names
                )

        return self._execute_fields(ctx, runtime_type, result, subfield_asts)

    def resolve_or_error(self, resolve_fn, source, args, info):
        curried_resolve_fn = functools.partial(resolve_fn, source, args, info)

        try:
            for middleware in self._execution_middlewares:
                if hasattr(middleware, 'run_resolve_fn'):
                    curried_resolve_fn = functools.partial(middleware.run_resolve_fn, curried_resolve_fn, resolve_fn)

            return curried_resolve_fn()
        except Exception as e:
            return e
