import re

from pypeg2 import List, contiguous, csl, name, optional, parse

from .exceptions import QueryFormatError


class IncludedField(List):
    grammar = name()


class ExcludedField(List):
    grammar = contiguous('-', name())


class AllFields(str):
    grammar = '*'


class BaseArgument(List):
    @property
    def value(self):
        return self[0]


class ArgumentWithoutQuotes(BaseArgument):
    grammar = name(), ':', re.compile(r'[^,:"\'\)]+')


class ArgumentWithSingleQuotes(BaseArgument):
    grammar = name(), ':', "'", re.compile(r'[^\']+'), "'"


class ArgumentWithDoubleQuotes(BaseArgument):
    grammar = name(), ':', '"', re.compile(r'[^"]+'), '"'


class Arguments(List):
    grammar = optional(csl(
        [
            ArgumentWithoutQuotes,
            ArgumentWithSingleQuotes,
            ArgumentWithDoubleQuotes
        ],
        separator=','
    ))


class ArgumentsBlock(List):
    grammar = optional('(', Arguments, ')')

    @property
    def arguments(self):
        if self[0] is None:
            return []  # No arguments
        return self[0]


class ParentField(List):
    """
    According to ParentField grammar:
    self[0]  returns IncludedField instance,
    self[1]  returns Block instance
    """
    @property
    def name(self):
        return self[0].name

    @property
    def block(self):
        return self[1]


class BlockBody(List):
    grammar = optional(csl(
        [ParentField, IncludedField, ExcludedField, AllFields],
        separator=','
    ))


class Block(List):
    grammar = ArgumentsBlock, '{', BlockBody, '}'

    @property
    def arguments(self):
        return self[0].arguments

    @property
    def body(self):
        return self[1]


# ParentField grammar,
# We don't include `ExcludedField` here because
# exclude operator(-) on a parent field should
# raise syntax error, e.g {name, -location{city}}
# `IncludedField` is a parent field and `Block`
# contains its sub fields
ParentField.grammar = IncludedField, Block


class Parser(object):
    def __init__(self, query):
        self._query = query

    def get_parsed(self):
        parse_tree = parse(self._query, Block)
        return self._transform_block(parse_tree)

    def _transform_block(self, block):
        fields = {
            "include": [],
            "exclude": [],
            "arguments": {}
        }

        for argument in block.arguments:
            argument = {str(argument.name): argument.value}
            fields['arguments'].update(argument)

        for field in block.body:
            # A field may be a parent or included field or excluded field
            field = self._transform_field(field)

            if isinstance(field, dict):
                # A field is a parent
                fields["include"].append(field)
            elif isinstance(field, IncludedField):
                fields["include"].append(str(field.name))
            elif isinstance(field, ExcludedField):
                fields["exclude"].append(str(field.name))
            elif isinstance(field, AllFields):
                # include all fields
                fields["include"].append("*")

        if fields["exclude"]:
            # fields['include'] should contain only nested fields

            # We should add `*` operator in fields['include']
            add_include_all_operator = True
            for field in fields["include"]:
                if field == "*":
                    # `*` operator is alredy in fields['include']
                    add_include_all_operator = False
                    continue

                if isinstance(field, str):
                    # Including and excluding fields on the same field level
                    msg = (
                        "Can not include and exclude fields on the same "
                        "field level"
                    )
                    raise QueryFormatError(msg)

            if add_include_all_operator:
                # Make sure we include * operator
                fields["include"].append("*")
        return fields

    def _transform_field(self, field):
        # A field may be a parent or included field or excluded field
        if isinstance(field, ParentField):
            return self._transform_parent_field(field)
        elif isinstance(field, (IncludedField, ExcludedField, AllFields)):
            return field

    def _transform_parent_field(self, parent_field):
        parent_field_name = str(parent_field.name)
        parent_field_value = self._transform_block(parent_field.block)
        return {parent_field_name: parent_field_value}
