from typing import Optional

from flexohr.errors import (
    FlexohrError,
    FlexohrTypeError,
    FlexohrValueError,
    register_paradigm_errors,
)


class CustomException(FlexohrError):
    """Exception that have a default message that can be extended. Default messages
    should be defined without trailing punctuation.
    """

    default_message = "Exception"

    def __init__(self, message: Optional[str] = None):
        if message:
            msg = f"{self.default_message}: {message}"
        else:
            msg = self.default_message + "."
        self.message = msg
        super().__init__(msg)


# region TypeErrors


class CustomTypeError(CustomException, FlexohrTypeError):
    """TypeErrors that have a default message that can be extended. Default messages
    should be defined without trailing punctuation.

    Examples:

        >>> raise CustomTypeError
        TypeError.
        >>> raise CustomTypeError("This is a custom message.")
        TypeError: This is a custom message.

    """

    default_message = "TypeError"


class OperationUndefined(CustomTypeError):
    """Raised when an operation is not defined for the given arguments."""

    default_message = "Operation not defined for the given types"


class PropertyUndefined(CustomTypeError):
    """Raised when a property is not defined for the given type.
    This usually means that an object needs to be converted to another type first.

    Examples:

        >>> EnharmonicPitchClass(1).fifths
        PropertyUndefined: Property not defined for this type: EnharmonicPitchClass
    """

    default_message = "Property not defined for this type"


class StepUnknown(OperationUndefined):
    """Raised when knowing a step (GP or GPC) would be required for an operation."""

    default_message = "Knowing the step would be required for the operation"


# endregion TypeErrors
# region ValueErrors


class CustomValueError(CustomException, FlexohrValueError):
    """ValueErrors that have a default message that can be extended. Default messages
    should be defined without trailing punctuation.
    """

    default_message = "ValueError"


class InvalidInitValue(CustomValueError):
    """Raised when an invalid value is passed to an object's initializer."""

    default_message = "Object cannot be initialized from the given value"

    def __init__(self, message: Optional[str] = None, cls=None, val=None):  # noqa: B042
        """Raised when an invalid value is passed to an object's initializer.

        Args:
            message: Custom message.
            cls: Class that cannot be initialized from the given value.
            val: Invalid value.
        """
        msg = message if message else ""
        if cls or val:
            msg += "\n"
        if cls:
            try:
                msg += f"Class: {cls.__module__}.{cls.__qualname__}"
            except Exception:
                msg += f"Class: {cls}"
            if val:
                msg += ", "
        if val:
            msg += f"Value: {val!r}"
        self.cls = cls
        self.val = val
        self.message = msg
        super().__init__(msg)


class InitValueNotConvertibleToSemitones(InvalidInitValue):
    default_message = "Value cannot be converted to semitones"


class InitValueNotConvertibleToFifths(InvalidInitValue):
    default_message = "Value cannot be converted to fifths"


class InitValueNotConvertibleToStep(InvalidInitValue):
    default_message = "Value cannot be converted to a step A-G"


# endregion ValueErrors


# Register pitchspace errors in the FlexOHR error hierarchy
register_paradigm_errors(
    "pitchspace",
    OperationUndefined,
    PropertyUndefined,
    StepUnknown,
    InvalidInitValue,
    InitValueNotConvertibleToSemitones,
    InitValueNotConvertibleToFifths,
    InitValueNotConvertibleToStep,
)
