from typing import Tuple, TypeVar, Union

# Point, Vector, and AbstractNumber are defined in flexohr.core.abstract
# and re-exported here for backward compatibility within pitchspace.
from flexohr.core.abstract import AbstractNumber, Point, Vector

from .errors import (
    InitValueNotConvertibleToFifths,
    InitValueNotConvertibleToSemitones,
    InitValueNotConvertibleToStep,
    OperationUndefined,
    PropertyUndefined,
)
from .utils import (
    fifths2semitones,
    fifths2steps,
    name2fifths,
    name2semitones,
    name2steps,
)


class PotentialPoint(Point):
    """A Point or a promise of a Point, that is, a vector that can yield a point once added to another point."""


class PitchClass(Point):
    @property
    def semitones(self) -> int:
        raise PropertyUndefined(type(self).__name__)

    @property
    def fifths(self) -> int:
        raise PropertyUndefined(type(self).__name__)

    @property
    def steps(self) -> int:
        raise PropertyUndefined(type(self).__name__)


class IntervalClass(Vector):
    @property
    def semitones(self) -> int:
        raise PropertyUndefined(type(self).__name__)

    @property
    def fifths(self) -> int:
        raise PropertyUndefined(type(self).__name__)

    @property
    def steps(self) -> int:
        raise PropertyUndefined(type(self).__name__)


T = TypeVar("T", bound="IntType")


class IntType(int, AbstractNumber):
    """Abstract class for custom integer types. For the usual numerical operators, instances behave
    like an integer when the other value is also an integer. If the other value, however, is an
    IntType, an NotImplementedError is thrown unless the operation is defined for the respective subclass of IntType.
    The actual int value is stored in the field _value. The various subclasses provide different properties for
    accessing this field.
    """

    @staticmethod
    def convert_init_value(value: Union[int, str]) -> int:
        """Used by the constructor to convert the initial value into the integer that will be stored."""
        return int(value)

    def __new__(cls, value: Union[int, str]) -> T:
        """Can be created from an integer or any type that int() accepts."""
        converted_value = cls.convert_init_value(value)
        instance = int.__new__(cls, converted_value)
        instance._value = converted_value
        return instance

    @property
    def name(self) -> str:
        return str(self._value)

    def __str__(self) -> str:
        """String representation should work as a constructor."""
        return f"{type(self).__name__}({self.name})"

    def __repr__(self) -> str:
        """Representation should be short for slim display within containers."""
        return f"{type(self).__name__}({self.name})"

    def __neg__(self) -> T:
        raise OperationUndefined(f"- {type(self)}")

    def __add__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(self)} + {type(other)}")
        return self.__class__(self._value + other)

    def __sub__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(self)} - {type(other)}")
        return self.__class__(self._value - other)

    def __mul__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(self)} * {type(other)}")
        return self.__class__(self._value * other)

    def __floordiv__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(self)} // {type(other)}")
        return self.__class__(self._value // other)

    def __truediv__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(self)} / {type(other)}")
        return self.__class__(int(self._value / other))

    def __mod__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(self)} % {type(other)}")
        return self.__class__(self._value % other)

    def __divmod__(self, other: Union[T, int]) -> Tuple[int, int]:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"divmod({type(self)}, {type(other)})")
        return int(self._value).__divmod__(other)

    def __radd__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(other)} + {type(self)}")
        return self.__class__(other + self._value)

    def __rsub__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(other)} - {type(self)}")
        return self.__class__(other - self._value)

    def __rmul__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(other)} * {type(self)}")
        return self.__class__(other * self._value)

    def __rfloordiv__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(other)} // {type(self)}")
        return self.__class__(other // self._value)

    def __rtruediv__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(other)} / {type(self)}")
        return self.__class__(int(other / self._value))

    def __rmod__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(other)} % {type(self)}")
        return self.__class__(other % self._value)

    def __rdivmod__(self, other: Union[T, int]) -> Tuple[int, int]:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"divmod({type(other)}, {type(self)})")
        return int(other).__divmod__(self._value)

    def __pow__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(self)} ** {type(other)}")
        return self.__class__(self._value**other)

    def __rpow__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(other)} ** {type(self)}")
        return self.__class__(other**self._value)

    def __and__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(self)} & {type(other)}")
        return self.__class__(self._value & other)

    def __or__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(self)} | {type(other)}")
        return self.__class__(self._value | other)

    def __xor__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(self)} ^ {type(other)}")
        return self.__class__(self._value ^ other)

    def __lshift__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(self)} << {type(other)}")
        return self.__class__(self._value << other)

    def __rshift__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(self)} >> {type(other)}")
        return self.__class__(self._value >> other)

    def __rand__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(other)} & {type(self)}")
        return self.__class__(other & self._value)

    def __ror__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(other)} | {type(self)}")
        return self.__class__(other | self._value)

    def __rxor__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(other)} ^ {type(self)}")
        return self.__class__(other ^ self._value)

    def __rlshift__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(other)} << {type(self)}")
        return self.__class__(other << self._value)

    def __rrshift__(self, other: Union[T, int]) -> T:
        if isinstance(other, (IntType, PairOfInts)):
            raise OperationUndefined(f"{type(other)} >> {type(self)}")
        return self.__class__(other >> self._value)

    def __eq__(self, other) -> bool:
        if isinstance(other, IntType) and not (type(self) is type(other)):
            raise OperationUndefined(f"{type(self)} == {type(other)}")
        return self._value == other

    def __lt__(self, other: Union[T, int]) -> bool:
        if isinstance(other, IntType) and not (type(self) is type(other)):
            raise OperationUndefined(f"{type(self)} < {type(other)}")
        return self._value < other

    def __le__(self, other: Union[T, int]) -> bool:
        if isinstance(other, IntType) and not (type(self) is type(other)):
            raise OperationUndefined(f"{type(self)} <= {type(other)}")
        return self._value <= other

    def __gt__(self, other: Union[T, int]) -> bool:
        if isinstance(other, IntType) and not (type(self) is type(other)):
            raise OperationUndefined(f"{type(self)} > {type(other)}")
        return self._value > other

    def __ge__(self, other: Union[T, int]) -> bool:
        if isinstance(other, IntType) and not (type(self) is type(other)):
            raise OperationUndefined(f"{type(self)} >= {type(other)}")
        return self._value >= other


