from __future__ import annotations

from bisect import bisect_left, bisect_right
from typing import Annotated, Iterator, Literal

from pydantic import Field, model_validator

from ..exceptions import DomainValidationError
from .base import UXTGModel


class Point(UXTGModel):
    time: float
    mark: str | None

    def shift(self, offset: float) -> "Point":
        self.time = self.time + offset
        return self


class Interval(UXTGModel):
    min_time: float
    max_time: float
    mark: str | None

    @model_validator(mode="after")
    def validate_bounds(self) -> "Interval":
        if self.min_time >= self.max_time:
            raise ValueError("min_time must be less than max_time")
        return self

    def duration(self) -> float:
        return self.max_time - self.min_time

    def bounds(self) -> tuple[float, float]:
        return self.min_time, self.max_time

    def overlaps(self, other: "Interval") -> bool:
        return other.min_time < self.max_time and self.min_time < other.max_time

    def contains_time(self, time: float) -> bool:
        return self.min_time <= time <= self.max_time

    def contains_interval(self, other: "Interval") -> bool:
        return self.min_time <= other.min_time and other.max_time <= self.max_time

    def shift(self, offset: float) -> "Interval":
        updated = type(self).model_validate(
            {
                "min_time": self.min_time + offset,
                "max_time": self.max_time + offset,
                "mark": self.mark,
            }
        )
        object.__setattr__(self, "min_time", updated.min_time)
        object.__setattr__(self, "max_time", updated.max_time)
        return self


class PointTier(UXTGModel):
    tier_class: Literal["TextTier"] = "TextTier"
    name: str | None = None
    min_time: float = 0.0
    max_time: float | None = None
    points: tuple[Point, ...] = ()

    @model_validator(mode="after")
    def validate_points(self) -> "PointTier":
        previous_time: float | None = None
        for point in self.points:
            if point.time < self.min_time:
                raise ValueError("point is before tier min_time")
            if self.max_time is not None and point.time > self.max_time:
                raise ValueError("point is after tier max_time")
            if previous_time is not None and point.time <= previous_time:
                raise ValueError("points must be sorted and unique by time")
            previous_time = point.time
        return self

    def __iter__(self) -> Iterator[Point]:  # type: ignore[override]
        return iter(self.points)

    def __len__(self) -> int:
        return len(self.points)

    def __getitem__(self, index: int) -> Point:
        return self.points[index]

    def bounds(self) -> tuple[float, float]:
        if self.max_time is not None:
            return self.min_time, self.max_time
        if not self.points:
            return self.min_time, self.min_time
        return self.min_time, self.points[-1].time

    def add(self, time: float, mark: str | None) -> None:
        self.add_point(Point(time=time, mark=mark))

    def add_point(self, point: Point) -> None:
        if point.time < self.min_time:
            raise DomainValidationError(f"point time {point.time} is before {self.min_time}")
        if self.max_time is not None and point.time > self.max_time:
            raise DomainValidationError(f"point time {point.time} is after {self.max_time}")

        times = [item.time for item in self.points]
        index = bisect_left(times, point.time)
        if index < len(self.points) and self.points[index].time == point.time:
            raise DomainValidationError(f"duplicate point time: {point.time}")
        self.points = (*self.points[:index], point, *self.points[index:])

    def remove(self, time: float, mark: str | None) -> None:
        self.remove_point(Point(time=time, mark=mark))

    def remove_point(self, point: Point) -> None:
        try:
            index = self.points.index(point)
        except ValueError as exc:
            raise DomainValidationError("point not found") from exc
        self.points = (*self.points[:index], *self.points[index + 1 :])

    @classmethod
    def from_string(cls, text: str, round_digits: int = 5) -> "PointTier":
        from ..codecs.praat import PraatTextGridReader

        return PraatTextGridReader().read_point_tier_text(text, round_digits=round_digits)

    @classmethod
    def from_file(
        cls,
        path: str,
        round_digits: int = 5,
        encoding: str | None = None,
    ) -> "PointTier":
        from ..codecs.praat import PraatTextGridReader

        return PraatTextGridReader().read_point_tier_file(
            path,
            round_digits=round_digits,
            encoding=encoding,
        )


