from __future__ import annotations

from typing import Any, Tuple, Union

from .abstract import (
    FifthsScalar,
    Interval,
    IntervalClass,
    IntType,
    Octaves,
    SemitonesScalar,
    StepsScalar,
)
from .errors import InvalidInitValue, StepUnknown
from .utils import (
    fifths2interval_name,
    fifths2semitones,
    fifths2steps,
    general_tuple,
    interval_tuple_from_sd,
    parse_interval,
    semitone_difference,
    specific_tuple,
    step_difference,
)


class EnharmonicIntervalClass(SemitonesScalar, IntervalClass):
    """Enharmonic Interval Class (equivalent to the IC of pitch-class set theory).
    An EIC value is always within [0, 11] and can be interpreted as the distance between two piano keys when
    projected into the same octave.

    Initialized from an integer within [0, 11]. Other values will be converted. For instance:

        * integers outside this interval are transformed with modulo 12, e.g. EIC(-3) => EIC(9)
        * floats are converted to an integer (i.e., rounded)

    Integers represented as string are accepted, too.
    """

    def __str__(self):
        return f"EIC({self.semitones})"

    def __repr__(self):
        return f"EIC({self.semitones})"

    def _repr_html_(self):
        from flexohr.core.html_colors import PITCHSPACE_COLORS

        c = PITCHSPACE_COLORS["EIC"]
        return f'<span style="font-family:monospace;color:{c}"><b>EIC</b>(<i>{self.semitones}</i>)</span>'

    def __neg__(self):
        return EnharmonicIntervalClass(-self.semitones)

    def __add__(self, other: Union[Interval, IntervalClass, int, IntType]) -> Union[IntervalClass]:
        if isinstance(
            other,
            EnharmonicInterval | EnharmonicIntervalClass | SpecificInterval | SpecificIntervalClass,
        ):
            return EnharmonicIntervalClass((self.semitones + other.semitones) % 12)
        elif isinstance(other, GenericInterval | GenericIntervalClass):
            raise StepUnknown(f"{type(self)} + {type(other)}")

        return super().__add__(other)

    def __sub__(self, other: Union[Interval, IntervalClass, int, IntType]) -> Union[IntervalClass]:
        if isinstance(
            other,
            EnharmonicInterval | EnharmonicIntervalClass | SpecificInterval | SpecificIntervalClass,
        ):
            return EnharmonicIntervalClass((self.semitones - other.semitones) % 12)
        elif isinstance(other, GenericInterval | GenericIntervalClass):
            raise StepUnknown(f"{type(self)} - {type(other)}")

        return super().__sub__(other)

    def to_point(self) -> "EnharmonicPitchClass":  # noqa: F821
        from .pitch import EnharmonicPitchClass

        return EnharmonicPitchClass(self.semitones)


class SpecificIntervalClass(FifthsScalar, IntervalClass):
    """Specific Interval Class, also called tonal interval class, or simply 'interval name'. An SIC is
    numerically represented as a stack of perfect fifths, e.g. SIC("P4")._value == -1

    Initialized from an interval name (e.g. "M3" or "m6") or from an integer representing a stack of
    perfect fifths. Integers represented as string are accepted, too. Floats are
    converted to an integer (i.e., rounded).
    """

    @property
    def name(self):
        return fifths2interval_name(self.fifths)

    def __neg__(self):
        return SpecificIntervalClass(-self.fifths)

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

    def __repr__(self):
        return self.name

    def _repr_html_(self):
        from flexohr.core.html_colors import PITCHSPACE_COLORS

        c = PITCHSPACE_COLORS["SIC"]
        return f'<span style="font-family:monospace;color:{c}"><b>SIC</b>(<i>{self.name}</i>)</span>'

    def __add__(self, other: Union[Interval, IntervalClass, int, IntType]) -> Union[IntervalClass]:
        if isinstance(other, EnharmonicInterval | EnharmonicIntervalClass):
            return EnharmonicIntervalClass((self.semitones + other.semitones) % 12)
        elif isinstance(other, SpecificInterval | SpecificIntervalClass):
            return SpecificIntervalClass(self.fifths + other.fifths)
        elif isinstance(other, GenericInterval | GenericIntervalClass):
            return GenericIntervalClass((self.steps + other.steps) % 7)

        return super().__add__(other)

    def __sub__(self, other: Union[Interval, IntervalClass, int, IntType]) -> Union[IntervalClass]:
        if isinstance(other, EnharmonicInterval | EnharmonicIntervalClass):
            return EnharmonicIntervalClass((self.semitones - other.semitones) % 12)
        elif isinstance(other, SpecificInterval | SpecificIntervalClass):
            return SpecificIntervalClass(self.fifths - other.fifths)
        elif isinstance(other, GenericInterval | GenericIntervalClass):
            return GenericIntervalClass((self.steps - other.steps) % 7)

        return super().__sub__(other)

    def to_point(self) -> "SpecificPitchClass":  # noqa: F821
        from .pitch import SpecificPitchClass

        return SpecificPitchClass(self.fifths)


