#!/usr/bin/env python3
"""Check a translated subtitle file against the source track before uploading it to YouTube.

Usage:
  python3 check_youtube_subtitles.py convert captions.sbv captions.srt
  python3 check_youtube_subtitles.py check source.srt translated.srt [--max-cps 20] [--max-line 42]

Reads .srt, .vtt, and .sbv. Uses only the Python standard library.
Defaults (42 characters per line, 20 characters per second) follow the Netflix
English (USA) Timed Text Style Guide for adult programs; adjust them per language.
"""
import argparse
import re
import sys

TIME = r"(?:(\d+):)?(\d{2}):(\d{2})[.,](\d{3})"


def to_ms(h, m, s, ms):
    return ((int(h or 0) * 60 + int(m)) * 60 + int(s)) * 1000 + int(ms)


def fmt_srt(ms):
    h, ms = divmod(ms, 3_600_000)
    m, ms = divmod(ms, 60_000)
    s, ms = divmod(ms, 1000)
    return f"{h:02}:{m:02}:{s:02},{ms:03}"


def read_text(path):
    raw = open(path, "rb").read()
    try:
        text = raw.decode("utf-8-sig")
    except UnicodeDecodeError as error:
        sys.exit(f"{path}: not valid UTF-8 ({error}). Re-save it as UTF-8 before uploading.")
    return text.replace("\r\n", "\n").replace("\r", "\n")


def parse(path):
    """Return a list of (start_ms, end_ms, text) cues."""
    text = read_text(path)
    cues = []
    for block in re.split(r"\n\s*\n", text.strip()):
        lines = block.split("\n")
        for i, line in enumerate(lines):
            srt_vtt = re.match(TIME + r"\s*-->\s*" + TIME, line.strip())
            sbv = re.match(TIME + r"," + TIME + r"$", line.strip())
            match = srt_vtt or sbv
            if match:
                g = match.groups()
                cues.append((to_ms(*g[:4]), to_ms(*g[4:]), "\n".join(lines[i + 1:]).strip()))
                break
    return cues


def convert(src, dst):
    cues = parse(src)
    blocks = [f"{n}\n{fmt_srt(start)} --> {fmt_srt(end)}\n{text}" for n, (start, end, text) in enumerate(cues, 1)]
    with open(dst, "w", encoding="utf-8", newline="\n") as out:
        out.write("\n\n".join(blocks) + "\n")
    print(f"Wrote {len(cues)} cues to {dst}")


def check(source, translated, max_cps, max_line):
    a, b = parse(source), parse(translated)
    for path, cues in ((source, a), (translated, b)):
        if not cues:
            sys.exit(f"{path}: no subtitle cues found. Check that it is an SRT, VTT, or SBV file.")
    problems = []
    if len(a) != len(b):
        problems.append(f"Cue count changed: source {len(a)}, translation {len(b)}")
    for n, (sa, sb) in enumerate(zip(a, b), 1):
        if (sa[0], sa[1]) != (sb[0], sb[1]):
            problems.append(f"Cue {n}: timing changed {fmt_srt(sa[0])}-{fmt_srt(sa[1])} -> {fmt_srt(sb[0])}-{fmt_srt(sb[1])}")
    for n, (start, end, text) in enumerate(b, 1):
        if not text:
            problems.append(f"Cue {n}: empty text")
        if end <= start:
            problems.append(f"Cue {n}: ends before it starts")
        if n < len(b) and b[n][0] < end:
            problems.append(f"Cue {n}: overlaps cue {n + 1}")
        chars = len(text.replace("\n", ""))
        seconds = max(end - start, 1) / 1000
        if chars / seconds > max_cps:
            problems.append(f"Cue {n}: {chars / seconds:.1f} characters/second (limit {max_cps})")
        for line in text.split("\n"):
            if len(line) > max_line:
                problems.append(f"Cue {n}: line has {len(line)} characters (limit {max_line})")
    print(f"Source cues: {len(a)}  Translated cues: {len(b)}")
    print("\n".join(problems) if problems else "No problems found.")
    return 1 if problems else 0


def main():
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    sub = parser.add_subparsers(dest="command", required=True)
    c = sub.add_parser("convert")
    c.add_argument("src")
    c.add_argument("dst")
    k = sub.add_parser("check")
    k.add_argument("source")
    k.add_argument("translated")
    k.add_argument("--max-cps", type=float, default=20)
    k.add_argument("--max-line", type=int, default=42)
    args = parser.parse_args()
    if args.command == "convert":
        convert(args.src, args.dst)
    else:
        sys.exit(check(args.source, args.translated, args.max_cps, args.max_line))


if __name__ == "__main__":
    main()
