// Prints to CLI and also reports back to browser

import {existsSync, mkdirSync} from 'node:fs';
import path from 'node:path';
import type {
	Browser,
	BrowserExecutable,
	CancelSignal,
	ChromeMode,
	ChromiumOptions,
	LogLevel,
	RenderMediaOnDownload,
	StillImageFormat,
} from '@remotion/renderer';
import {RenderInternals} from '@remotion/renderer';
import {BrowserSafeApis} from '@remotion/renderer/client';
import type {
	AggregateRenderProgress,
	JobProgressCallback,
} from '@remotion/studio-server';
import type {BrowserDownloadState} from '@remotion/studio-shared';
import {NoReactInternals} from 'remotion/no-react';
import {defaultBrowserDownloadProgress} from '../browser-download-bar';
import {chalk} from '../chalk';
import {registerCleanupJob} from '../cleanup-before-quit';
import {determineFinalStillImageFormat} from '../determine-image-format';
import {getAndValidateAbsoluteOutputFile} from '../get-cli-options';
import {getCompositionWithDimensionOverride} from '../get-composition-with-dimension-override';
import {makeHyperlink} from '../hyperlinks/make-link';
import {Log} from '../log';
import {makeOnDownload} from '../make-on-download';
import {handleOnArtifact} from '../on-artifact';
import {parsedCli, quietFlagProvided} from '../parsed-cli';
import type {OverwriteableCliOutput} from '../progress-bar';
import {
	LABEL_WIDTH,
	createOverwriteableCliOutput,
	makeRenderingAndStitchingProgress,
	printFact,
} from '../progress-bar';
import {initialAggregateRenderProgress} from '../progress-types';
import {bundleOnCliOrTakeServeUrl} from '../setup-cache';
import {shouldUseNonOverlayingLogger} from '../should-use-non-overlaying-logger';
import {truthy} from '../truthy';
import {
	getOutputLocation,
	getUserPassedOutputLocation,
} from '../user-passed-output-location';
import {addLogToAggregateProgress} from './add-log-to-aggregate-progress';

