import { formatTimestampMs, parseTimestampStringToMs, parseTimestampToMs, } from "./transcript/timestamps.js";
export { parseTimestampStringToMs };
function isRecord(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
function trackLabel(track) {
    if (typeof track.label === "string")
        return track.label.trim();
    if (!isRecord(track.name))
        return "";
    if (typeof track.name.simpleText === "string")
        return track.name.simpleText.trim();
    if (!Array.isArray(track.name.runs))
        return "";
    return track.name.runs
        .map((run) => (isRecord(run) && typeof run.text === "string" ? run.text : ""))
        .join("")
        .trim();
}
function normalizeTrack(value, automaticGroup) {
    if (!isRecord(value))
        return null;
    const track = value;
    const baseUrl = typeof track.baseUrl === "string"
        ? track.baseUrl
        : typeof track.url === "string"
            ? track.url
            : null;
    if (!baseUrl)
        return null;
    const languageCode = typeof track.languageCode === "string" ? track.languageCode.toLowerCase() : "";
    const kind = typeof track.kind === "string" ? track.kind.toLowerCase() : "";
    const label = trackLabel(track);
    return {
        baseUrl,
        languageCode,
        kind,
        label,
        automatic: automaticGroup || kind === "asr" || label.toLowerCase().includes("auto-generated"),
    };
}
export function rankYoutubeCaptionTracks({ captionTracks, automaticCaptionTracks = [], skipAutoGenerated = false, priority = "manual-first", }) {
    const tracks = [
        ...captionTracks.map((track) => normalizeTrack(track, false)),
        ...automaticCaptionTracks.map((track) => normalizeTrack(track, true)),
    ].filter((track) => track !== null);
    const score = (track) => {
        const english = track.languageCode === "en" || track.languageCode.startsWith("en-");
        const namedEnglish = track.label.toLowerCase().includes("english");
        return priority === "english-first"
            ? [english ? 0 : 1, track.automatic ? 1 : 0, namedEnglish ? 0 : 1]
            : [track.automatic ? 1 : 0, english ? 0 : 1, namedEnglish ? 0 : 1];
    };
    return tracks
        .filter((track) => !skipAutoGenerated || !track.automatic)
        .toSorted((left, right) => {
        const leftScore = score(left);
        const rightScore = score(right);
        for (let index = 0; index < leftScore.length; index += 1) {
            const difference = (leftScore[index] ?? 0) - (rightScore[index] ?? 0);
            if (difference !== 0)
                return difference;
        }
        return 0;
    });
}
function urlWithFormat(baseUrl, format, includeAlt = false) {
    try {
        const url = new URL(baseUrl);
        url.searchParams.set("fmt", format);
        if (includeAlt)
            url.searchParams.set("alt", "json");
        else
            url.searchParams.delete("alt");
        return url.toString();
    }
    catch {
        const separator = baseUrl.includes("?") ? "&" : "?";
        return `${baseUrl}${separator}fmt=${format}${includeAlt ? "&alt=json" : ""}`;
    }
}
function xmlCaptionUrl(baseUrl) {
    try {
        const url = new URL(baseUrl);
        url.searchParams.delete("fmt");
        url.searchParams.delete("alt");
        return url.toString();
    }
    catch {
        return baseUrl
            .replaceAll(/([?&])fmt=[^&]*/g, "$1")
            .replaceAll(/([?&])alt=[^&]*/g, "$1")
            .replace(/[?&]+$/, "")
            .replace("?&", "?")
            .replaceAll(/&&+/g, "&");
    }
}
export function buildYoutubeCaptionTrackUrls(baseUrl) {
    return Array.from(new Set([
        urlWithFormat(baseUrl, "json3", true),
        xmlCaptionUrl(baseUrl),
        urlWithFormat(baseUrl, "vtt"),
    ]));
}
export function normalizeYoutubeCaptionText(text) {
    return text
        .replaceAll("&amp;", "&")
        .replaceAll("&lt;", "<")
        .replaceAll("&gt;", ">")
        .replaceAll("&quot;", '"')
        .replaceAll("&#39;", "'")
        .replaceAll("&#x27;", "'")
        .replaceAll("&#x2F;", "/")
        .replaceAll("&nbsp;", " ")
        .replace(/\s+/g, " ")
        .trim();
}
function transcriptFromLines(lines) {
    const normalized = lines.filter((line) => line.text.length > 0);
    if (normalized.length === 0)
        return null;
    const segments = normalized
        .filter((line) => line.startMs !== null)
        .map((line) => ({
        startMs: line.startMs,
        endMs: line.endMs,
        text: line.text,
    }));
    return {
        text: normalized.map((line) => line.text).join("\n"),
        lines: normalized,
        segments: segments.length > 0 ? segments : null,
    };
}
function parseJson3(raw) {
    try {
        const parsed = JSON.parse(raw);
        if (!isRecord(parsed) || !Array.isArray(parsed.events))
            return null;
        const lines = [];
        for (const event of parsed.events) {
            if (!isRecord(event) || !Array.isArray(event.segs))
                continue;
            const text = normalizeYoutubeCaptionText(event.segs
                .map((segment) => isRecord(segment) && typeof segment.utf8 === "string" ? segment.utf8 : "")
                .join(""));
            if (!text)
                continue;
            const startMs = parseTimestampToMs(event.tStartMs, false);
            const durationMs = parseTimestampToMs(event.dDurationMs, false);
            lines.push({
                startMs,
                endMs: startMs !== null && durationMs !== null ? startMs + durationMs : null,
                text,
            });
        }
        return transcriptFromLines(lines);
    }
    catch {
        return null;
    }
}
function parseXml(raw) {
    const pattern = /<text([^>]*)>([\s\S]*?)<\/text>/gi;
    const lines = [];
    let match = pattern.exec(raw);
    while (match) {
        const attributes = match[1] ?? "";
        const text = normalizeYoutubeCaptionText(match[2] ?? "");
        if (text) {
            const startValue = /\bstart\s*=\s*(['"])([^'"]+)\1/i.exec(attributes)?.[2];
            const durationValue = /\bdur\s*=\s*(['"])([^'"]+)\1/i.exec(attributes)?.[2];
            const startMs = startValue ? parseTimestampToMs(startValue, true) : null;
            const durationMs = durationValue ? parseTimestampToMs(durationValue, true) : null;
            lines.push({
                startMs,
                endMs: startMs !== null && durationMs !== null ? startMs + durationMs : null,
                text,
            });
        }
        match = pattern.exec(raw);
    }
    return transcriptFromLines(lines);
}
function parseVtt(raw) {
    const lines = [];
    let hasTiming = false;
    const blocks = raw.replace(/\r\n?/g, "\n").split(/\n[ \t]*\n/);
    for (const block of blocks) {
        const blockLines = block
            .split("\n")
            .map((line) => line.trim())
            .filter(Boolean);
        const firstLine = blockLines[0] ?? "";
        if (!firstLine ||
            /^WEBVTT(?:[ \t].*)?$/i.test(firstLine) ||
            /^(NOTE|STYLE|REGION)(?:[ \t]|$)/i.test(firstLine)) {
            continue;
        }
        const timingIndex = blockLines
            .slice(0, 2)
            .findIndex((line) => /^(\S+)\s+-->\s+(\S+)/.test(line));
        if (timingIndex < 0)
            continue;
        const timing = /^(\S+)\s+-->\s+(\S+)/.exec(blockLines[timingIndex] ?? "");
        if (!timing)
            continue;
        hasTiming = true;
        const text = normalizeYoutubeCaptionText(blockLines.slice(timingIndex + 1).join(" "));
        if (!text)
            continue;
        lines.push({
            startMs: timing[1] ? parseTimestampStringToMs(timing[1]) : null,
            endMs: timing[2] ? parseTimestampStringToMs(timing[2]) : null,
            text,
        });
    }
    return hasTiming ? transcriptFromLines(lines) : null;
}
export function parseYoutubeCaptionPayload(raw) {
    const trimmed = raw.trim();
    if (!trimmed)
        return null;
    if (trimmed.startsWith("WEBVTT"))
        return parseVtt(trimmed);
    if (trimmed.startsWith("{")) {
        const json = parseJson3(trimmed);
        if (json)
            return json;
    }
    return parseXml(trimmed) ?? parseJson3(trimmed) ?? parseVtt(trimmed);
}
export async function resolveYoutubeCaptionTrack(baseUrl, loadText) {
    for (const url of buildYoutubeCaptionTrackUrls(baseUrl)) {
        try {
            const raw = await loadText(url);
            if (!raw)
                continue;
            const parsed = parseYoutubeCaptionPayload(raw);
            if (parsed)
                return parsed;
        }
        catch {
            // Try the next representation.
        }
    }
    return null;
}
export function formatYoutubeCaptionLines(lines) {
    return lines
        .map((line) => line.startMs === null ? line.text : `[${formatTimestampMs(line.startMs)}] ${line.text}`)
        .join("\n");
}
//# sourceMappingURL=youtube-captions.js.map