const defaultMetadata = "{}";
const profileCardLayoutKey = "profile_card_layout";
const sampleFrameCount = 6;
const alphaTransparentThreshold = 245;
const alphaContentThreshold = 16;
const alphaMaskBrightnessThreshold = 80;
const alphaMaskContentRowRatio = 0.5;
const rgbDiffThreshold = 24;
const minContentRowRatio = 0.01;
const consecutiveRows = 3;
const stableTopQuantile = 0.75;
const contentTopSnapPixels = 4;
const pagWasmURL = "/vendor/libpag.wasm";
let pagModulePromise;

export async function detectProfileCardLayout(file) {
    if (!file) {
        throw new Error("资料卡素材不存在");
    }
    if (isVideoFile(file)) {
        return detectVideoLayout(file);
    }
    if (isPagFile(file)) {
        return detectPagLayout(file);
    }
    if (isImageFile(file)) {
        return detectImageLayout(file);
    }
    throw new Error("当前素材类型暂不支持自动解析内容高度");
}

export function profileCardMetadataJSON(metadataJson) {
    const layout = profileCardLayoutFromMetadata(metadataJson);
    if (!layout) {
        return defaultMetadata;
    }
    return JSON.stringify({ [profileCardLayoutKey]: layout });
}

export function mergeProfileCardLayoutMetadata(metadataJson, layout) {
    const sanitized = sanitizeProfileCardLayout(layout);
    if (!sanitized) {
        return profileCardMetadataJSON(metadataJson);
    }
    return JSON.stringify({ [profileCardLayoutKey]: sanitized });
}

export function profileCardLayoutFromMetadata(metadataJson) {
    if (!metadataJson) {
        return null;
    }
    try {
        const metadata = JSON.parse(String(metadataJson).trim());
        return sanitizeProfileCardLayout(metadata?.[profileCardLayoutKey]);
    } catch {
        return null;
    }
}

export function measureProfileCardContent(frames, options = {}) {
    const validFrames = (frames || []).filter(
        (frame) =>
            frame?.data &&
            Number.isInteger(frame.width) &&
            Number.isInteger(frame.height) &&
            frame.width > 0 &&
            frame.height > 0,
    );
    if (!validFrames.length) {
        throw new Error("资料卡素材无法读取有效画面");
    }

    const vapcLayout = sanitizeVapcLayout(options.vapcLayout, validFrames[0]);
    if (vapcLayout) {
        const vapcMeasure = measureVapcProfileCardContent(validFrames, vapcLayout);
        if (vapcMeasure) {
            return vapcMeasure;
        }
    }

    const bounds = validFrames.map((frame) => scanFrameBounds(frame)).filter(Boolean);
    if (!bounds.length) {
        throw new Error("资料卡素材没有解析到有效内容");
    }
    const firstFrame = validFrames[0];
    const top = Math.min(...bounds.map((item) => item.contentTop));
    const bottom = Math.max(...bounds.map((item) => item.contentBottom));
    const scanWidth = Math.min(...bounds.map((item) => item.colorContentWidth).filter((value) => value > 0));
    const contentHeight = Math.max(0, bottom - top + 1);

    return {
        source_width: firstFrame.width,
        source_height: firstFrame.height,
        color_content_width: scanWidth || firstFrame.width,
        content_top: top,
        content_bottom: bottom,
        content_height: contentHeight,
        content_top_ratio: ratio(top, firstFrame.height),
        content_height_ratio: ratio(contentHeight, firstFrame.height),
        detect_version: 1,
    };
}

function measureVapcProfileCardContent(frames, vapcLayout) {
    const bounds = frames
        .map((frame) =>
            scanFrameBounds(frame, {
                scanLeft: vapcLayout.rgbFrame.x,
                scanWidth: vapcLayout.rgbFrame.width,
            }),
        )
        .filter(Boolean);
    const alphaTops = frames
        .map((frame) => scanVapcAlphaTop(frame, vapcLayout))
        .filter((value) => Number.isInteger(value) && value >= 0);
    if (!bounds.length || !alphaTops.length) {
        return null;
    }

    const firstFrame = frames[0];
    const top = clampInteger(
        snapPixel(upperQuantile(alphaTops, stableTopQuantile), contentTopSnapPixels),
        0,
        firstFrame.height - 1,
    );
    const bottom = Math.max(...bounds.map((item) => item.contentBottom));
    if (bottom < top) {
        return null;
    }
    const contentHeight = bottom - top + 1;

    return {
        source_width: firstFrame.width,
        source_height: firstFrame.height,
        color_content_width: vapcLayout.rgbFrame.width,
        content_top: top,
        content_bottom: bottom,
        content_height: contentHeight,
        content_top_ratio: ratio(top, firstFrame.height),
        content_height_ratio: ratio(contentHeight, firstFrame.height),
        detect_version: 2,
    };
}

