export const mp4AlphaLayoutKey = "mp4_alpha_layout";
export const mp4AlphaDetectVersion = 1;

export const mp4AlphaLayoutOptions = [
    ["normal", "普通 MP4"],
    ["alpha_left_rgb_right", "左 Alpha / 右 RGB"],
    ["rgb_left_alpha_right", "左 RGB / 右 Alpha"],
    ["alpha_top_rgb_bottom", "上 Alpha / 下 RGB"],
    ["rgb_top_alpha_bottom", "上 RGB / 下 Alpha"],
    ["custom", "自定义"],
];

const transparentLayoutKinds = new Set([
    "alpha_left_rgb_right",
    "rgb_left_alpha_right",
    "alpha_top_rgb_bottom",
    "rgb_top_alpha_bottom",
    "custom",
]);
const videoFrameStepSeconds = 1 / 24;
const maxSeekFrameCount = 360;
const videoLoadTimeoutMs = 8000;
const videoSeekTimeoutMs = 5000;
const mp4BoxHeaderSize = 8;
const mp4LargeBoxHeaderSize = 16;
const vapcMarkerBytes = [118, 97, 112, 99];

export function isMp4File(file) {
    if (!file?.name) {
        return false;
    }
    const filename = String(file.name).toLowerCase();
    const type = String(file.type || "").toLowerCase();
    return filename.endsWith(".mp4") || type === "video/mp4";
}

export function isMp4Source(source = "") {
    const normalized = String(source || "").trim();
    if (!normalized) {
        return false;
    }
    try {
        const url = new URL(normalized, "https://admin.local");
        return url.pathname.toLowerCase().endsWith(".mp4");
    } catch {
        return normalized.toLowerCase().split("?")[0].endsWith(".mp4");
    }
}

export function createMp4AlphaLayout({
    alphaLayout = "normal",
    videoW,
    videoH,
    rgbFrame,
    alphaFrame = null,
    confirmed = false,
    confidence = 0,
    detectVersion = mp4AlphaDetectVersion,
} = {}) {
    const videoWidth = positiveInteger(videoW);
    const videoHeight = positiveInteger(videoH);
    if (videoWidth <= 0 || videoHeight <= 0) {
        return null;
    }
    const layoutKind = normalizeAlphaLayout(alphaLayout);
    const defaultFrames = framePairForLayout(layoutKind, videoWidth, videoHeight);
    const normalizedRgbFrame = sanitizeFrame(rgbFrame, videoWidth, videoHeight) || defaultFrames.rgbFrame;
    const normalizedAlphaFrame =
        layoutKind === "normal" ? null : sanitizeFrame(alphaFrame, videoWidth, videoHeight) || defaultFrames.alphaFrame;

    if (!normalizedRgbFrame || (layoutKind !== "normal" && !normalizedAlphaFrame)) {
        return null;
    }

    return {
        alpha_layout: layoutKind,
        video_w: videoWidth,
        video_h: videoHeight,
        rgb_frame: normalizedRgbFrame,
        alpha_frame: normalizedAlphaFrame,
        confirmed: Boolean(confirmed),
        confidence: roundConfidence(confidence),
        detect_version: positiveInteger(detectVersion) || mp4AlphaDetectVersion,
    };
}

export function mp4AlphaLayoutFromMetadata(metadataJson) {
    const metadata = parseMetadataObject(metadataJson);
    return sanitizeMp4AlphaLayout(metadata?.[mp4AlphaLayoutKey]);
}

export function mergeMp4AlphaLayoutMetadata(metadataJson, layout) {
    const sanitizedLayout = sanitizeMp4AlphaLayout(layout);
    if (!sanitizedLayout) {
        return normalizeMetadataJSON(metadataJson);
    }
    return stringifyMetadata({
        ...parseMetadataObject(metadataJson),
        animation_format: "mp4",
        [mp4AlphaLayoutKey]: sanitizedLayout,
    });
}

export function removeMp4AlphaLayoutMetadata(metadataJson) {
    const metadata = parseMetadataObject(metadataJson);
    delete metadata[mp4AlphaLayoutKey];
    if (metadata.animation_format === "mp4") {
        delete metadata.animation_format;
    }
    return stringifyMetadata(metadata);
}

