import puppeteer from 'puppeteer';
import fs$1 from 'fs';
import * as path from 'path';
import path__default from 'path';
import { exec } from 'child_process';
import { isSpeakAction } from '@fullstackcraftllc/codevideo-types';
import say from 'say';
import * as fs from 'fs/promises';
import * as util from 'util';
import fetch from 'isomorphic-fetch';
import crypto from 'crypto';
import { PuppeteerScreenRecorder } from 'puppeteer-screen-recorder';

const executeAction = async (page, id, action) => {
    await page.evaluate(async (id, action) => {
        // WARNING - you can't refactor this const to some other function because the 'browser' can't load local typescript files of course!
        // YOU HAVE BEEN WARNED!
        const KEYBOARD_TYPING_PAUSE_MS = 75;
        let startTime = -1;
        const editor = window.editor;
        // @ts-ignore
        editor.getSupportedActions().forEach((value) => {
            console.log(value);
        });
        // define the human typing here in the puppeteer environment
        const simulateHumanTyping = (editor, code) => {
            return new Promise((resolve) => {
                const characters = code.split("");
                let index = 0;
                function typeNextCharacter() {
                    if (index < characters.length) {
                        const char = characters[index];
                        const selection = editor.getSelection();
                        editor.executeEdits("simulateTyping", [
                            {
                                range: {
                                    startLineNumber: selection?.selectionStartLineNumber || 1,
                                    startColumn: selection?.selectionStartColumn || 1,
                                    endLineNumber: selection?.endLineNumber || 1,
                                    endColumn: selection?.endColumn || 1,
                                },
                                text: char,
                                forceMoveMarkers: true,
                            },
                        ]);
                        index++;
                        setTimeout(typeNextCharacter, KEYBOARD_TYPING_PAUSE_MS);
                    }
                    else {
                        resolve();
                    }
                }
                typeNextCharacter();
            });
        };
        const simulateKeyboardPause = async () => {
            await new Promise((resolve) => setTimeout(resolve, KEYBOARD_TYPING_PAUSE_MS));
        };
        const highlightText = (editor, searchText) => {
            const model = editor.getModel();
            // Find the position of the searchText in the model
            // @ts-ignore
            const searchTextPosition = model.findNextMatch(searchText, 
            // @ts-ignore
            new monaco.Position(1, 1));
            // If searchText is found
            if (searchTextPosition) {
                const line = searchTextPosition.range.startLineNumber;
                const column = searchTextPosition.range.startColumn;
                // Move the cursor to the beginning of the searchText
                editor.setPosition({ lineNumber: line, column });
                // Reveal the line in the center
                editor.revealLineInCenter(line);
                // Calculate the range of the searchText
                const searchTextLength = searchText.length;
                // @ts-ignore
                const range = new monaco.Range(line, column, line, column + searchTextLength);
                // Set the selection to highlight the searchText
                editor.setSelection(range);
                // Reveal the range in the center
                editor.revealRangeInCenter(range);
            }
        };
        // try to parse the 'times' value as an integer, if it fails, default to 1
        // the times doesn't always apply to some actions, so we do that action just once
        let times = parseInt(action.value) || 1;
        console.log("times", times);
        let pos;
        for (let i = 0; i < times; i++) {
            // actual logic
            switch (action.name) {
                // case "speak-before":
                //   await playAudioInPuppeteer(
                //     id,
                //     `./audio/${action.value}.mp3`
                //   );
                //   break;
                case "arrow-down":
                    pos = editor.getPosition();
                    // @ts-ignore
                    pos.lineNumber = pos.lineNumber + 1;
                    console.log("moving pos to", pos);
                    editor.setPosition(pos);
                    await simulateKeyboardPause();
                    break;
                case "arrow-up":
                    pos = editor.getPosition();
                    // @ts-ignore
                    pos.lineNumber = pos.lineNumber - 1;
                    console.log("moving pos to", pos);
                    editor.setPosition(pos);
                    await simulateKeyboardPause();
                    break;
                case "tab":
                    pos = editor.getPosition();
                    // @ts-ignore
                    pos.lineNumber = pos.lineNumber + 2;
                    console.log("moving pos to", pos);
                    editor.setPosition(pos);
                    await simulateKeyboardPause();
                    break;
                case "arrow-left":
                    pos = editor.getPosition();
                    // @ts-ignore
                    pos.column = pos.column - 1;
                    console.log("moving pos to", pos);
                    editor.setPosition(pos);
                    await simulateKeyboardPause();
                    break;
                case "arrow-right":
                    pos = editor.getPosition();
                    // @ts-ignore
                    pos.column = pos.column + 1;
                    console.log("moving pos to", pos);
                    editor.setPosition(pos);
                    await simulateKeyboardPause();
                    break;
                case "enter":
                    await simulateHumanTyping(editor, "\n");
                    break;
                case "delete-line":
                    console.log("deleting line");
                    // @ts-ignore
                    let line = editor.getPosition().lineNumber;
                    editor.executeEdits("", [
                        // @ts-ignore
                        { range: new monaco.Range(line, 1, line + 1, 1), text: null },
                    ]);
                    await simulateKeyboardPause();
                    break;
                case "command-right":
                    // simulate moving to the end of the current line
                    // @ts-ignore
                    pos = editor.getPosition();
                    // @ts-ignore
                    pos.column = 100000;
                    editor.setPosition(pos);
                    await simulateKeyboardPause();
                    break;
                default:
                    break;
                case "highlight-code":
                    highlightText(editor, action.value);
                case "space":
                    await simulateHumanTyping(editor, " ");
                    break;
                case "backspace":
                    // @ts-ignore
                    editor.trigger(monaco.KeyCode.Backspace, "deleteLeft");
                    await simulateKeyboardPause();
                    break;
                case "type-editor":
                    await simulateHumanTyping(editor, action.value);
                    break;
            }
        }
        return startTime;
    }, id, action);
};