class Octaves(IntType):
    """Integer type whose _value field represents a number of octaves
    for combination with all types of pitch and interval classes."""

    @property
    def semitones(self) -> int:
        """This scalar's value."""
        return

    @property
    def fifths(self) -> int:
        """Semitones expressed as the number of fifths. Will require inference logic or additional information."""
        return

    @property
    def octaves(self) -> int:
        """Octave information will depend on the subtype."""
        return self._value

    @property
    def steps(self) -> int:
        return

    def __add__(self, other) -> int:
        if isinstance(other, Octaves):
            return self._value + other._value
        return super().__add__(other)

    def __sub__(self, other) -> int:
        if isinstance(other, Octaves):
            return self._value - other._value
        return super().__add__(other)

    def __neg__(self):
        return self.__new__(-self._value)


class OctaveNumber(Octaves):
    """This :class:`Octaves` integer represents a particular octave where 4 stands for the
    "middle" octave (the pitches of which would be marked with a prime ' in Helmholtz notation).
    In this convention, MIDI number 0 has OctaveNumber -1.
    """

    def __repr__(self):
        return f"Octave({self.name})"

    def __str__(self):
        return f"Octave({self.name})"


semitones = TypeVar("semitones", bound="SemitonesScalar")


class SemitonesScalar(IntType):
    """Integer type whose _value field represents a number of semitones,
    such as a MIDI number or chromatic pitch class."""

    @staticmethod
    def convert_init_value(value: Union[int, str, IntType]) -> int:
        if isinstance(value, FifthsScalar):
            converted = fifths2semitones(value)
        elif isinstance(value, StepsScalar):
            raise TypeError(f"Cannot convert {type(value)} to semitones.")
        elif isinstance(value, str):
            converted = name2semitones(value)
        else:
            converted = int(value)
        return converted % 12

    @property
    def semitones(self) -> int:
        """This scalar's value."""
        return self._value

    @property
    def fifths(self) -> int:
        """Semitones expressed as the number of fifths. Will require inference logic or additional information."""
        raise PropertyUndefined(type(self).__name__)

    @property
    def octave(self) -> int:
        """OctaveNumber information will depend on the subtype."""
        raise PropertyUndefined(type(self).__name__)

    @property
    def octaves(self) -> int:
        """Octave information will depend on the subtype."""
        raise PropertyUndefined(type(self).__name__)

    def __new__(cls, value: Union[int, str]) -> semitones:
        try:
            return super().__new__(cls, value)
        except (TypeError, ValueError):
            raise InitValueNotConvertibleToSemitones(cls=cls, val=value)

    def __neg__(self) -> int:
        return super().__neg__()