export function confirmMp4AlphaLayoutMetadata(metadataJson) {
    const layout = mp4AlphaLayoutFromMetadata(metadataJson);
    if (!layout) {
        return normalizeMetadataJSON(metadataJson);
    }
    return mergeMp4AlphaLayoutMetadata(metadataJson, { ...layout, confirmed: true });
}

export function updateMp4AlphaLayoutKind(metadataJson, alphaLayout) {
    const layout = mp4AlphaLayoutFromMetadata(metadataJson);
    if (!layout) {
        return normalizeMetadataJSON(metadataJson);
    }
    const keepCustomFrames = alphaLayout === "custom";
    const nextLayout = createMp4AlphaLayout({
        alphaLayout,
        // 左右/上下方向变化时必须按新方向重算矩形；沿用旧矩形会让预览仍显示上一个通道顺序。
        alphaFrame: keepCustomFrames ? layout.alpha_frame : undefined,
        confirmed: false,
        confidence: layout.confidence,
        detectVersion: layout.detect_version,
        rgbFrame: keepCustomFrames ? layout.rgb_frame : undefined,
        videoH: layout.video_h,
        videoW: layout.video_w,
    });
    return nextLayout ? mergeMp4AlphaLayoutMetadata(metadataJson, nextLayout) : normalizeMetadataJSON(metadataJson);
}

export function updateMp4AlphaLayoutFrame(metadataJson, key, value) {
    const layout = mp4AlphaLayoutFromMetadata(metadataJson);
    if (!layout) {
        return normalizeMetadataJSON(metadataJson);
    }
    const frame = parseFrameText(value);
    const nextLayout = createMp4AlphaLayout({
        alphaLayout: layout.alpha_layout,
        rgbFrame: key === "rgb_frame" ? frame : layout.rgb_frame,
        alphaFrame: key === "alpha_frame" ? frame : layout.alpha_frame,
        confirmed: false,
        confidence: layout.confidence,
        detectVersion: layout.detect_version,
        videoH: layout.video_h,
        videoW: layout.video_w,
    });
    return nextLayout ? mergeMp4AlphaLayoutMetadata(metadataJson, nextLayout) : normalizeMetadataJSON(metadataJson);
}

export function hasUnconfirmedMp4Layout(metadataJson) {
    const layout = mp4AlphaLayoutFromMetadata(metadataJson);
    return Boolean(layout && !layout.confirmed);
}

export function mp4LayoutLabel(layout) {
    const kind = typeof layout === "string" ? layout : layout?.alpha_layout;
    return mp4AlphaLayoutOptions.find(([value]) => value === kind)?.[1] || "未解析";
}

export function frameText(frame) {
    return Array.isArray(frame) ? frame.join(",") : "";
}

export async function detectMp4AlphaLayout(file) {
    if (!isMp4File(file)) {
        throw new Error("请选择 MP4 动效素材");
    }
    const vapcLayout = await detectVapcMp4AlphaLayout(file);
    if (vapcLayout) {
        return vapcLayout;
    }
    const frames = await readVideoFrames(file);
    if (!frames.length) {
        const info = await readMp4VideoInfo(file);
        return createMp4AlphaLayout({
            alphaLayout: "normal",
            videoW: info.width,
            videoH: info.height,
            confirmed: false,
        });
    }
    return detectMp4AlphaLayoutFromFrames(frames);
}

async function detectVapcMp4AlphaLayout(file) {
    const info = await parseVapcInfo(file).catch(() => null);
    if (!info || typeof info !== "object") {
        return null;
    }
    return createMp4AlphaLayout({
        alphaLayout: "custom",
        alphaFrame: info.aFrame ?? info.a_frame,
        confirmed: false,
        confidence: 0.99,
        rgbFrame: info.rgbFrame ?? info.rgb_frame,
        videoH: info.videoH ?? info.video_height,
        videoW: info.videoW ?? info.video_width,
    });
}

async function parseVapcInfo(file) {
    if (!file?.arrayBuffer) {
        return null;
    }
    const bytes = new Uint8Array(await file.arrayBuffer());
    const boxPayload = findMp4BoxPayload(bytes, vapcMarkerBytes);
    return boxPayload ? parseVapcJsonPayload(boxPayload, 0, boxPayload.length) : null;
}