class GenericIntervalClass(StepsScalar, IntervalClass):
    """Generic Interval Class, also called 'interval number'. A GIC value is always within [0, 6] and can be
    interpreted as the distance between white piano keys projected into the same octave.

    Initialized from an integer within [0, 6]. Other values will be converted. For instance:

        * integers outside this interval are transformed with modulo 7, e.g. GIC(-3) => GIC(4)
        * interval names are mapped to their step, e.g. GIC("P5") => GIC(4)
        * floats are converted to an integer (i.e., rounded)

    Integers represented as string are accepted, too.
    """

    def __str__(self):
        return f"GIC({self.steps})"

    def __repr__(self):
        return f"GIC({self.steps})"

    def _repr_html_(self):
        from flexohr.core.html_colors import PITCHSPACE_COLORS

        c = PITCHSPACE_COLORS["GIC"]
        return f'<span style="font-family:monospace;color:{c}"><b>GIC</b>(<i>{self.steps}</i>)</span>'

    def __neg__(self):
        return GenericIntervalClass(-self.steps)

    def __add__(self, other: Union[Interval, IntervalClass, int, IntType]) -> Union[IntervalClass]:
        if isinstance(other, EnharmonicInterval | EnharmonicIntervalClass):
            raise StepUnknown(f"{type(self)} + {type(other)}")
        elif isinstance(
            other,
            GenericInterval | GenericIntervalClass | SpecificInterval | SpecificIntervalClass,
        ):
            return GenericIntervalClass((self.steps + other.steps) % 7)

        return super().__add__(other)

    def __sub__(self, other: Union[Interval, IntervalClass, int, IntType]) -> Union[IntervalClass]:
        if isinstance(other, EnharmonicInterval | EnharmonicIntervalClass):
            raise StepUnknown(f"{type(self)} - {type(other)}")
        elif isinstance(
            other,
            GenericInterval | GenericIntervalClass | SpecificInterval | SpecificIntervalClass,
        ):
            return GenericIntervalClass((self.steps - other.steps) % 7)

        return super().__sub__(other)


