#!/usr/bin/env bash

# Version: 3.0.0

# If ffmpeg_executable is not set, use ffmpeg
if [[ -z $ffmpeg_executable ]]; then
	# If have jellyfin-ffmpeg installed, use it
	if [[ -e /usr/lib/jellyfin-ffmpeg/ffmpeg ]]; then
		ffmpeg_executable="/usr/lib/jellyfin-ffmpeg/ffmpeg"
	else
		ffmpeg_executable="ffmpeg"
	fi
fi

# If show_executable is set, show the ffmpeg executable path
if [[ $show_executable == 1 ]]; then
	echo "$ffmpeg_executable"
	exit 0
fi

# GPU detection helpers.
#
# Neither vainfo nor lspci is required: vainfo is not used anywhere in this
# script, and lspci is missing on minimal installs (Ubuntu inside an AppImage,
# containers), where refusing to run would break every GPU conversion. When
# lspci is unavailable the PCI vendor IDs exported by the kernel are used
# instead, formatted so the vendor matching below keeps working.
detect_gpu_devices() {
	if command -v lspci >/dev/null; then
		lspci
		return
	fi

	local vendor_file vendor
	for vendor_file in /sys/class/drm/card[0-9]*/device/vendor; do
		[[ -r $vendor_file ]] || continue
		vendor=$(<"$vendor_file")
		case $vendor in
		0x10de) echo "VGA compatible controller: NVIDIA Corporation device" ;;
		0x1002 | 0x1022) echo "VGA compatible controller: AMD/ATI device" ;;
		0x8086) echo "VGA compatible controller: Intel Corporation device" ;;
		esac
	done | sort -u
}

# Define color variables
COLOR_RESET="\e[0m"
COLOR_GREEN="\e[32m"
COLOR_YELLOW="\e[33m"
COLOR_CYAN="\e[36m"
COLOR_BLUE="\e[34m"

# Show help if no arguments are passed or the first argument is -h or --help
if [[ $# -eq 0 || $1 == "-h" || $1 == "--help" ]]; then
	echo -e "${COLOR_CYAN}Usage:${COLOR_RESET} ${COLOR_YELLOW}variables=value${COLOR_RESET} $0 ${COLOR_YELLOW}input_file${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_CYAN}You can specify various variables before the command. Below are the available options and examples:${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}1. Specifying the GPU:${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}You can specify which GPU to use for the operation. Options include:${COLOR_GREEN} auto, nvidia, amd, intel, software${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}Example:${COLOR_RESET} ${COLOR_YELLOW}gpu=nvidia${COLOR_RESET} $0 ${COLOR_YELLOW}video.mkv${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}2. Specifying the Output File:${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}You can specify the name of the output file. If this variable is not specified, a default name will be used.${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}Example:${COLOR_RESET} ${COLOR_YELLOW}output_file=video_converted.mp4${COLOR_RESET} $0 ${COLOR_YELLOW}video.mkv${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}3. Specifying the Output Folder:${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}You can specify the folder where the output file will be saved. This will save the file with the same name as the input file in the specified folder.${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}If ${COLOR_YELLOW}output_file${COLOR_CYAN} is specified, this variable is ignored.${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}Example:${COLOR_RESET} ${COLOR_YELLOW}output_folder=/home/user/Videos${COLOR_RESET} $0 ${COLOR_YELLOW}video.mkv${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}4. Specifying Video Quality:${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}You can specify the quality of the video. Options include:${COLOR_GREEN} veryhigh, high, medium, low, verylow${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}Example:${COLOR_RESET} ${COLOR_YELLOW}video_quality=high${COLOR_RESET} $0 ${COLOR_YELLOW}video.mkv${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}5. Specifying Audio Bitrate:${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}You can specify the bitrate of the audio. Example:${COLOR_GREEN} 128k, 192k, 256k${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}Default is 32k per channel, stereo audio will have 64k bitrate, and 5.1 audio will have 192k bitrate${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}Example:${COLOR_RESET} ${COLOR_YELLOW}audio_bitrate=192k${COLOR_RESET} $0 ${COLOR_YELLOW}video.mkv${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}6. Specifying Audio Channels:${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}You can specify the number of audio channels. If not specified, the default will be used.${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}Example:${COLOR_RESET} ${COLOR_YELLOW}audio_channels=2${COLOR_RESET} $0 ${COLOR_YELLOW}video.mkv${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}7. Video Filter:${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}You can specify the video filter for ffmpeg. Example:${COLOR_GREEN} -vf scale=1280x720${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}8. Specifying Compression Preset:${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}You can specify the preset for compression.:${COLOR_GREEN} ultrafast, veryfast, faster, medium, slow, veryslow${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}Default is medium for GPU encode and faster for software encode${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}Example:${COLOR_RESET} ${COLOR_YELLOW}preset=veryslow${COLOR_RESET} $0 ${COLOR_YELLOW}video.mkv${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}9. Passing Additional Options to FFmpeg:${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}You can pass additional options directly to FFmpeg using the ${COLOR_YELLOW}options${COLOR_CYAN} variable.${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}Example to cut a video from 1 minute to 30 seconds:${COLOR_RESET}"
	echo -e "   ${COLOR_YELLOW}options=\"-ss 60 -t 30\"${COLOR_RESET} $0 ${COLOR_YELLOW}video.mkv${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}10. Specifying Subtitle Handling:${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}You can specify how to handle subtitles. Options include:${COLOR_GREEN} extract, embedded, none${COLOR_RESET} Default is extract"
	echo -e "   ${COLOR_CYAN}Example to extract subtitles:${COLOR_RESET} ${COLOR_YELLOW}subtitle_extract=extract${COLOR_RESET} $0 ${COLOR_YELLOW}video.mkv${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}11. Specifying Audio Handling:${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}You can specify how to handle audio. Options include:${COLOR_GREEN} copy, reencode, none${COLOR_RESET} Default is copy"
	echo -e "   ${COLOR_CYAN}Example to copy audio without reencoding:${COLOR_RESET} ${COLOR_YELLOW}audio_handling=copy${COLOR_RESET} $0 ${COLOR_YELLOW}video.mkv${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}12. Specifying Audio Codec for Re-encoding:${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}You can specify the audio codec when re-encoding. Options include:${COLOR_GREEN} aac, opus, ac3${COLOR_RESET} Default is aac"
	echo -e "   ${COLOR_CYAN}Example to use opus codec:${COLOR_RESET} ${COLOR_YELLOW}audio_codec=opus${COLOR_RESET} $0 ${COLOR_YELLOW}video.mkv${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}13. Specifying Video Encoder:${COLOR_RESET}"
	echo -e "   ${COLOR_CYAN}You can specify the video encoder. Options include:${COLOR_GREEN} h264, h265, av1, vp9${COLOR_RESET} Default is h264"
	echo -e "   ${COLOR_CYAN}Example to use h265 encoder:${COLOR_RESET} ${COLOR_YELLOW}video_encoder=h265${COLOR_RESET} $0 ${COLOR_YELLOW}video.mkv${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}14. Force Gpu partial mode, decode using CPU and encode using GPU:${COLOR_RESET}"
	echo -e "   ${COLOR_YELLOW}gpu_partial=1${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}15. Force CPU decode and encode:${COLOR_RESET}"
	echo -e "   ${COLOR_YELLOW}force_software=1${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}16. Copy video without reencode:${COLOR_RESET}"
	echo -e "   ${COLOR_YELLOW}force_copy_video=1${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}17. Only extract subtitles to .srt files:${COLOR_RESET}"
	echo -e "   ${COLOR_YELLOW}only_extract_subtitles=1${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}18. Show ffmpeg executable path:${COLOR_RESET}"
	echo -e "   ${COLOR_YELLOW}show_executable=1${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}19. Force ffmpeg encoder. Options: nvenc, vulkan, vaapi, qsv, amf, software ${COLOR_RESET}"
	echo -e "   ${COLOR_YELLOW}force_encoder=encoder_name${COLOR_RESET}"
	echo ""
	echo -e "${COLOR_BLUE}20. Force ffmpeg decoder. Options: cuda, vaapi, qsv, amf, software ${COLOR_RESET}"
	echo -e "   ${COLOR_YELLOW}force_decoder=decoder_name${COLOR_RESET}"
	exit 0
fi

# Use the first argument as the input file
input_file="$1"

# Validate that input file exists and is readable
if [[ ! -f "$input_file" ]]; then
	echo "ERROR: Input file not found: $input_file"
	exit 1
fi

if [[ ! -r "$input_file" ]]; then
	echo "ERROR: Input file is not readable: $input_file"
	exit 1
fi

#############################################################################
# Probing helpers
#
# ffprobe exits non-zero and prints nothing when it cannot open a decoder for
# ANY stream of the file. Some MKVs with dvd_subtitle tracks trigger this on
# FFmpeg 9 ("Could not open codec for input stream N") even though the
# container and the video stream are perfectly readable and ffmpeg converts
# the file without any trouble. So never treat an ffprobe failure as "the file
# is corrupted": fall back to parsing `ffmpeg -i` output instead.
#############################################################################
ffmpeg_probe_output=""

# Raw `ffmpeg -i` report for the input file (cached).
ffmpeg_probe() {
	if [[ -z $ffmpeg_probe_output ]]; then
		ffmpeg_probe_output=$($ffmpeg_executable -hide_banner -i "$input_file" -t 0 -f null - 2>&1)
	fi
	printf '%s\n' "$ffmpeg_probe_output"
}

# Only the input stream declarations, so the Output/mapping lines don't leak in.
ffmpeg_probe_input_block() {
	ffmpeg_probe | sed -n '/^Input #0/,/^Output #/p'
}

# Emits "index|language|codec" for every input stream of the given kind
# (Video, Audio or Subtitle), parsed from ffmpeg's own report.
ffmpeg_probe_streams() {
	local kind="$1" line
	local stream_re='Stream #0:([0-9]+)(\(([a-zA-Z]{2,3})\))?[^:]*: ([A-Za-z]+): ([A-Za-z0-9_]+)'
	while IFS= read -r line; do
		if [[ $line =~ $stream_re ]]; then
			[[ ${BASH_REMATCH[4]} == "$kind" ]] || continue
			printf '%s|%s|%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[3]}" "${BASH_REMATCH[5]}"
		fi
	done < <(ffmpeg_probe_input_block)
}

# Fallbacks mirroring the CSV layout of the ffprobe calls they replace.
fb_audio_info() { # index,language
	local idx lang codec
	while IFS='|' read -r idx lang codec; do
		printf '%s,%s\n' "$idx" "$lang"
	done < <(ffmpeg_probe_streams Audio)
}

fb_subtitle_codecs() { # index,codec_name
	local idx lang codec
	while IFS='|' read -r idx lang codec; do
		printf '%s,%s\n' "$idx" "$codec"
	done < <(ffmpeg_probe_streams Subtitle)
}

fb_subtitle_info() { # index,forced,language,title
	local idx lang codec
	while IFS='|' read -r idx lang codec; do
		printf '%s,0,%s,\n' "$idx" "$lang"
	done < <(ffmpeg_probe_streams Subtitle)
}

# Channel count / layout of an audio stream, by absolute stream index.
fb_audio_layout() {
	local want="$1" line
	local layout_re="Stream #0:${want}[^:]*: Audio: [^,]+, [0-9]+ Hz, ([^,]+)"
	while IFS= read -r line; do
		[[ $line =~ $layout_re ]] || continue
		printf '%s\n' "${BASH_REMATCH[1]}"
		return
	done < <(ffmpeg_probe_input_block)
}

fb_audio_channels() {
	local layout
	layout=$(fb_audio_layout "$1")
	case ${layout%%(*} in
	mono) echo 1 ;;
	stereo) echo 2 ;;
	2.1 | 3.0) echo 3 ;;
	quad | 4.0 | 3.1) echo 4 ;;
	5.0) echo 5 ;;
	5.1 | 6.0) echo 6 ;;
	6.1 | 7.0) echo 7 ;;
	7.1) echo 8 ;;
	*) echo "" ;;
	esac
}