fifths = TypeVar("fifths", bound="FifthsScalar")


class FifthsScalar(IntType):
    """Integer type whose _value field represents a number of perfect fifth intervals,
    such as a spelled pitch class or a specific interval."""

    @staticmethod
    def convert_init_value(value: Union[int, str, IntType]) -> int:
        if isinstance(value, str):
            converted = name2fifths(value)
        elif isinstance(value, Union[SemitonesScalar, StepsScalar]):
            raise TypeError(f"Cannot convert {type(value)} to fifths.")
        else:
            converted = int(value)
        return converted

    @property
    def fifths(self) -> int:
        """This scalar's value."""
        return self._value

    @property
    def semitones(self) -> int:
        """The stack of fifths represented by this scalar, expressed in semitones."""
        return fifths2semitones(self._value)

    @property
    def steps(self) -> int:
        return fifths2steps(self._value)

    @property
    def octave(self) -> int:
        """OctaveNumber information will depend on the subtype."""
        raise PropertyUndefined(type(self).__name__)

    @property
    def octaves(self) -> int:
        """Octave information will depend on the subtype."""
        raise PropertyUndefined(type(self).__name__)

    def __new__(cls, value: Union[int, str]) -> fifths:
        try:
            return super().__new__(cls, value)
        except (TypeError, ValueError):
            raise InitValueNotConvertibleToFifths(cls=cls, val=value)

    def __str__(self) -> str:
        """Constructors of subtypes will work with string representations."""
        return f"{type(self).__name__}('{self.name}')"

    def __repr__(self) -> str:
        return self.name

    def __neg__(self) -> int:
        return super().__neg__()


steps = TypeVar("steps", bound="StepsScalar")


class StepsScalar(IntType):
    """Integer type whose _value field represents a number of steps,
    such as a generic pitch class or a generic interval."""

    @staticmethod
    def convert_init_value(value: Union[int, str, IntType]) -> int:

        if isinstance(value, str):
            converted = name2steps(value)
        elif isinstance(value, SemitonesScalar):
            raise TypeError(f"Cannot convert {type(value)} to steps.")
        elif isinstance(value, FifthsScalar):
            converted = fifths2steps(value)
        else:
            converted = int(value)
        return converted % 7

    def __new__(cls, value: Union[int, str]) -> fifths:
        try:
            return super().__new__(cls, value)
        except (TypeError, ValueError):
            raise InitValueNotConvertibleToStep(cls=cls, val=value)

    def __str__(self) -> str:
        """Constructors of subtypes will work with string representations."""
        return f"{type(self).__name__}('{self.name}')"

    def __repr__(self) -> str:
        return self.name

    def __neg__(self) -> int:
        return super().__neg__()

    @property
    def fifths(self) -> int:
        raise PropertyUndefined(type(self).__name__)

    @property
    def semitones(self) -> int:
        raise PropertyUndefined(type(self).__name__)

    @property
    def steps(self) -> int:
        return self._value

    @property
    def octave(self) -> int:
        """OctaveNumber information will depend on the subtype."""
        raise PropertyUndefined(type(self).__name__)

    @property
    def octaves(self) -> int:
        """Octave information will depend on the subtype."""
        raise PropertyUndefined(type(self).__name__)


U = TypeVar("U", bound=IntType)
P = TypeVar("P", bound="PairOfInts")