function scanFrameBounds(frame, options = {}) {
    const { data, width, height } = frame;
    const alphaMode = hasMeaningfulAlpha(data);
    const scanLeft = clampInteger(options.scanLeft ?? 0, 0, width - 1);
    const maxScanWidth = width - scanLeft;
    const colorContentWidth = alphaMode
        ? maxScanWidth
        : options.scanWidth || detectColorContentWidth(data, width, height);
    const scanWidth = Math.max(1, Math.min(maxScanWidth, colorContentWidth));
    const background = alphaMode ? null : sampleBackgroundColor(data, width, height, scanLeft, scanWidth);
    const minPixels = Math.max(3, Math.floor(scanWidth * minContentRowRatio));
    const rowHasContent = (y) =>
        rowContentPixels(data, width, scanLeft, scanWidth, y, background, alphaMode) >= minPixels;
    const contentTop = findFirstContentRow(height, rowHasContent);
    const contentBottom = findLastContentRow(height, rowHasContent);

    if (contentTop < 0 || contentBottom < contentTop) {
        return null;
    }
    return {
        colorContentWidth: scanWidth,
        contentTop,
        contentBottom,
    };
}

function scanVapcAlphaTop(frame, vapcLayout) {
    const { data, width, height } = frame;
    const alphaFrame = vapcLayout.alphaFrame;
    const left = clampInteger(alphaFrame.x, 0, width - 1);
    const top = clampInteger(alphaFrame.y, 0, height - 1);
    const scanWidth = Math.max(1, Math.min(alphaFrame.width, width - left));
    const scanHeight = Math.max(1, Math.min(alphaFrame.height, height - top));
    const minPixels = Math.max(3, Math.floor(scanWidth * alphaMaskContentRowRatio));
    const rowHasContent = (y) => rowAlphaMaskPixels(data, width, left, scanWidth, y) >= minPixels;
    let run = 0;
    for (let y = top; y < top + scanHeight; y += 1) {
        run = rowHasContent(y) ? run + 1 : 0;
        if (run >= consecutiveRows) {
            return y - consecutiveRows + 1;
        }
    }
    return -1;
}

function findFirstContentRow(height, rowHasContent) {
    let run = 0;
    for (let y = 0; y < height; y += 1) {
        run = rowHasContent(y) ? run + 1 : 0;
        if (run >= consecutiveRows) {
            return y - consecutiveRows + 1;
        }
    }
    return -1;
}

function findLastContentRow(height, rowHasContent) {
    let run = 0;
    for (let y = height - 1; y >= 0; y -= 1) {
        run = rowHasContent(y) ? run + 1 : 0;
        if (run >= consecutiveRows) {
            return y + consecutiveRows - 1;
        }
    }
    return -1;
}

function rowContentPixels(data, width, scanLeft, scanWidth, y, background, alphaMode) {
    let count = 0;
    const offset = y * width * 4;
    for (let x = scanLeft; x < scanLeft + scanWidth; x += 1) {
        const index = offset + x * 4;
        const alpha = data[index + 3];
        if (alphaMode) {
            if (alpha > alphaContentThreshold) {
                count += 1;
            }
            continue;
        }
        if (alpha <= alphaContentThreshold) {
            continue;
        }
        const diff =
            Math.abs(data[index] - background.r) +
            Math.abs(data[index + 1] - background.g) +
            Math.abs(data[index + 2] - background.b);
        if (diff > rgbDiffThreshold) {
            count += 1;
        }
    }
    return count;
}

function rowAlphaMaskPixels(data, width, scanLeft, scanWidth, y) {
    let count = 0;
    const offset = y * width * 4;
    for (let x = scanLeft; x < scanLeft + scanWidth; x += 1) {
        const index = offset + x * 4;
        const brightness = (data[index] + data[index + 1] + data[index + 2]) / 3;
        if (brightness > alphaMaskBrightnessThreshold) {
            count += 1;
        }
    }
    return count;
}