function parseVapcJsonPayload(bytes, start, end) {
    const jsonStart = findByte(bytes, 123, start, end);
    if (jsonStart < 0) {
        return null;
    }
    const jsonEnd = findJsonObjectEnd(bytes, jsonStart, end);
    if (jsonEnd <= jsonStart) {
        return null;
    }
    const payload = JSON.parse(new TextDecoder().decode(bytes.slice(jsonStart, jsonEnd + 1)));
    return payload?.info || null;
}

export async function mp4FileFromSource(source) {
    const normalizedSource = String(source || "").trim();
    if (!isMp4Source(normalizedSource)) {
        throw new Error("动态资源不是 MP4");
    }
    let response;
    try {
        response = await fetch(normalizedSource);
    } catch {
        throw new Error("MP4 素材下载失败，请确认素材 URL 可访问");
    }
    if (!response.ok) {
        throw new Error("MP4 素材下载失败，请确认素材 URL 可访问");
    }
    const blob = await response.blob();
    return new File([blob], filenameFromSource(normalizedSource), { type: blob.type || "video/mp4" });
}

export async function readMp4VideoInfo(file) {
    const video = await loadVideo(file);
    try {
        return {
            duration: Number.isFinite(video.duration) ? video.duration : 0,
            height: video.videoHeight || 0,
            width: video.videoWidth || 0,
        };
    } finally {
        releaseVideo(video);
    }
}

export function detectMp4AlphaLayoutFromFrames(frames) {
    const validFrames = (frames || []).filter((frame) => frame?.data && frame.width > 0 && frame.height > 0);
    if (!validFrames.length) {
        return null;
    }
    const { width, height } = validFrames[0];
    const candidates = candidateLayoutsForVideo(width, height).map((alphaLayout) => {
        const pair = framePairForLayout(alphaLayout, width, height);
        return {
            alphaLayout,
            ...pair,
            score: scoreCandidate(validFrames, pair),
        };
    });
    candidates.sort((left, right) => right.score - left.score);
    const selected = candidates[0];
    if (!selected || selected.score < 0.42) {
        return createMp4AlphaLayout({
            alphaLayout: "normal",
            videoW: width,
            videoH: height,
            confirmed: false,
            confidence: selected?.score || 0,
        });
    }
    return createMp4AlphaLayout({
        alphaLayout: selected.alphaLayout,
        videoW: width,
        videoH: height,
        rgbFrame: selected.rgbFrame,
        alphaFrame: selected.alphaFrame,
        confirmed: false,
        confidence: selected.score,
    });
}

function candidateLayoutsForVideo(width, height) {
    const aspectRatio = width / height;
    const layouts = [];
    if (aspectRatio >= 0.85) {
        layouts.push("alpha_left_rgb_right", "rgb_left_alpha_right");
    }
    if (aspectRatio <= 0.52) {
        layouts.push("alpha_top_rgb_bottom", "rgb_top_alpha_bottom");
    }
    return layouts;
}

export function sanitizeMp4AlphaLayout(layout) {
    if (!layout || typeof layout !== "object") {
        return null;
    }
    return createMp4AlphaLayout({
        alphaLayout: layout.alpha_layout ?? layout.alphaLayout,
        videoW: layout.video_w ?? layout.videoW,
        videoH: layout.video_h ?? layout.videoH,
        rgbFrame: layout.rgb_frame ?? layout.rgbFrame,
        alphaFrame: layout.alpha_frame ?? layout.alphaFrame,
        confirmed: layout.confirmed,
        confidence: layout.confidence,
        detectVersion: layout.detect_version ?? layout.detectVersion,
    });
}

function normalizeAlphaLayout(value) {
    const normalized = String(value || "")
        .trim()
        .toLowerCase();
    if (normalized === "normal" || transparentLayoutKinds.has(normalized)) {
        return normalized;
    }
    return "normal";
}