# Is ffprobe usable for this particular file?
ffprobe_usable=true
if [[ -z $(ffprobe -v error -show_entries format=format_name -of csv=p=0 "$input_file" 2>/dev/null) ]]; then
	ffprobe_usable=false
fi

# Validate the input: only reject the file when ffmpeg itself sees no video.
if [[ $ffprobe_usable == false ]]; then
	if ! grep -qE 'Stream #0:[0-9]+.*: Video:' <<<"$(ffmpeg_probe_input_block)"; then
		echo "ERROR: Input file is corrupted or not a valid video file: $input_file"
		echo "The file container could not be read. It may be a corrupted download or an unsupported format."
		exit 2
	fi
	echo "⚠️  ffprobe could not analyze this file; using ffmpeg metadata instead."
fi

# Detect bit depth and codec of input video
echo "Iniciando o processo FFmpeg..."
echo "Analyzing input video..."
if [[ $ffprobe_usable == true ]]; then
	pix_fmt=$(ffprobe -v error -select_streams v:0 -show_entries stream=pix_fmt -of csv=p=0 "$input_file" 2>/dev/null | sed 's/,$//')
	video_profile=$(ffprobe -v error -select_streams v:0 -show_entries stream=profile -of csv=p=0 "$input_file" 2>/dev/null | sed 's/,$//')
else
	# "Video: h264 (Main), yuv420p(tv, bt709), 1920x820" → profile / pixel format
	probe_video_line=$(ffmpeg_probe_input_block | grep -m1 -E 'Stream #0:[0-9]+.*: Video:')
	pix_fmt=""
	video_profile=""
	profile_re='Video: [A-Za-z0-9_]+ \(([^)]*)\)'
	pix_fmt_re='Video: [^,]+, ([a-z0-9]+)'
	if [[ $probe_video_line =~ $profile_re ]]; then
		video_profile="${BASH_REMATCH[1]}"
	fi
	if [[ $probe_video_line =~ $pix_fmt_re ]]; then
		pix_fmt="${BASH_REMATCH[1]}"
	fi
fi

# Determine if video is 10-bit based on pixel format and profile
is_10bit=false
if [[ $pix_fmt =~ yuv.*p10 || $pix_fmt =~ yuv.*10le || $pix_fmt =~ .*10bit.* || $pix_fmt =~ yuv.*p12 || $pix_fmt =~ yuv.*12le ]]; then
	is_10bit=true
	echo "Detected high bit depth video input (pixel format: $pix_fmt)"
elif [[ $video_profile =~ .*10.* || $video_profile =~ .*High\ 10.* || $video_profile =~ .*Main\ 10.* ]]; then
	is_10bit=true
	echo "Detected 10-bit video input (profile: $video_profile)"
else
	if [[ -z "$pix_fmt" ]]; then
		echo "⚠️  Warning: Could not detect pixel format - file may be corrupted or use an unusual format"
	fi
	echo "ℹ️  Detected 8-bit video ($pix_fmt) - using standard profile"
fi

# Detect pixel formats with implicit GBR color matrix (e.g. ProRes 4444/XQ yuv444p12le)
# These formats cause swscaler conversion failures and need zscale to override color matrix
needs_gbr_matrix_fix=false
if [[ $pix_fmt =~ 444p12 || $pix_fmt =~ ^gbrp ]]; then
	needs_gbr_matrix_fix=true
	echo "Detected video with GBR-incompatible pixel format ($pix_fmt) - zscale color fix will be applied"
fi

echo "Detecting source video codec..."
if [[ $ffprobe_usable == true ]]; then
	source_video_codec=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 "$input_file" 2>/dev/null | head -n1)
else
	source_video_codec=$(ffmpeg_probe_streams Video | head -n1 | cut -d'|' -f3)
fi
echo "Source video codec detected: $source_video_codec"

# Determine if we need to convert 10-bit video to 8-bit for H.264 compatibility
needs_10bit_to_8bit_conversion=false
if [[ $is_10bit == true && ($video_encoder == "h264" || -z "$video_encoder") ]]; then
	needs_10bit_to_8bit_conversion=true
	echo "Detected 10-bit input. Forcing 8-bit pixel format (nv12) for H.264 compatibility."
elif [[ ($video_encoder == "h264" || -z "$video_encoder") && "$source_video_codec" != "h264" && $is_10bit == false ]]; then
	echo "Converting from $source_video_codec to h264 (8-bit input, no bit depth conversion needed)."
fi