function hasMeaningfulAlpha(data) {
    const totalPixels = Math.floor(data.length / 4);
    if (!totalPixels) {
        return false;
    }
    let transparentPixels = 0;
    for (let index = 3; index < data.length; index += 4) {
        if (data[index] < alphaTransparentThreshold) {
            transparentPixels += 1;
        }
    }
    return transparentPixels / totalPixels > 0.005;
}

function sampleBackgroundColor(data, width, height, scanLeft, scanWidth) {
    const sampleRows = Math.max(1, Math.floor(height * 0.02));
    const sampleStep = Math.max(1, Math.floor(scanWidth / 160));
    let r = 0;
    let g = 0;
    let b = 0;
    let count = 0;
    for (let y = 0; y < sampleRows; y += 1) {
        const offset = y * width * 4;
        for (let x = scanLeft; x < scanLeft + scanWidth; x += sampleStep) {
            const index = offset + x * 4;
            r += data[index];
            g += data[index + 1];
            b += data[index + 2];
            count += 1;
        }
    }
    return count
        ? {
              r: Math.round(r / count),
              g: Math.round(g / count),
              b: Math.round(b / count),
          }
        : { r: 0, g: 0, b: 0 };
}

function detectColorContentWidth(data, width, height) {
    const minMaskWidth = Math.max(1, Math.floor(width * 0.15));
    const minStart = Math.floor(width * 0.45);
    const rowStep = Math.max(1, Math.floor(height / 240));
    let run = 0;
    let start = width;

    for (let x = width - 1; x >= 0; x -= 1) {
        if (isLowChromaColumn(data, width, height, x, rowStep)) {
            run += 1;
            start = x;
            continue;
        }
        if (run >= minMaskWidth && start >= minStart) {
            return start;
        }
        run = 0;
        start = width;
    }
    if (run >= minMaskWidth && start >= minStart) {
        return start;
    }
    return width;
}

function isLowChromaColumn(data, width, height, x, rowStep) {
    let lowChromaPixels = 0;
    let sampled = 0;
    for (let y = 0; y < height; y += rowStep) {
        const index = (y * width + x) * 4;
        const max = Math.max(data[index], data[index + 1], data[index + 2]);
        const min = Math.min(data[index], data[index + 1], data[index + 2]);
        if (max - min <= 8) {
            lowChromaPixels += 1;
        }
        sampled += 1;
    }
    return sampled > 0 && lowChromaPixels / sampled > 0.92;
}

async function detectImageLayout(file) {
    const bitmap = await createImageBitmap(file);
    try {
        const frame = drawBitmapFrame(bitmap, bitmap.width, bitmap.height);
        return measureProfileCardContent([frame]);
    } finally {
        bitmap.close?.();
    }
}

async function detectVideoLayout(file) {
    const video = document.createElement("video");
    const url = URL.createObjectURL(file);
    try {
        const vapcLayoutPromise = parseVapcLayout(file).catch(() => null);
        video.muted = true;
        video.preload = "metadata";
        video.playsInline = true;
        video.src = url;
        await waitForVideoEvent(video, "loadedmetadata");
        await waitForVideoReady(video);
        const duration = Number.isFinite(video.duration) && video.duration > 0 ? video.duration : 0;
        const width = video.videoWidth;
        const height = video.videoHeight;
        if (!width || !height) {
            throw new Error("资料卡视频尺寸无效");
        }
        const sampleTimes = videoSampleTimes(duration);
        const frames = [];
        for (const time of sampleTimes) {
            await seekVideo(video, time);
            frames.push(drawBitmapFrame(video, width, height));
        }
        return measureProfileCardContent(frames, { vapcLayout: await vapcLayoutPromise });
    } finally {
        video.removeAttribute("src");
        video.load();
        URL.revokeObjectURL(url);
    }
}

async function parseVapcLayout(file) {
    const bytes = new Uint8Array(await file.arrayBuffer());
    const markerIndex = indexOfBytes(bytes, [118, 97, 112, 99]);
    if (markerIndex < 0) {
        return null;
    }
    const searchEnd = Math.min(bytes.length, markerIndex + 4096);
    const jsonStart = findByte(bytes, 123, markerIndex + 4, searchEnd);
    if (jsonStart < 0) {
        return null;
    }
    const jsonEnd = findJsonObjectEnd(bytes, jsonStart, searchEnd);
    if (jsonEnd <= jsonStart) {
        return null;
    }
    const payload = JSON.parse(new TextDecoder().decode(bytes.slice(jsonStart, jsonEnd + 1)));
    return payload?.info || null;
}