function framePairForLayout(alphaLayout, width, height) {
    if (alphaLayout === "alpha_left_rgb_right") {
        const half = Math.floor(width / 2);
        return {
            alphaFrame: [0, 0, half, height],
            rgbFrame: [half, 0, width - half, height],
        };
    }
    if (alphaLayout === "rgb_left_alpha_right") {
        const half = Math.floor(width / 2);
        return {
            alphaFrame: [half, 0, width - half, height],
            rgbFrame: [0, 0, half, height],
        };
    }
    if (alphaLayout === "alpha_top_rgb_bottom") {
        const half = Math.floor(height / 2);
        return {
            alphaFrame: [0, 0, width, half],
            rgbFrame: [0, half, width, height - half],
        };
    }
    if (alphaLayout === "rgb_top_alpha_bottom") {
        const half = Math.floor(height / 2);
        return {
            alphaFrame: [0, half, width, height - half],
            rgbFrame: [0, 0, width, half],
        };
    }
    return {
        alphaFrame: null,
        rgbFrame: [0, 0, width, height],
    };
}

function sanitizeFrame(value, videoWidth, videoHeight) {
    const values = Array.isArray(value) ? value : parseFrameText(value);
    if (!Array.isArray(values) || values.length < 4) {
        return null;
    }
    const frame = values.slice(0, 4).map(positiveIntegerOrZero);
    if (frame[0] < 0 || frame[1] < 0) {
        return null;
    }
    if (frame[2] <= 0 || frame[3] <= 0) {
        return null;
    }
    if (frame[0] + frame[2] > videoWidth || frame[1] + frame[3] > videoHeight) {
        return null;
    }
    return frame;
}

function parseFrameText(value) {
    if (Array.isArray(value)) {
        return value;
    }
    const text = String(value || "").trim();
    if (!text) {
        return null;
    }
    const values = text.split(",").map((item) => Number(item.trim()));
    return values.length >= 4 && values.every(Number.isFinite) ? values.slice(0, 4) : null;
}

function findMp4BoxPayload(bytes, boxType) {
    let offset = 0;
    while (offset + mp4BoxHeaderSize <= bytes.length) {
        const size = readUint32(bytes, offset);
        const headerSize = size === 1 ? mp4LargeBoxHeaderSize : mp4BoxHeaderSize;
        if (offset + headerSize > bytes.length) {
            return null;
        }
        const boxEnd = size === 0 ? bytes.length : offset + (size === 1 ? readUint64(bytes, offset + 8) : size);
        if (boxEnd < offset + headerSize || boxEnd > bytes.length) {
            return null;
        }
        if (bytesMatchAt(bytes, boxType, offset + 4)) {
            return bytes.slice(offset + headerSize, boxEnd);
        }
        offset = boxEnd;
    }
    return null;
}

function bytesMatchAt(bytes, pattern, offset) {
    if (!bytes?.length || !pattern?.length || offset < 0 || offset + pattern.length > bytes.length) {
        return false;
    }
    for (let index = 0; index < pattern.length; index += 1) {
        if (bytes[offset + index] !== pattern[index]) {
            return false;
        }
    }
    return true;
}

function readUint32(bytes, offset) {
    return bytes[offset] * 2 ** 24 + bytes[offset + 1] * 2 ** 16 + bytes[offset + 2] * 2 ** 8 + bytes[offset + 3];
}

function readUint64(bytes, offset) {
    const value = readUint32(bytes, offset) * 2 ** 32 + readUint32(bytes, offset + 4);
    return Number.isSafeInteger(value) ? value : 0;
}

function findByte(bytes, target, start, end) {
    for (let index = Math.max(0, start); index < Math.min(bytes.length, end); index += 1) {
        if (bytes[index] === target) {
            return index;
        }
    }
    return -1;
}

function findJsonObjectEnd(bytes, start, end) {
    let depth = 0;
    let inString = false;
    let escaping = false;
    for (let index = start; index < Math.min(bytes.length, end); index += 1) {
        const byte = bytes[index];
        if (inString) {
            if (escaping) {
                escaping = false;
            } else if (byte === 92) {
                escaping = true;
            } else if (byte === 34) {
                inString = false;
            }
            continue;
        }
        if (byte === 34) {
            inString = true;
            continue;
        }
        if (byte === 123) {
            depth += 1;
            continue;
        }
        if (byte === 125) {
            depth -= 1;
            if (depth === 0) {
                return index;
            }
        }
    }
    return -1;
}