class PairOfInts(Tuple[T, U]):
    """Abstract class representing a pair (tuple with 2 elements) of IntType instances. Its purpose is to
    bundle functionality for all pitches and intervals, which are being represented as combinations of
    pitch/interval class and an Octaves values.
    Mirroring the IntType implementation, the usual numerical operators operate on the left element (the
    pitch/interval class) when `other` is an integer or IntType, leaving the right element (the Octaves) untouched.
    If the other value, however, is another PairOfInts or a tuple of integers,
    an NotImplementedError is thrown unless the operation is defined for the respective subclass of PairOfInts.
    """

    @staticmethod
    def convert_init_value(
        value: Union[Tuple[T | int, U | int], str, int, Tuple[Tuple]],
    ) -> Tuple[T, U]:
        """Used by the constructor to convert the initial value into a pair of IntType."""
        if isinstance(value, str):
            raise NotImplementedError
        if isinstance(value, int):
            raise NotImplementedError
        a, b = value
        if not isinstance(a, IntType):
            a = IntType(a)
        if not isinstance(b, IntType):
            b = IntType(b)
        return (a, b)

    def __new__(cls, value: Union[Tuple[T | int, U | int], str, int]) -> P:
        if type(value) is cls:
            converted_value = value
        else:
            converted_value = cls.convert_init_value(value)
        instance = tuple.__new__(cls, converted_value)
        instance._value = converted_value
        return instance

    @property
    def name(self) -> str:
        return f"{self[0].name}{self[1].name}"

    def __str__(self) -> str:
        """String representation should work as a constructor."""
        return f"{type(self).__name__}({self.name})"

    def __repr__(self) -> str:
        """Representation should be short for slim display within containers."""
        return f"{type(self).__name__}({self.name})"

    def __neg__(self) -> Tuple[T, U]:
        raise OperationUndefined(f"- {type(self)}")

    def __add__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, PairOfInts):
            raise OperationUndefined(f"{type(self)} + {type(other)}")
        if isinstance(other, tuple):
            t, u = other
            return self.__class__((self[0] + t, self[1] + u))
        return self.__class__((self[0] + other, self[1]))

    def __sub__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, PairOfInts):
            raise OperationUndefined(f"{type(self)} - {type(other)}")
        if isinstance(other, tuple):
            t, u = other
            return self.__class__((self[0] - t, self[1] - u))
        return self.__class__((self[0] - other, self[1]))

    def __mul__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(self)} * {type(other)}")
        return self.__class__((self[0] * other, self[1]))

    def __floordiv__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(self)} // {type(other)}")
        return self.__class__((self[0] // other, self[1]))

    def __truediv__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(self)} / {type(other)}")
        return self.__class__((self[0] / other, self[1]))

    def __mod__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(self)} % {type(other)}")
        return self.__class__((self[0] % other, self[1]))

    def __divmod__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[int, int]:
        raise OperationUndefined(f"divmod({type(self)}, {type(other)})")

    def __radd__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, PairOfInts):
            raise OperationUndefined(f"{type(self)} + {type(other)}")
        if isinstance(other, tuple):
            t, u = other
            return self.__class__((t + self[0], u + self[1]))
        return self.__class__((other + self[0], self[1]))

    def __rsub__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, PairOfInts):
            raise OperationUndefined(f"{type(self)} - {type(other)}")
        if isinstance(other, tuple):
            t, u = other
            return self.__class__((t - self[0], u - self[1]))
        return self.__class__((other - self[0], self[1]))

    def __rmul__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(self)} * {type(other)}")
        return self.__class__((other * self[0], self[1]))

    def __rfloordiv__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(self)} // {type(other)}")
        return self.__class__((other // self[0], self[1]))

    def __rtruediv__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(self)} / {type(other)}")
        return self.__class__((other / self[0], self[1]))

    def __rmod__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(self)} % {type(other)}")
        return self.__class__((other % self[0], self[1]))

    def __rdivmod__(self, other: Union[T, int]) -> Tuple[int, int]:
        raise OperationUndefined(f"divmod({type(other)}, {type(self)})")

    def __pow__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(self)} ** {type(other)}")
        return self.__class__((self[0] ** other, self[1]))

    def __rpow__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(other)} ** {type(self)}")
        return self.__class__((other ** self[0], self[1]))

    def __and__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(self)} & {type(other)}")
        return self.__class__((self[0] & other, self[1]))

    def __or__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(self)} | {type(other)}")
        return self.__class__((self[0] | other, self[1]))

    def __xor__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(self)} ^ {type(other)}")
        return self.__class__((self[0] ^ other, self[1]))

    def __lshift__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(self)} << {type(other)}")
        return self.__class__((self[0] << other, self[1]))

    def __rshift__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(self)} >> {type(other)}")
        return self.__class__((self[0] >> other, self[1]))

    def __rand__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(other)} & {type(self)}")
        return self.__class__((other & self[0], self[1]))

    def __ror__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(other)} | {type(self)}")
        return self.__class__((other | self[0], self[1]))

    def __rxor__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(other)} ^ {type(self)}")
        return self.__class__((other ^ self[0], self[1]))

    def __rlshift__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(other)} << {type(self)}")
        return self.__class__((other << self[0], self[1]))

    def __rrshift__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> Tuple[T, U]:
        if isinstance(other, (PairOfInts, tuple)):
            raise OperationUndefined(f"{type(other)} >> {type(self)}")
        return self.__class__((other >> self[0], self[1]))

    def __eq__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> bool:
        if isinstance(other, int) or (isinstance(other, PairOfInts) and not isinstance(self, type(other))):
            raise OperationUndefined(f"{type(self)} == {type(other)}")
        return self == other

    def __lt__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> bool:
        if isinstance(other, int) or (isinstance(other, PairOfInts) and not isinstance(self, type(other))):
            raise OperationUndefined(f"{type(self)} < {type(other)}")
        return self < other

    def __le__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> bool:
        if isinstance(other, int) or (isinstance(other, PairOfInts) and not isinstance(self, type(other))):
            raise OperationUndefined(f"{type(self)} <= {type(other)}")
        return self <= other

    def __gt__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> bool:
        if isinstance(other, int) or (isinstance(other, PairOfInts) and not isinstance(self, type(other))):
            raise OperationUndefined(f"{type(self)} > {type(other)}")
        return self > other

    def __ge__(self, other: Union[T, int, P, Tuple[T | int, U | int]]) -> bool:
        if isinstance(other, int) or (isinstance(other, PairOfInts) and not isinstance(self, type(other))):
            raise OperationUndefined(f"{type(self)} >= {type(other)}")
        return self >= other