function sanitizeVapcLayout(value, frame) {
    if (!value || typeof value !== "object") {
        return null;
    }
    const sourceWidth = positiveInteger(value.videoW ?? value.video_width);
    const sourceHeight = positiveInteger(value.videoH ?? value.video_height);
    if (frame && (sourceWidth !== frame.width || sourceHeight !== frame.height)) {
        return null;
    }
    const rgbFrame = parseVapcFrame(value.rgbFrame ?? value.rgb_frame);
    const alphaFrame = parseVapcFrame(value.aFrame ?? value.a_frame);
    if (!sourceWidth || !sourceHeight || !rgbFrame || !alphaFrame) {
        return null;
    }
    if (!rectFitsFrame(rgbFrame, sourceWidth, sourceHeight) || !rectFitsFrame(alphaFrame, sourceWidth, sourceHeight)) {
        return null;
    }
    return {
        sourceWidth,
        sourceHeight,
        contentWidth: positiveInteger(value.w) || rgbFrame.width,
        contentHeight: positiveInteger(value.h) || rgbFrame.height,
        rgbFrame,
        alphaFrame,
    };
}

function parseVapcFrame(value) {
    const raw = Array.isArray(value)
        ? value
        : typeof value === "string"
          ? value.split(",").map((item) => Number(item.trim()))
          : [];
    if (raw.length < 4) {
        return null;
    }
    const rect = {
        x: nonNegativeInteger(raw[0]),
        y: nonNegativeInteger(raw[1]),
        width: positiveInteger(raw[2]),
        height: positiveInteger(raw[3]),
    };
    if (rect.x === null || rect.y === null || !rect.width || !rect.height) {
        return null;
    }
    return rect;
}

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

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 rectFitsFrame(rect, width, height) {
    return (
        rect.x >= 0 &&
        rect.y >= 0 &&
        rect.width > 0 &&
        rect.height > 0 &&
        rect.x + rect.width <= width &&
        rect.y + rect.height <= height
    );
}

async function detectPagLayout(file) {
    const PAG = await getPAGModule();
    let pagFile;
    let pagView;
    try {
        const buffer = await file.arrayBuffer();
        pagFile = await PAG.PAGFile.load(buffer);
        const width = pagFile.width();
        const height = pagFile.height();
        if (!width || !height) {
            throw new Error("资料卡 PAG 尺寸无效");
        }
        const canvas = document.createElement("canvas");
        canvas.width = width;
        canvas.height = height;
        pagView = await PAG.PAGView.init(pagFile, canvas, { firstFrame: false, useScale: false });
        if (!pagView) {
            throw new Error("资料卡 PAG 读取失败");
        }
        const frames = [];
        for (const progress of pagSampleProgresses()) {
            pagView.setProgress(progress);
            await pagView.flush();
            frames.push(drawBitmapFrame(canvas, width, height));
        }
        return measureProfileCardContent(frames);
    } finally {
        if (pagView) {
            pagView.destroy();
        }
        pagFile?.destroy?.();
    }
}

function drawBitmapFrame(source, width, height) {
    const canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;
    const context = canvas.getContext("2d", { willReadFrequently: true });
    if (!context) {
        throw new Error("当前浏览器无法读取资料卡画面");
    }
    context.clearRect(0, 0, width, height);
    context.drawImage(source, 0, 0, width, height);
    const imageData = context.getImageData(0, 0, width, height);
    return {
        data: imageData.data,
        width,
        height,
    };
}

function videoSampleTimes(duration) {
    if (!duration) {
        return [0];
    }
    const last = Math.max(0, duration - 0.05);
    if (sampleFrameCount <= 1) {
        return [Math.min(0.05, last)];
    }
    return Array.from({ length: sampleFrameCount }, (_, index) => {
        const progress = index / (sampleFrameCount - 1);
        return Math.min(last, Math.max(0, progress * last));
    });
}

function pagSampleProgresses() {
    return Array.from({ length: sampleFrameCount }, (_, index) =>
        sampleFrameCount <= 1 ? 0 : index / (sampleFrameCount - 1),
    );
}

function waitForVideoEvent(video, eventName) {
    return new Promise((resolve, reject) => {
        const cleanup = () => {
            video.removeEventListener(eventName, handleSuccess);
            video.removeEventListener("error", handleError);
        };
        const handleSuccess = () => {
            cleanup();
            resolve();
        };
        const handleError = () => {
            cleanup();
            reject(new Error("资料卡视频读取失败"));
        };
        video.addEventListener(eventName, handleSuccess, { once: true });
        video.addEventListener("error", handleError, { once: true });
    });
}

