from abc import ABC, abstractmethod
from typing import Iterator, Callable, Any

from monads.functional_types import Predicate, Function
from monads.monad import Monad, I, O, U


class Option(Monad[I], ABC):
    """
    Represents an optional value: either Some(value) or Nothing.

    Use `Option.of(value)` to create an Option that is Some if value is not None,
    or Nothing if value is None.

    This is inspired by Scala's Option monad.
    """

    @staticmethod
    def of(value: I) -> "Option[I]":
        """
        Creates an Option wrapping the given value.

        Returns:
            Some(value) if value is not None,
            Nothing if value is None.
        """
        return Nothing if value is None else Some(value)

    @property
    @abstractmethod
    def is_empty(self) -> bool:
        """
        Returns True if this Option is empty (Nothing).
        """
        pass

    @property
    @abstractmethod
    def is_defined(self) -> bool:
        """
        Returns True if this Option contains a value (Some).
        """
        pass

    @abstractmethod
    def fold(self, default: O, mapping_fn: Function[I, O]) -> O:
        """
        Extracts a value from this Option by applying one of two cases:

        - If Some(value), applies `mapping_fn(value)`.
        - If Nothing, returns `default`.

        Args:
            default: The fallback value if empty.
            mapping_fn: Function to apply if value is present.

        Returns:
            Result of type O.
        """
        pass


class Some(Option[I]):
    """
    Represents an Option containing a non-None value.
    """

    def __init__(self, value: I):
        if value is None:
            raise ValueError("value cannot be None")
        self._value: I = value

    @property
    def is_empty(self) -> bool:
        return False

    @property
    def is_defined(self) -> bool:
        return True

    def for_each(self, fn: Function[I, U]) -> None:
        """
        Applies `fn` for side effects on the contained value.
        """
        fn(self._value)

    def flatten(self) -> "Option[I]":
        """
        If the contained value is itself an Option, returns it (flatten one level).
        Otherwise returns self.
        """
        return self._value if isinstance(self._value, Option) else self

    def map(self, mapping_fn: Function[I, O]) -> "Option[O]":
        """
        Transforms the contained value using `mapping_fn`, returning an Option.
        """
        return Option.of(mapping_fn(self._value))

    def flat_map(self, mapping_fn: Function[I, "Option[O]"]) -> "Option[O]":
        """
        Applies a function returning an Option to the contained value, flattening the result.
        """
        return mapping_fn(self._value)

    def get_or_else(self, default: I) -> I:
        """
        Returns the contained value.
        """
        return self._value

    def or_else(self, default: "Option[I]") -> "Option[I]":
        """
        Returns self, ignoring the provided default Option.
        """
        return self

    def fold(self, default: O, mapping_fn: Function[I, O]) -> O:
        """
        Applies `mapping_fn` to the contained value.
        """
        return mapping_fn(self._value)

    def filter(self, predicate: Predicate[I]) -> "Option[I]":
        """
        Returns self if the predicate holds for the value, otherwise Nothing.
        """
        return self if predicate(self._value) else Nothing

    def __iter__(self) -> Iterator[I]:
        """
        Iterates over the contained value.
        """
        yield self._value


class _Nothing(Option[None]):
    """
    Represents an empty Option (no value).
    """

    @property
    def is_empty(self) -> bool:
        return True

    @property
    def is_defined(self) -> bool:
        return False

    def for_each(self, fn: Function[I, U]) -> None:
        """
        Does nothing because there is no value.
        """
        return

    def map(self, mapping_fn: Function[I, O]) -> "_Nothing":
        """
        Returns Nothing unchanged.
        """
        return Nothing

    def flatten(self) -> "Option[I]":
        """
        Returns Nothing unchanged.
        """
        return Nothing

    def flat_map(self, mapping_fn: Function[I, "Option[O]"]) -> "Option[I]":
        """
        Returns Nothing unchanged.
        """
        return Nothing

    def get_or_else(self, default: I) -> I:
        """
        Returns the provided default value.
        """
        return default

    def or_else(self, default: "Option[I]") -> "Option[I]":
        """
        Returns the provided fallback Option.
        """
        return default

    def fold(self, default: O, mapping_fn: Function[I, O]) -> O:
        """
        Returns the default value.
        """
        return default

    def filter(self, predicate: Predicate[I]) -> "Option[I]":
        """
        Returns Nothing unchanged.
        """
        return Nothing

    def __iter__(self) -> Iterator[I]:
        """
        Yields nothing because there is no value.
        """
        return iter([])


Nothing = _Nothing()


def to_option(fn: Callable[..., Any]) -> Callable[..., "Option[Any]"]:
    """
    Decorator that wraps a function's return value into an Option.

    Usage:

        @to_option
        def maybe_get_value():
            # returns a value or None
            return compute_something()

        maybe_get_value()  # Some(value) or Nothing

    Args:
        fn: The function to wrap.

    Returns:
        A function returning Option wrapping the original function's output.
    """
    def wrapper(*args, **kwargs) -> "Option[Any]":
        return Option.of(fn(*args, **kwargs))
    return wrapper
