# -*- coding: utf-8 -*-
# pylint: disable=missing-docstring
r"""
Provides support to compose user-defined parse types.

Cardinality
------------

It is often useful to constrain how often a data type occurs.
This is also called the cardinality of a data type (in a context).
The supported cardinality are:

  * 0..1  zero_or_one,  optional<T>: T or None
  * 0..N  zero_or_more, list_of<T>
  * 1..N  one_or_more,  list_of<T> (many)


.. doctest:: cardinality

    >>> from parse_type import TypeBuilder
    >>> from parse import Parser

    >>> def parse_number(text):
    ...     return int(text)
    >>> parse_number.pattern = r"\d+"

    >>> parse_many_numbers = TypeBuilder.with_many(parse_number)
    >>> more_types = { "Numbers": parse_many_numbers }
    >>> parser = Parser("List: {numbers:Numbers}", more_types)
    >>> parser.parse("List: 1, 2, 3")
    <Result () {'numbers': [1, 2, 3]}>


Enumeration Type (Name-to-Value Mappings)
-----------------------------------------

An Enumeration data type allows to select one of several enum values by using
its name. The converter function returns the selected enum value.

.. doctest:: make_enum

    >>> parse_enum_yesno = TypeBuilder.make_enum({"yes": True, "no": False})
    >>> more_types = { "YesNo": parse_enum_yesno }
    >>> parser = Parser("Answer: {answer:YesNo}", more_types)
    >>> parser.parse("Answer: yes")
    <Result () {'answer': True}>


Choice (Name Enumerations)
-----------------------------

A Choice data type allows to select one of several strings.

.. doctest:: make_choice

    >>> parse_choice_yesno = TypeBuilder.make_choice(["yes", "no"])
    >>> more_types = { "ChoiceYesNo": parse_choice_yesno }
    >>> parser = Parser("Answer: {answer:ChoiceYesNo}", more_types)
    >>> parser.parse("Answer: yes")
    <Result () {'answer': 'yes'}>

"""

from __future__ import absolute_import
import inspect
import re
import enum
from parse_type.cardinality import pattern_group_count, \
    Cardinality, TypeBuilder as CardinalityTypeBuilder

__all__ = ["TypeBuilder", "build_type_dict", "parse_anything"]


