r"""Implement handler to check if the objects are equal."""

from __future__ import annotations

__all__ = [
    "EqualHandler",
    "EqualNanHandler",
    "SupportsEqual",
    "SupportsEqualNan",
]

import logging
from typing import TYPE_CHECKING, Protocol

from coola.equality.handler.base import BaseEqualityHandler
from coola.equality.handler.format import format_value_difference
from coola.equality.handler.mixin import HandlerEqualityMixin

if TYPE_CHECKING:
    from coola.equality.config import EqualityConfig

logger: logging.Logger = logging.getLogger(__name__)


class SupportsEqual(Protocol):
    r"""Implement a protocol to represent objects with a ``equal``
    method."""

    def equal(self, other: object) -> bool:
        r"""Return ``True`` if the two objects are equal, otherwise
        ``False``.

        Args:
            other: The value to compare with.

        Returns:
            ``True`` if the two objects are equal, otherwise ``False``
        """


class SupportsEqualNan(Protocol):
    r"""Implement a protocol to represent objects with a ``equal``
    method with an option compare NaNs."""

    def equal(self, other: object, equal_nan: bool = False) -> bool:
        r"""Return ``True`` if the two objects are equal, otherwise
        ``False``.

        Args:
            other: The value to compare with.
            equal_nan: Whether to compare NaN's as equal. If ``True``,
                NaN's in both objects will be considered equal.

        Returns:
            ``True`` if the two objects are equal, otherwise ``False``
        """


class EqualHandler(HandlerEqualityMixin, BaseEqualityHandler):
    r"""Check if the two objects have the same data.

    This handler returns ``False`` if the two objects are different
    data, otherwise it returns ``True``. The first object must have
    a ``equal`` attribute which indicates if the two objects are
    equal or not.

    Example:
        ```pycon
        >>> import math
        >>> from coola.equality.config import EqualityConfig
        >>> from coola.equality.handler import EqualHandler
        >>> class MyFloat:
        ...     def __init__(self, value: float) -> None:
        ...         self._value = float(value)
        ...     def equal(self, other: object) -> bool:
        ...         if type(other) is not type(self):
        ...             return False
        ...         return self._value == other._value
        ...
        >>> config = EqualityConfig()
        >>> handler = EqualHandler()
        >>> handler.handle(MyFloat(42), MyFloat(42), config)
        True
        >>> handler.handle(MyFloat(42), 42, config)
        False
        >>> handler.handle(MyFloat(42), float("nan"), config)
        False

        ```
    """

    def handle(self, actual: SupportsEqual, expected: object, config: EqualityConfig) -> bool:
        if not actual.equal(expected):
            if config.show_difference:
                logger.info(format_value_difference(actual=actual, expected=expected))
            return False
        return True


class EqualNanHandler(HandlerEqualityMixin, BaseEqualityHandler):
    r"""Check if the two objects have the same data.

    This handler returns ``False`` if the two objects are different
    data, otherwise it returns ``True``. The first object must have
    a ``equal`` attribute which indicates if the two objects are
    equal or not.

    Example:
        ```pycon
        >>> import math
        >>> from coola.equality.config import EqualityConfig
        >>> from coola.equality.handler import EqualNanHandler
        >>> class MyFloat:
        ...     def __init__(self, value: float) -> None:
        ...         self._value = float(value)
        ...     def equal(self, other: object, equal_nan: bool = False) -> bool:
        ...         if type(other) is not type(self):
        ...             return False
        ...         if equal_nan and math.isnan(self._value) and math.isnan(other._value):
        ...             return True
        ...         return self._value == other._value
        ...
        >>> config = EqualityConfig()
        >>> handler = EqualNanHandler()
        >>> handler.handle(MyFloat(42), MyFloat(42), config)
        True
        >>> handler.handle(MyFloat(42), 42, config)
        False
        >>> handler.handle(MyFloat(float("nan")), MyFloat(float("nan")), config)
        False
        >>> config.equal_nan = True
        >>> handler.handle(MyFloat(float("nan")), MyFloat(float("nan")), config)
        True

        ```
    """

    def handle(self, actual: SupportsEqualNan, expected: object, config: EqualityConfig) -> bool:
        if not actual.equal(expected, equal_nan=config.equal_nan):
            if config.show_difference:
                logger.info(format_value_difference(actual=actual, expected=expected))
            return False
        return True
