# Copyright 2016-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tools for working with `collations`_.

.. _collations: https://www.mongodb.com/docs/manual/reference/collation/

.. seealso:: This module is compatible with both the synchronous and asynchronous PyMongo APIs.
"""
from __future__ import annotations

from typing import Any, Mapping, Optional, Union

from pymongo import common
from pymongo.write_concern import validate_boolean


class CollationStrength:
    """
    An enum that defines values for `strength` on a
    :class:`~pymongo.collation.Collation`.
    """

    PRIMARY = 1
    """Differentiate base (unadorned) characters."""

    SECONDARY = 2
    """Differentiate character accents."""

    TERTIARY = 3
    """Differentiate character case."""

    QUATERNARY = 4
    """Differentiate words with and without punctuation."""

    IDENTICAL = 5
    """Differentiate unicode code point (characters are exactly identical)."""


class CollationAlternate:
    """
    An enum that defines values for `alternate` on a
    :class:`~pymongo.collation.Collation`.
    """

    NON_IGNORABLE = "non-ignorable"
    """Spaces and punctuation are treated as base characters."""

    SHIFTED = "shifted"
    """Spaces and punctuation are *not* considered base characters.

    Spaces and punctuation are distinguished regardless when the
    :class:`~pymongo.collation.Collation` strength is at least
    :data:`~pymongo.collation.CollationStrength.QUATERNARY`.

    """


class CollationMaxVariable:
    """
    An enum that defines values for `max_variable` on a
    :class:`~pymongo.collation.Collation`.
    """

    PUNCT = "punct"
    """Both punctuation and spaces are ignored."""

    SPACE = "space"
    """Spaces alone are ignored."""


class CollationCaseFirst:
    """
    An enum that defines values for `case_first` on a
    :class:`~pymongo.collation.Collation`.
    """

    UPPER = "upper"
    """Sort uppercase characters first."""

    LOWER = "lower"
    """Sort lowercase characters first."""

    OFF = "off"
    """Default for locale or collation strength."""


class Collation:
    """Collation

    :param locale: (string) The locale of the collation. This should be a string
        that identifies an `ICU locale ID` exactly. For example, ``en_US`` is
        valid, but ``en_us`` and ``en-US`` are not. Consult the MongoDB
        documentation for a list of supported locales.
    :param caseLevel: (optional) If ``True``, turn on case sensitivity if
        `strength` is 1 or 2 (case sensitivity is implied if `strength` is
        greater than 2). Defaults to ``False``.
    :param caseFirst: (optional) Specify that either uppercase or lowercase
        characters take precedence. Must be one of the following values:

          * :data:`~CollationCaseFirst.UPPER`
          * :data:`~CollationCaseFirst.LOWER`
          * :data:`~CollationCaseFirst.OFF` (the default)

    :param strength: Specify the comparison strength. This is also
        known as the ICU comparison level. This must be one of the following
        values:

          * :data:`~CollationStrength.PRIMARY`
          * :data:`~CollationStrength.SECONDARY`
          * :data:`~CollationStrength.TERTIARY` (the default)
          * :data:`~CollationStrength.QUATERNARY`
          * :data:`~CollationStrength.IDENTICAL`

        Each successive level builds upon the previous. For example, a
        `strength` of :data:`~CollationStrength.SECONDARY` differentiates
        characters based both on the unadorned base character and its accents.

    :param numericOrdering: If ``True``, order numbers numerically
        instead of in collation order (defaults to ``False``).
    :param alternate: Specify whether spaces and punctuation are
        considered base characters. This must be one of the following values:

          * :data:`~CollationAlternate.NON_IGNORABLE` (the default)
          * :data:`~CollationAlternate.SHIFTED`

    :param maxVariable: When `alternate` is
        :data:`~CollationAlternate.SHIFTED`, this option specifies what
        characters may be ignored. This must be one of the following values:

          * :data:`~CollationMaxVariable.PUNCT` (the default)
          * :data:`~CollationMaxVariable.SPACE`

    :param normalization: If ``True``, normalizes text into Unicode
        NFD. Defaults to ``False``.
    :param backwards: If ``True``, accents on characters are
        considered from the back of the word to the front, as it is done in some
        French dictionary ordering traditions. Defaults to ``False``.
    :param kwargs: Keyword arguments supplying any additional options
        to be sent with this Collation object.

    .. versionadded: 3.4

    """

    __slots__ = ("__document",)

    def __init__(
        self,
        locale: str,
        caseLevel: Optional[bool] = None,
        caseFirst: Optional[str] = None,
        strength: Optional[int] = None,
        numericOrdering: Optional[bool] = None,
        alternate: Optional[str] = None,
        maxVariable: Optional[str] = None,
        normalization: Optional[bool] = None,
        backwards: Optional[bool] = None,
        **kwargs: Any,
    ) -> None:
        locale = common.validate_string("locale", locale)
        self.__document: dict[str, Any] = {"locale": locale}
        if caseLevel is not None:
            self.__document["caseLevel"] = validate_boolean("caseLevel", caseLevel)
        if caseFirst is not None:
            self.__document["caseFirst"] = common.validate_string("caseFirst", caseFirst)
        if strength is not None:
            self.__document["strength"] = common.validate_integer("strength", strength)
        if numericOrdering is not None:
            self.__document["numericOrdering"] = validate_boolean(
                "numericOrdering", numericOrdering
            )
        if alternate is not None:
            self.__document["alternate"] = common.validate_string("alternate", alternate)
        if maxVariable is not None:
            self.__document["maxVariable"] = common.validate_string("maxVariable", maxVariable)
        if normalization is not None:
            self.__document["normalization"] = validate_boolean("normalization", normalization)
        if backwards is not None:
            self.__document["backwards"] = validate_boolean("backwards", backwards)
        self.__document.update(kwargs)

    @property
    def document(self) -> dict[str, Any]:
        """The document representation of this collation.

        .. note::
          :class:`Collation` is immutable. Mutating the value of
          :attr:`document` does not mutate this :class:`Collation`.
        """
        return self.__document.copy()

    def __repr__(self) -> str:
        document = self.document
        return "Collation({})".format(", ".join(f"{key}={document[key]!r}" for key in document))

    def __eq__(self, other: Any) -> bool:
        if isinstance(other, Collation):
            return self.document == other.document
        return NotImplemented

    def __ne__(self, other: Any) -> bool:
        return not self == other


def validate_collation_or_none(
    value: Optional[Union[Mapping[str, Any], Collation]]
) -> Optional[dict[str, Any]]:
    if value is None:
        return None
    if isinstance(value, Collation):
        return value.document
    if isinstance(value, dict):
        return value
    raise TypeError("collation must be a dict, an instance of collation.Collation, or None")