Pi = TypeVar("Pi", bound=PitchClass)
Ocs = TypeVar("Ocs", bound=Octaves)
Oc = TypeVar("Oc", bound=OctaveNumber)
Iv = TypeVar("Iv", bound=IntervalClass)


class Pitch(PairOfInts[Pi, Oc], Point):
    @property
    def octave(self) -> Oc:
        return self[1]

    @property
    def pitch_class(self) -> Pi:
        return self[0]

    @property
    def semitones(self) -> int:
        raise PropertyUndefined(type(self).__name__)

    @property
    def fifths(self) -> int:
        raise PropertyUndefined(type(self).__name__)

    @property
    def steps(self) -> int:
        raise PropertyUndefined(type(self).__name__)


class Interval(PairOfInts[Iv, Ocs], Vector):
    @property
    def octaves(self) -> Ocs:
        return self[1]

    @property
    def interval_class(self) -> Iv:
        return self[0]

    @property
    def semitones(self) -> int:
        raise PropertyUndefined(type(self).__name__)

    @property
    def fifths(self) -> int:
        raise PropertyUndefined(type(self).__name__)

    @property
    def steps(self) -> int:
        raise PropertyUndefined(type(self).__name__)


if __name__ == "__main__":
    a = SemitonesScalar(4)
    b = SemitonesScalar("5")
    c = FifthsScalar(6)
    d = FifthsScalar("7")
    # these operations will return integers
    print(f"{a} + 1 = {a + 1}")
    print(f"{b} * -1 = {b * -1}")
    print(f"{c} // 2 = {c // 2}")
    print(f"{d} ** 2 = {d ** 2}")
    # these operations will fail
    try:
        a + b
    except NotImplementedError as e:
        print(f'{a} + {b} failed with "{e}"')
    try:
        b * c
    except NotImplementedError as e:
        print(f'{b} * {c} failed with "{e}"')
    try:
        -a
    except NotImplementedError as e:
        print(f'-{a} failed with "{e}"')
