"""MicroDVD caption format reader and writer.

MicroDVD uses frame-based timing: {start_frame}{end_frame}text|text.
A special line {0}{0}fps_value sets the framerate (default 25 fps).
"""

import re
from copy import deepcopy

from .base import (
    DEFAULT_LANGUAGE_CODE,
    BaseReader,
    BaseWriter,
    Caption,
    CaptionList,
    CaptionNode,
    CaptionSet,
)
from .exceptions import (
    CaptionReadNoCaptions,
    CaptionReadSyntaxError,
    CaptionReadTimingError,
)
from .geometry import HorizontalAlignmentEnum


class MicroDVDReader(BaseReader):
    """Reads MicroDVD subtitle files into a CaptionSet."""

    def detect(self, content):
        """Return True if content starts with MicroDVD frame markers."""
        content = self._decode_content(content)
        return re.match(r"{\d+}{\d+}", content) is not None

    def read(self, content, lang=DEFAULT_LANGUAGE_CODE):
        """Parse MicroDVD content into a CaptionSet.

        :param content: Raw MicroDVD file content.
        :param lang: Language code to assign.
        :rtype: CaptionSet
        """
        content = self._decode_content(content)

        lines = content.splitlines()
        captions = CaptionList()
        fps = 25.0
        for line in lines:
            if not line:
                continue

            m = re.match(r"{(\d+)}{(\d+)}(.*)", line)
            if not m:
                raise CaptionReadSyntaxError("Line does not match expected format")

            start, end, txt = m.groups()

            if start == "0" and end == "0":
                fps = self._parse_fps(txt)
                continue

            caption_start = self._framestomicro(int(start), fps)
            caption_end = self._framestomicro(int(end), fps)
            nodes = self._parse_caption_nodes(txt)

            if nodes:
                captions.append(Caption(caption_start, caption_end, nodes))

        caption_set = CaptionSet(
            {lang: captions},
            visual_alignment_default=HorizontalAlignmentEnum.CENTER,
        )
        caption_set.set_captions(lang, captions)

        if caption_set.is_empty():
            raise CaptionReadNoCaptions("Empty caption file")

        return caption_set

    @staticmethod
    def _parse_fps(txt):
        """Parse FPS value from the {0}{0} metadata line."""
        try:
            return float(txt)
        except ValueError:
            raise CaptionReadTimingError("FPS information is not provided")

    @staticmethod
    def _parse_caption_nodes(txt):
        """Build caption nodes from pipe-delimited text."""
        nodes = []
        for segment in txt.split("|"):
            if segment != "":
                nodes.append(CaptionNode.create_text(segment))
                nodes.append(CaptionNode.create_break())
        if nodes:
            nodes.pop()
        return nodes

    @staticmethod
    def _framestomicro(framenum, fps=25.0):
        """Convert a frame number to microseconds."""
        return int(framenum / fps * (10**6))


class MicroDVDWriter(BaseWriter):
    """Serializes a CaptionSet to MicroDVD format."""

    def write(self, caption_set, **kwargs):
        """Write a CaptionSet as a MicroDVD string.

        :type caption_set: CaptionSet
        :rtype: str
        """
        caption_set = deepcopy(caption_set)

        captions = []

        for lang in caption_set.get_languages():
            captions.append(self._recreate_lang(caption_set.get_captions(lang)))

        return "".join(captions)

    @staticmethod
    def _microtoframes(micro, fps=25.0):
        """Convert microseconds to a frame number."""
        return int(micro * fps / (10**6))

    def _recreate_lang(self, captions):
        """Serialize one language's captions to MicroDVD text."""
        sub = ""

        for caption in captions:
            start = self._microtoframes(caption.start)
            end = self._microtoframes(caption.end)
            sub += f"{{{start}}}{{{end}}}"

            new_content = ""
            for node in caption.nodes:
                new_content = self._recreate_line(new_content, node)

            # Eliminate excessive line breaks
            new_content = new_content.strip() + "\n"
            while "\n\n" in new_content:
                new_content = new_content.replace("\n\n", "\n")
            # Break unnecessary on last line
            while "|\n" in new_content:
                new_content = new_content.replace("|\n", "\n")

            sub += new_content

        return sub

    @staticmethod
    def _recreate_line(sub, line):
        """Append a CaptionNode's content to the output string."""
        if line.type_ == CaptionNode.TEXT:
            return sub + line.content
        elif line.type_ == CaptionNode.BREAK:
            return sub + "|"
        else:
            return sub