class IntervalTier(UXTGModel):
    tier_class: Literal["IntervalTier"] = "IntervalTier"
    name: str | None = None
    min_time: float = 0.0
    max_time: float | None = None
    intervals: tuple[Interval, ...] = ()
    strict: bool = True

    @model_validator(mode="after")
    def validate_intervals(self) -> "IntervalTier":
        previous: Interval | None = None
        for interval in self.intervals:
            if interval.min_time < self.min_time:
                raise ValueError("interval is before tier min_time")
            if self.max_time is not None and interval.max_time > self.max_time:
                raise ValueError("interval is after tier max_time")
            if previous is not None:
                if interval.min_time < previous.min_time:
                    raise ValueError("intervals must be sorted by min_time")
                if self.strict and previous.overlaps(interval):
                    raise ValueError("overlapping intervals are not allowed in strict mode")
            previous = interval
        return self

    def __iter__(self) -> Iterator[Interval]:  # type: ignore[override]
        return iter(self.intervals)

    def __len__(self) -> int:
        return len(self.intervals)

    def __getitem__(self, index: int) -> Interval:
        return self.intervals[index]

    def bounds(self) -> tuple[float, float]:
        if self.max_time is not None:
            return self.min_time, self.max_time
        if not self.intervals:
            return self.min_time, self.min_time
        return self.min_time, self.intervals[-1].max_time

    def add(self, min_time: float, max_time: float, mark: str | None) -> None:
        self.add_interval(Interval(min_time=min_time, max_time=max_time, mark=mark))

    def add_interval(self, interval: Interval) -> None:
        if interval.min_time < self.min_time:
            raise DomainValidationError(
                f"interval start {interval.min_time} is before {self.min_time}"
            )
        if self.max_time is not None and interval.max_time > self.max_time:
            raise DomainValidationError(
                f"interval end {interval.max_time} is after {self.max_time}"
            )

        starts = [item.min_time for item in self.intervals]
        index = bisect_right(starts, interval.min_time)
        previous = self.intervals[index - 1] if index > 0 else None
        following = self.intervals[index] if index < len(self.intervals) else None

        if self.strict:
            if previous is not None and previous.overlaps(interval):
                raise DomainValidationError("interval overlaps with previous interval")
            if following is not None and interval.overlaps(following):
                raise DomainValidationError("interval overlaps with following interval")

        self.intervals = (*self.intervals[:index], interval, *self.intervals[index:])

    def remove(self, min_time: float, max_time: float, mark: str | None) -> None:
        self.remove_interval(Interval(min_time=min_time, max_time=max_time, mark=mark))

    def remove_interval(self, interval: Interval) -> None:
        try:
            index = self.intervals.index(interval)
        except ValueError as exc:
            raise DomainValidationError("interval not found") from exc
        self.intervals = (*self.intervals[:index], *self.intervals[index + 1 :])

    def index_containing(self, time: float) -> int | None:
        starts = [interval.min_time for interval in self.intervals]
        index = bisect_right(starts, time) - 1
        if index < 0:
            return None
        if self.strict:
            return index if self.intervals[index].contains_time(time) else None
        while index >= 0:
            if self.intervals[index].contains_time(time):
                return index
            index -= 1
        return None

    def interval_containing(self, time: float) -> Interval | None:
        index = self.index_containing(time)
        return self.intervals[index] if index is not None else None

    def fill_gaps(self, null: str | None = "") -> tuple[Interval, ...]:
        prev_time = self.min_time
        output: list[Interval] = []
        for interval in self.intervals:
            if prev_time < interval.min_time:
                output.append(Interval(min_time=prev_time, max_time=interval.min_time, mark=null))
            output.append(interval)
            prev_time = interval.max_time
        if self.max_time is not None and prev_time < self.max_time:
            output.append(Interval(min_time=prev_time, max_time=self.max_time, mark=null))
        return tuple(output)

    @classmethod
    def from_string(
        cls,
        text: str,
        round_digits: int = 5,
        strict: bool = True,
    ) -> "IntervalTier":
        from ..codecs.praat import PraatTextGridReader

        return PraatTextGridReader().read_interval_tier_text(
            text,
            round_digits=round_digits,
            strict=strict,
        )

    @classmethod
    def from_file(
        cls,
        path: str,
        round_digits: int = 5,
        encoding: str | None = None,
        strict: bool = True,
    ) -> "IntervalTier":
        from ..codecs.praat import PraatTextGridReader

        return PraatTextGridReader().read_interval_tier_file(
            path,
            round_digits=round_digits,
            encoding=encoding,
            strict=strict,
        )