class TypeBuilder(CardinalityTypeBuilder):
    """
    Provides a utility class to build type-converters (parse_types) for
    the :mod:`parse` module.
    """
    default_strict = True
    default_re_opts = (re.IGNORECASE | re.DOTALL)

    @classmethod
    def make_list(cls, item_converter=None, listsep=','):
        """
        Create a type converter for a list of items (many := 1..*).
        The parser accepts anything and the converter needs to fail on errors.

        :param item_converter:  Type converter for an item.
        :param listsep:  List separator to use (as string).
        :return: Type converter function object for the list.
        """
        if not item_converter:
            item_converter = parse_anything
        return cls.with_cardinality(Cardinality.many, item_converter,
                                    pattern=cls.anything_pattern,
                                    listsep=listsep)

    @staticmethod
    def make_enum(enum_mappings):
        """
        Creates a type converter for an enumeration or text-to-value mapping.

        :param enum_mappings: Defines enumeration names and values.
        :return: Type converter function object for the enum/mapping.
        """
        if (inspect.isclass(enum_mappings) and
                issubclass(enum_mappings, enum.Enum)):
            enum_class = enum_mappings
            enum_mappings = enum_class.__members__

        def convert_enum(text):
            if text not in convert_enum.mappings:
                text = text.lower()     # REQUIRED-BY: parse re.IGNORECASE
            return convert_enum.mappings[text]    #< text.lower() ???
        convert_enum.pattern = r"|".join(enum_mappings.keys())
        convert_enum.mappings = enum_mappings
        return convert_enum

    @staticmethod
    def _normalize_choices(choices, transform):
        assert transform is None or callable(transform)
        if transform:
            choices = [transform(value)  for value in choices]
        else:
            choices = list(choices)
        return choices

    @classmethod
    def make_choice(cls, choices, transform=None, strict=None):
        """
        Creates a type-converter function to select one from a list of strings.
        The type-converter function returns the selected choice_text.
        The :param:`transform()` function is applied in the type converter.
        It can be used to enforce the case (because parser uses re.IGNORECASE).

        :param choices: List of strings as choice.
        :param transform: Optional, initial transform function for parsed text.
        :return: Type converter function object for this choices.
        """
        # -- NOTE: Parser uses re.IGNORECASE flag
        #    => transform may enforce case.
        choices = cls._normalize_choices(choices, transform)
        if strict is None:
            strict = cls.default_strict

        def convert_choice(text):
            if transform:
                text = transform(text)
            if strict and text not in convert_choice.choices:
                values = ", ".join(convert_choice.choices)
                raise ValueError("%s not in: %s" % (text, values))
            return text
        convert_choice.pattern = r"|".join(choices)
        convert_choice.choices = choices
        return convert_choice

    @classmethod
    def make_choice2(cls, choices, transform=None, strict=None):
        """
        Creates a type converter to select one item from a list of strings.
        The type converter function returns a tuple (index, choice_text).

        :param choices: List of strings as choice.
        :param transform: Optional, initial transform function for parsed text.
        :return: Type converter function object for this choices.
        """
        choices = cls._normalize_choices(choices, transform)
        if strict is None:
            strict = cls.default_strict

        def convert_choice2(text):
            if transform:
                text = transform(text)
            if strict and text not in convert_choice2.choices:
                values = ", ".join(convert_choice2.choices)
                raise ValueError("%s not in: %s" % (text, values))
            index = convert_choice2.choices.index(text)
            return index, text
        convert_choice2.pattern = r"|".join(choices)
        convert_choice2.choices = choices
        return convert_choice2

    @classmethod
    def make_variant(cls, converters, re_opts=None, compiled=False, strict=True):
        """
        Creates a type converter for a number of type converter alternatives.
        The first matching type converter is used.

        REQUIRES: type_converter.pattern attribute

        :param converters: List of type converters as alternatives.
        :param re_opts:  Regular expression options zu use (=default_re_opts).
        :param compiled: Use compiled regexp matcher, if true (=False).
        :param strict:   Enable assertion checks.
        :return: Type converter function object.

        .. note::

            Works only with named fields in :class:`parse.Parser`.
            Parser needs group_index delta for unnamed/fixed fields.
            This is not supported for user-defined types.
            Otherwise, you need to use :class:`parse_type.parse.Parser`
            (patched version of the :mod:`parse` module).
        """
        # -- NOTE: Uses double-dispatch with regex pattern rematch because
        #          match is not passed through to primary type converter.
        assert converters, "REQUIRE: Non-empty list."
        if len(converters) == 1:
            return converters[0]
        if re_opts is None:
            re_opts = cls.default_re_opts

        pattern = r")|(".join([tc.pattern for tc in converters])
        pattern = r"("+ pattern + ")"
        group_count = len(converters)
        for converter in converters:
            group_count += pattern_group_count(converter.pattern)

        if compiled:
            convert_variant = cls.__create_convert_variant_compiled(converters,
                                                                    re_opts,
                                                                    strict)
        else:
            convert_variant = cls.__create_convert_variant(re_opts, strict)
        convert_variant.pattern = pattern
        convert_variant.converters = tuple(converters)
        convert_variant.regex_group_count = group_count
        return convert_variant

    @staticmethod
    def __create_convert_variant(re_opts, strict):
        # -- USE: Regular expression pattern (compiled on use).
        def convert_variant(text, m=None):
            # pylint: disable=invalid-name, unused-argument, missing-docstring
            for converter in convert_variant.converters:
                if re.match(converter.pattern, text, re_opts):
                    return converter(text)
            # -- pragma: no cover
            assert not strict, "OOPS-VARIANT-MISMATCH: %s" % text
            return None
        return convert_variant

    @staticmethod
    def __create_convert_variant_compiled(converters, re_opts, strict):
        # -- USE: Compiled regular expression matcher.
        for converter in converters:
            matcher = getattr(converter, "matcher", None)
            if not matcher:
                converter.matcher = re.compile(converter.pattern, re_opts)

        def convert_variant(text, m=None):
            # pylint: disable=invalid-name, unused-argument, missing-docstring
            for converter in convert_variant.converters:
                if converter.matcher.match(text):
                    return converter(text)
            # -- pragma: no cover
            assert not strict, "OOPS-VARIANT-MISMATCH: %s" % text
            return None
        return convert_variant


def build_type_dict(converters):
    """
    Builds type dictionary for user-defined type converters,
    used by :mod:`parse` module.
    This requires that each type converter has a "name" attribute.

    :param converters: List of type converters (parse_types)
    :return: Type converter dictionary
    """
    more_types = {}
    for converter in converters:
        assert callable(converter)
        more_types[converter.name] = converter
    return more_types

# -----------------------------------------------------------------------------
# COMMON TYPE CONVERTERS
# -----------------------------------------------------------------------------
def parse_anything(text, match=None, match_start=0):
    """
    Provides a generic type converter that accepts anything and returns
    the text (unchanged).

    :param text:  Text to convert (as string).
    :return: Same text (as string).
    """
    # pylint: disable=unused-argument
    return text
parse_anything.pattern = TypeBuilder.anything_pattern


# -----------------------------------------------------------------------------
# Copyright (c) 2012-2020 by Jens Engel (https://github/jenisys/parse_type)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in
#  all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