export const renderStillFlow = async ({
	remotionRoot,
	fullEntryPoint,
	entryPointReason,
	remainingArgs,
	browser,
	browserExecutable,
	chromiumOptions,
	envVariables,
	height,
	width,
	fps,
	durationInFrames,
	serializedInputPropsWithCustomSchema,
	overwrite,
	port,
	publicDir,
	puppeteerTimeout,
	jpegQuality,
	scale,
	stillFrame,
	compositionIdFromUi,
	imageFormatFromUi,
	logLevel,
	onProgress,
	indent,
	addCleanupCallback,
	cancelSignal,
	outputLocationFromUi,
	offthreadVideoCacheSizeInBytes,
	binariesDirectory,
	publicPath,
	chromeMode,
	offthreadVideoThreads,
	audioLatencyHint,
	mediaCacheSizeInBytes,
	rspack,
	askAIEnabled,
	experimentalClientSideRenderingEnabled,
	experimentalVisualModeEnabled,
	keyboardShortcutsEnabled,
	shouldCache,
}: {
	remotionRoot: string;
	fullEntryPoint: string;
	entryPointReason: string;
	remainingArgs: (string | number)[];
	serializedInputPropsWithCustomSchema: string;
	envVariables: Record<string, string>;
	jpegQuality: number;
	browser: Browser;
	stillFrame: number;
	browserExecutable: BrowserExecutable;
	chromiumOptions: ChromiumOptions;
	scale: number;
	overwrite: boolean;
	puppeteerTimeout: number;
	port: number | null;
	publicDir: string | null;
	height: number | null;
	width: number | null;
	fps: number | null;
	durationInFrames: number | null;
	compositionIdFromUi: string | null;
	imageFormatFromUi: StillImageFormat | null;
	logLevel: LogLevel;
	onProgress: JobProgressCallback;
	indent: boolean;
	addCleanupCallback: (label: string, cb: () => void) => void;
	cancelSignal: CancelSignal | null;
	outputLocationFromUi: string | null;
	offthreadVideoCacheSizeInBytes: number | null;
	offthreadVideoThreads: number | null;
	binariesDirectory: string | null;
	publicPath: string | null;
	chromeMode: ChromeMode;
	audioLatencyHint: AudioContextLatencyCategory | null;
	mediaCacheSizeInBytes: number | null;
	rspack: boolean;
	askAIEnabled: boolean;
	experimentalClientSideRenderingEnabled: boolean;
	experimentalVisualModeEnabled: boolean;
	keyboardShortcutsEnabled: boolean;
	shouldCache: boolean;
}) => {
	const isVerbose = RenderInternals.isEqualOrBelowLogLevel(logLevel, 'verbose');
	Log.verbose(
		{indent, logLevel},
		chalk.gray(`Entry point = ${fullEntryPoint} (${entryPointReason})`),
	);

	const aggregate: AggregateRenderProgress = initialAggregateRenderProgress();
	const updatesDontOverwrite = shouldUseNonOverlayingLogger({logLevel});

	const renderProgress: OverwriteableCliOutput = createOverwriteableCliOutput({
		quiet: quietFlagProvided(),
		cancelSignal,
		updatesDontOverwrite: shouldUseNonOverlayingLogger({logLevel}),
		indent,
	});

	const updateRenderProgress = ({
		newline,
		printToConsole,
	}: {
		newline: boolean;
		printToConsole: boolean;
	}) => {
		const {output, progress, message} = makeRenderingAndStitchingProgress({
			prog: aggregate,
			isUsingParallelEncoding: false,
		});
		if (printToConsole) {
			renderProgress.update(updatesDontOverwrite ? message : output, newline);
		}

		onProgress({message, value: progress, ...aggregate});
	};

	function updateBrowserProgress(progress: BrowserDownloadState) {
		aggregate.browser = progress;
		onProgress({
			message: `Downloading ${chromeMode === 'chrome-for-testing' ? 'Chrome for Testing' : 'Headless Shell'} ${Math.round(progress.progress * 100)}%`,
			value: 0,
			...aggregate,
		});
	}

	const onBrowserDownload = defaultBrowserDownloadProgress({
		quiet: quietFlagProvided(),
		indent,
		logLevel,
		onProgress: updateBrowserProgress,
	});

	await RenderInternals.internalEnsureBrowser({
		browserExecutable,
		indent,
		logLevel,
		onBrowserDownload,
		chromeMode,
	});

	const browserInstance = RenderInternals.internalOpenBrowser({
		browser,
		browserExecutable,
		chromiumOptions,
		forceDeviceScaleFactor: scale,
		indent,
		viewport: null,
		logLevel,
		onBrowserDownload,
		chromeMode,
	});

	const {cleanup: cleanupBundle, urlOrBundle} = await bundleOnCliOrTakeServeUrl(
		{
			fullPath: fullEntryPoint,
			remotionRoot,
			publicDir,
			onProgress: ({copying, bundling}) => {
				aggregate.bundling = bundling;
				aggregate.copyingState = copying;
				updateRenderProgress({
					newline: false,
					printToConsole: true,
				});
			},
			indentOutput: indent,
			logLevel,
			onDirectoryCreated: (dir) => {
				registerCleanupJob(`Delete ${dir}`, () => {
					RenderInternals.deleteDirectory(dir);
				});
			},
			quietProgress: updatesDontOverwrite,
			quietFlag: quietFlagProvided(),
			outDir: null,
			// Not needed for still
			gitSource: null,
			bufferStateDelayInMilliseconds: null,
			maxTimelineTracks: null,
			publicPath,
			audioLatencyHint,
			experimentalClientSideRenderingEnabled,
			experimentalVisualModeEnabled,
			askAIEnabled,
			keyboardShortcutsEnabled,
			rspack,
			shouldCache,
		},
	);

	const server = await RenderInternals.prepareServer({
		offthreadVideoThreads:
			offthreadVideoThreads ??
			RenderInternals.DEFAULT_RENDER_FRAMES_OFFTHREAD_VIDEO_THREADS,
		indent,
		port,
		remotionRoot,
		logLevel,
		webpackConfigOrServeUrl: urlOrBundle,
		offthreadVideoCacheSizeInBytes,
		binariesDirectory,
		forceIPv4: false,
	});

	addCleanupCallback(`Close server`, () => server.closeServer(false));

	addCleanupCallback(`Cleanup bundle`, () => cleanupBundle());

	const puppeteerInstance = await browserInstance;
	addCleanupCallback(`Close browser`, () =>
		puppeteerInstance.close({silent: false}),
	);

	const {compositionId, config, reason, argsAfterComposition} =
		await getCompositionWithDimensionOverride({
			height,
			width,
			fps,
			durationInFrames,
			args: remainingArgs,
			compositionIdFromUi,
			browserExecutable,
			chromiumOptions,
			envVariables,
			indent,
			serializedInputPropsWithCustomSchema,
			port,
			puppeteerInstance,
			serveUrlOrWebpackUrl: urlOrBundle,
			timeoutInMilliseconds: puppeteerTimeout,
			logLevel,
			server,
			offthreadVideoCacheSizeInBytes,
			offthreadVideoThreads,
			binariesDirectory,
			onBrowserDownload,
			chromeMode,
			mediaCacheSizeInBytes,
		});

	const {format: imageFormat, source} = determineFinalStillImageFormat({
		configuredImageFormat:
			BrowserSafeApis.options.stillImageFormatOption.getValue({
				commandLine: parsedCli,
			}).value,
		downloadName: null,
		outName: getUserPassedOutputLocation(
			argsAfterComposition,
			outputLocationFromUi,
		),
		isLambda: false,
		fromUi: imageFormatFromUi,
	});

	const relativeOutputLocation = getOutputLocation({
		compositionId,
		defaultExtension: imageFormat,
		args: argsAfterComposition,
		type: 'asset',
		outputLocationFromUi,
		compositionDefaultOutName: config.defaultOutName,
	});

	const absoluteOutputLocation = getAndValidateAbsoluteOutputFile(
		relativeOutputLocation,
		overwrite,
		logLevel,
	);
	const exists = existsSync(absoluteOutputLocation);

	mkdirSync(path.join(absoluteOutputLocation, '..'), {
		recursive: true,
	});

	printFact('info')({
		indent,
		left: 'Composition',
		link: 'https://remotion.dev/docs/terminology/composition',
		logLevel,
		right: [compositionId, isVerbose ? `(${reason})` : null]
			.filter(truthy)
			.join(' '),
		color: 'gray',
	});
	printFact('info')({
		indent,
		left: 'Format',
		logLevel,
		right: [imageFormat, isVerbose ? `(${source})` : null]
			.filter(truthy)
			.join(' '),
		color: 'gray',
	});
	printFact('info')({
		indent,
		left: 'Output',
		logLevel,
		right: relativeOutputLocation,
		color: 'gray',
	});

	const renderStart = Date.now();

	aggregate.rendering = {
		frames: 0,
		doneIn: null,
		totalFrames: 1,
		timeRemainingInMilliseconds: null,
	};

	updateRenderProgress({
		newline: false,
		printToConsole: true,
	});

	const onDownload: RenderMediaOnDownload = makeOnDownload({
		downloads: aggregate.downloads,
		indent,
		logLevel,
		updateRenderProgress,
		updatesDontOverwrite,
		isUsingParallelEncoding: false,
	});

	const {onArtifact} = handleOnArtifact({
		artifactState: aggregate.artifactState,
		compositionId,
		onProgress: (progress) => {
			aggregate.artifactState = progress;

			updateRenderProgress({
				newline: false,
				printToConsole: true,
			});
		},
	});

	await RenderInternals.internalRenderStill({
		composition: config,
		frame: stillFrame,
		output: absoluteOutputLocation,
		serveUrl: urlOrBundle,
		jpegQuality,
		envVariables,
		imageFormat,
		serializedInputPropsWithCustomSchema,
		chromiumOptions,
		timeoutInMilliseconds: puppeteerTimeout,
		scale,
		browserExecutable,
		overwrite,
		onDownload,
		port,
		puppeteerInstance,
		server,
		cancelSignal,
		indent,
		onBrowserLog: null,
		logLevel,
		serializedResolvedPropsWithCustomSchema:
			NoReactInternals.serializeJSONWithSpecialTypes({
				indent: undefined,
				staticBase: null,
				data: config.props,
			}).serializedString,
		offthreadVideoCacheSizeInBytes,
		binariesDirectory,
		onBrowserDownload,
		onArtifact,
		chromeMode,
		offthreadVideoThreads,
		mediaCacheSizeInBytes,
		onLog: ({logLevel: logLogLevel, previewString, tag}) => {
			addLogToAggregateProgress({
				logs: aggregate.logs,
				logLogLevel,
				previewString,
				tag,
				logLevel,
			});
			updateRenderProgress({
				newline: false,
				printToConsole: true,
			});
		},
		licenseKey: null,
		isProduction: null,
	});

	aggregate.rendering = {
		frames: 1,
		doneIn: Date.now() - renderStart,
		totalFrames: 1,
		timeRemainingInMilliseconds: null,
	};
	updateRenderProgress({
		newline: true,
		printToConsole: true,
	});
	Log.info(
		{indent, logLevel},
		chalk.blue(
			`${(exists ? '○' : '+').padEnd(LABEL_WIDTH)} ${makeHyperlink({url: 'file://' + absoluteOutputLocation, text: relativeOutputLocation, fallback: relativeOutputLocation})}`,
		),
	);
};
