import collections
import copy
import re

from ..language import ast


def is_type(type):
    return isinstance(type, (
        GraphQLScalarType,
        GraphQLObjectType,
        GraphQLInterfaceType,
        GraphQLUnionType,
        GraphQLEnumType,
        GraphQLInputObjectType,
        GraphQLList,
        GraphQLNonNull
    ))


def is_input_type(type):
    named_type = get_named_type(type)
    return isinstance(named_type, (
        GraphQLScalarType,
        GraphQLEnumType,
        GraphQLInputObjectType,
    ))


def is_output_type(type):
    named_type = get_named_type(type)
    return isinstance(named_type, (
        GraphQLScalarType,
        GraphQLObjectType,
        GraphQLInterfaceType,
        GraphQLUnionType,
        GraphQLEnumType
    ))


def is_leaf_type(type):
    named_type = get_named_type(type)
    return isinstance(named_type, (
        GraphQLScalarType,
        GraphQLEnumType,
    ))


def is_composite_type(type):
    named_type = get_named_type(type)
    return isinstance(named_type, (
        GraphQLObjectType,
        GraphQLInterfaceType,
        GraphQLUnionType,
    ))


def is_abstract_type(type):
    return isinstance(type, (
        GraphQLInterfaceType,
        GraphQLUnionType
    ))


def get_nullable_type(type):
    if isinstance(type, GraphQLNonNull):
        return type.of_type
    return type


def get_named_type(type):
    unmodified_type = type
    while isinstance(unmodified_type, (GraphQLList, GraphQLNonNull)):
        unmodified_type = unmodified_type.of_type

    return unmodified_type


class GraphQLType(object):
    __slots__ = 'name',

    def __str__(self):
        return self.name

    def is_same_type(self, other):
        return self.__class__ is other.__class__ and self.name == other.name


class GraphQLScalarType(GraphQLType):
    """Scalar Type Definition

    The leaf values of any request and input values to arguments are
    Scalars (or Enums) and are defined with a name and a series of coercion
    functions used to ensure validity.

    Example:

        def coerce_odd(value):
            if value % 2 == 1:
                return value
            return None

        OddType = GraphQLScalarType(name='Odd', serialize=coerce_odd)
    """

    __slots__ = 'name', 'description', '_serialize', '_parse_value', '_parse_literal'

    def __init__(self, name, description=None, serialize=None, parse_value=None, parse_literal=None):
        assert name, 'Type must be named.'
        assert_valid_name(name)
        self.name = name
        self.description = description

        assert callable(serialize), (
            '{} must provide "serialize" function. If this custom Scalar is '
            'also used as an input type, ensure "parse_value" and "parse_literal" '
            'functions are also provided.'
        ).format(self)

        if parse_value is not None or parse_literal is not None:
            assert callable(parse_value) and callable(parse_literal), (
                '{} must provide both "parse_value" and "parse_literal" functions.'.format(self)
            )

        self._serialize = serialize
        self._parse_value = parse_value
        self._parse_literal = parse_literal

    def serialize(self, value):
        return self._serialize(value)

    def parse_value(self, value):
        if self._parse_value:
            return self._parse_value(value)

        return None

    def parse_literal(self, value_ast):
        if self._parse_literal:
            return self._parse_literal(value_ast)

        return None

    def __str__(self):
        return self.name


