import ffmpeg from "@modernized/fluent-ffmpeg";
import { GraphAILogger } from "graphai";
import { isFile } from "./file.js";
import fs from "fs";
import { Readable, PassThrough } from "node:stream";
export const setFfmpegPath = (ffmpegPath) => {
    ffmpeg.setFfmpegPath(ffmpegPath);
};
export const setFfprobePath = (ffprobePath) => {
    ffmpeg.setFfprobePath(ffprobePath);
};
export const FfmpegContextInit = () => {
    return {
        command: ffmpeg(),
        inputCount: 0,
        filterComplex: [],
    };
};
export const FfmpegContextAddInput = (context, input, inputOptions) => {
    if (inputOptions) {
        context.command.input(input).inputOptions(inputOptions);
    }
    else {
        context.command.input(input);
    }
    context.inputCount++;
    return context.inputCount - 1; // returned the index of the input
};
export const FfmpegContextPushFormattedAudio = (context, sourceId, outputId, duration = undefined) => {
    if (duration !== undefined) {
        context.filterComplex.push(`${sourceId}atrim=duration=${duration},aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo${outputId}`);
    }
    else {
        context.filterComplex.push(`${sourceId}aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo${outputId}`);
    }
};
export const FfmpegContextInputFormattedAudio = (context, input, duration = undefined, inputOptions) => {
    const index = FfmpegContextAddInput(context, input, inputOptions);
    const audioId = `[a${index}]`;
    FfmpegContextPushFormattedAudio(context, `[${index}:a]`, audioId, duration);
    return audioId;
};
export const FfmpegContextGenerateOutput = (context, output, options = []) => {
    return new Promise((resolve, reject) => {
        context.command
            .complexFilter(context.filterComplex)
            .outputOptions(options)
            .output(output)
            .on("start", (cmdLine) => {
            GraphAILogger.log("Started FFmpeg with command:", cmdLine);
        })
            .on("error", (err, stdout, stderr) => {
            GraphAILogger.error("Error occurred:", err);
            GraphAILogger.error("FFmpeg stdout:", stdout);
            GraphAILogger.error("FFmpeg stderr:", stderr);
            GraphAILogger.info("Video/Audio creation failed.", err.message);
            reject(err);
        })
            .on("end", () => {
            resolve(0);
        })
            .run();
    });
};
/** Round up odd dimensions to even (required by libx264 yuv420p) */
export const normalizeEvenDimensions = (width, height) => {
    return {
        width: width % 2 === 0 ? width : width + 1,
        height: height % 2 === 0 ? height : height + 1,
    };
};
/**
 * Convert a sequence of PNG frames into a video file.
 * Expects files named frame_00000.png, frame_00001.png, etc. in framesDir.
 */
export const framesToVideo = (framesDir, outputPath, fps, width, height) => {
    const safe = normalizeEvenDimensions(width, height);
    if (safe.width !== width || safe.height !== height) {
        GraphAILogger.info(`framesToVideo: adjusted ${width}x${height} → ${safe.width}x${safe.height} (libx264 yuv420p requires even dimensions)`);
    }
    return new Promise((resolve, reject) => {
        ffmpeg()
            .input(`${framesDir}/frame_%05d.png`)
            .inputOptions(["-framerate", String(fps)])
            .outputOptions(["-c:v", "libx264", "-pix_fmt", "yuv420p", "-r", String(fps), "-vf", `scale=${safe.width}:${safe.height}`])
            .output(outputPath)
            .on("end", () => resolve())
            .on("error", (err) => reject(err))
            .run();
    });
};
export const ffmpegGetMediaDuration = (filePath) => {
    return new Promise((resolve, reject) => {
        // Only check file existence for local paths, not URLs
        if (!filePath.startsWith("http://") && !filePath.startsWith("https://")) {
            if (!fs.existsSync(filePath)) {
                // NOTE: We don't reject here for scripts/test/test_hello_image.json, which uses mock image agent.
                // reject(new Error(`File not found: ${filePath}`));
                resolve({ duration: 0, hasAudio: false });
                return;
            }
            const stat = fs.statSync(filePath);
            if (!stat.isFile()) {
                reject("ffmpegGetMediaDuration: path is not file");
                return;
            }
        }
        ffmpeg.ffprobe(filePath, (err, metadata) => {
            if (err) {
                GraphAILogger.info("Error while getting metadata:", err);
                reject(err);
            }
            else {
                const hasAudio = metadata.streams?.some((stream) => stream.codec_type === "audio") ?? false;
                resolve({ duration: metadata.format.duration, hasAudio });
            }
        });
    });
};
export const extractImageFromMovie = (movieFile, imagePath, useLastFrame = false) => {
    return new Promise((resolve, reject) => {
        const command = ffmpeg(movieFile);
        if (useLastFrame) {
            command.inputOptions(["-sseof", "-0.1"]);
        }
        command
            .outputOptions(["-frames:v 1"])
            .output(imagePath)
            .on("end", () => resolve({}))
            .on("error", (err) => reject(err))
            .run();
    });
};
export const trimMusic = (inputFile, startTime, duration) => {
    return new Promise((resolve, reject) => {
        if (!inputFile.startsWith("http://") && !inputFile.startsWith("https://") && !isFile(inputFile)) {
            reject(new Error(`File not found: ${inputFile}`));
            return;
        }
        if (duration <= 0) {
            reject(new Error(`Invalid duration: duration (${duration}) must be greater than 0`));
            return;
        }
        const chunks = [];
        ffmpeg(inputFile)
            .seekInput(startTime)
            .duration(duration)
            .format("mp3")
            .on("start", () => {
            GraphAILogger.log(`Trimming audio from ${startTime}s for ${duration}s...`);
        })
            .on("error", (err) => {
            GraphAILogger.error("Error occurred while trimming audio:", err);
            reject(err);
        })
            .on("end", () => {
            const buffer = Buffer.concat(chunks);
            GraphAILogger.log(`Audio trimmed successfully, buffer size: ${buffer.length} bytes`);
            resolve(buffer);
        })
            .pipe()
            .on("data", (chunk) => {
            chunks.push(chunk);
        });
    });
};
export const createSilentAudio = (filePath, durationSec) => {
    const filter = `anullsrc=r=44100:cl=stereo,atrim=duration=${durationSec},aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo[a]`;
    return new Promise((resolve, reject) => {
        ffmpeg()
            .complexFilter([filter])
            .outputOptions(["-map", "[a]"])
            .output(filePath)
            .on("end", () => resolve())
            .on("error", (err) => reject(err))
            .run();
    });
};
export const pcmToMp3 = (rawPcm, sampleRate = 24000) => {
    return new Promise((resolve, reject) => {
        const inputStream = new Readable({
            read() {
                this.push(rawPcm);
                this.push(null);
            },
        });
        const outputChunks = [];
        const outputStream = new PassThrough();
        outputStream.on("data", (chunk) => outputChunks.push(chunk));
        outputStream.on("end", () => resolve(Buffer.concat(outputChunks)));
        outputStream.on("error", reject);
        ffmpeg(inputStream)
            .inputFormat("s16le")
            .inputOptions([`-ar ${sampleRate}`, "-ac 1"])
            .audioCodec("libmp3lame")
            .format("mp3")
            .on("error", reject)
            .pipe(outputStream);
    });
};