class EnharmonicInterval(Interval[EnharmonicIntervalClass, Octaves]):
    """An Enharmonic Interval is an :class:`EnharmonicIntervalClass` paired with an octave number and
    equivalent to a semitone distance (i.e., step and quality are not known).

    Initialized from a pair of two values (EIC, Octave) or from an integer representing a semitone
    distance. When initialized from a pair, the first value can be any value that can be used for
    initializing an EIC. Strings representing interval names (e.g., "P5:1") are also accepted.
    Integers represented as string are accepted, too.
    """

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

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

    def _repr_html_(self):
        from flexohr.core.html_colors import PITCHSPACE_COLORS

        c = PITCHSPACE_COLORS["EI"]
        return f'<span style="font-family:monospace;color:{c}"><b>EI</b>(<i>{self.name}</i>)</span>'

    def __neg__(self):
        return EnharmonicInterval((-self.interval_class.semitones, -self.octaves))

    @staticmethod
    def convert_init_value(
        value: Union[
            Tuple[EnharmonicIntervalClass | int, Octaves | int],
            str,
            int,
            "SpecificInterval",
        ],
    ) -> Tuple[EnharmonicIntervalClass, Octaves]:
        """Used by the constructor to convert the initial value into a pair of IntType."""
        if isinstance(value, tuple):
            interval_class, octave = value
            while interval_class < 0:
                interval_class += 12
                octave -= 1
            while interval_class >= 12:
                interval_class -= 12
                octave += 1
        elif (isinstance(value, str) and value.isdigit()) or isinstance(value, int):
            value_int = int(value)
            interval_class = value_int
            octave = value_int // 12
        elif isinstance(value, str):
            sign, fifths, octave = parse_interval(value)
            interval_class = fifths2semitones(fifths)
        else:
            raise InvalidInitValue(
                f"An EnharmonicInterval cannot be expressed as a {type(value)}. Pass an interval name as a string "
                f"or an (IntervalClass, Octave) tuple."
            )
        return EnharmonicIntervalClass(interval_class), Octaves(octave)

    def midi(self) -> int:
        if self.semitones in [0, 127]:
            return self.semitones
        raise ValueError(f"Cannot convert {self} to MIDI number.")

    @property
    def semitones(self) -> int:
        return int(self.octaves) * 12 + self.interval_class.semitones

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

    def __add__(
        self,
        other: Union[
            Interval,
            IntervalClass,
            int,
            IntType,
            Tuple[EnharmonicIntervalClass | int, Octaves | int],
        ],
    ) -> (
        EnharmonicInterval
        | EnharmonicIntervalClass
        | Interval
        | IntervalClass
        | tuple[EnharmonicIntervalClass, Octaves]
        | tuple[IntervalClass | Any, Octaves | Any]
    ):
        if isinstance(other, EnharmonicInterval | SpecificInterval):
            return EnharmonicInterval(
                general_tuple(
                    self.interval_class.semitones,
                    self.octaves,
                    other.interval_class.semitones,
                    other.octaves,
                    12,
                    1,
                )
            )
        elif isinstance(other, EnharmonicIntervalClass | SpecificIntervalClass):
            return EnharmonicIntervalClass((self.interval_class.semitones + other.semitones) % 12)
        elif isinstance(other, GenericIntervalClass | GenericInterval):
            raise StepUnknown(f"{type(self)} + {type(other)}")

        elif Tuple == type(other):
            return self + EnharmonicInterval(other)
        return super().__add__(other)

    def __sub__(
        self,
        other: Union[
            Interval,
            IntervalClass,
            IntType,
            int,
            Tuple[EnharmonicIntervalClass | int, Octaves | int],
        ],
    ) -> (
        EnharmonicInterval
        | EnharmonicIntervalClass
        | int
        | IntType
        | IntervalClass
        | Interval
        | tuple[EnharmonicIntervalClass, Octaves]
        | tuple[IntervalClass | Any, Octaves | Any]
    ):
        if isinstance(other, EnharmonicInterval | SpecificInterval):
            sd = semitone_difference(
                self.interval_class.semitones,
                self.octaves,
                other.interval_class.semitones,
                other.octaves,
            )
            return EnharmonicInterval(interval_tuple_from_sd(sd, 12))
        elif isinstance(other, EnharmonicIntervalClass | SpecificIntervalClass):
            return EnharmonicIntervalClass((self.interval_class.semitones - other.semitones) % 12)
        elif isinstance(other, GenericInterval | GenericIntervalClass):
            raise StepUnknown(f"{type(self)} - {type(other)}")

        elif Tuple == type(other):
            return self - EnharmonicInterval(other)
        return super().__sub__(other)

    def __eq__(self, other):
        if isinstance(other, EnharmonicInterval):
            return self.interval_class.semitones == other.interval_class.semitones and self.octaves == other.octaves
        return False

    def to_point(self) -> "EnharmonicPitchClass":  # noqa: F821
        from .pitch import EnharmonicPitchClass

        return EnharmonicPitchClass(self.semitones)


