"""Scale degree types for the pitchspace paradigm.

GenericScaleDegree (GSD)     — Selector
GenericScaleDegreeInterval (GSDI) — Vector
SpecificScaleDegree (SD)     — Point (already resolved)
SpecificScaleDegreeInterval (SDI) — Vector
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

from flexohr.core.abstract import Point, Selector, Vector
from flexohr.paradigms.pitchspace.accidental import Accidental
from flexohr.paradigms.pitchspace.interval import GenericIntervalClass as GIC
from flexohr.paradigms.pitchspace.interval import SpecificIntervalClass as SIC

if TYPE_CHECKING:
    from flexohr.core.ohr import OHR
    from flexohr.harmony.harmony_enums import CollectionType


# ══════════════════════════════════════════════════════════════════════
#  GenericScaleDegree (GSD) — Selector
# ══════════════════════════════════════════════════════════════════════


@dataclass(frozen=True)
class GenericScaleDegree(Selector):
    """A scale degree without resolved specific interval.

    Resolves against a Scale's body by index + accidental offset.
    """

    degree: GIC
    accidental: Accidental = Accidental.NATURAL

    def __post_init__(self):
        if isinstance(self.degree, int) and not isinstance(self.degree, GIC):
            object.__setattr__(self, "degree", GIC(self.degree - 1))

    _DEGREE_PATTERN = re.compile(r"^([#b\u266f\u266d\-]*)(\d+)$")

    @classmethod
    def from_string(cls, s: str) -> GenericScaleDegree:
        """Parse a scale degree string like ``"#7"``, ``"b3"``, ``"5"``.

        Dispatches to ``from_format()`` with the default codec.
        """
        m = cls._DEGREE_PATTERN.match(s)
        if not m:
            raise ValueError(f"Cannot parse scale degree string: {s!r}")
        acc_str, degree_str = m.groups()
        accidental = Accidental.from_string(acc_str) if acc_str else Accidental.NATURAL
        return cls(int(degree_str), accidental)

    def resolve_against(self, ohr: OHR) -> Any:
        from flexohr.paradigms.pitchspace.scale import Scale

        if not isinstance(ohr, Scale):
            raise TypeError(f"GSD can only resolve against a Scale, got {type(ohr).__name__}")
        return ohr.get_scale_degree(self)

    @property
    def degree_number(self) -> int:
        return int(self.degree) + 1

    def __repr__(self):
        acc = self.accidental.to_string() if self.accidental.n != 0 else ""
        return f"GSD({acc}{self.degree_number})"

    def __str__(self):
        return self.__repr__()

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

        acc = self.accidental.to_string() if self.accidental.n != 0 else ""
        return (
            f'<span style="font-family:monospace;color:{GENERIC_SCALE_DEGREE_COLOR}">'
            f"<b>GSD</b>({acc}{self.degree_number})</span>"
        )

    def __hash__(self):
        return hash(("GSD", int(self.degree), self.accidental.n))

    def __eq__(self, other):
        if isinstance(other, GenericScaleDegree):
            return self.degree == other.degree and self.accidental == other.accidental
        return NotImplemented

    def __add__(self, other):
        if isinstance(other, GenericScaleDegreeInterval):
            return GenericScaleDegree(degree=self.degree + other.degree, accidental=self.accidental + other.accidental)
        if isinstance(other, SpecificScaleDegreeInterval):
            return GenericScaleDegree(degree=self.degree + other.degree, accidental=self.accidental + other.accidental)
        if isinstance(other, GIC):
            return GenericScaleDegree(degree=self.degree + other, accidental=Accidental.NATURAL)
        if isinstance(other, SIC):
            return GenericScaleDegree(degree=GIC(int(self.degree) + other.steps), accidental=Accidental.NATURAL)
        if isinstance(other, GenericScaleDegree):
            from flexohr.paradigms.pitchspace.errors import OperationUndefined

            raise OperationUndefined("GSD + GSD: Point + Point is undefined")
        return NotImplemented

    def __sub__(self, other):
        if isinstance(other, GenericScaleDegreeInterval):
            return GenericScaleDegree(degree=self.degree - other.degree, accidental=self.accidental - other.accidental)
        if isinstance(other, SpecificScaleDegreeInterval):
            return GenericScaleDegree(degree=self.degree - other.degree, accidental=self.accidental - other.accidental)
        if isinstance(other, GenericScaleDegree):
            return GenericScaleDegreeInterval(
                degree=self.degree - other.degree, accidental=self.accidental - other.accidental
            )
        if isinstance(other, SpecificScaleDegree):
            return GenericScaleDegreeInterval(
                degree=self.degree - other.degree, accidental=self.accidental - other.accidental
            )
        if isinstance(other, GIC):
            return GenericScaleDegree(degree=self.degree - other, accidental=Accidental.NATURAL)
        if isinstance(other, SIC):
            return GenericScaleDegree(degree=GIC(int(self.degree) - other.steps), accidental=Accidental.NATURAL)
        return NotImplemented


# ══════════════════════════════════════════════════════════════════════
#  GenericScaleDegreeInterval (GSDI) — Vector
# ══════════════════════════════════════════════════════════════════════


@dataclass(frozen=True)
class GenericScaleDegreeInterval(Vector):
    """Displacement between two generic scale degree positions."""

    degree: GIC
    accidental: Accidental = Accidental.NATURAL

    def __post_init__(self):
        if isinstance(self.degree, int) and not isinstance(self.degree, GIC):
            object.__setattr__(self, "degree", GIC(self.degree - 1))

    @property
    def degree_number(self) -> int:
        return int(self.degree) + 1

    def __repr__(self):
        acc = self.accidental.to_string() if self.accidental.n != 0 else ""
        return f"GSDI({acc}{self.degree_number})"

    def __str__(self):
        return self.__repr__()

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

        acc = self.accidental.to_string() if self.accidental.n != 0 else ""
        return (
            f'<span style="font-family:monospace;color:{GENERIC_SCALE_DEGREE_COLOR}">'
            f"<b>GSDI</b>({acc}{self.degree_number})</span>"
        )

    def __hash__(self):
        return hash(("GSDI", int(self.degree), self.accidental.n))

    def __eq__(self, other):
        if isinstance(other, GenericScaleDegreeInterval):
            return self.degree == other.degree and self.accidental == other.accidental
        return NotImplemented

    def __neg__(self):
        return GenericScaleDegreeInterval(degree=-self.degree, accidental=-self.accidental)

    def __add__(self, other):
        if isinstance(other, GenericScaleDegreeInterval):
            return GenericScaleDegreeInterval(
                degree=self.degree + other.degree, accidental=self.accidental + other.accidental
            )
        return NotImplemented

    def __sub__(self, other):
        if isinstance(other, GenericScaleDegreeInterval):
            return GenericScaleDegreeInterval(
                degree=self.degree - other.degree, accidental=self.accidental - other.accidental
            )
        return NotImplemented


# ══════════════════════════════════════════════════════════════════════
#  SpecificScaleDegree (SD) — Point (NOT Selector)
# ══════════════════════════════════════════════════════════════════════


@dataclass(frozen=True)
class SpecificScaleDegree(Point):
    """A scale degree resolved against a specific CollectionType.

    SD is a Point, not a Selector — it is already resolved and carries
    its SIC directly.
    """

    degree: GIC
    accidental: Accidental
    sic: SIC
    collection_type: CollectionType

    @property
    def degree_number(self) -> int:
        return int(self.degree) + 1

    def __repr__(self):
        acc = self.accidental.to_string() if self.accidental.n != 0 else ""
        return f"SD({acc}{self.degree_number}, {self.collection_type.short_name})"

    def __str__(self):
        return self.__repr__()

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

        c = get_scale_degree_color(self.collection_type)
        acc = self.accidental.to_string() if self.accidental.n != 0 else ""
        return (
            f'<span style="font-family:monospace;color:{c}">'
            f"<b>SD</b>({acc}{self.degree_number}, {self.collection_type.short_name})</span>"
        )

    def __hash__(self):
        return hash(("SD", int(self.degree), self.accidental.n, self.sic.fifths, self.collection_type))

    def __eq__(self, other):
        if isinstance(other, SpecificScaleDegree):
            return (
                self.degree == other.degree
                and self.accidental == other.accidental
                and self.sic == other.sic
                and self.collection_type is other.collection_type
            )
        return NotImplemented

    def __add__(self, other):
        if isinstance(other, SpecificScaleDegreeInterval):
            if other.collection_type is not self.collection_type:
                from flexohr.paradigms.pitchspace.errors import OperationUndefined

                raise OperationUndefined("SD + SDI: CollectionType mismatch")
            from flexohr.paradigms.pitchspace.scale import get_scale

            return get_scale(self.collection_type).make_scale_degree(self.sic + other.sic)
        if isinstance(other, GenericScaleDegreeInterval):
            return GenericScaleDegree(degree=self.degree + other.degree, accidental=self.accidental + other.accidental)
        if isinstance(other, SIC):
            from flexohr.paradigms.pitchspace.scale import get_scale

            return get_scale(self.collection_type).make_scale_degree(self.sic + other)
        if isinstance(other, GIC):
            return GenericScaleDegree(degree=self.degree + other, accidental=Accidental.NATURAL)
        if isinstance(other, SpecificScaleDegree):
            from flexohr.paradigms.pitchspace.errors import OperationUndefined

            raise OperationUndefined("SD + SD: Point + Point is undefined")
        return NotImplemented

    def __sub__(self, other):
        if isinstance(other, SpecificScaleDegreeInterval):
            if other.collection_type is not self.collection_type:
                from flexohr.paradigms.pitchspace.errors import OperationUndefined

                raise OperationUndefined("SD - SDI: CollectionType mismatch")
            from flexohr.paradigms.pitchspace.scale import get_scale

            return get_scale(self.collection_type).make_scale_degree(self.sic - other.sic)
        if isinstance(other, GenericScaleDegreeInterval):
            return GenericScaleDegree(degree=self.degree - other.degree, accidental=self.accidental - other.accidental)
        if isinstance(other, SpecificScaleDegree):
            if other.collection_type is not self.collection_type:
                from flexohr.paradigms.pitchspace.errors import OperationUndefined

                raise OperationUndefined("SD - SD: CollectionType mismatch")
            return SpecificScaleDegreeInterval(
                degree=self.degree - other.degree,
                accidental=self.accidental - other.accidental,
                sic=self.sic - other.sic,
                collection_type=self.collection_type,
            )
        if isinstance(other, GenericScaleDegree):
            return GenericScaleDegreeInterval(
                degree=self.degree - other.degree, accidental=self.accidental - other.accidental
            )
        if isinstance(other, SIC):
            from flexohr.paradigms.pitchspace.scale import get_scale

            return get_scale(self.collection_type).make_scale_degree(self.sic - other)
        if isinstance(other, GIC):
            return GenericScaleDegree(degree=self.degree - other, accidental=Accidental.NATURAL)
        return NotImplemented

    @classmethod
    def from_string(
        cls, s: str, collection_type: CollectionType | None = None, reference_ohr: OHR | None = None
    ) -> SpecificScaleDegree:
        """Parse a scale degree string like ``"#7"``, ``"b3"``, ``"5"``.

        Requires *collection_type* or *reference_ohr* to resolve against.
        """
        gsd = GenericScaleDegree.from_string(s)
        if reference_ohr is not None:
            from flexohr.paradigms.pitchspace.scale import Scale

            if isinstance(reference_ohr, Scale):
                return reference_ohr.get_scale_degree(gsd)
            raise TypeError(f"reference_ohr must be a Scale, got {type(reference_ohr).__name__}")
        if collection_type is not None:
            from flexohr.paradigms.pitchspace.scale import get_scale

            return get_scale(collection_type).get_scale_degree(gsd)
        raise ValueError("SD.from_string() requires either collection_type= or reference_ohr=")

    # ── DCML codec ──

    _NUMERAL_PATTERN = re.compile(r"^([#b\u266f\u266d\-]*)([IiVv]+)$")
    _ROMAN_TO_DEGREE = {
        "I": 1,
        "II": 2,
        "III": 3,
        "IV": 4,
        "V": 5,
        "VI": 6,
        "VII": 7,
        "i": 1,
        "ii": 2,
        "iii": 3,
        "iv": 4,
        "v": 5,
        "vi": 6,
        "vii": 7,
    }

    @classmethod
    def from_format(
        cls,
        value: str,
        standard: str = "dcml",
        collection_type: CollectionType | None = None,
        reference_ohr: OHR | None = None,
    ) -> SpecificScaleDegree:
        """Parse a scale degree from a standard format string."""
        if standard != "dcml":
            raise NotImplementedError(f"SD.from_format: standard {standard!r} not yet supported")
        m = cls._NUMERAL_PATTERN.match(value)
        if not m:
            raise ValueError(f"Cannot parse DCML numeral: {value!r}")
        acc_str, numeral = m.groups()
        numeral_upper = numeral.upper()
        if numeral_upper not in cls._ROMAN_TO_DEGREE:
            raise ValueError(f"Unknown Roman numeral: {numeral!r}")
        degree_num = cls._ROMAN_TO_DEGREE[numeral_upper]
        accidental = Accidental.from_string(acc_str) if acc_str else Accidental.NATURAL
        gsd = GenericScaleDegree(degree_num, accidental)
        if reference_ohr is not None:
            from flexohr.paradigms.pitchspace.scale import Scale

            if isinstance(reference_ohr, Scale):
                return reference_ohr.get_scale_degree(gsd)
            raise TypeError(f"reference_ohr must be a Scale, got {type(reference_ohr).__name__}")
        if collection_type is not None:
            from flexohr.paradigms.pitchspace.scale import get_scale

            return get_scale(collection_type).get_scale_degree(gsd)
        raise ValueError("SD.from_format() requires either collection_type= or reference_ohr=")


# ══════════════════════════════════════════════════════════════════════
#  SpecificScaleDegreeInterval (SDI) — Vector
# ══════════════════════════════════════════════════════════════════════


@dataclass(frozen=True)
class SpecificScaleDegreeInterval(Vector):
    """The specific distance between two SDs in the same CollectionType."""

    degree: GIC
    accidental: Accidental
    sic: SIC
    collection_type: CollectionType

    def __post_init__(self):
        if isinstance(self.degree, int) and not isinstance(self.degree, GIC):
            object.__setattr__(self, "degree", GIC(self.degree - 1))

    @property
    def degree_number(self) -> int:
        return int(self.degree) + 1

    def __repr__(self):
        acc = self.accidental.to_string() if self.accidental.n != 0 else ""
        return f"SDI({acc}{self.degree_number}, {self.collection_type.short_name})"

    def __str__(self):
        return self.__repr__()

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

        c = get_scale_degree_color(self.collection_type)
        acc = self.accidental.to_string() if self.accidental.n != 0 else ""
        return (
            f'<span style="font-family:monospace;color:{c}">'
            f"<b>SDI</b>({acc}{self.degree_number}, {self.collection_type.short_name})</span>"
        )

    def __hash__(self):
        return hash(("SDI", int(self.degree), self.accidental.n, self.sic.fifths, self.collection_type))

    def __eq__(self, other):
        if isinstance(other, SpecificScaleDegreeInterval):
            return (
                self.degree == other.degree
                and self.accidental == other.accidental
                and self.sic == other.sic
                and self.collection_type is other.collection_type
            )
        return NotImplemented

    def __neg__(self):
        return SpecificScaleDegreeInterval(
            degree=-self.degree, accidental=-self.accidental, sic=-self.sic, collection_type=self.collection_type
        )

    def __add__(self, other):
        if isinstance(other, SpecificScaleDegreeInterval):
            if other.collection_type is not self.collection_type:
                from flexohr.paradigms.pitchspace.errors import OperationUndefined

                raise OperationUndefined("SDI + SDI: CollectionType mismatch")
            return SpecificScaleDegreeInterval(
                degree=self.degree + other.degree,
                accidental=self.accidental + other.accidental,
                sic=self.sic + other.sic,
                collection_type=self.collection_type,
            )
        return NotImplemented

    def __sub__(self, other):
        if isinstance(other, SpecificScaleDegreeInterval):
            if other.collection_type is not self.collection_type:
                from flexohr.paradigms.pitchspace.errors import OperationUndefined

                raise OperationUndefined("SDI - SDI: CollectionType mismatch")
            return SpecificScaleDegreeInterval(
                degree=self.degree - other.degree,
                accidental=self.accidental - other.accidental,
                sic=self.sic - other.sic,
                collection_type=self.collection_type,
            )
        return NotImplemented


# ── Short aliases ──

GSD = GenericScaleDegree
SD = SpecificScaleDegree
GSDI = GenericScaleDegreeInterval
SDI = SpecificScaleDegreeInterval