const addAudioToVideo = async (videoFolder, videoFile, audioFile) => {
    const videoFileNoExtension = path__default.basename(videoFile, path__default.extname(videoFile));
    const convert = new Promise((resolve, reject) => {
        const ffmpegCommand = `ffmpeg -i ${videoFile} -i ${audioFile}/combined.mp3 -c:v copy -map 0:v:0 -map 1:a:0 -shortest ${videoFolder}/combined.mp4 -y`;
        console.log(`Executing FFmpeg command: ${ffmpegCommand}`);
        exec(ffmpegCommand, (error, stdout, stderr) => {
            if (error) {
                console.error(`Error executing ffmpeg command: ${error.message}`);
                reject(error);
            }
            else {
                console.log(`FFmpeg command executed successfully.`);
                resolve();
            }
        });
    });
    await convert;
    // now get rid of final by renaming it and overwriting the original video file
    fs$1.renameSync(`${videoFolder}/combined.mp4`, videoFile);
    console.log(`Video file '${videoFileNoExtension}.mp4' created successfully.`);
};

const buildAudioFile = (audioFolderPath, audioFiles, audioStartTimes) => {
    console.log("Combining audio files...");
    // reduce each start time by the first start time
    // TODO: figure out this audio start delay thing once and for all
    const trueAudioStartTimes = audioStartTimes.map((time, index) => {
        if (index === 0) {
            return time - audioStartTimes[0];
        }
        else {
            return time - audioStartTimes[0] - 200;
        }
    });
    return new Promise((resolve, reject) => {
        const inputFiles = audioFiles.map((file, index) => `-i ${file}`).join(' ');
        const filterComplex = trueAudioStartTimes
            .map((delay, index) => `[${index}]adelay=${delay}:all=true[a${index}];`)
            .join(' ');
        const amix = audioStartTimes.map((_, index) => `[a${index}]`).join('');
        const ffmpegCommand = `ffmpeg ${inputFiles} -filter_complex "${filterComplex} ${amix}amix=inputs=${audioStartTimes.length}:normalize=0 [out]" -map "[out]" ${audioFolderPath}/combined.mp3 -async 1 -y`;
        exec(ffmpegCommand, (error, stdout, stderr) => {
            if (error) {
                console.error(`Error executing ffmpeg command: ${error.message}`);
                reject(error);
            }
            else {
                console.log(`Audio files combined successfully.`);
                resolve();
            }
        });
    });
};