class SpecificInterval(Interval[SpecificIntervalClass, Octaves]):
    """A Specific Interval, also called "spelled interval", is a :class:`SpecificIntervalClass`
    paired with an octave number. It is the most complete interval representation because number,
    quality, and octave are known.

    Initialized from a pair of two values (SIC, Octave) or from an interval name such as "M3:1".
    When initialized from a pair, the first value can be any value that can be used for
    initializing an SIC.
    """

    @staticmethod
    def convert_init_value(
        value: Union[Tuple[SpecificIntervalClass | int, Octaves | int], str, int],
    ) -> Tuple[SpecificIntervalClass, Octaves]:
        """Used by the constructor to convert the initial value into a pair of IntType."""
        if isinstance(value, tuple):
            interval_class, octave = value
        elif isinstance(value, str):
            sign, interval_class, octave = parse_interval(value)
        elif isinstance(value, int):
            interval_class = value
            octave = 0
        else:
            raise InvalidInitValue(
                f"A SpecificInterval cannot be expressed as a {type(value)}. Pass an interval name as a string "
                f"(e.g., 'a4:1' for an augmented fourth plus an octave) "
                f"or an (IntervalClass, Octave) tuple."
            )
        return SpecificIntervalClass(interval_class), Octaves(octave)

    @property
    def fifths(self) -> int:
        return self.interval_class.fifths

    @property
    def midi(self) -> int:
        """Alias of the semitones property, representing the MIDI number of the pitch."""
        if self.semitones in [0, 127]:
            return self.semitones
        raise ValueError(f"Cannot convert {self} to MIDI number.")

    @property
    def name(self) -> str:
        return f"{self.interval_class.name}:{int(self.octaves)}"

    @property
    def semitones(self) -> int:
        """For an SP, the value of the semitones property correspond to the MIDI number."""
        return int(self.octaves) * 12 + self.interval_class.semitones

    @property
    def steps(self) -> int:
        return int(self.octaves) * 7 + self.interval_class.steps

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

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

    def _repr_html_(self):
        from flexohr.core.html_colors import PITCHSPACE_COLORS

        c = PITCHSPACE_COLORS["SI"]
        return f'<span style="font-family:monospace;color:{c}"><b>SI</b>(<i>{self.name}</i>)</span>'

    def __neg__(self):
        return SpecificInterval((-self.fifths, -self.octaves - 1))

    def __add__(
        self,
        other: Union[
            Interval,
            IntervalClass,
            int,
            IntType,
            Tuple[SpecificIntervalClass | int, Octaves | int],
        ],
    ) -> Interval | IntervalClass | tuple[SpecificIntervalClass, Octaves] | tuple[IntervalClass | Any, Octaves | Any]:
        if isinstance(other, EnharmonicInterval):
            return EnharmonicInterval(
                general_tuple(
                    self.interval_class.semitones,
                    self.octaves,
                    other.interval_class.semitones,
                    other.octaves,
                    12,
                    1,
                )
            )
        elif isinstance(other, SpecificInterval):
            return SpecificInterval(
                specific_tuple(
                    self.fifths,
                    self.interval_class.semitones,
                    self.octaves,
                    other.fifths,
                    other.interval_class.semitones,
                    other.octaves,
                    1,
                )
            )
        elif isinstance(other, GenericInterval):
            return GenericInterval(
                general_tuple(
                    self.interval_class.steps,
                    self.octaves,
                    other.interval_class.steps,
                    other.octaves,
                    7,
                    1,
                )
            )
        elif isinstance(other, EnharmonicIntervalClass):
            return EnharmonicIntervalClass((self.interval_class.semitones + other.semitones) % 12)
        elif isinstance(other, SpecificIntervalClass):
            return SpecificIntervalClass(self.fifths + other.fifths)
        elif isinstance(other, GenericIntervalClass):
            return GenericIntervalClass((self.interval_class.steps + other.steps) % 7)

        elif Tuple == type(other):
            return self + SpecificInterval(other)
        return super().__add__(other)

    def __sub__(
        self,
        other: Union[
            Interval,
            IntervalClass,
            int,
            IntType,
            Tuple[SpecificIntervalClass | int, Octaves | int],
        ],
    ) -> Interval | IntervalClass | tuple[SpecificIntervalClass, Octaves] | tuple[IntervalClass | Any, Octaves | Any]:
        if isinstance(other, EnharmonicInterval):
            sd = semitone_difference(
                self.interval_class.semitones,
                self.octaves,
                other.interval_class.semitones,
                other.octaves,
            )
            return EnharmonicInterval(interval_tuple_from_sd(sd, 12))
        elif isinstance(other, SpecificInterval):
            return SpecificInterval(
                specific_tuple(
                    self.fifths,
                    self.interval_class.semitones,
                    self.octaves,
                    other.fifths,
                    other.interval_class.semitones,
                    other.octaves,
                    -1,
                )
            )
        elif isinstance(other, GenericInterval):
            return GenericInterval(
                general_tuple(
                    self.interval_class.steps,
                    self.octaves,
                    other.interval_class.steps,
                    other.octaves,
                    7,
                    -1,
                )
            )
        elif isinstance(other, EnharmonicIntervalClass):
            return EnharmonicIntervalClass((self.interval_class.semitones - other.semitones) % 12)
        elif isinstance(other, SpecificIntervalClass):
            return SpecificIntervalClass(self.fifths - other.fifths)
        elif isinstance(other, GenericIntervalClass):
            return GenericIntervalClass(self.interval_class.steps - other.steps)

        elif Tuple == type(other):
            return self - SpecificInterval(other)
        return super().__sub__(other)

    def __eq__(self, other):
        if isinstance(other, SpecificInterval):
            return self.fifths == other.fifths and self.octaves == other.octaves
        return False

    def to_point(self) -> "SpecificPitchClass":  # noqa: F821
        from .pitch import SpecificPitchClass

        return SpecificPitchClass(self.fifths)