# Remove extension from the input file
input_file_without_extension=${input_file##*/}
input_file_without_extension=${input_file_without_extension%.*}

# Check if the output_folder variable exists and variable output_file not exists
if [[ -n $output_folder && -z $output_file ]]; then
	output_file="$output_folder/$input_file_without_extension"
fi

# If variable output_file is not set, write next to the input file. Using just
# the basename would put the result in the caller's working directory, which is
# not where the user expects it (and is unwritable in some launchers).
if [[ -z $output_file ]]; then
	# Pure parameter expansion instead of dirname: no external command needed,
	# which also keeps this working in minimal environments.
	input_dir=${input_file%/*}
	[[ $input_dir == "$input_file" ]] && input_dir="."
	output_file="$input_dir/$input_file_without_extension"
fi

# Change to case insensitive
shopt -s nocasematch

# Improved version that uses output_format when available
if [[ -n "$output_format" ]]; then
	# Remove any existing extension
	output_basename="${output_file%.*}"
	# Add the output format extension
	output_file="${output_basename}.${output_format}"
elif [[ ! "$output_file" =~ \.(mp4|avi|mkv|mov|wmv|flv|webm|m4v|mpeg|mpg)$ ]]; then
	# If no format is defined and no recognized extension is present, use mp4
	output_file="$output_file.mp4"
fi

# Back to case sensitive
shopt -u nocasematch

# Never let the output overwrite the input: ffmpeg would truncate the source
# file while still reading from it, destroying the original. "-ef" is a bash
# builtin comparing device+inode, so symlinks and different spellings of the
# same path are covered without depending on realpath.
if [[ -e $output_file && $output_file -ef $input_file ]]; then
	echo "ERROR: The output file is the same as the input file: $output_file"
	echo "Choose a different output folder or format."
	exit 1
fi

# Info about the input file
if [[ $ffprobe_usable == true ]]; then
	ffprobe_result=$(ffprobe -v error -select_streams s -show_entries stream=index:stream_tags=language,title:stream_disposition=forced -of csv=p=0 "$input_file" | sed 's/,$//')
else
	ffprobe_result=$(fb_subtitle_info)
fi

##############
# Subtitles
##############
# Get subtitle codec information to filter only SRT subtitles
if [[ $ffprobe_usable == true ]]; then
	subtitle_codec_info=$(ffprobe -v error -select_streams s -show_entries stream=index,codec_name -of csv=p=0 "$input_file" | sed 's/,$//')
else
	subtitle_codec_info=$(fb_subtitle_codecs)
fi

# Extract IDs of SRT (subrip) subtitles
subtitle_list_srt_ids=""
while IFS=, read -r index codec_name; do
	if [[ "$codec_name" == "subrip" ]]; then
		subtitle_list_srt_ids+="$index "
	fi
done <<<"$subtitle_codec_info"

# Filter simplified
subtitle_list_simplified=$(grep '^[0-9],' <<<"$ffprobe_result")

# Convert to an array
mapfile -t subtitles <<<"$subtitle_list_simplified"

# If not specified, use extract
: ${subtitle_extract:=extract}

# Subtitle handling
if [[ $subtitle_extract == "embedded" ]]; then
	output_extension="${output_file##*.}"
	subtitle_cmd="" # Initialize as empty

	if [[ "$output_extension" == "mp4" ]]; then
		# For MP4, find compatible SRT streams and convert them to mov_text
		map_cmd=""
		subtitle_info_for_embed="$subtitle_codec_info"
		while IFS=, read -r index codec_name; do
			if [[ "$codec_name" == "subrip" ]]; then
				map_cmd+=" -map 0:$index"
			fi
		done <<<"$subtitle_info_for_embed"
		# If we found any SRT streams, set the command
		if [[ -n "$map_cmd" ]]; then
			subtitle_cmd="$map_cmd -c:s mov_text"
		fi
	else
		# For other containers like MKV, copy all subtitle streams
		subtitle_cmd="-map 0:s? -c:s copy"
	fi
elif [[ $subtitle_extract == "extract" ]]; then
	if [[ -n $subtitle_list_srt_ids ]]; then
		# Declare an associative array to track language code usage
		declare -A language_code_usage

		# Loop through the array
		for i in "${subtitles[@]}"; do

			IFS=',' read -r index disposition language_code title <<<"$i"

			# Only process the subtitle if it is in the list of srt subtitles
			# Use word boundary matching to avoid false positives (e.g., index 4 matching in 14)
			if [[ " $subtitle_list_srt_ids " =~ " $index " ]]; then

				# Remove white spaces and special characters from the language code
				language_code=${language_code// /}
				# Remove quotes, parentheses and other problematic characters
				language_code=${language_code//\"/}
				language_code=${language_code//\'/}
				language_code=${language_code//\(/}
				language_code=${language_code//\)/}
				language_code=${language_code//\[/}
				language_code=${language_code//\]/}

				# Verify if the language code is valid
				if [[ -z $language_code || $language_code == "0" ]]; then
					language_code="und" # undetermined, if the language code is empty
				fi

				# Check if this is a forced subtitle
				if [[ $title == *"(Forced)"* || $disposition == "1" ]]; then
					suffix=".forced"
				else
					suffix=""
				fi

				# Only increment the counter for non-forced subtitles
				# Forced subtitles have a different suffix, so they don't need numbering
				if [[ -z $suffix ]]; then
					# Check if the language code has been used before
					if [[ -n "${language_code_usage[$language_code]}" ]]; then
						language_code_count=${language_code_usage[$language_code]}
						((language_code_count++))
						language_code_usage[$language_code]=$language_code_count
						language_code="${language_code}${language_code_count}"
					else
						language_code_usage[$language_code]=1
					fi
				fi

				output_file_srt="${output_file%.*}.$language_code$suffix.srt"

				subtitle_cmd+=" -map 0:$index -c copy \"$output_file_srt\" "

			fi
		done
	fi
elif [[ $subtitle_extract == "none" ]]; then
	subtitle_cmd=""
fi

##############
# Audio
##############
# If not specified, use copy.
: ${audio_handling:=copy}
original_audio_handling=$audio_handling

# If audio codec not specified, use aac
: ${audio_codec:=aac}

# Floating point maths. bc is not installed by default on Debian/Ubuntu
# (and therefore inside minimal AppImage hosts), so fall back to awk, which is
# part of every base system. Expressions are written in awk syntax
# (exp/log/sqrt) and translated for bc when bc is used.
calc() {
	local expr="$1"
	if command -v bc >/dev/null; then
		expr=${expr//exp(/e(}
		expr=${expr//log(/l(}
		echo "scale=6; $expr" | bc -l
	else
		awk "BEGIN { printf \"%.6f\", $expr }"
	fi
}

# Evaluates a comparison, echoing 1 or 0.
calc_bool() {
	awk "BEGIN { print (($1) ? 1 : 0) }"
}

##############
##############
# Audio Processing Filter Chain
# Build filter chain: HPF → Compressor → Normalize → [NR] → Gate → EQ
# Each filter works independently — NR is just one optional filter
noise_audio_filter=""
nr_requires_mono=false
local_filters=()

# 1. High-Pass Filter
if [[ $hpf_enabled == "1" ]]; then
	: ${hpf_frequency:=80}
	local_filters+=("highpass=f=${hpf_frequency}:poles=2")
	echo "HPF enabled: frequency=${hpf_frequency}Hz"
fi

# 2. Compressor (before NR to even out dynamics)
if [[ $compressor_enabled == "1" ]]; then
	: ${compressor_intensity:=1.0}
	comp_threshold_db=$(calc "-20 - ${compressor_intensity} * 20")
	comp_ratio=$(calc "3 + ${compressor_intensity} * 7")
	comp_makeup_db=$(calc "6 + ${compressor_intensity} * 12")
	comp_knee_db=$(calc "12 + ${compressor_intensity} * 4")
	comp_threshold_linear=$(calc "exp(${comp_threshold_db}/20 * log(10))")
	comp_makeup_linear=$(calc "exp(${comp_makeup_db}/20 * log(10))")
	comp_knee_linear=$(calc "exp(${comp_knee_db}/20 * log(10))")
	local_filters+=("acompressor=threshold=${comp_threshold_linear}:ratio=${comp_ratio}:attack=150:release=800:makeup=${comp_makeup_linear}:knee=${comp_knee_linear}:detection=rms")
	echo "Compressor enabled: intensity=${compressor_intensity} threshold=${comp_threshold_db}dB ratio=${comp_ratio}"
fi

# 3. Volume Normalization (before NR for consistent input level)
# speechnorm: fast, no internal resampling, no pumping artifacts
if [[ $normalize_enabled == "1" ]]; then
	local_filters+=("speechnorm=e=12.5:r=0.0001:l=1")
	echo "Volume normalization enabled (speechnorm)"
fi

# 4. GTCRN Noise Reduction (requires mono processing via LADSPA)
if [[ $noise_reduction == "1" ]]; then
	if [[ -e /usr/lib/ladspa/libgtcrn_ladspa.so ]]; then
		: ${noise_strength:=1.0}
		: ${noise_model:=0}
		: ${noise_speech_strength:=1.0}
		: ${noise_lookahead:=0}
		: ${noise_model_blend:=0}
		: ${noise_voice_recovery:=0.75}
		local_filters+=("ladspa=file=libgtcrn_ladspa:plugin=gtcrn_mono:controls=c0=1|c1=${noise_strength}|c2=${noise_model}|c3=${noise_speech_strength}|c4=${noise_lookahead}|c5=${noise_model_blend}|c6=${noise_voice_recovery}")
		echo "Noise reduction enabled: strength=${noise_strength} model=${noise_model}"
		nr_requires_mono=true
	else
		echo "WARNING: Noise reduction requested but GTCRN LADSPA plugin not found at /usr/lib/ladspa/libgtcrn_ladspa.so"
	fi
fi

# 5. Noise Gate (after NR — post-NR audio is mostly speech, so full-band detection is effective)
if [[ $noise_gate == "1" ]]; then
	: ${gate_intensity:=0.5}
	gate_threshold_db=$(calc "-50 + sqrt(${gate_intensity}) * 35")
	gate_range_db=$(calc "-40 - sqrt(${gate_intensity}) * 50")
	gate_threshold_linear=$(calc "exp(${gate_threshold_db}/20 * log(10))")
	gate_range_linear=$(calc "exp(${gate_range_db}/20 * log(10))")
	local_filters+=("agate=threshold=${gate_threshold_linear}:range=${gate_range_linear}:attack=10:release=250:ratio=4:detection=rms")
	echo "Noise gate enabled: intensity=${gate_intensity} threshold=${gate_threshold_db}dB range=${gate_range_db}dB"
fi

# 6. Equalizer (10-band parametric)
if [[ $eq_enabled == "1" ]]; then
	: ${eq_bands:="0,0,0,0,0,0,0,0,0,0"}
	IFS=',' read -ra bands_arr <<< "$eq_bands"
	eq_freqs=(31 63 125 250 500 1000 2000 4000 8000 16000)
	eq_widths=(1.5 1.5 1.5 1.5 1.5 1.5 1.5 1.5 1.5 1.5)
	eq_parts=()
	for idx in "${!eq_freqs[@]}"; do
		gain="${bands_arr[$idx]:-0}"
		if [[ $(calc_bool "${gain} != 0") -eq 1 ]]; then
			eq_parts+=("equalizer=f=${eq_freqs[$idx]}:width_type=o:w=${eq_widths[$idx]}:g=${gain}")
		fi
	done
	if [[ ${#eq_parts[@]} -gt 0 ]]; then
		eq_filter=$(IFS=','; echo "${eq_parts[*]}")
		local_filters+=("${eq_filter}")
		echo "Equalizer enabled: ${#eq_parts[@]} active bands"
	fi
fi

# Join all filters with comma
if [[ ${#local_filters[@]} -gt 0 ]]; then
	noise_audio_filter=$(IFS=','; echo "${local_filters[*]}")
	echo "Audio filter chain: ${noise_audio_filter}"

	# Force re-encode if audio is set to copy (audio processing requires it)
	if [[ $audio_handling == "copy" ]]; then
		echo "Audio processing requires audio re-encoding, switching from copy to reencode"
		audio_handling="reencode"
	fi
fi

# Audio stream queries, honouring the ffprobe fallback.
# probe_audio_info emits "index,language"; the channel/layout helpers take the
# relative audio stream number and its absolute index in the container.
function probe_audio_info() {
	if [[ $ffprobe_usable == true ]]; then
		ffprobe -v error -select_streams a -show_entries stream=index:stream_tags=language -of csv=p=0 "$input_file" | sed 's/,$//'
	else
		fb_audio_info
	fi
}

function probe_audio_channels() {
	local rel="$1" abs="$2"
	if [[ $ffprobe_usable == true ]]; then
		ffprobe -v error -select_streams "a:$rel" -show_entries stream=channels -of csv=p=0 "$input_file" | sed 's/,$//'
	else
		fb_audio_channels "$abs"
	fi
}

function probe_audio_layout() {
	local rel="$1" abs="$2"
	if [[ $ffprobe_usable == true ]]; then
		ffprobe -v error -select_streams "a:$rel" -show_entries stream=channel_layout -of csv=p=0 "$input_file" | sed 's/,$//'
	else
		fb_audio_layout "$abs"
	fi
}

# Function to build the audio command string based on the current audio_handling value
function build_audio_cmd() {
	audio_cmd="" # Reset audio command
	audio_inputs="" # Additional audio inputs for NR pre-processing
	nr_deferred_preprocess=false # Reset deferred NR flag
	if [[ $audio_handling == "copy" ]]; then
		audio_info=$(probe_audio_info)
		mapfile -t audio_streams <<<"$audio_info"
		echo "DEBUG: Found ${#audio_streams[@]} audio streams"
		for i in "${!audio_streams[@]}"; do
			IFS=',' read -r index_audio language_audio <<<"${audio_streams[$i]}"
			echo "DEBUG: Mapping audio stream $index_audio (language: $language_audio)"
			audio_cmd+="-map 0:$index_audio -c:a:$i copy "
		done
		echo "DEBUG: Final audio_cmd: $audio_cmd"
	elif [[ $audio_handling == "reencode" ]]; then
		audio_info=$(probe_audio_info)
		mapfile -t audio_streams <<<"$audio_info"
		local num_audio=${#audio_streams[@]}

		if [[ -n $noise_audio_filter && $nr_requires_mono == true ]]; then
			# LADSPA NR active — defer to parallel preprocess (mono split/merge)
			nr_deferred_preprocess=true
			nr_temp_dir=$(mktemp -d)
			trap 'rm -rf "$nr_temp_dir"' EXIT

			# Collect per-stream info for NR processing and mux
			nr_stream_channels=()
			nr_stream_layouts=()
			nr_stream_indices=()
			nr_stream_bitrates=()

			for i in "${!audio_streams[@]}"; do
				IFS=',' read -r index_audio language_audio <<<"${audio_streams[$i]}"
				if [[ -n $audio_channels ]]; then
					channels=$audio_channels
				else
					channels=$(probe_audio_channels "$i" "$index_audio")
				fi
				local nr_layout
				nr_layout=$(probe_audio_layout "$i" "$index_audio")
				: "${channels:=2}"
				: "${nr_layout:=stereo}"

				if [[ -z $audio_bitrate ]]; then
					audio_bitrate_value=$((channels * 32))k
				else
					audio_bitrate_value=$audio_bitrate
				fi

				nr_stream_channels+=("$channels")
				nr_stream_layouts+=("$nr_layout")
				nr_stream_indices+=("$index_audio")
				nr_stream_bitrates+=("$audio_bitrate_value")
			done

			# Build audio_cmd referencing temp files (created by preprocess_audio_nr in background)
			# Used for the mux step after parallel video+audio processing
			for i in "${!audio_streams[@]}"; do
				local temp_audio="$nr_temp_dir/audio_${i}.flac"
				local input_idx=$((i + 1))
				audio_inputs+="-i \"$temp_audio\" "
				local brate="${nr_stream_bitrates[$i]}"
				local ch="${nr_stream_channels[$i]}"
				case $audio_codec in
				opus)
					audio_cmd+="-map $input_idx:a:0 -c:a:$i libopus -b:a:$i $brate -ac:a:$i $ch "
					;;
				ac3)
					audio_cmd+="-map $input_idx:a:0 -c:a:$i ac3 -b:a:$i $brate -ac:a:$i $ch "
					;;
				*)
					audio_cmd+="-map $input_idx:a:0 -c:a:$i aac -aac_coder fast -profile:a aac_low -b:a:$i $brate -ac:a:$i $ch "
					;;
				esac
			done

			echo "Audio NR deferred for parallel processing ($num_audio streams)"
		else
			# No NR, or non-NR filters only — build audio_cmd with optional inline filters
			for i in "${!audio_streams[@]}"; do
				IFS=',' read -r index_audio language_audio <<<"${audio_streams[$i]}"

				if [[ -n $audio_channels ]]; then
					channels=$audio_channels
				else
					channels=$(probe_audio_channels "$i" "$index_audio")
				fi
				language_audio=${language_audio// /}
				if [[ -z $language_audio ]]; then
					language_audio="und"
				fi

				if [[ -z $audio_bitrate ]]; then
					audio_bitrate_value=$((channels * 32))k
				else
					audio_bitrate_value=$audio_bitrate
				fi

				local filter_opt=""
				if [[ -n $noise_audio_filter ]]; then
					filter_opt="-filter:a:$i $noise_audio_filter "
				fi

				case $audio_codec in
				opus)
					audio_cmd+="-map 0:$index_audio -c:a:$i libopus -b:a:$i $audio_bitrate_value -ac:a:$i $channels $filter_opt"
					;;
				ac3)
					audio_cmd+="-map 0:$index_audio -c:a:$i ac3 -b:a:$i $audio_bitrate_value -ac:a:$i $channels $filter_opt"
					;;
				*)
					audio_cmd+="-map 0:$index_audio -c:a:$i aac -aac_coder fast -profile:a aac_low -b:a:$i $audio_bitrate_value -ac:a:$i $channels $filter_opt"
					;;
				esac
			done
		fi
	elif [[ $audio_handling == "none" ]]; then
		audio_cmd="-an"
	fi
}

# Pre-process all audio streams with noise reduction.
# Called from attempt_conversion() as a background process to run in parallel
# with video encoding. Creates $nr_temp_dir/audio_N.flac for each stream.
# Uses 3-stage pipeline: extract → NR per-channel (parallel) → merge.
function preprocess_audio_nr() {
	local num_streams=${#nr_stream_indices[@]}
	echo "NR: Processing $num_streams audio streams in parallel..."

	# Stage A: Extract each audio stream / apply NR directly for mono
	local -a extract_pids=()
	for i in $(seq 0 $((num_streams - 1))); do
		local index_audio="${nr_stream_indices[$i]}"
		local channels="${nr_stream_channels[$i]}"
		local nr_layout="${nr_stream_layouts[$i]}"
		local temp_audio="$nr_temp_dir/audio_${i}.flac"

		echo "  NR stream $i: index=$index_audio, ${channels}ch, layout=$nr_layout"

		if [[ $channels -le 1 ]]; then
			# Mono: apply NR directly (no split/merge needed)
			$ffmpeg_executable -i "$input_file" -map "0:$index_audio" \
				-filter:a "$noise_audio_filter" \
				-c:a flac -y "$temp_audio" </dev/null 2>/dev/null &
			extract_pids+=("$!:direct")
		else
			# Multi-channel: extract stream as FLAC first
			local stream_raw="$nr_temp_dir/raw_stream_${i}.flac"
			$ffmpeg_executable -i "$input_file" -vn -map "0:$index_audio" \
				-c:a flac -y "$stream_raw" </dev/null 2>/dev/null &
			extract_pids+=("$!:extract:$i")
		fi
	done

	# Wait for Stage A
	local -a extract_failed=()
	for entry in "${extract_pids[@]}"; do
		local pid="${entry%%:*}"
		if ! wait "$pid"; then
			extract_failed+=("$entry")
		fi
	done
	if [[ ${#extract_failed[@]} -gt 0 ]]; then
		echo "WARNING: Audio extraction failed for: ${extract_failed[*]}"
	fi

	# Stage B: Split into mono channels + apply NR in parallel
	local -a nr_pids=()
	local -a nr_pid_stream=()
	for i in $(seq 0 $((num_streams - 1))); do
		local channels="${nr_stream_channels[$i]}"
		[[ $channels -le 1 ]] && continue

		local stream_raw="$nr_temp_dir/raw_stream_${i}.flac"
		local ch_dir="$nr_temp_dir/stream_${i}"
		mkdir -p "$ch_dir"

		for ((ch=0; ch<channels; ch++)); do
			$ffmpeg_executable -i "$stream_raw" \
				-filter:a "pan=mono|c0=c${ch},${noise_audio_filter}" \
				-c:a flac -y "$ch_dir/ch${ch}.flac" </dev/null 2>/dev/null &
			nr_pids+=($!)
			nr_pid_stream+=("$i:$ch")
		done
	done

	if [[ ${#nr_pids[@]} -gt 0 ]]; then
		echo "  NR: Waiting for ${#nr_pids[@]} per-channel processes..."
		local -a nr_failed=()
		for pidx in "${!nr_pids[@]}"; do
			if ! wait "${nr_pids[$pidx]}"; then
				nr_failed+=("${nr_pid_stream[$pidx]}")
			fi
		done
		if [[ ${#nr_failed[@]} -gt 0 ]]; then
			echo "WARNING: NR per-channel failed for: ${nr_failed[*]}"
		fi
	fi

	# Stage C: Merge channels back to original layout (parallel per stream)
	local -a merge_pids=()
	for i in $(seq 0 $((num_streams - 1))); do
		local channels="${nr_stream_channels[$i]}"
		[[ $channels -le 1 ]] && continue

		local nr_layout="${nr_stream_layouts[$i]}"
		local temp_audio="$nr_temp_dir/audio_${i}.flac"
		local ch_dir="$nr_temp_dir/stream_${i}"

		# Build pan expression to restore original channel layout
		local pan_expr=""
		case "$nr_layout" in
		mono)        pan_expr="mono|c0=c0" ;;
		stereo)      pan_expr="stereo|FL=c0|FR=c1" ;;
		3.0)         pan_expr="3.0|FL=c0|FR=c1|FC=c2" ;;
		quad)        pan_expr="quad|FL=c0|FR=c1|BL=c2|BR=c3" ;;
		5.0*)        pan_expr="5.0|FL=c0|FR=c1|FC=c2|BL=c3|BR=c4" ;;
		5.1*)        pan_expr="5.1|FL=c0|FR=c1|FC=c2|LFE=c3|BL=c4|BR=c5" ;;
		6.1*)        pan_expr="6.1|FL=c0|FR=c1|FC=c2|LFE=c3|BL=c4|BR=c5|BC=c6" ;;
		7.1*)        pan_expr="7.1|FL=c0|FR=c1|FC=c2|LFE=c3|BL=c4|BR=c5|SL=c6|SR=c7" ;;
		*)           pan_expr="" ;;
		esac

		local merge_inputs=""
		for ((ch=0; ch<channels; ch++)); do
			merge_inputs+="-i $ch_dir/ch${ch}.flac "
		done
		local merge_filter="amerge=inputs=${channels}"
		if [[ -n "$pan_expr" ]]; then
			merge_filter+=",pan=${pan_expr}"
		fi

		eval $ffmpeg_executable $merge_inputs \
			-filter_complex "'${merge_filter}'" \
			-c:a flac -y "'$temp_audio'" </dev/null 2>/dev/null &
		merge_pids+=($!)
	done

	# Wait for merges
	for pid in "${merge_pids[@]}"; do
		wait "$pid"
	done

	# Clean up intermediate files
	rm -f "$nr_temp_dir"/raw_stream_*.flac
	rm -rf "$nr_temp_dir"/stream_*

	echo "  NR: All audio streams processed."
}

##############
# Video
##############
# if video_quality is default, remove this part
video_quality=${video_quality/default/}

# If quality is not specified, use medium
: ${video_quality:=medium}

case $video_quality in
veryhigh)
	cq_value=19
	qp_value=18
	global_quality=18
	cq_value_nvidia=19
	qp_hevc=22
	;;
high)
	cq_value=24
	qp_value=21
	global_quality=21
	cq_value_nvidia=24
	qp_hevc=25
	;;
medium)
	cq_value=28
	qp_value=24
	global_quality=24
	cq_value_nvidia=28
	qp_hevc=28
	;;
low)
	cq_value=31
	qp_value=27
	global_quality=27
	cq_value_nvidia=31
	qp_hevc=31
	;;
verylow)
	cq_value=34
	qp_value=30
	global_quality=30
	cq_value_nvidia=34
	qp_hevc=34
	;;
superlow)
	cq_value=38
	qp_value=33
	global_quality=33
	cq_value_nvidia=38
	qp_hevc=37
	;;
esac

# QSV HEVC needs lower global_quality values than NVENC qp for same visual quality
qp_hevc_qsv=$((qp_hevc - 4))

if [[ $preset == "default" ]]; then
	unset preset
fi

# If preset is not specified, use medium for GPU and faster for software encoding
if [[ -z $preset ]]; then
	preset=medium
	software_preset=faster
else
	software_preset=$preset
fi

# Convert preset for NVIDIA
case $preset in
ultrafast) nvidia_preset=1 ;;
veryfast) nvidia_preset=2 ;;
faster) nvidia_preset=3 ;;
slow) nvidia_preset=5 ;;
veryslow) nvidia_preset=6 ;;
*) nvidia_preset=4 ;; # Default to medium
esac

# Convert preset for SVT-AV1 (6=best quality, 12=fastest)
# Presets below 6 add minimal quality gain with extreme slowdown
case $software_preset in
ultrafast) svtav1_preset=12 ;;
veryfast) svtav1_preset=11 ;;
faster) svtav1_preset=10 ;;
medium) svtav1_preset=9 ;;
slow) svtav1_preset=7 ;;
veryslow) svtav1_preset=6 ;;
*) svtav1_preset=10 ;; # Default to faster
esac

# Convert preset for VP9 cpu-used (0=slowest/best, 8=fastest/worst)
case $software_preset in
ultrafast) vpx_cpu_used=6 ;;
veryfast) vpx_cpu_used=5 ;;
faster) vpx_cpu_used=4 ;;
medium) vpx_cpu_used=3 ;;
slow) vpx_cpu_used=2 ;;
veryslow) vpx_cpu_used=1 ;;
*) vpx_cpu_used=3 ;; # Default to medium
esac

#######################
# Create ffmpeg command
#######################
# Function to build the main encoder command strings
function build_encoder_commands() {
	general_params="-i \"$input_file\" $audio_inputs -map 0:v:0 $audio_cmd"
	copy_video="$general_params -c:v copy"

	# Select appropriate encoder command based on the specified video encoder
	case $video_encoder in
	h265)
		encoder_nvenc="$general_params -c:v hevc_nvenc -rc:v vbr -cq $cq_value_nvidia -tune:v hq -preset:v p$nvidia_preset -temporal-aq 1 -spatial-aq 1 -b_ref_mode middle -rc-lookahead 100"
		encoder_vulkan="$general_params -c:v hevc_vulkan -qp $qp_hevc -tune hq"
		encoder_vaapi="$general_params -c:v hevc_vaapi -global_quality $qp_hevc -rc_mode ICQ"
		encoder_qsv="$general_params -c:v hevc_qsv -preset $preset -look_ahead_depth 100 -low_delay_brc 1 -extbrc 1 -global_quality $qp_hevc_qsv -mbbrc 1 -adaptive_i 1 -adaptive_b 1"
		encoder_software="$general_params -c:v libx265 -preset $software_preset -crf $qp_hevc -x265-params log-level=warning"
		;;
	av1)
		encoder_nvenc="$general_params -c:v av1_nvenc -rc:v vbr -cq $cq_value_nvidia -tune:v hq -preset:v p$nvidia_preset -temporal-aq 1 -spatial-aq 1 -b_ref_mode middle -rc-lookahead 100"
		encoder_vulkan="$general_params -c:v av1_vulkan -qp $qp_value -tune hq"
		encoder_vaapi="$general_params -c:v av1_vaapi -global_quality $global_quality -rc_mode CQP"
		encoder_qsv="$general_params -c:v av1_qsv -preset $preset -look_ahead_depth 100 -global_quality $global_quality"
		encoder_software="$general_params -c:v libsvtav1 -preset $svtav1_preset -crf $cq_value -pix_fmt yuv420p10le -svtav1-params tune=0"
		;;
	vp9)
		encoder_nvenc="$general_params -c:v vp9_nvenc -rc:v vbr -cq $cq_value_nvidia -tune:v hq -preset:v p$nvidia_preset -temporal-aq 1 -rc-lookahead 100"
		encoder_vaapi="$general_params -c:v vp9_vaapi -global_quality $global_quality -rc_mode CQP"
		encoder_qsv="$general_params -c:v vp9_qsv -preset $preset -look_ahead_depth 100 -global_quality $global_quality"
		encoder_software="$general_params -c:v libvpx-vp9 -crf $cq_value -b:v 0 -quality good -cpu-used $vpx_cpu_used -row-mt 1"
		;;
	prores)
		# ProRes is a software-only codec, map quality to profile
		# profile 0=Proxy, 1=LT, 2=Standard, 3=HQ, 4=4444, 5=4444XQ
		local prores_profile
		case $video_quality in
			superlow|verylow) prores_profile=0 ;; # Proxy
			low) prores_profile=1 ;;               # LT
			medium|default) prores_profile=2 ;;    # Standard
			high) prores_profile=3 ;;              # HQ
			veryhigh) prores_profile=4 ;;          # 4444
			*) prores_profile=2 ;;                 # Standard
		esac
		encoder_nvenc=""
		encoder_vulkan=""
		encoder_vaapi=""
		encoder_qsv=""
		encoder_software="$general_params -c:v prores_ks -profile:v $prores_profile -vendor apl0 -pix_fmt yuva444p10le"
		;;
	*)
		# Default is h264
		encoder_nvenc="$general_params -c:v h264_nvenc -rc:v vbr -cq $cq_value_nvidia -tune:v hq -preset:v p$nvidia_preset -temporal-aq 1 -rc-lookahead 100"
		encoder_vulkan="$general_params -c:v h264_vulkan -qp $global_quality"
		encoder_vaapi="$general_params -c:v h264_vaapi -profile:v high -level:v 4.1 -rc_mode CQP -global_quality $global_quality"
		encoder_qsv="$general_params -c:v h264_qsv -preset $preset -profile:v high -level:v 4.1 -look_ahead_depth 100 -low_delay_brc 1 -extbrc 1 -global_quality $global_quality -mbbrc 1 -adaptive_i 1 -adaptive_b 1"
		# -pix_fmt yuv420p is required: H.264 "high" profile has no 4:4:4/4:2:2
		# support, so 444/422 sources (ProRes, screen captures) fail to encode
		# without an explicit downsample.
		encoder_software="$general_params -c:v libx264 -preset $software_preset -profile:v high -level:v 4.1 -crf $qp_value -pix_fmt yuv420p"
		;;
	esac
}

# If gpu = auto change gpu to empty
gpu=${gpu/auto/}

# Generic options for ffmpeg
# Only apply -movflags +faststart for MP4/MOV containers (it's invalid for MKV/WebM)
output_extension="${output_file##*.}"
if [[ "${output_extension,,}" == "mp4" || "${output_extension,,}" == "mov" || "${output_extension,,}" == "m4v" ]]; then
	ffmpeg_generic_options="-movflags +faststart -y \"$output_file\""
else
	ffmpeg_generic_options="-y \"$output_file\""
fi

if [[ $only_extract_subtitles = 1 ]]; then
	general_params="-i \"$input_file\" $subtitle_cmd"
	echo "Running command: $ffmpeg_executable $general_params"
	eval $ffmpeg_executable "$general_params"
	exit $?
fi

# Function to attempt the conversion using the 3-stage fallback (GPU -> Partial -> CPU)
function attempt_conversion() {
	# This function now re-initializes all hardware-specific variables to ensure a clean state

	# If gpu empty auto detect
	if [[ -z $gpu ]]; then
		devices=$(detect_gpu_devices)
		if [[ -z $devices ]]; then
			echo "No GPU detected (lspci and sysfs gave no result) - using software encoding"
		fi
	else
		devices="VGA $gpu"
	fi

	# Never hardcode kernel_driver=i915: Arrow Lake and newer Intel GPUs are
	# driven by "xe", and asking for i915 there fails with "Device creation
	# failed", which silently pushes every Intel conversion down to software
	# encoding. Use the driver the kernel actually reports for the Intel GPU,
	# and omit the filter when it cannot be determined.
	intel_kernel_driver=""
	for _vendor_file in /sys/class/drm/card[0-9]*/device/vendor; do
		[[ -r $_vendor_file ]] || continue
		[[ $(<"$_vendor_file") == "0x8086" ]] || continue
		intel_kernel_driver=$(basename "$(readlink -f "${_vendor_file%/vendor}/driver" 2>/dev/null)")
		[[ -n $intel_kernel_driver ]] && break
	done
	if [[ -n $intel_kernel_driver ]]; then
		init_qsv="-init_hw_device vaapi=va:,driver=iHD,kernel_driver=$intel_kernel_driver -init_hw_device qsv=qs@va"
	else
		init_qsv='-init_hw_device vaapi=va:,driver=iHD -init_hw_device qsv=qs@va'
	fi
	init_vaapi='-init_hw_device vaapi'
	init_vulkan='-init_hw_device vulkan'
	init_nvidia='-init_hw_device cuda'

	# Use specific GPU device if provided
	if [[ -n "$gpu_device" ]]; then
		init_vaapi="-init_hw_device vaapi=va:$gpu_device"
		init_vulkan="-init_hw_device vulkan=vk:$gpu_device"
		init_nvidia="-init_hw_device cuda=cu:$gpu_device"
	fi
	decoder_qsv='-filter_hw_device qs -hwaccel qsv -hwaccel_output_format qsv'
	decoder_vaapi='-hwaccel vaapi -hwaccel_output_format vaapi'
	decoder_vulkan='-hwaccel vulkan -hwaccel_output_format vulkan'
	decoder_cuda='-hwaccel cuda -hwaccel_output_format cuda'

	local scale_filter_name="scale"
	local detected_encoder_var=""  # Track which encoder var was selected

	# Handle explicit software encoding request
	if [[ "$gpu" == "software" ]]; then
		encoder_format=$encoder_software
		detected_encoder_var="encoder_software"
		force_software=1
	# ProRes is always software-only
	elif [[ "$video_encoder" == "prores" ]]; then
		encoder_format=$encoder_software
		detected_encoder_var="encoder_software"
		force_software=1
	elif [[ "$force_encoder" == "vulkan" || "$gpu" == "vulkan" ]]; then
		decoder_format="$decoder_vulkan"
		encoder_format="$encoder_vulkan"
		detected_encoder_var="encoder_vulkan"
		init_hardware="$init_vulkan"
		scale_filter_name="scale_vulkan"
	elif grep -qiE '(VGA|Display controller|3d).*nvidia' <<<"$devices"; then
		decoder_format="$decoder_cuda"
		encoder_format="$encoder_nvenc"
		detected_encoder_var="encoder_nvenc"
		init_hardware="$init_nvidia"
		scale_filter_name="scale_cuda"
	elif grep -qiE '(VGA|Display controller).*(\bAMD\b|\bATI\b)' <<<"$devices"; then
		# AMD VAAPI doesn't support ICQ mode - use CQP for HEVC
		if [[ "$video_encoder" == "h265" ]]; then
			encoder_vaapi="$general_params -c:v hevc_vaapi -global_quality $qp_hevc -rc_mode CQP"
		fi
		decoder_format="$decoder_vaapi"
		encoder_format="$encoder_vaapi"
		detected_encoder_var="encoder_vaapi"
		init_hardware="$init_vaapi"
		scale_filter_name="scale_vaapi"
	elif grep -qiE '(VGA|Display controller).*intel' <<<"$devices"; then
		decoder_format="$decoder_qsv"
		encoder_format="$encoder_qsv"
		detected_encoder_var="encoder_qsv"
		init_hardware="$init_qsv"
		scale_filter_name="scale_qsv"
	else
		encoder_format=$encoder_software
		detected_encoder_var="encoder_software"
	fi

	if [[ $force_software = 1 ]]; then
		encoder_format=$encoder_software
		detected_encoder_var="encoder_software"
	fi

	if [[ $force_copy_video = 1 ]]; then
		encoder_format=$copy_video
		detected_encoder_var="copy_video"
	fi
	if [[ -n $force_encoder ]]; then
		encoder_var="encoder_${force_encoder}"
		encoder_format=${!encoder_var}
		detected_encoder_var="$encoder_var"
		init_hardware="${!encoder_var/init/}"
	fi

	if [[ -n $force_decoder ]]; then
		decoder_var="decoder_${force_decoder}"
		decoder_format=${!decoder_var}
		init_hardware="${!decoder_var/init/}"
	fi

	#####################################################
	# --- Encode and Decode video in GPU filter chain ---
	#####################################################
	local video_filter_chain=""
	local filter_parts=()

	# # 1. Add 10-bit to 8-bit conversion filter if needed (only for GPU encode with hardware decode)
	# Note: For 8-bit video, this conversion is NOT needed
	if [[ $needs_10bit_to_8bit_conversion == true && -z "$video_filter" ]]; then
		force_8bit_color="${scale_filter_name}=format=nv12,"
	fi

	# if [[ -n "$video_filter" ]]; then
	# 	filter_parts+=("hwdownload,format=nv12")
	# fi

	# 2. Add user-provided filters (like scaling)
	if [[ -n "$video_filter" ]]; then
		local user_filters=$video_filter
		filter_parts+=("$user_filters")
	fi

	if [[ "$video_resolution" && ${#filter_parts[@]} -gt 0 ]]; then
		video_filter_chain="-vf ${force_8bit_color}hwdownload,format=nv12,"
		video_filter_chain+="$(
			IFS=,
			echo "${filter_parts[*]}"
		)"
		video_filter_chain+=",hwupload,$scale_filter_name=${video_resolution/x/:}"

	elif [[ ${#filter_parts[@]} -gt 0 ]]; then
		video_filter_chain="-vf ${force_8bit_color}hwdownload,format=nv12,"
		video_filter_chain+="$(
			IFS=,
			echo "${filter_parts[*]}"
		)"

	elif [[ -n "$video_resolution" ]]; then
		video_filter_chain="-vf ${force_8bit_color}$scale_filter_name=${video_resolution/x/:}"

	elif [[ $needs_10bit_to_8bit_conversion == true ]]; then
		video_filter_chain="-vf $force_8bit_color"

	fi

	# Force software decode on NVIDIA when vflip is present.
	# CUDA hwdownload→vflip→hwupload pipeline produces green screen for HEVC/AV1 decoders.
	# H264 already fails Stage 1 and falls back, but HEVC/AV1 silently corrupt frames.
	# Skipping directly to Stage 2 (software decode + GPU encode) avoids the issue.
	if [[ "$video_filter" == *"vflip"* && "$detected_encoder_var" == "encoder_nvenc" ]]; then
		gpu_partial=1
		echo "vflip detected on NVIDIA: forcing software decode (Stage 2)"
	fi

	# --- Execute Conversion Attempts ---

	# Extract subtitles first if needed (separate command to avoid output conflicts)
	# Only extract once - skip if already extracted in a previous attempt_conversion call
	if [[ -n "$subtitle_cmd" && "$subtitle_extract" == "extract" && "$subtitles_already_extracted" != "true" ]]; then
		echo "Extracting subtitles..."
		if eval $ffmpeg_executable -y -i \"$input_file\" $subtitle_cmd 2>/dev/null; then
			subtitles_already_extracted=true
		else
			echo "Warning: Subtitle extraction failed (non-critical, continuing with conversion)"
		fi
	fi

	# === Parallel NR Pipeline ===
	# When NR is active, audio and video are processed in parallel:
	#   1. Audio NR runs in background (per-channel parallel)
	#   2. Video encodes through 3-stage fallback (GPU → partial → software)
	#   3. Wait for audio NR, then mux video + audio
	# Total time ≈ max(video, audio) instead of audio + video sequentially.
	# Works with ALL encoders (CUDA, VAAPI, QSV, software).
	local nr_parallel_mode=false
	local nr_bg_pid=""
	local nr_video_file=""
	local saved_audio_cmd=""
	local saved_audio_inputs=""
	local saved_ffmpeg_generic=""

	if [[ $nr_deferred_preprocess == true ]]; then
		nr_parallel_mode=true
		nr_video_file=$(mktemp --suffix=.mkv)

		# Start audio NR in background
		preprocess_audio_nr &
		nr_bg_pid=$!
		echo "Parallel pipeline: audio NR (PID $nr_bg_pid) + video encoding"

		# Switch to video-only encoding
		saved_audio_cmd="$audio_cmd"
		saved_audio_inputs="$audio_inputs"
		saved_ffmpeg_generic="$ffmpeg_generic_options"
		audio_cmd="-an"
		audio_inputs=""
		build_encoder_commands
		# Re-derive encoder_format after rebuild (it was a value copy from detection)
		if [[ -n "$detected_encoder_var" ]]; then
			encoder_format="${!detected_encoder_var}"
		fi
		ffmpeg_generic_options="-y \"$nr_video_file\""
	fi

	if [[ "$force_copy_video" = "1" ]]; then
		echo "Generating file without re-encoding"
		echo "Running command: $ffmpeg_executable $options $decoder_format $encoder_format  $ffmpeg_generic_options"
		eval $ffmpeg_executable "$options" "$decoder_format" "$encoder_format" "$ffmpeg_generic_options"
		exit_code=$?
		if [[ $nr_parallel_mode != true ]]; then
			# After copy always exit (when not in parallel NR mode)
			exit $exit_code
		fi
	fi

	# --- 3-stage encoder fallback ---
	# Stage 1: Full GPU (decode + encode on GPU)
	# Skip if force_copy_video already handled the video
	if [[ "$force_copy_video" != "1" && "$encoder_format" != "$encoder_software" && "$gpu_partial" != "1" ]]; then
		# The GUI parses "Encode mode:" and "Running command:"; keep one line
		# each and let the interface handle wording and translation.
		echo "Encode mode: Decode GPU, encode GPU"
		echo "Running command: $ffmpeg_executable $init_hardware $options $decoder_format $encoder_format $video_filter_chain $ffmpeg_generic_options"
		eval $ffmpeg_executable "$init_hardware" "$options" "$decoder_format" "$encoder_format" "$video_filter_chain" "$ffmpeg_generic_options"
		exit_code=$?
		if [[ $exit_code != 0 ]]; then
			echo "Conversion failed!"
		fi
	else
		exit_code=1
	fi

	# Stage 2: Partial GPU (decode CPU, encode GPU)
	if [[ ($exit_code != 0 && $exit_code != 255 && $force_software != 1 && $force_copy_video != 1) || "$gpu_partial" == "1" ]]; then
		echo "Encode mode: Decode Software, Encode GPU"

		local partial_video_filter_chain=""
		local partial_filter_parts=()

		if [[ $needs_gbr_matrix_fix == true ]]; then
			partial_filter_parts+=("zscale=min=709:m=709:tin=709:t=709:pin=709:p=709:r=tv")
		fi

		# User video filters (crop, rotation, flip, etc) — must run in software space
		if [[ -n "$video_filter" ]]; then
			partial_filter_parts+=("$video_filter")
		fi

		local hw_pix_fmt="nv12"
		if [[ $video_encoder == "av1" ]]; then
			hw_pix_fmt="p010le"
		elif [[ $is_10bit == true && $needs_10bit_to_8bit_conversion == false ]]; then
			hw_pix_fmt="p010le"
		fi

		if [[ ${encoder_format,,} =~ nvenc ]]; then
			partial_filter_parts+=("format=$hw_pix_fmt" "hwupload_cuda")
		elif [[ ${encoder_format,,} =~ vulkan ]]; then
			partial_filter_parts+=("format=$hw_pix_fmt" "hwupload=derive_device=vulkan")
		elif [[ ${encoder_format,,} =~ vaapi ]]; then
			partial_filter_parts+=("format=$hw_pix_fmt" "hwupload=derive_device=vaapi")
		elif [[ ${encoder_format,,} =~ qsv ]]; then
			partial_filter_parts+=("format=$hw_pix_fmt" "hwupload=extra_hw_frames=64,format=qsv")
		fi

		if [[ -n "$video_resolution" ]]; then
			partial_filter_parts+=("$scale_filter_name=${video_resolution/x/:}")
		fi

		if [[ ${#partial_filter_parts[@]} -gt 0 ]]; then
			partial_video_filter_chain="-vf $(IFS=,; echo "${partial_filter_parts[*]}")"
		fi

		echo "Running command: $ffmpeg_executable $init_hardware $options $encoder_format $partial_video_filter_chain $ffmpeg_generic_options"
		eval $ffmpeg_executable "$init_hardware" "$options" "$encoder_format" "$partial_video_filter_chain" "$ffmpeg_generic_options"
		exit_code=$?
		if [[ $exit_code != 0 ]]; then
			echo "Conversion failed!"
		fi
	fi

	# Stage 3: Full software (decode + encode on CPU)
	if [[ ($exit_code != 0 && $exit_code != 255 && $force_software != 1 && $force_copy_video != 1) || $force_software == 1 || ("$encoder_format" == "$encoder_software") ]]; then
		echo "Encode mode: Decode Software, Encode Software"
		local software_filter_chain=""
		local sw_filter_parts=()

		if [[ $needs_gbr_matrix_fix == true ]]; then
			sw_filter_parts+=("zscale=min=709:m=709:tin=709:t=709:pin=709:p=709:r=tv")
			if [[ $video_encoder == "av1" ]]; then
				sw_filter_parts+=("format=yuv420p10le")
			elif [[ $is_10bit == true && $needs_10bit_to_8bit_conversion == false ]]; then
				sw_filter_parts+=("format=yuv420p10le")
			else
				sw_filter_parts+=("format=yuv420p")
			fi
		fi

		if [[ -n "$video_filter" ]]; then
			sw_filter_parts+=("$video_filter")
		fi

		if [[ -n "$video_resolution" ]]; then
			sw_filter_parts+=("scale=${video_resolution/x/:}")
		fi

		if [[ ${#sw_filter_parts[@]} -gt 0 ]]; then
			software_filter_chain="-vf $(IFS=,; echo "${sw_filter_parts[*]}")"
		fi

		echo "Running command: $ffmpeg_executable $options $encoder_software $software_filter_chain $ffmpeg_generic_options"
		eval $ffmpeg_executable "$options" "$encoder_software" "$software_filter_chain" "$ffmpeg_generic_options"
		exit_code=$?
	fi

	# === NR Parallel Teardown: wait for audio NR and mux ===
	if [[ $nr_parallel_mode == true ]]; then
		# Restore audio state for potential fallback passes
		audio_cmd="$saved_audio_cmd"
		audio_inputs="$saved_audio_inputs"
		ffmpeg_generic_options="$saved_ffmpeg_generic"
		build_encoder_commands

		if [[ $exit_code -eq 0 && -s "$nr_video_file" ]]; then
			echo "Video encoding done. Waiting for audio NR to finish..."
			wait "$nr_bg_pid"
			local nr_exit=$?

			if [[ $nr_exit -eq 0 ]]; then
				echo "Audio NR complete. Muxing video + processed audio..."
				# Build mux command: video from temp, audio from temp FLAC files
				local mux_audio_inputs=""
				local mux_audio_map=""
				local num_streams=${#nr_stream_indices[@]}
				for i in $(seq 0 $((num_streams - 1))); do
					local temp_audio="$nr_temp_dir/audio_${i}.flac"
					if [[ -s "$temp_audio" ]]; then
						mux_audio_inputs+="-i \"$temp_audio\" "
						local input_idx=$((i + 1))
						local channels="${nr_stream_channels[$i]}"
						local brate="${nr_stream_bitrates[$i]}"
						case $audio_codec in
						opus)
							mux_audio_map+="-map $input_idx:a:0 -c:a:$i libopus -b:a:$i $brate -ac:a:$i $channels "
							;;
						ac3)
							mux_audio_map+="-map $input_idx:a:0 -c:a:$i ac3 -b:a:$i $brate -ac:a:$i $channels "
							;;
						*)
							mux_audio_map+="-map $input_idx:a:0 -c:a:$i aac -aac_coder fast -profile:a aac_low -b:a:$i $brate -ac:a:$i $channels "
							;;
						esac
					else
						echo "WARNING: NR output missing for stream $i"
					fi
				done

				# Movflags for MP4/MOV containers
				local mux_movflags=""
				local final_ext="${output_file##*.}"
				if [[ "${final_ext,,}" == "mp4" || "${final_ext,,}" == "mov" || "${final_ext,,}" == "m4v" ]]; then
					mux_movflags="-movflags +faststart"
				fi

				eval $ffmpeg_executable -i \"$nr_video_file\" $mux_audio_inputs \
					-map 0:v:0 $mux_audio_map \
					-c:v copy $mux_movflags -y \"$output_file\"
				exit_code=$?

				if [[ $exit_code -eq 0 ]]; then
					echo "Muxing complete: $output_file"
				else
					echo "WARNING: Muxing failed (exit $exit_code)"
				fi
			else
				echo "WARNING: Audio NR failed (exit $nr_exit)"
				exit_code=1
			fi
		else
			echo "Video encoding failed. Killing audio NR..."
			kill "$nr_bg_pid" 2>/dev/null
			wait "$nr_bg_pid" 2>/dev/null
		fi

		rm -f "$nr_video_file"
	fi
}

# --- First Pass ---
# Track if subtitles were already extracted to avoid re-extracting in fallback
subtitles_already_extracted=false

# Build commands with initial user settings and attempt conversion.
build_audio_cmd
build_encoder_commands
attempt_conversion

# --- Audio Fallback Pass ---
# If the first pass failed and we were trying to copy audio, it's likely due to an
# incompatible audio codec for the container. Let's try again by re-encoding the audio.
if [[ $exit_code != 0 && $exit_code != 255 && $exit_code != 2 && "$original_audio_handling" == "copy" ]]; then
	echo -e "${COLOR_YELLOW}Audio stream copy may have failed. Retrying with audio re-encoding...${COLOR_RESET}"

	# Force re-encode and rebuild all commands
	audio_handling="reencode"
	build_audio_cmd
	build_encoder_commands

	# Attempt the conversion again with the new settings
	attempt_conversion
fi

# --- Audio Codec Fallback Pass ---
# If re-encoding with a specific codec failed (e.g. Opus in MP4), try with AAC
if [[ $exit_code != 0 && $exit_code != 255 && "$audio_handling" == "reencode" && "$audio_codec" != "aac" ]]; then
	echo -e "${COLOR_YELLOW}Audio re-encoding with $audio_codec may have failed. Retrying with AAC codec...${COLOR_RESET}"

	# Switch to AAC (most compatible codec) and rebuild all commands
	audio_codec="aac"
	build_audio_cmd
	build_encoder_commands

	# Attempt the conversion again with AAC
	attempt_conversion
fi

# --- Final Exit Code Check ---
if [[ $exit_code != 0 ]]; then
	if [[ $exit_code == 255 ]]; then
		echo -e "${COLOR_YELLOW}Conversion interrupted by user.${COLOR_RESET}"
	else
		echo -e "${COLOR_YELLOW}Conversion failed with an unexpected error. Exit code: $exit_code.${COLOR_RESET}"
	fi
	exit $exit_code
fi