function filenameFromSource(source) {
    try {
        const url = new URL(source, "https://admin.local");
        const pathname = decodeURIComponent(url.pathname || "");
        return pathname.split("/").filter(Boolean).pop() || "animation.mp4";
    } catch {
        const clean = source.split("?")[0].split("#")[0];
        return decodeURIComponent(clean.split("/").filter(Boolean).pop() || "animation.mp4");
    }
}

function parseMetadataObject(metadataJson) {
    const normalized = String(metadataJson || "").trim();
    if (!normalized) {
        return {};
    }
    try {
        const parsed = JSON.parse(normalized);
        return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
    } catch {
        return {};
    }
}

function normalizeMetadataJSON(metadataJson) {
    return stringifyMetadata(parseMetadataObject(metadataJson));
}

function stringifyMetadata(metadata) {
    const clean = {};
    Object.entries(metadata || {}).forEach(([key, value]) => {
        if (value !== undefined && value !== null && value !== "") {
            clean[key] = value;
        }
    });
    return Object.keys(clean).length ? JSON.stringify(clean) : "{}";
}

function positiveInteger(value) {
    const number = Number(value);
    return Number.isInteger(number) && number > 0 ? number : 0;
}

function positiveIntegerOrZero(value) {
    const number = Number(value);
    return Number.isInteger(number) && number >= 0 ? number : -1;
}

function roundConfidence(value) {
    const number = Number(value);
    if (!Number.isFinite(number) || number <= 0) {
        return 0;
    }
    return Math.round(Math.min(number, 1) * 1000) / 1000;
}

async function readVideoFrames(file) {
    const video = await loadVideo(file);
    try {
        const canvas = document.createElement("canvas");
        canvas.width = video.videoWidth;
        canvas.height = video.videoHeight;
        const context = canvas.getContext("2d", { willReadFrequently: true });
        if (!context || canvas.width <= 0 || canvas.height <= 0) {
            throw new Error("MP4 画面无法读取");
        }
        await waitForVideoCurrentData(video);
        const duration = Number.isFinite(video.duration) && video.duration > 0 ? video.duration : 0;
        const frameCount = Math.min(Math.max(Math.ceil(duration / videoFrameStepSeconds), 1), maxSeekFrameCount);
        const frames = [];
        for (let index = 0; index < frameCount; index += 1) {
            const time = duration > 0 ? Math.min(duration - 0.001, index * videoFrameStepSeconds) : 0;
            await seekVideo(video, Math.max(0, time));
            context.drawImage(video, 0, 0, canvas.width, canvas.height);
            frames.push({
                data: context.getImageData(0, 0, canvas.width, canvas.height).data,
                height: canvas.height,
                width: canvas.width,
            });
        }
        return frames;
    } finally {
        releaseVideo(video);
    }
}

function loadVideo(file) {
    return new Promise((resolve, reject) => {
        const video = document.createElement("video");
        let settled = false;
        let timeoutId = null;
        const finish = (callback, value) => {
            if (settled) {
                return;
            }
            settled = true;
            window.clearTimeout(timeoutId);
            video.onloadedmetadata = null;
            video.onerror = null;
            callback(value);
        };
        video.muted = true;
        video.playsInline = true;
        video.preload = "auto";
        video.src = URL.createObjectURL(file);
        timeoutId = window.setTimeout(() => {
            finish(reject, new Error("MP4 元数据读取超时"));
            releaseVideo(video);
        }, videoLoadTimeoutMs);
        video.onloadedmetadata = () => finish(resolve, video);
        video.onerror = () => {
            finish(reject, new Error("MP4 元数据读取失败"));
            releaseVideo(video);
        };
    });
}