class GraphQLObjectType(GraphQLType):
    """Object Type Definition

    Almost all of the GraphQL types you define will be object types.
    Object types have a name, but most importantly describe their fields.

    Example:

        AddressType = GraphQLObjectType('Address', {
            'street': GraphQLField(GraphQLString),
            'number': GraphQLField(GraphQLInt),
            'formatted': GraphQLField(GraphQLString,
                resolver=lambda obj, args, info: obj.number + ' ' + obj.street),
        })

    When two types need to refer to each other, or a type needs to refer to
    itself in a field, you can use a static method to supply the fields
    lazily.

    Example:

        PersonType = GraphQLObjectType('Person', lambda: {
            'name': GraphQLField(GraphQLString),
            'bestFriend': GraphQLField(PersonType)
        })
    """
    __slots__ = 'name', 'description', 'is_type_of', '_fields', '_provided_interfaces', '_field_map', '_interfaces'

    def __init__(self, name, fields, interfaces=None, is_type_of=None, description=None):
        assert name, 'Type must be named.'
        assert_valid_name(name)
        self.name = name
        self.description = description

        if is_type_of is not None:
            assert callable(is_type_of), '{} must provide "is_type_of" as a function.'.format(self)

        self.is_type_of = is_type_of
        self._fields = fields
        self._provided_interfaces = interfaces
        self._field_map = None
        self._interfaces = None
        add_impl_to_interfaces(self)

    def get_fields(self):
        if self._field_map is None:
            self._field_map = define_field_map(self, self._fields)

        return self._field_map

    def get_interfaces(self):
        if self._interfaces is None:
            self._interfaces = define_interfaces(self, self._provided_interfaces)

        return self._interfaces


def define_field_map(type, field_map):
    if callable(field_map):
        field_map = field_map()

    assert isinstance(field_map, collections.Mapping) and len(field_map) > 0, (
        '{} fields must be a mapping (dict / OrderedDict) with field names as keys or a '
        'function which returns such a mapping.'
    ).format(type)

    if not isinstance(field_map, collections.OrderedDict):
        field_map = collections.OrderedDict(sorted(list(field_map.items())))

    result_field_map = collections.OrderedDict()
    for field_name, field in field_map.items():
        assert_valid_name(field_name)
        assert isinstance(field, GraphQLField), (
            '{}.{} must be an instance of GraphQLField.'.format(type, field_name)
        )

        field = copy.copy(field)
        field.name = field_name

        assert is_output_type(field.type), (
            '{}.{} field type must be Output Type but got: {}.'.format(type, field_name, field.type)
        )

        if not field.args:
            field.args = []

        else:
            field_args = field.args
            assert isinstance(field_args, collections.Mapping), (
                '{}.{} args must be a mapping (dict / OrderedDict) with argument names as keys.'.format(type,
                                                                                                        field_name)
            )
            args = []
            if not isinstance(field_args, collections.OrderedDict):
                field_args = collections.OrderedDict(sorted(list(field_args.items())))

            for arg_name, arg in field_args.items():
                assert_valid_name(arg_name)
                assert isinstance(arg, GraphQLArgument), (
                    '{}.{}({}:) argument must be an instance of GraphQLArgument.'.format(type, field_name, arg_name)
                )
                assert is_input_type(arg.type), (
                    '{}.{}({}:) argument type must be Input Type but got: {}.'.format(type, field_name, arg_name,
                                                                                      arg.type)
                )

                arg = copy.copy(arg)
                arg.name = arg_name
                args.append(arg)

            field.args = args

        result_field_map[field_name] = field

    return result_field_map


def define_interfaces(type, interfaces):
    if callable(interfaces):
        interfaces = interfaces()

    if interfaces is None:
        interfaces = []

    assert isinstance(interfaces, (list, tuple)), (
        '{} interfaces must be a list/tuple or a function which returns a list/tuple.'.format(type)
    )

    for interface in interfaces:
        assert isinstance(interface, GraphQLInterfaceType), (
            '{} may only implement Interface types, it cannot implement: {}.'.format(type, interface)
        )

        if not callable(interface.type_resolver):
            assert callable(type.is_type_of), (
                'Interface Type {} does not provide a "resolve_type" function '
                'and implementing Type {} does not provide a "is_type_of" '
                'function. There is no way to resolve this implementing type '
                'during execution.'
            ).format(interface, type)

    return interfaces


def add_impl_to_interfaces(impl):
    for type in impl.get_interfaces():
        type._impls.append(impl)


class GraphQLField(object):
    __slots__ = 'name', 'type', 'args', 'resolver', 'deprecation_reason', 'description'

    def __init__(self, type, args=None, resolver=None, deprecation_reason=None, description=None):
        self.name = None
        self.type = type
        self.args = args
        self.resolver = resolver
        self.deprecation_reason = deprecation_reason
        self.description = description

    def __eq__(self, other):
        return (
            self is other or (
                isinstance(other, GraphQLField) and
                self.name == other.name and
                self.type == other.type and
                self.args == other.args and
                self.resolver == other.resolver and
                self.deprecation_reason == other.deprecation_reason and
                self.description == other.description
            )
        )

    def __hash__(self):
        return id(self)