function seekVideo(video, time) {
    return new Promise((resolve, reject) => {
        const targetTime = Math.min(Math.max(0, time), video.duration || time || 0);
        if (video.readyState >= 2 && Math.abs(video.currentTime - targetTime) < 0.01) {
            waitForPresentedVideoFrame(video).then(resolve).catch(reject);
            return;
        }
        const cleanup = () => {
            video.removeEventListener("seeked", handleSeeked);
            video.removeEventListener("error", handleError);
        };
        const handleSeeked = () => {
            cleanup();
            waitForPresentedVideoFrame(video).then(resolve).catch(reject);
        };
        const handleError = () => {
            cleanup();
            reject(new Error("资料卡视频抽帧失败"));
        };
        video.addEventListener("seeked", handleSeeked, { once: true });
        video.addEventListener("error", handleError, { once: true });
        video.currentTime = targetTime;
    });
}

function waitForVideoReady(video) {
    if (video.readyState >= 2) {
        return waitForPresentedVideoFrame(video);
    }
    return waitForVideoEvent(video, "loadeddata").then(() => waitForPresentedVideoFrame(video));
}

function waitForPresentedVideoFrame(video) {
    if (typeof video.requestVideoFrameCallback === "function") {
        return new Promise((resolve) => {
            const timeout = window.setTimeout(resolve, 120);
            video.requestVideoFrameCallback(() => {
                window.clearTimeout(timeout);
                resolve();
            });
        });
    }
    return new Promise((resolve) => {
        requestAnimationFrame(() => requestAnimationFrame(resolve));
    });
}

function sanitizeProfileCardLayout(value) {
    if (!value || typeof value !== "object") {
        return null;
    }
    const sourceWidth = positiveInteger(value.source_width);
    const sourceHeight = positiveInteger(value.source_height);
    const contentTop = nonNegativeInteger(value.content_top);
    const contentBottom = nonNegativeInteger(value.content_bottom);
    const contentHeight = positiveInteger(value.content_height);
    if (!sourceWidth || !sourceHeight || contentTop === null || contentBottom === null || !contentHeight) {
        return null;
    }
    if (
        contentBottom < contentTop ||
        contentBottom >= sourceHeight ||
        contentHeight !== contentBottom - contentTop + 1
    ) {
        return null;
    }
    return {
        source_width: sourceWidth,
        source_height: sourceHeight,
        color_content_width: positiveInteger(value.color_content_width) || sourceWidth,
        content_top: contentTop,
        content_bottom: contentBottom,
        content_height: contentHeight,
        content_top_ratio: decimalRatio(value.content_top_ratio, ratio(contentTop, sourceHeight)),
        content_height_ratio: decimalRatio(value.content_height_ratio, ratio(contentHeight, sourceHeight)),
        detect_version: positiveInteger(value.detect_version) || 1,
    };
}

function isVideoFile(file) {
    return file.type?.startsWith("video/") || /\.(mp4|mov|m4v|webm)$/i.test(file.name || "");
}

function isPagFile(file) {
    return file.type?.includes("pag") || /\.pag$/i.test(file.name || "");
}

function isImageFile(file) {
    return file.type?.startsWith("image/") || /\.(avif|gif|jpe?g|png|webp)$/i.test(file.name || "");
}

function getPAGModule() {
    if (!pagModulePromise) {
        pagModulePromise = import("libpag").then(({ PAGInit }) => PAGInit({ locateFile: () => pagWasmURL }));
    }
    return pagModulePromise;
}

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

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

function ratio(value, total) {
    return Number((value / total).toFixed(6));
}

function decimalRatio(value, fallback) {
    const number = Number(value);
    return Number.isFinite(number) && number >= 0 && number <= 1 ? Number(number.toFixed(6)) : fallback;
}

function clampInteger(value, min, max) {
    const number = Number(value);
    if (!Number.isInteger(number)) {
        return min;
    }
    return Math.min(max, Math.max(min, number));
}

function upperQuantile(values, quantile) {
    const sorted = values.filter((value) => Number.isFinite(value)).sort((left, right) => left - right);
    if (!sorted.length) {
        return 0;
    }
    const index = Math.min(sorted.length - 1, Math.ceil((sorted.length - 1) * quantile));
    return sorted[index];
}

function snapPixel(value, size) {
    if (!size || size <= 1) {
        return Math.round(value);
    }
    return Math.round(value / size) * size;
}