class GenericInterval(Interval[GenericIntervalClass, Octaves]):
    """A Generic Interval is a :class:`GenericIntervalClass` paired with an octave number.
    It represents an interval where the number and the octave are known, but not the quality.
    E.g., think of a generic third as "any third", including minor, major, diminished, etc.

    Initialized from a pair of two values (GIC, Octave) or from an interval name such as "P5:1".
    When initialized from a pair, the first value can be any value that can be used for
    initializing a GIC. Integers are also accepted where the number is the interval step count,
    e.g., GenericInterval(5) for a 5th spanning 0 octaves.
    """

    @staticmethod
    def convert_init_value(
        value: Union[
            Tuple[GenericIntervalClass | int, Octaves | int],
            str,
            int,
            "SpecificInterval",
        ],
    ) -> Tuple[GenericIntervalClass, Octaves]:
        """Used by the constructor to convert the initial value into a pair of IntType."""
        if isinstance(value, tuple):
            interval_class, octave = value
            while interval_class < 0:
                interval_class += 7
                octave -= 1
            while interval_class >= 7:
                interval_class -= 7
                octave += 1
        elif isinstance(value, str):
            sign, fifths, octave = parse_interval(value)
            interval_class = fifths2steps(fifths)
        elif isinstance(value, int):
            interval_class = value % 7
            octave = value // 7
        else:
            raise InvalidInitValue(
                f"A GenericInterval cannot be expressed as a {type(value)}. Pass an interval name as a string "
                f"(e.g., 'P5:1' for a fifth plus an octave) "
                f"or an (GenericInterval, Octave) tuple."
            )
        return GenericIntervalClass(interval_class), Octaves(octave)

    @property
    def name(self) -> str:
        return f"{self.interval_class.name}:{int(self.octaves)}"

    @property
    def steps(self) -> int:
        return int(self.octaves) * 7 + self.interval_class.steps

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

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

    def _repr_html_(self):
        from flexohr.core.html_colors import PITCHSPACE_COLORS

        c = PITCHSPACE_COLORS["GI"]
        return f'<span style="font-family:monospace;color:{c}"><b>GI</b>(<i>{self.name}</i>)</span>'

    def __neg__(self):
        return GenericInterval((-self.interval_class.steps, -self.octaves))

    def __add__(
        self,
        other: Union[
            Interval,
            IntervalClass,
            int,
            IntType,
            Tuple[GenericIntervalClass | int, Octaves | int],
        ],
    ) -> (
        GenericInterval
        | GenericIntervalClass
        | Interval
        | IntervalClass
        | tuple[GenericIntervalClass, Octaves]
        | tuple[IntervalClass | Any, Octaves | Any]
    ):
        if isinstance(other, GenericInterval | SpecificInterval):
            return GenericInterval(
                general_tuple(
                    self.interval_class.steps,
                    self.octaves,
                    other.interval_class.steps,
                    other.octaves,
                    7,
                    1,
                )
            )
        elif isinstance(other, GenericIntervalClass | SpecificIntervalClass):
            return GenericIntervalClass((self.interval_class.steps + other.steps) % 7)
        elif isinstance(other, EnharmonicInterval | EnharmonicIntervalClass):
            raise StepUnknown(f"{type(self)} + {type(other)}")

        elif Tuple == type(other):
            return self + GenericInterval(other)
        return super().__add__(other)

    def __sub__(
        self,
        other: Union[
            Interval,
            IntervalClass,
            int,
            IntType,
            Tuple[GenericIntervalClass | int, Octaves | int],
        ],
    ) -> (
        GenericInterval
        | GenericIntervalClass
        | Interval
        | IntervalClass
        | tuple[GenericIntervalClass, Octaves]
        | tuple[IntervalClass | Any, Octaves | Any]
    ):
        if isinstance(other, GenericInterval | SpecificInterval):
            sd = step_difference(
                self.interval_class.steps,
                self.octaves,
                other.interval_class.steps,
                other.octaves,
            )
            return GenericInterval(interval_tuple_from_sd(sd, 7))

        elif isinstance(other, GenericIntervalClass | SpecificIntervalClass):
            return GenericIntervalClass((self.interval_class.steps - other.steps) % 7)
        elif isinstance(other, EnharmonicInterval | EnharmonicIntervalClass):
            raise StepUnknown(f"{type(self)} - {type(other)}")

        elif Tuple == type(other):
            return self - GenericInterval(other)
        return super().__sub__(other)

    def __eq__(self, other):
        if isinstance(other, GenericInterval):
            return self.interval_class.steps == other.interval_class.steps and self.octaves == other.octaves
        return False


EIC = EnharmonicIntervalClass
SIC = SpecificIntervalClass
GIC = GenericIntervalClass
EI = EnharmonicInterval
SI = SpecificInterval
GI = GenericInterval