class GraphQLArgument(object):
    __slots__ = 'name', 'type', 'default_value', 'description'

    def __init__(self, type, default_value=None, description=None):
        self.name = None
        self.type = type
        self.default_value = default_value
        self.description = description

    def __eq__(self, other):
        return (
            self is other or (
                isinstance(other, GraphQLArgument) and
                self.name == other.name and
                self.type == other.type and
                self.default_value == other.default_value and
                self.description == other.description
            )
        )

    def __hash__(self):
        return id(self)


class GraphQLInterfaceType(GraphQLType):
    """Interface Type Definition

    When a field can return one of a heterogeneous set of types, a Interface type is used to describe what types are possible,
    what fields are in common across all types, as well as a function to determine which type is actually used when the field is resolved.

    Example:

        EntityType = GraphQLInterfaceType(
            name='Entity',
            fields={
                'name': GraphQLField(GraphQLString),
            })
    """
    __slots__ = 'name', 'description', 'type_resolver', '_fields', '_impls', '_field_map', '_possible_type_names'

    def __init__(self, name, fields=None, resolve_type=None, description=None):
        assert name, 'Type must be named.'
        assert_valid_name(name)
        self.name = name
        self.description = description

        if resolve_type is not None:
            assert callable(resolve_type), '{} must provide "resolve_type" as a function.'.format(self)

        self.type_resolver = resolve_type
        self._fields = fields

        self._impls = []
        self._field_map = None
        self._possible_type_names = None

    def get_fields(self):
        if self._field_map is None:
            self._field_map = define_field_map(self, self._fields)

        return self._field_map

    def get_possible_types(self):
        return self._impls

    def is_possible_type(self, type):
        if self._possible_type_names is None:
            self._possible_type_names = set(
                t.name for t in self.get_possible_types()
            )
        return type.name in self._possible_type_names

    def resolve_type(self, value, info):
        if self.type_resolver:
            return self.type_resolver(value, info)

        return get_type_of(value, info, self)


def get_type_of(value, info, abstract_type):
    possible_types = abstract_type.get_possible_types()
    for type in possible_types:
        if callable(type.is_type_of) and type.is_type_of(value, info):
            return type


class GraphQLUnionType(GraphQLType):
    """Union Type Definition

    When a field can return one of a heterogeneous set of types, a Union type is used to describe what types are possible
    as well as providing a function to determine which type is actually used when the field is resolved.

    Example:

        class PetType(GraphQLUnionType):
            name = 'Pet'
            types = [DogType, CatType]

            def resolve_type(self, value):
                if isinstance(value, Dog):
                    return DogType()
                if isinstance(value, Cat):
                    return CatType()
    """
    __slots__ = 'name', 'description', '_resolve_type', '_types', '_possible_type_names', '_possible_types'

    def __init__(self, name, types=None, resolve_type=None, description=None):
        assert name, 'Type must be named.'
        assert_valid_name(name)
        self.name = name
        self.description = description

        if resolve_type is not None:
            assert callable(resolve_type), '{} must provide "resolve_type" as a function.'.format(self)

        self._resolve_type = resolve_type
        self._types = types
        self._possible_types = None
        self._possible_type_names = None

    def get_possible_types(self):
        if self._possible_types is None:
            self._possible_types = define_types(self, self._types)

        return self._possible_types

    def is_possible_type(self, type):
        if self._possible_type_names is None:
            self._possible_type_names = set(
                t.name for t in self.get_possible_types()
            )

        return type.name in self._possible_type_names

    def resolve_type(self, value, info):
        if self._resolve_type:
            return self._resolve_type(value, info)

        return get_type_of(value, info, self)