function seekVideo(video, time) {
    return new Promise((resolve, reject) => {
        if (Math.abs((video.currentTime || 0) - time) < 0.001 && video.readyState >= 2) {
            resolve();
            return;
        }
        let timeoutId = null;
        const done = () => {
            window.clearTimeout(timeoutId);
            video.removeEventListener("seeked", done);
            video.removeEventListener("error", fail);
            resolve();
        };
        const fail = () => {
            window.clearTimeout(timeoutId);
            video.removeEventListener("seeked", done);
            video.removeEventListener("error", fail);
            reject(new Error("MP4 画面解析失败"));
        };
        timeoutId = window.setTimeout(() => {
            video.removeEventListener("seeked", done);
            video.removeEventListener("error", fail);
            reject(new Error("MP4 画面解析超时"));
        }, videoSeekTimeoutMs);
        video.addEventListener("seeked", done, { once: true });
        video.addEventListener("error", fail, { once: true });
        try {
            video.currentTime = time;
        } catch {
            fail();
        }
    });
}

function waitForVideoCurrentData(video) {
    return new Promise((resolve, reject) => {
        if (video.readyState >= 2) {
            resolve();
            return;
        }
        let timeoutId = null;
        const done = () => {
            window.clearTimeout(timeoutId);
            video.removeEventListener("loadeddata", done);
            video.removeEventListener("canplay", done);
            video.removeEventListener("error", fail);
            resolve();
        };
        const fail = () => {
            window.clearTimeout(timeoutId);
            video.removeEventListener("loadeddata", done);
            video.removeEventListener("canplay", done);
            video.removeEventListener("error", fail);
            reject(new Error("MP4 画面读取失败"));
        };
        timeoutId = window.setTimeout(() => {
            video.removeEventListener("loadeddata", done);
            video.removeEventListener("canplay", done);
            video.removeEventListener("error", fail);
            reject(new Error("MP4 画面读取超时"));
        }, videoLoadTimeoutMs);
        video.addEventListener("loadeddata", done, { once: true });
        video.addEventListener("canplay", done, { once: true });
        video.addEventListener("error", fail, { once: true });
    });
}

function releaseVideo(video) {
    if (!video) {
        return;
    }
    if (video.src) {
        URL.revokeObjectURL(video.src);
    }
    video.removeAttribute("src");
    video.load();
}

function scoreCandidate(frames, candidate) {
    const scores = frames.map((frame) => scoreFrameCandidate(frame, candidate));
    if (!scores.length) {
        return 0;
    }
    const average = scores.reduce((sum, value) => sum + value, 0) / scores.length;
    return Math.max(0, Math.min(1, average));
}

function scoreFrameCandidate(frame, candidate) {
    const alpha = regionStats(frame, candidate.alphaFrame);
    const rgb = regionStats(frame, candidate.rgbFrame);
    const maskScore = alpha.lowChromaRatio * 0.45 + alpha.brightnessSpread * 0.35 + alpha.darkOrBrightRatio * 0.2;
    const rgbScore = Math.max(0, 1 - rgb.lowChromaRatio) * 0.6 + rgb.brightnessSpread * 0.4;
    return maskScore * 0.7 + rgbScore * 0.3;
}

function regionStats(frame, rect) {
    const [left, top, width, height] = rect;
    const xStep = Math.max(1, Math.floor(width / 90));
    const yStep = Math.max(1, Math.floor(height / 90));
    let count = 0;
    let lowChroma = 0;
    let darkOrBright = 0;
    let minBrightness = 255;
    let maxBrightness = 0;
    for (let y = top; y < top + height; y += yStep) {
        for (let x = left; x < left + width; x += xStep) {
            const index = (y * frame.width + x) * 4;
            const r = frame.data[index];
            const g = frame.data[index + 1];
            const b = frame.data[index + 2];
            const brightness = (r + g + b) / 3;
            const chroma = Math.max(r, g, b) - Math.min(r, g, b);
            count += 1;
            if (chroma <= 16) {
                lowChroma += 1;
            }
            if (brightness <= 32 || brightness >= 224) {
                darkOrBright += 1;
            }
            minBrightness = Math.min(minBrightness, brightness);
            maxBrightness = Math.max(maxBrightness, brightness);
        }
    }
    return {
        brightnessSpread: count ? (maxBrightness - minBrightness) / 255 : 0,
        darkOrBrightRatio: count ? darkOrBright / count : 0,
        lowChromaRatio: count ? lowChroma / count : 0,
    };
}
