package subsync

import (
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"
)

// ErrNoCues means nothing in the file looked like a subtitle.
var ErrNoCues = errors.New("subsync: no subtitle cues found")

// Parse reads SRT or WebVTT.
//
// One parser for both because the only difference that matters here is a comma or a full
// stop between the seconds and the milliseconds, and Emby will hand back either depending
// on the route asked and the codec underneath. Everything a format carries that this
// package does not need — cue identifiers, WebVTT positioning, styling blocks, the byte
// order mark a Windows editor leaves behind — is skipped rather than rejected, because a
// subtitle somebody is trying to fix is by definition one that is already not perfect.
func Parse(data []byte) ([]Cue, error) {
	text := strings.ReplaceAll(string(data), "\r\n", "\n")
	text = strings.TrimPrefix(text, "\ufeff")

	var cues []Cue
	lines := strings.Split(text, "\n")
	for i := 0; i < len(lines); i++ {
		start, end, ok := parseTimingLine(lines[i])
		if !ok {
			continue
		}
		body := []string{}
		for i++; i < len(lines) && strings.TrimSpace(lines[i]) != ""; i++ {
			// A timing line with no blank line before it ends the previous cue: some
			// files in the wild are written that way, and reading the next cue's timing
			// as this one's text would put the whole file one cue out.
			if _, _, isTiming := parseTimingLine(lines[i]); isTiming {
				i--
				break
			}
			body = append(body, lines[i])
		}
		cues = append(cues, Cue{Start: start, End: end, Text: strings.Join(body, "\n")})
	}
	if len(cues) == 0 {
		return nil, ErrNoCues
	}
	return Normalise(cues), nil
}

func parseTimingLine(line string) (time.Duration, time.Duration, bool) {
	before, after, ok := strings.Cut(line, "-->")
	if !ok {
		return 0, 0, false
	}
	start, ok := parseTimestamp(before)
	if !ok {
		return 0, 0, false
	}
	// WebVTT puts cue settings after the end timestamp ("align:start position:50%"), so
	// only the first field of what follows is a time.
	end, ok := parseTimestamp(strings.Fields(after)[0])
	if !ok {
		return 0, 0, false
	}
	return start, end, true
}

// parseTimestamp reads HH:MM:SS,mmm and every variation of it that turns up: a full stop
// instead of the comma, the hours omitted as WebVTT allows, and fewer than three digits
// after the separator.
func parseTimestamp(field string) (time.Duration, bool) {
	text := strings.TrimSpace(field)
	if text == "" {
		return 0, false
	}
	seconds, fraction, _ := strings.Cut(strings.ReplaceAll(text, ",", "."), ".")

	parts := strings.Split(seconds, ":")
	if len(parts) < 2 || len(parts) > 3 {
		return 0, false
	}
	var total time.Duration
	units := []time.Duration{time.Hour, time.Minute, time.Second}
	units = units[len(units)-len(parts):]
	for i, part := range parts {
		value, err := strconv.Atoi(strings.TrimSpace(part))
		if err != nil || value < 0 {
			return 0, false
		}
		total += time.Duration(value) * units[i]
	}

	if fraction != "" {
		digits := fraction
		if len(digits) > 3 {
			digits = digits[:3]
		}
		value, err := strconv.Atoi(digits)
		if err != nil {
			return 0, false
		}
		for len(digits) < 3 {
			value *= 10
			digits += "0"
		}
		total += time.Duration(value) * time.Millisecond
	}
	return total, true
}

// FormatSRT writes cues back out as SubRip.
//
// SRT rather than the format that came in, because it is the one every player reads and
// the one the stored-subtitle table already declares. Renumbered from one: the indices in
// a file being repaired are frequently wrong already, and they carry no meaning worth
// preserving.
func FormatSRT(cues []Cue) []byte {
	var out strings.Builder
	for i, cue := range cues {
		fmt.Fprintf(&out, "%d\n%s --> %s\n%s\n\n",
			i+1, formatTimestamp(cue.Start), formatTimestamp(cue.End), cue.Text)
	}
	return []byte(out.String())
}

func formatTimestamp(d time.Duration) string {
	if d < 0 {
		d = 0
	}
	milliseconds := d.Milliseconds()
	return fmt.Sprintf("%02d:%02d:%02d,%03d",
		milliseconds/3_600_000,
		milliseconds/60_000%60,
		milliseconds/1000%60,
		milliseconds%1000)
}