def define_types(union_type, types):
    if callable(types):
        types = types()

    assert isinstance(types, (list, tuple)) and len(
        types) > 0, 'Must provide types for Union {}.'.format(union_type.name)
    has_resolve_type_fn = callable(union_type._resolve_type)

    for type in types:
        assert isinstance(type, GraphQLObjectType), (
            '{} may only contain Object types, it cannot contain: {}.'.format(union_type, type)
        )

        if not has_resolve_type_fn:
            assert callable(type.is_type_of), (
                'Union Type {} does not provide a "resolve_type" function '
                'and possible Type {} does not provide a "is_type_of" '
                'function. There is no way to resolve this possible type '
                'during execution.'
            ).format(union_type, type)

    return types


class GraphQLEnumType(GraphQLType):
    """Enum Type Definition

    Some leaf values of requests and input values are Enums. GraphQL serializes Enum values as strings,
    however internally Enums can be represented by any kind of type, often integers.

    Example:

        RGBType = GraphQLEnumType('RGB', {
            'RED': 0,
            'GREEN': 1,
            'BLUE': 2,
        })

    Note: If a value is not provided in a definition, the name of the enum value will be used as it's internal value.
    """
    __slots__ = 'name', 'description', '_values', '_value_lookup', '_name_lookup'

    def __init__(self, name, values, description=None):
        assert name, 'Type must provide name.'
        assert_valid_name(name)
        self.name = name
        self.description = description

        self._values = define_enum_values(self, values)
        self._value_lookup = None
        self._name_lookup = None

    def get_values(self):
        return self._values

    def serialize(self, value):
        if isinstance(value, collections.Hashable):
            enum_value = self._get_value_lookup().get(value)

            if enum_value:
                return enum_value.name

        return None

    def parse_value(self, value):
        if isinstance(value, collections.Hashable):
            enum_value = self._get_name_lookup().get(value)

            if enum_value:
                return enum_value.value

        return None

    def parse_literal(self, value_ast):
        if isinstance(value_ast, ast.EnumValue):
            enum_value = self._get_name_lookup().get(value_ast.value)

            if enum_value:
                return enum_value.value

    def _get_value_lookup(self):
        if self._value_lookup is None:
            self._value_lookup = {value.value: value for value in self.get_values()}

        return self._value_lookup

    def _get_name_lookup(self):
        if self._name_lookup is None:
            self._name_lookup = {value.name: value for value in self.get_values()}

        return self._name_lookup


def define_enum_values(type, value_map):
    assert isinstance(value_map, collections.Mapping) and len(value_map) > 0, (
        '{} values must be a mapping (dict / OrderedDict) with value names as keys.'.format(type)
    )

    values = []
    if not isinstance(value_map, collections.OrderedDict):
        value_map = collections.OrderedDict(sorted(list(value_map.items())))

    for value_name, value in value_map.items():
        assert_valid_name(value_name)
        assert isinstance(value, GraphQLEnumValue), (
            '{}.{} must be an instance of GraphQLEnumValue, but got: {}'.format(type, value_name, value)
        )
        value = copy.copy(value)
        value.name = value_name
        if value.value is None:
            value.value = value_name

        values.append(value)

    return values


class GraphQLEnumValue(object):
    __slots__ = 'name', 'value', 'deprecation_reason', 'description'

    def __init__(self, value=None, deprecation_reason=None, description=None, name=None):
        self.name = name
        self.value = value
        self.deprecation_reason = deprecation_reason
        self.description = description

    def __eq__(self, other):
        return (
            self is other or (
                isinstance(other, GraphQLEnumValue) and
                self.name == other.name and
                self.value == other.value and
                self.deprecation_reason == other.deprecation_reason and
                self.description == other.description
            )
        )


