from __future__ import annotations

import codecs
import io
from collections import deque
from pathlib import Path
from typing import TextIO, cast

from pydantic import ValidationError

from .._praat import detect_encoding, format_mark, get_mark, parse_header, parse_line
from ..constants import DEFAULT_TEXTGRID_PRECISION
from ..domain.models import Interval, IntervalTier, Point, PointTier, TextGrid
from ..exceptions import ErrorContext, ParseError, SerializationError


class _TrackedSource:
    def __init__(self, source: TextIO, *, path: Path | None = None):
        self._source = source
        self.path = path
        self.line_number = 0
        self._recent: deque[str] = deque(maxlen=3)

    def readline(self) -> str:
        line = self._source.readline()
        if line:
            self.line_number += 1
            self._recent.append(line.rstrip("\n"))
        return line

    def snippet(self) -> str:
        return "\n".join(self._recent)


class PraatTextGridReader:
    @staticmethod
    def _parse_float(line: str, short: bool, round_digits: int) -> float:
        value = parse_line(line, short, round_digits)
        if not isinstance(value, float):
            raise ValueError(f"expected numeric line, got {value!r}")
        return value

    @staticmethod
    def _parse_text(line: str, short: bool, round_digits: int) -> str:
        value = parse_line(line, short, round_digits)
        if not isinstance(value, str):
            raise ValueError(f"expected text line, got {value!r}")
        return value

    def read_textgrid_text(
        self,
        text: str,
        *,
        round_digits: int = DEFAULT_TEXTGRID_PRECISION,
        strict: bool = True,
    ) -> TextGrid:
        return self._read_textgrid(
            _TrackedSource(io.StringIO(text)),
            round_digits=round_digits,
            strict=strict,
        )

    def read_textgrid_file(
        self,
        path: str | Path,
        *,
        round_digits: int = DEFAULT_TEXTGRID_PRECISION,
        encoding: str | None = None,
        strict: bool = True,
    ) -> TextGrid:
        file_path = Path(path)
        file_encoding = encoding or detect_encoding(file_path)
        with codecs.open(str(file_path), "r", encoding=file_encoding) as source:
            return self._read_textgrid(
                _TrackedSource(source, path=file_path),
                round_digits=round_digits,
                strict=strict,
            )

    def read_point_tier_text(
        self,
        text: str,
        *,
        round_digits: int = DEFAULT_TEXTGRID_PRECISION,
    ) -> PointTier:
        return self._read_point_tier(_TrackedSource(io.StringIO(text)), round_digits=round_digits)

    def read_point_tier_file(
        self,
        path: str | Path,
        *,
        round_digits: int = DEFAULT_TEXTGRID_PRECISION,
        encoding: str | None = None,
    ) -> PointTier:
        file_path = Path(path)
        file_encoding = encoding or detect_encoding(file_path)
        with codecs.open(str(file_path), "r", encoding=file_encoding) as source:
            return self._read_point_tier(
                _TrackedSource(source, path=file_path),
                round_digits=round_digits,
            )

    def read_interval_tier_text(
        self,
        text: str,
        *,
        round_digits: int = DEFAULT_TEXTGRID_PRECISION,
        strict: bool = True,
    ) -> IntervalTier:
        return self._read_interval_tier(
            _TrackedSource(io.StringIO(text)),
            round_digits=round_digits,
            strict=strict,
        )

    def read_interval_tier_file(
        self,
        path: str | Path,
        *,
        round_digits: int = DEFAULT_TEXTGRID_PRECISION,
        encoding: str | None = None,
        strict: bool = True,
    ) -> IntervalTier:
        file_path = Path(path)
        file_encoding = encoding or detect_encoding(file_path)
        with codecs.open(str(file_path), "r", encoding=file_encoding) as source:
            return self._read_interval_tier(
                _TrackedSource(source, path=file_path),
                round_digits=round_digits,
                strict=strict,
            )

    def _wrap_error(
        self,
        exc: Exception,
        *,
        format_name: str,
        phase: str,
        source: _TrackedSource,
    ) -> ParseError:
        return ParseError(
            str(exc),
            context=ErrorContext(
                format_name=format_name,
                phase=phase,
                line_number=source.line_number,
                path=source.path,
                snippet=source.snippet(),
            ),
        )

    def _read_point_tier(
        self,
        source: _TrackedSource,
        *,
        round_digits: int,
    ) -> PointTier:
        try:
            file_type, short = parse_header(source)
            if file_type != "TextTier":
                raise ParseError("expected TextTier header")
            min_time = self._parse_float(source.readline(), short, round_digits)
            max_time = self._parse_float(source.readline(), short, round_digits)
            point_count = int(self._parse_float(source.readline(), short, round_digits))
            points: list[Point] = []
            for _ in range(point_count):
                if not short:
                    source.readline()
                point_time = self._parse_float(source.readline(), short, round_digits)
                point_mark = cast(str, get_mark(source, short))
                points.append(Point(time=point_time, mark=point_mark))
            return PointTier(min_time=min_time, max_time=max_time, points=tuple(points))
        except (ValueError, ValidationError, ParseError) as exc:
            raise self._wrap_error(
                exc,
                format_name="TextTier",
                phase="read",
                source=source,
            ) from exc

    def _read_interval_tier(
        self,
        source: _TrackedSource,
        *,
        round_digits: int,
        strict: bool,
    ) -> IntervalTier:
        try:
            file_type, short = parse_header(source)
            if file_type != "IntervalTier":
                raise ParseError("expected IntervalTier header")
            min_time = self._parse_float(source.readline(), short, round_digits)
            max_time = self._parse_float(source.readline(), short, round_digits)
            interval_count = int(self._parse_float(source.readline(), short, round_digits))
            intervals: list[Interval] = []
            for _ in range(interval_count):
                if not short:
                    source.readline()
                interval_min = self._parse_float(source.readline(), short, round_digits)
                interval_max = self._parse_float(source.readline(), short, round_digits)
                interval_mark = cast(str, get_mark(source, short))
                if interval_min < interval_max:
                    intervals.append(
                        Interval(min_time=interval_min, max_time=interval_max, mark=interval_mark)
                    )
            return IntervalTier(
                min_time=min_time,
                max_time=max_time,
                intervals=tuple(intervals),
                strict=strict,
            )
        except (ValueError, ValidationError, ParseError) as exc:
            raise self._wrap_error(
                exc,
                format_name="IntervalTier",
                phase="read",
                source=source,
            ) from exc

    def _read_textgrid(
        self,
        source: _TrackedSource,
        *,
        round_digits: int,
        strict: bool,
    ) -> TextGrid:
        try:
            file_type, short = parse_header(source)
            if file_type != "TextGrid":
                raise ParseError("expected TextGrid header")

            first_line = source.readline()
            try:
                self._parse_float(first_line, short, round_digits)
            except ValueError:
                short = True

            min_time = self._parse_float(first_line, short, round_digits)
            max_time = self._parse_float(source.readline(), short, round_digits)
            source.readline()

            if short:
                tier_count = int(source.readline().strip())
            else:
                tier_count = int(source.readline().strip().split()[2])
                source.readline()

            tiers: list[IntervalTier | PointTier] = []
            for _ in range(tier_count):
                if not short:
                    source.readline()
                tier_type = self._parse_text(source.readline(), short, round_digits)
                tier_name = self._parse_text(source.readline(), short, round_digits)
                tier_min = self._parse_float(source.readline(), short, round_digits)
                tier_max = self._parse_float(source.readline(), short, round_digits)
                if tier_type == "IntervalTier":
                    item_count = int(self._parse_float(source.readline(), short, round_digits))
                    intervals: list[Interval] = []
                    for _ in range(item_count):
                        if not short:
                            source.readline()
                        item_min = self._parse_float(source.readline(), short, round_digits)
                        item_max = self._parse_float(source.readline(), short, round_digits)
                        item_mark = cast(str, get_mark(source, short))
                        if item_min < item_max:
                            intervals.append(
                                Interval(min_time=item_min, max_time=item_max, mark=item_mark)
                            )
                    tiers.append(
                        IntervalTier(
                            name=tier_name,
                            min_time=tier_min,
                            max_time=tier_max,
                            intervals=tuple(intervals),
                            strict=strict,
                        )
                    )
                elif tier_type == "TextTier":
                    item_count = int(self._parse_float(source.readline(), short, round_digits))
                    points: list[Point] = []
                    for _ in range(item_count):
                        if not short:
                            source.readline()
                        point_time = self._parse_float(source.readline(), short, round_digits)
                        point_mark = cast(str, get_mark(source, short))
                        points.append(Point(time=point_time, mark=point_mark))
                    tiers.append(
                        PointTier(
                            name=tier_name,
                            min_time=tier_min,
                            max_time=tier_max,
                            points=tuple(points),
                        )
                    )
                else:
                    raise ParseError(f"unsupported tier type: {tier_type}")

            return TextGrid(min_time=min_time, max_time=max_time, tiers=tuple(tiers), strict=strict)
        except (ValueError, ValidationError, ParseError) as exc:
            raise self._wrap_error(
                exc,
                format_name="TextGrid",
                phase="read",
                source=source,
            ) from exc