const execPromise = util.promisify(exec);
const convertWavToMp3AndDeleteWav = async (wavFile, forceOverwrite) => {
    try {
        const folderToWriteTo = path.dirname(wavFile);
        const outputFilePath = path.join(folderToWriteTo, `${path.parse(wavFile).name}.mp3`);
        const command = `ffmpeg -i "${wavFile}" -codec:a libmp3lame -q:a 2 "${outputFilePath}"`;
        console.log("Executing FFmpeg command:", command);
        // Execute FFmpeg command
        const { stdout, stderr } = await execPromise(command);
        // Delete the original wav file
        await fs.unlink(wavFile);
        console.log(`Conversion for ${wavFile} completed. Original wav file deleted.`);
        console.log("FFmpeg command output:", stdout);
        console.error("FFmpeg command error:", stderr);
    }
    catch (error) {
        console.error("Error:", error);
    }
};

const saveToFileSay = async (id, text, audioFolderPath, forceOverwrite) => {
    console.log(`Writing audio file to ${audioFolderPath}`);
    return new Promise((resolve, reject) => {
        const filePath = `${audioFolderPath}/${id}.mp3`;
        if (fs$1.existsSync(filePath) && !forceOverwrite) {
            console.log(`File with hash ${id} already exists. Skipping...`);
            resolve(); // Resolve immediately if file exists and no force overwrite
        }
        else {
            const wavFile = `${audioFolderPath}/${id}.wav`;
            say.export(text, "Daniel", 1, wavFile, async (err) => {
                if (err) {
                    console.error(err);
                    reject(err); // Reject promise if there's an error during export
                }
                else {
                    try {
                        await convertWavToMp3AndDeleteWav(wavFile, forceOverwrite);
                        console.log(`Script for step ${id} converted to audio with say.`);
                        resolve(); // Resolve promise after conversion and deletion
                    }
                    catch (error) {
                        reject(error); // Reject promise if there's an error during conversion/deletion
                    }
                }
            });
        }
    });
};