class GraphQLInputObjectType(GraphQLType):
    """Input Object Type Definition

    An input object defines a structured collection of fields which may be
    supplied to a field argument.

    Using `NonNull` will ensure that a value must be provided by the query

    Example:

        NonNullFloat = GraphQLNonNull(GraphQLFloat())

        class GeoPoint(GraphQLInputObjectType):
            name = 'GeoPoint'
            fields = {
                'lat': GraphQLInputObjectField(NonNullFloat),
                'lon': GraphQLInputObjectField(NonNullFloat),
                'alt': GraphQLInputObjectField(GraphQLFloat(),
                    default_value=0)
            }
    """
    __slots__ = 'name', 'description', '_fields', '_field_map'

    def __init__(self, name, fields, description=None):
        assert name, 'Type must be named.'
        self.name = name
        self.description = description

        self._fields = fields
        self._field_map = None

    def get_fields(self):
        if self._field_map is None:
            self._field_map = self._define_field_map()

        return self._field_map

    def _define_field_map(self):
        fields = self._fields
        if callable(fields):
            fields = fields()

        assert isinstance(fields, collections.Mapping) and len(fields) > 0, (
            '{} fields must be a mapping (dict / OrderedDict) with field names as keys or a '
            'function which returns such a mapping.'
        ).format(self)

        if not isinstance(fields, collections.OrderedDict):
            fields = collections.OrderedDict(sorted(list(fields.items())))

        field_map = collections.OrderedDict()
        for field_name, field in fields.items():
            assert_valid_name(field_name)
            assert isinstance(field, GraphQLInputObjectField), (
                '{}.{} must be an instance of GraphQLInputObjectField.'.format(self, field_name)
            )

            field = copy.copy(field)
            field.name = field_name

            assert is_input_type(field.type), (
                '{}.{} field type must be Input Type but got: {}.'.format(self, field_name, field.type)
            )

            field_map[field_name] = field

        return field_map


class GraphQLInputObjectField(object):
    __slots__ = 'name', 'type', 'default_value', 'description'

    def __init__(self, type, default_value=None, description=None):
        self.name = None
        self.type = type
        self.default_value = default_value
        self.description = description

    def __eq__(self, other):
        return (
            self is other or (
                isinstance(other, GraphQLInputObjectField) and
                self.name == other.name and
                self.type == other.type and
                self.description == other.description
            )
        )


class GraphQLList(GraphQLType):
    """List Modifier

    A list is a kind of type marker, a wrapping type which points to another
    type. Lists are often created within the context of defining the fields
    of an object type.

    Example:

        class PersonType(GraphQLObjectType):
            name = 'Person'

            def get_fields(self):
                return {
                    'parents': GraphQLField(GraphQLList(PersonType())),
                    'children': GraphQLField(GraphQLList(PersonType())),
                }
    """
    __slots__ = 'of_type',

    def __init__(self, type):
        assert is_type(type), 'Can only create List of a GraphQLType but got: {}.'.format(type)
        self.of_type = type

    def __str__(self):
        return '[' + str(self.of_type) + ']'

    def is_same_type(self, other):
        return isinstance(other, GraphQLList) and self.of_type.is_same_type(other.of_type)


class GraphQLNonNull(GraphQLType):
    """Non-Null Modifier

    A non-null is a kind of type marker, a wrapping type which points to another type. Non-null types enforce that their values are never null
    and can ensure an error is raised if this ever occurs during a request. It is useful for fields which you can make a strong guarantee on
    non-nullability, for example usually the id field of a database row will never be null.

    Example:

        class RowType(GraphQLObjectType):
            name = 'Row'
            fields = {
                'id': GraphQLField(type=GraphQLNonNull(GraphQLString()))
            }

    Note: the enforcement of non-nullability occurs within the executor.
    """
    __slots__ = 'of_type',

    def __init__(self, type):
        assert is_type(type) and not isinstance(type, GraphQLNonNull), (
            'Can only create NonNull of a Nullable GraphQLType but got: {}.'.format(type)
        )
        self.of_type = type

    def __str__(self):
        return str(self.of_type) + '!'

    def is_same_type(self, other):
        return isinstance(other, GraphQLNonNull) and self.of_type.is_same_type(other.of_type)


NAME_PATTERN = r'^[_a-zA-Z][_a-zA-Z0-9]*$'
COMPILED_NAME_PATTERN = re.compile(NAME_PATTERN)


def assert_valid_name(name):
    assert COMPILED_NAME_PATTERN.match(name), 'Names must match /{}/ but "{}" does not.'.format(NAME_PATTERN, name)
