"""Functions that can be used to test encodings."""
from typing import Any, Callable

from moptipy.api.encoding import Encoding, check_encoding
from moptipy.api.space import Space
from moptipy.tests.component import validate_component
from moptipy.utils.types import type_error


def validate_encoding(encoding: Encoding,
                      search_space: Space | None = None,
                      solution_space: Space | None = None,
                      make_search_space_element_valid:
                      Callable[[Any], Any] | None = lambda x: x,
                      is_deterministic: bool = True) -> None:
    """
    Check whether an object is a proper moptipy encoding.

    :param encoding: the encoding to test
    :param search_space: the search space
    :param make_search_space_element_valid: a method that can turn a point
        from the space into a valid point
    :param solution_space: the solution space
    :param is_deterministic: is the mapping deterministic?
    :raises ValueError: if `encoding` is not a valid
        :class:`~moptipy.api.encoding.Encoding`
    :raises TypeError: if `encoding` is of the wrong type or a wrong type is
        encountered
    """
    if not isinstance(encoding, Encoding):
        raise type_error(encoding, "encoding", Encoding)
    check_encoding(encoding)
    validate_component(encoding)

    count: int = 0
    if search_space is not None:
        count += 1
    if make_search_space_element_valid is not None:
        count += 1
    if solution_space is not None:
        count += 1
    if count <= 0:
        return
    if count < 3:
        raise ValueError(
            "either provide all of search_space, "
            "make_search_space_element_valid, and solution_space or none.")

    x1 = search_space.create()
    if x1 is None:
        raise ValueError("Provided search space created None?")
    x1 = make_search_space_element_valid(x1)
    search_space.validate(x1)

    y1 = solution_space.create()
    if y1 is None:
        raise ValueError("Provided solution space created None?")

    if not (hasattr(encoding, "decode") and callable(getattr(
            encoding, "decode"))):
        raise ValueError("encoding must have method decode.")

    encoding.decode(x1, y1)
    solution_space.validate(y1)
    s1: str = solution_space.to_str(y1)
    if s1 is None:
        raise ValueError("to_str() returned None!")
    if len(s1) <= 0:
        raise ValueError("to_str() return empty string")

    y2 = solution_space.create()
    if y2 is None:
        raise ValueError("Provided solution space created None?")
    if y1 is y2:
        raise ValueError("Provided solution space created "
                         "identical points?")
    encoding.decode(x1, y2)
    solution_space.validate(y2)
    s2: str = solution_space.to_str(y2)
    if s2 is None:
        raise ValueError("to_str() returned None!")
    if len(s2) <= 0:
        raise ValueError("to_str() return empty string")
    if is_deterministic:
        if not solution_space.is_equal(y1, y2):
            raise ValueError("Encoding must be deterministic and decode "
                             "identical points to same result.")
        if s1 != s2:
            raise ValueError(f"to_str(y1)={s1!r} but to_str(y2)={s2!r}!")

    x2 = search_space.create()
    if x2 is None:
        raise ValueError("Provided search space created None?")
    if x1 is x2:
        raise ValueError("Provided search space created "
                         "identical points?")

    search_space.copy(x2, x1)
    if not search_space.is_equal(x1, x2):
        raise ValueError("Copy method of search space did not result in "
                         "is_equal becoming true?")
    search_space.validate(x2)

    encoding.decode(x2, y2)
    solution_space.validate(y2)
    s2 = solution_space.to_str(y2)
    if s2 is None:
        raise ValueError("to_str() returned None!")
    if len(s2) <= 0:
        raise ValueError("to_str() return empty string")

    if is_deterministic:
        if not solution_space.is_equal(y1, y2):
            raise ValueError("Encoding must be deterministic and decode "
                             "equal points to same result.")
        if s1 != s2:
            raise ValueError(f"to_str(y1)={s1!r} but to_str(y2)={s2!r}!")