class PraatTextGridWriter:
    def _format_point_tier(self, tier: PointTier) -> str:
        sink = io.StringIO()
        _, max_time = tier.bounds()
        print('File type = "ooTextFile"', file=sink)
        print('Object class = "TextTier"\n', file=sink)
        print(f"xmin = {tier.min_time}", file=sink)
        print(f"xmax = {max_time}", file=sink)
        print(f"points: size = {len(tier)}", file=sink)
        for index, point in enumerate(tier, start=1):
            print(f"points [{index}]:", file=sink)
            print(f"\ttime = {point.time}", file=sink)
            print(f'\tmark = "{format_mark(point.mark)}"', file=sink)
        return sink.getvalue()

    def _format_interval_tier(self, tier: IntervalTier, *, null: str | None = "") -> str:
        sink = io.StringIO()
        _, max_time = tier.bounds()
        output = tier.fill_gaps(null)
        print('File type = "ooTextFile"', file=sink)
        print('Object class = "IntervalTier"\n', file=sink)
        print(f"xmin = {tier.min_time}", file=sink)
        print(f"xmax = {max_time}", file=sink)
        print(f"intervals: size = {len(output)}", file=sink)
        for index, interval in enumerate(output, start=1):
            print(f"intervals [{index}]:", file=sink)
            print(f"\txmin = {interval.min_time}", file=sink)
            print(f"\txmax = {interval.max_time}", file=sink)
            print(f'\ttext = "{format_mark(interval.mark)}"', file=sink)
        return sink.getvalue()

    def write_point_tier(self, tier: PointTier) -> str:
        return self._format_point_tier(tier)

    def write_interval_tier(self, tier: IntervalTier, *, null: str | None = "") -> str:
        return self._format_interval_tier(tier, null=null)

    def write_textgrid(self, textgrid: TextGrid, *, null: str | None = "") -> str:
        try:
            sink = io.StringIO()
            _, max_time = textgrid.bounds()
            print('File type = "ooTextFile"', file=sink)
            print('Object class = "TextGrid"\n', file=sink)
            print(f"xmin = {textgrid.min_time}", file=sink)
            print(f"xmax = {max_time}", file=sink)
            print("tiers? <exists>", file=sink)
            print(f"size = {len(textgrid)}", file=sink)
            print("item []:", file=sink)
            for tier_index, tier in enumerate(textgrid, start=1):
                tier_min, tier_max = tier.bounds()
                print(f"\titem [{tier_index}]:", file=sink)
                if isinstance(tier, IntervalTier):
                    output = tier.fill_gaps(null)
                    print('\t\tclass = "IntervalTier"', file=sink)
                    print(f'\t\tname = "{format_mark(tier.name)}"', file=sink)
                    print(f"\t\txmin = {tier_min}", file=sink)
                    print(f"\t\txmax = {tier_max}", file=sink)
                    print(f"\t\tintervals: size = {len(output)}", file=sink)
                    for item_index, interval in enumerate(output, start=1):
                        print(f"\t\t\tintervals [{item_index}]:", file=sink)
                        print(f"\t\t\t\txmin = {interval.min_time}", file=sink)
                        print(f"\t\t\t\txmax = {interval.max_time}", file=sink)
                        print(f'\t\t\t\ttext = "{format_mark(interval.mark)}"', file=sink)
                else:
                    print('\t\tclass = "TextTier"', file=sink)
                    print(f'\t\tname = "{format_mark(tier.name)}"', file=sink)
                    print(f"\t\txmin = {tier_min}", file=sink)
                    print(f"\t\txmax = {tier_max}", file=sink)
                    print(f"\t\tpoints: size = {len(tier)}", file=sink)
                    for item_index, point in enumerate(tier, start=1):
                        print(f"\t\t\tpoints [{item_index}]:", file=sink)
                        print(f"\t\t\t\ttime = {point.time}", file=sink)
                        print(f'\t\t\t\tmark = "{format_mark(point.mark)}"', file=sink)
            return sink.getvalue()
        except Exception as exc:
            raise SerializationError(str(exc)) from exc