// eleven lab's can't handle reading of "C#" very well
const customTransforms = {
    "C#": "C sharp",
};
const saveToFileElevenLabs = async (filename, textToSpeak, audioFolderPath, forceOverwrite) => {
    // apply custom transforms to the text
    for (const key in customTransforms) {
        if (textToSpeak.includes(key)) {
            textToSpeak = textToSpeak.replace(new RegExp(key, 'g'), customTransforms[key]);
        }
    }
    // if the file exists already, don't do anything - save money :)
    const filePath = `${audioFolderPath}/${filename}.mp3`;
    if (fs$1.existsSync(filePath) && !forceOverwrite) {
        console.log(`File with hash ${filename} already exists. Skipping...`);
        return;
    }
    // headers for elevenlabs
    const headers = {
        'Content-Type': 'application/json',
        'xi-api-key': process.env.ELEVEN_LABS_API_KEY || '',
        'Accept': 'audio/mpeg',
    };
    const body = {
        text: textToSpeak,
        model_id: 'eleven_turbo_v2',
        voice_settings: {
            stability: 0.5,
            similarity_boost: 0.95,
        },
    };
    try {
        // elli voice id MF3mGyEYCl7XYWbV9V6O
        // Chris voice (my own professional voice) id 1RLeGxy9FHYB5ScpFkts
        const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${process.env.ELEVEN_LABS_VOICE_ID || ''}`, {
            method: 'POST',
            headers: headers,
            body: JSON.stringify(body),
        });
        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
        }
        // return the byte data generated by elevenlabs' boss bots
        // return await response.arrayBuffer();
        // write the byte data to an mp3 file
        const buffer = await response.arrayBuffer();
        const filePath = `${audioFolderPath}/${filename}.mp3`;
        // write the file with fs
        fs$1.writeFileSync(filePath, Buffer.from(buffer));
        console.log(`Script for step ${filename} converted to audio with Eleven Labs.`);
    }
    catch (error) {
        console.error(error);
        return null;
    }
};

const sha256Hash = (data) => {
    const hash = crypto.createHash("sha256");
    hash.update(data);
    return hash.digest("hex");
};

const saveToFileOpenAI = async (filename, textToSpeak, audioFolderPath, forceOverwrite) => {
    const filePath = `${audioFolderPath}/${filename}.mp3`;
    if (fs$1.existsSync(filePath) && !forceOverwrite) {
        console.log(`File with hash ${filename} already exists. Skipping...`);
        return;
    }
    const headers = {
        Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
        "Content-Type": "application/json",
    };
    const data = {
        model: "tts-1",
        voice: "echo",
        input: textToSpeak,
        response_format: "mp3",
        speed: 1.0,
    };
    try {
        const response = await fetch("https://api.openai.com/v1/audio/speech", {
            method: "POST",
            headers: headers,
            body: JSON.stringify(data),
        });
        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
        }
        // write the byte data to an mp3 file
        const buffer = await response.arrayBuffer();
        const filePath = `${audioFolderPath}/${filename}.mp3`;
        // write the file with fs
        fs$1.writeFileSync(filePath, Buffer.from(buffer));
        console.log(`Script for step ${filename} converted to audio with Open AI.`);
    }
    catch (error) {
        console.error(error);
    }
};

// loop over each step, converting "script" property to audio with say
const convertSpeakActionsToAudio = async (actions, audioFolderPath, forceOverwrite, textToSpeechOption) => {
    const audioFiles = [];
    // get all speak actions
    const speakActions = actions.filter(isSpeakAction);
    console.log(`Of the ${actions.length} actions, ${speakActions.length} are speak actions.`);
    for (let i = 0; i < speakActions.length; i++) {
        if (!isSpeakAction(speakActions[i])) {
            continue;
        }
        // id of the audio file is the sha-256 hash of the text
        const hash = sha256Hash(speakActions[i].value);
        const index = i + 1;
        console.log(`Converting text at step index ${index} to audio... (hash is ${hash})`);
        const textToSpeak = speakActions[i].value;
        // free version with say (installed outofthebox on mac)
        switch (textToSpeechOption) {
            case "sayjs":
                await saveToFileSay(hash, textToSpeak, audioFolderPath, forceOverwrite);
                break;
            case "elevenlabs":
                // real version with professional voice from elevenlabs
                await saveToFileElevenLabs(hash, textToSpeak, audioFolderPath, forceOverwrite);
                break;
            case "openai":
                // tts with open OpenAI
                await saveToFileOpenAI(hash, textToSpeak, audioFolderPath, forceOverwrite);
                break;
            default:
                console.error(`Invalid text to speech option '${textToSpeechOption}'`);
                break;
        }
        audioFiles.push(`${audioFolderPath}/${hash}.mp3`);
    }
    return audioFiles;
};

const loadActions = async () => {
    // first argument to this script is the path to the actions file
    if (process.argv.length < 3) {
        console.error("Please provide a path to the actions file");
        process.exit(1);
    }
    // if we have a 4th argument, then we know we have a text to speech option
    let textToSpeechOption = "sayjs";
    if (process.argv.length === 4) {
        textToSpeechOption = process.argv[3];
    }
    const inputStepsFilePath = process.argv[2];
    const actions = [];
    const currentWorkingDirectory = process.cwd();
    // join the actions file path with the current working directory
    const actionsFilePath = path__default.join(currentWorkingDirectory, inputStepsFilePath);
    // ensure the file exist relative to the current working directory
    if (!fs$1.existsSync(actionsFilePath)) {
        console.error(`File not found: ${actionsFilePath}`);
        process.exit(1);
    }
    // try typescript first
    if (inputStepsFilePath.endsWith(".ts")) {
        // dynamically require the typescript file
        const { default: typeScriptActions } = await import(actionsFilePath);
        // if it's null, throw error that they need to use 'export default' syntax
        if (typeScriptActions === null) {
            console.error("Failed to load TypeScript actions. Use 'export default' syntax in your typescript file to export the actions.");
            process.exit(1);
        }
        actions.push(...typeScriptActions);
    }
    else if (inputStepsFilePath.endsWith(".json")) {
        // we can safely require the actions file
        const { default: jsonActions } = await import(actionsFilePath, { assert: { type: "json" } });
        actions.push(...jsonActions);
    }
    else {
        console.error("Please provide a path to a .ts or .json file with your actions");
        process.exit(1);
    }
    console.log(`Found ${actions.length} actions in file ${actionsFilePath}`);
    const fileNameWithoutExtension = path__default.basename(actionsFilePath, path__default.extname(actionsFilePath));
    const actionsAudioDirectory = `${currentWorkingDirectory}/audio/${fileNameWithoutExtension}`;
    const actionsVideoDirectory = `${currentWorkingDirectory}/video/${fileNameWithoutExtension}`;
    const url = `file://${currentWorkingDirectory}/editor.html`;
    const videoFile = `./video/${fileNameWithoutExtension}.mp4`;
    // if we see at least one 'type-terminal' action, then we know this has to run on a codespaces
    // if (actions.map((a) => a.name).includes("type-terminal")) {
    //   url = `https://prod.liveshare.vsengsaas.visualstudio.com/join?049AB4AD9BC16BE338A13263281272C72C49`;
    // }
    return {
        url,
        actions,
        currentWorkingDirectory,
        videoFile,
        actionsAudioDirectory,
        actionsVideoDirectory,
        textToSpeechOption
    };
};

class VideoGenerator {
    trueAudioStartTime = 0;
    actions = [];
    url = "";
    actionsAudioDirectory = "";
    forceOverwriteAudioFiles = false;
    textToSpeechOption = "sayjs";
    videoFile = "";
    audioStartTimes = [];
    // in constructor, set all the properties
    initialize = async () => {
        const { actions, url, videoFile, actionsAudioDirectory, textToSpeechOption, } = await loadActions();
        // create audio directory if it doesn't exist
        if (!fs$1.existsSync(actionsAudioDirectory)) {
            fs$1.mkdirSync(actionsAudioDirectory);
        }
        this.actions = actions;
        this.url = url;
        this.actionsAudioDirectory = actionsAudioDirectory;
        this.textToSpeechOption = textToSpeechOption;
        this.videoFile = videoFile;
    };
    //  audio playback logic
    // TODO: would be nice to move in with executeAction, but the page makes a closure
    playAudioInPuppeteer = async (page, audioHash, filePath) => {
        const scriptContent = `
    window.audioPlaybackPromiseResolved = false;
    const audio${audioHash} = new Audio('${filePath}');
    const playPromise${audioHash} = audio${audioHash}.play();

    audio${audioHash}.addEventListener('ended', () => {
      window.audioPlaybackPromiseResolved = true;
    });
  `;
        // Add the script tag to the page
        await page.addScriptTag({ content: scriptContent });
        // add the start time to the array
        const startTime = Math.round(performance.now()) - this.trueAudioStartTime;
        console.log(`audio ${audioHash} (${filePath}) start time set to: ${startTime}`);
        this.audioStartTimes.push(startTime);
        // Wait for the audio playback to complete
        await page.waitForFunction(() => window.audioPlaybackPromiseResolved === true);
    };
    runPuppeteerAutomation = async (url) => {
        console.log("recording video...");
        // then run the automation
        const browser = await puppeteer.launch({
            headless: "new",
            // headless: false, // for debugging
            // devtools: true, // for debugging
            // headless: "new",
            // executablePath:
            //   "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
            defaultViewport: {
                width: 1920,
                height: 1080,
            },
        });
        const page = await browser.newPage();
        await page.goto(url);
        await page.setViewport({
            width: 1920,
            height: 1080,
        });
        // Wait for the Monaco Editor to load
        await page.waitForFunction(() => window.monaco !== undefined);
        // create and start the recording
        const recorder = new PuppeteerScreenRecorder(page);
        await recorder.start(this.videoFile);
        // add a 2 second delay before starting the steps
        await new Promise((resolve) => setTimeout(resolve, 2000));
        // before beginning steps, save what current time we are in execution of this program
        // needed for accurately calculating the audio start times
        //trueAudioStartTime = 5000 //Math.round(performance.now()) + 1999;
        // Automation commands based on steps
        for (let i = 0; i < this.actions.length; i++) {
            // treat index as 'step id'
            const id = i + 1;
            const action = this.actions[i];
            console.log(`Step ${id} action: ${action.name}`);
            const audioHash = sha256Hash(action.value);
            // special case is audio playback
            if (action.name === "speak-before") {
                await this.playAudioInPuppeteer(page, audioHash, `${this.actionsAudioDirectory}/${audioHash}.mp3`);
                continue;
            }
            else if (action.name === "speak-during") {
                // use promise.all to play audio and execute action at the same time
                await Promise.all([
                    this.playAudioInPuppeteer(page, audioHash, `${this.actionsAudioDirectory}/${audioHash}.mp3`),
                    executeAction(page, id, action),
                ]);
            }
            else if (action.name === "type-terminal") ;
            else {
                await executeAction(page, id, action);
            }
            console.log(`Step ${id} complete`);
        }
        // wait 15 more seconds for any audio to finish playing
        await new Promise((resolve) => setTimeout(resolve, 15000));
        // stop the recording
        await recorder.stop();
        // Close the browser
        await browser.close();
        console.log("video recorded");
    };
    // finally the function to orchestrate them all!!!
    makeVideo = async () => {
        // first convert scripts to audio
        const audioFiles = await convertSpeakActionsToAudio(this.actions, this.actionsAudioDirectory, this.forceOverwriteAudioFiles, this.textToSpeechOption);
        await this.runPuppeteerAutomation(this.url);
        // now that we have the offset delays for each audio, build the audio file
        await buildAudioFile(this.actionsAudioDirectory, audioFiles, this.audioStartTimes);
        // then combine the audio and video files
        // TODO: quick fix for videoDirectory is the videoFile without the file name
        const videoDirectory = this.videoFile.replace(/\/[^/]+$/, "");
        await addAudioToVideo(videoDirectory, this.videoFile, this.actionsAudioDirectory);
    };
    getVideoAsBuffer = async () => {
        const videoBuffer = fs$1.readFileSync(this.videoFile);
        return videoBuffer;
    };
}

const playAudioInPuppeteer = async (page, audioHash, filePath) => {
    const scriptContent = `
    window.audioPlaybackPromiseResolved = false;
    const audio${audioHash} = new Audio('${filePath}');
    const playPromise${audioHash} = audio${audioHash}.play();

    audio${audioHash}.addEventListener('ended', () => {
      window.audioPlaybackPromiseResolved = true;
    });
  `;
    // Add the script tag to the page
    await page.addScriptTag({ content: scriptContent });
    // add the start time to the array
    const audioStartTime = Math.round(performance.now());
    console.log(`audio ${audioHash} (${filePath}) start time set to: ${audioStartTime}`);
    // Wait for the audio playback to complete
    await page.waitForFunction(() => window.audioPlaybackPromiseResolved === true);
    return audioStartTime;
};

const runPuppeteerAutomation = async (url, videoFile, actions, actionsAudioDirectory) => {
    const audioStartTimes = [];
    console.log("recording video...");
    // then run the automation
    const browser = await puppeteer.launch({
        headless: "new",
        // headless: false, // for debugging
        // devtools: true, // for debugging
        // headless: "new",
        // executablePath:
        //   "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
        defaultViewport: {
            width: 1920,
            height: 1080,
        },
    });
    const page = await browser.newPage();
    await page.goto(url);
    await page.setViewport({
        width: 1920,
        height: 1080,
    });
    // Wait for the Monaco Editor to load
    await page.waitForFunction(() => window.monaco !== undefined);
    // create and start the recording
    const recorder = new PuppeteerScreenRecorder(page);
    await recorder.start(videoFile);
    // add a 2 second delay before starting the steps
    await new Promise((resolve) => setTimeout(resolve, 2000));
    // before beginning steps, save what current time we are in execution of this program
    // needed for accurately calculating the audio start times
    //trueAudioStartTime = 5000 //Math.round(performance.now()) + 1999;
    // Automation commands based on steps
    for (let i = 0; i < actions.length; i++) {
        // treat index as 'step id'
        const id = i + 1;
        const action = actions[i];
        console.log(`Step ${id} action: ${action.name}`);
        const audioHash = sha256Hash(action.value);
        // special case is audio playback
        if (action.name === "speak-before") {
            const audioStartTime = await playAudioInPuppeteer(page, audioHash, `${actionsAudioDirectory}/${audioHash}.mp3`);
            audioStartTimes.push(audioStartTime);
            continue;
        }
        else if (action.name === "speak-during") {
            // use promise.all to play audio and execute action at the same time
            const [audioStartTime, _] = await Promise.all([
                playAudioInPuppeteer(page, audioHash, `${actionsAudioDirectory}/${audioHash}.mp3`),
                executeAction(page, id, action),
            ]);
            audioStartTimes.push(audioStartTime);
        }
        else if (action.name === "type-terminal") ;
        else {
            await executeAction(page, id, action);
        }
        console.log(`Step ${id} complete`);
    }
    // wait 15 more seconds for any audio to finish playing
    await new Promise((resolve) => setTimeout(resolve, 15000));
    // stop the recording
    await recorder.stop();
    // Close the browser
    await browser.close();
    console.log("video recorded");
    // return the audio start times
    return audioStartTimes;
};

// TODO: using VideoGenerator class?
// export const generateVideoFromActions = async (actions: Array<IAction>): Promise<Buffer> => {
//   const videoGenerator = new VideoGenerator();
//   await videoGenerator.makeVideo();
//   return videoGenerator.getVideoAsBuffer();
// };
// using series of functions
const generateVideoFromActions = async (actions) => {
    // const { url, videoFile, actionsAudioDirectory, textToSpeechOption } =
    //   await loadActions();
    const fileNameWithoutExtension = "tmp";
    const currentWorkingDirectory = process.cwd();
    const textToSpeechOption = "sayjs";
    const editorUrl = `file://${currentWorkingDirectory}/editor.html`;
    // create all folders as needed if they don't exist
    fs$1.mkdirSync(`${currentWorkingDirectory}/tmp`, { recursive: true });
    fs$1.mkdirSync(`${currentWorkingDirectory}/tmp/video`, { recursive: true });
    fs$1.mkdirSync(`${currentWorkingDirectory}/tmp/audio/${fileNameWithoutExtension}`, { recursive: true });
    const audioDirectory = `${currentWorkingDirectory}/tmp/audio/${fileNameWithoutExtension}`;
    const videoDirectory = `${currentWorkingDirectory}/tmp/video`;
    const videoFile = `${currentWorkingDirectory}/tmp/video/${fileNameWithoutExtension}.mp4`;
    // first convert scripts to audio
    const audioFiles = await convertSpeakActionsToAudio(actions, audioDirectory, false, textToSpeechOption);
    // then run the puppeteer automation, which records the video and returns the start times of each audio
    const audioStartTimes = await runPuppeteerAutomation(editorUrl, videoFile, actions, audioDirectory);
    // now that we have the offset delays for each audio, build the audio file
    await buildAudioFile(audioDirectory, audioFiles, audioStartTimes);
    // then combine the audio and video files
    await addAudioToVideo(videoDirectory, videoFile, audioDirectory);
    // finally load the video and return it as a buffer
    return fs$1.promises.readFile(videoFile);
};

export { VideoGenerator, generateVideoFromActions };
