# Copyright CNRS/Inria/UNS
# Contributor(s): Eric Debreuve (since 2021)
#
# eric.debreuve@cnrs.fr
#
# This software is governed by the CeCILL  license under French law and
# abiding by the rules of distribution of free software.  You can  use,
# modify and/ or redistribute the software under the terms of the CeCILL
# license as circulated by CEA, CNRS and INRIA at the following URL
# "http://www.cecill.info".
#
# As a counterpart to the access to the source code and  rights to copy,
# modify and redistribute granted by the license, users are provided only
# with a limited warranty  and the software's author,  the holder of the
# economic rights,  and the successive licensors  have only  limited
# liability.
#
# In this respect, the user's attention is drawn to the risks associated
# with loading,  using,  modifying and/or developing or reproducing the
# software by the user in light of its specific status of free software,
# that may mean  that it is complicated to manipulate,  and  that  also
# therefore means  that it is reserved for developers  and  experienced
# professionals having in-depth computer knowledge. Users are therefore
# encouraged to load and test the software's suitability as regards their
# requirements in conditions enabling the security of their systems and/or
# data to be ensured and,  more generally, to use and operate it in the
# same conditions as regards security.
#
# The fact that you are presently reading this means that you have had
# knowledge of the CeCILL license and that you accept its terms.

from __future__ import annotations

import dataclasses as dtcl
from pathlib import Path as path_t
from types import EllipsisType, UnionType
from typing import Any, Iterable

from conf_ini_g.extension.parser.type_hint import hint_tree_t
from conf_ini_g.extension.type import any_hint_h, none_t, simple_hint_h


@dtcl.dataclass(slots=True, repr=False, eq=False)
class _value_node_t:
    consolidated: Any
    # Leave elements to the tree


@dtcl.dataclass(slots=True, repr=False, eq=False)
class value_tree_t(_value_node_t):
    elements: tuple[value_tree_t, ...] = None

    @classmethod
    def NewFromValue(cls, value: Any, /) -> value_tree_t:
        """"""
        if isinstance(value, Iterable) and not isinstance(value, str):
            elements = tuple(cls.NewFromValue(_elm) for _elm in value)
            return cls(consolidated=value, elements=elements)

        return cls(consolidated=value)

    def CastValue(
        self, hint_tree: hint_tree_t, /, *, only_check_validity: bool = False
    ) -> tuple[Any, bool] | bool:
        """"""
        if self._CastToHint(hint_tree):
            if only_check_validity:
                return True
            else:
                return self.consolidated, True  # _Value(), True

        if only_check_validity:
            return False
        else:
            return None, False

    def _CastToHint(self, hint_node: hint_tree_t, /) -> bool:
        """
        Returned value=the value tree has been successfully cast to the hint tree
        specification, or not.
        """
        hn_type = hint_node.type
        hn_elements = hint_node.elements

        if hn_type is Any:
            return self._CompliesWithAnnotations(hint_node)

        if hn_type is none_t:
            # None is not supposed to have annotations. They are ignored if it does.
            return self.consolidated is None

        if hn_type is UnionType:
            return any(self._CastToHint(_elm) for _elm in hn_elements)

        if not isinstance(self.consolidated, hn_type):
            self.consolidated, success = SimplyCastValue(self.consolidated, hn_type)
            if not success:
                return False

        if (self.elements is None) and (hn_elements is None):
            # self.consolidated has been cast above by "SimplyCastValue" if needed.
            return self._CompliesWithAnnotations(hint_node)

        if (self.elements is None) and (hn_elements is not None):
            return False

        # For then on, self.elements is not None.

        if hn_elements is None:
            # The type hint does not fully specify valid values, so anything is valid.
            # self.consolidated has been cast above by "SimplyCastValue" if needed.
            return self._CompliesWithAnnotations(hint_node)

        n_value_elements = self.elements.__len__()
        n_hint_elements = hn_elements.__len__()
        has_ellipsis = (n_hint_elements == 2) and (hn_elements[1].type is EllipsisType)
        should_fake_ellipsis = (n_hint_elements == 1) and issubclass(
            hn_type, (list, set)
        )
        if (
            has_ellipsis
            or should_fake_ellipsis
            or (n_value_elements == n_hint_elements)
        ):
            if has_ellipsis or should_fake_ellipsis:
                adjusted_hn_elements = n_value_elements * (hn_elements[0],)
            else:
                adjusted_hn_elements = hn_elements
            for value_elm, hint_elm in zip(self.elements, adjusted_hn_elements):
                if not value_elm._CastToHint(hint_elm):
                    return False

            # self.consolidated has been cast above by "SimplyCastValue" if needed.
            return self._CompliesWithAnnotations(hint_node)

        return False

    def _CompliesWithAnnotations(self, hint_node: hint_tree_t, /) -> bool:
        """"""
        return all(
            nnt.ValueIsCompliant(self.consolidated) for nnt in hint_node.annotations
        )


def SimplyCastValue(value: Any, expected_type: simple_hint_h, /) -> tuple[Any, bool]:
    """
    Works for string interpretation (e.g., value == "1.0" and expected_type is float),
    or compatible type conversion (e.g., value == 1 and expected_type is float).

    If value is a string, the expected type might be instantiable from it, e.g.
    float("1.0"). However, a success does not mean that the interpretation is valid,
    e.g. tuple("(1, 2, 3)") which creates a tuple of the characters. To confirm that a
    success is also a correct interpretation, the string representation of the
    interpreted value is compared with the original string.
    """
    try:
        interpreted = expected_type(value)
        # If "expected_type" is a path and the value has a trailing folder separator,
        # then the general-purpose test fails. But the conversion is always valid.
        if issubclass(expected_type, path_t):  # Then value must be a string.
            success = True
        else:
            # Note: str(value) below is "redundant" if already a string.
            success = str(interpreted).replace(" ", "") == str(value).replace(" ", "")
    except:
        interpreted, success = None, False

    return interpreted, success


def CastValue(
    value: Any, hint: any_hint_h | hint_tree_t, /, *, only_check_validity: bool = False
) -> tuple[Any, bool] | bool:
    """"""
    value_tree = value_tree_t.NewFromValue(value)
    if not isinstance(hint, hint_tree_t):
        hint = hint_tree_t.NewFromTypeHint(hint)

    return value_tree.CastValue(hint, only_check_validity=only_check_validity)