Tier = Annotated[IntervalTier | PointTier, Field(discriminator="tier_class")]


class TextGrid(UXTGModel):
    file_type: Literal["ooTextFile"] = "ooTextFile"
    object_class: Literal["TextGrid"] = "TextGrid"
    name: str | None = None
    min_time: float = 0.0
    max_time: float | None = None
    tiers: tuple[Tier, ...] = ()
    strict: bool = True

    @model_validator(mode="after")
    def validate_tiers(self) -> "TextGrid":
        for tier in self.tiers:
            tier_min, tier_max = tier.bounds()
            if tier_min < self.min_time:
                raise ValueError("tier is before textgrid min_time")
            if self.max_time is not None and tier_max > self.max_time:
                raise ValueError("tier is after textgrid max_time")
        return self

    def __iter__(self) -> Iterator[IntervalTier | PointTier]:  # type: ignore[override]
        return iter(self.tiers)

    def __len__(self) -> int:
        return len(self.tiers)

    def __getitem__(self, index: int) -> IntervalTier | PointTier:
        return self.tiers[index]

    def bounds(self) -> tuple[float, float]:
        if self.max_time is not None:
            return self.min_time, self.max_time
        if not self.tiers:
            return self.min_time, self.min_time
        return self.min_time, max(tier.bounds()[1] for tier in self.tiers)

    def get_first(self, tier_name: str) -> IntervalTier | PointTier | None:
        for tier in self.tiers:
            if tier.name == tier_name:
                return tier
        return None

    def get_list(self, tier_name: str) -> list[IntervalTier | PointTier]:
        return [tier for tier in self.tiers if tier.name == tier_name]

    def get_names(self) -> list[str | None]:
        return [tier.name for tier in self.tiers]

    def append(self, tier: IntervalTier | PointTier) -> None:
        tier_min, tier_max = tier.bounds()
        if tier_min < self.min_time:
            raise DomainValidationError(f"tier starts before {self.min_time}")
        if self.max_time is not None and tier_max > self.max_time:
            raise DomainValidationError(f"tier ends after {self.max_time}")
        adjusted = (
            tier.model_copy(update={"strict": self.strict})
            if isinstance(tier, IntervalTier)
            else tier
        )
        self.tiers = (*self.tiers, adjusted)

    def extend(
        self,
        tiers: tuple[IntervalTier | PointTier, ...] | list[IntervalTier | PointTier],
    ) -> None:
        for tier in tiers:
            self.append(tier)

    def pop(self, index: int | None = None) -> IntervalTier | PointTier:
        if not self.tiers:
            raise DomainValidationError("cannot pop from an empty TextGrid")
        resolved = len(self.tiers) - 1 if index is None else index
        item = self.tiers[resolved]
        self.tiers = (*self.tiers[:resolved], *self.tiers[resolved + 1 :])
        return item

    @classmethod
    def from_string(
        cls,
        text: str,
        round_digits: int = 5,
        strict: bool = True,
    ) -> "TextGrid":
        from ..codecs.praat import PraatTextGridReader

        return PraatTextGridReader().read_textgrid_text(
            text,
            round_digits=round_digits,
            strict=strict,
        )

    @classmethod
    def from_file(
        cls,
        path: str,
        round_digits: int = 5,
        encoding: str | None = None,
        strict: bool = True,
    ) -> "TextGrid":
        from ..codecs.praat import PraatTextGridReader

        return PraatTextGridReader().read_textgrid_file(
            path,
            round_digits=round_digits,
            encoding=encoding,
            strict=strict,
        )
