#!/bin/bash
# accepted vars: MODE (RenPy|RPGM|HTML), GAMEDIR (./game|./www|.), FFMPEG (`which ffmpeg`), CONVERT (`which convert`), QUALITY (75), THREADS (8)

shopt -s dotglob	# .files matching in globs
shopt -s globstar	# ** in globs
shopt -s nocaseglob	# case-insensitive globs
declare -A COUNT	# associative array

function set-term-title { echo -en "\033]2;$1\007"; }
function locate { find -iwholename "$1" | head -n1; } # ignore-case search

function parse-args {
	NAME=`basename "$0"`
	if (( $# > 0 )); then
		while getopts "m:d:q:o:t:l:pa1" OPTION; do
			case "$OPTION" in
			    m) MODE=$OPTARG ;;
			    d) GAMEDIR=$OPTARG ;;
			    q) QUALITY=$OPTARG ;;
			    o) OPACITY=$OPTARG ;;
			    t) THREADS=$OPTARG ;;
			    l) LOGLEVEL=${OPTARG:--} ;;
			    p) PRETEND=true ;;
			    a) ACCEL=false ;;
			    1) PARALLEL_=- ;;
			    ?)
				cat <<-END
				Usage: $0 [ -m MODE ] [ -d GAMEDIR ] [ -q QUALITY ] [ -o OPACITY] [ -t THREADS ] [ -l LOGLEVEL ] [ -p ] [ -1 ] [ MODE ]
				    MODE	RenPy | RPGM | Ace | HTML (default is based on script name or detected)
				    GAMEDIR	Folder to be scanned for updates (default is based on MODE: ./game | ./www | .)
				    QUALITY	WebP/JPEG quality (default 75)
				    OPACITY	minimum PNG opacity at which it's not considered transparent (default 10000, maximum 65535)
				    THREADS	ffmpeg threads (default 8)
				    LOGLEVEL	ffmpeg loglevel settings (default '-hide_banner -loglevel warning -stats')
				    (PRETEND	is set to 'true' by -p; this enables pretend mode, skipping actual file conversion)
				    (ACCEL	is set to 'true' by -a; this enables ffmpeg acceleration for VP9 which may affect compression/quality)
				    (PARALLEL_	is set to '-' by -1; this disables optional parallel processing if it's available)
				
				    All these and FFMPEG/CONVERT/PARALLEL_/RPATOOL/MVDECRYPTOR
				    (paths to ffmpeg/convert/parallel/rpatool/MvDecryptor executables) can be also passed as variables.
				END
				exit 1
				;;
			esac
		done
		(( $# - OPTIND >= 0 )) && MODE=${@:$OPTIND:1}
	fi
	if [ -z "$MODE" ]; then
		if [[ "$NAME" = RenCruncher* ]]; then
			MODE=RenPy
		elif [[ "$NAME" = RPGMCruncher* ]]; then
			MODE=RPGM
		elif [[ "$NAME" = AceCruncher* ]]; then
			MODE=Ace
		elif [[ "$NAME" = ResourceCruncher* ]]; then
			MODE=HTML
		elif [ -d ./game ] && [ -d ./renpy ] && [ -d ./lib ]; then
			MODE=RenPy
		elif [ -f "$(locate './www/data/System.json')" ]; then
			MODE=RPGM
		elif [ -f "$(locate './Game.ini')" ] && [ -f "$(locate './System/RGSS*.dll')" ]; then
			MODE=Ace
		else
			MODE=HTML
		fi
	fi
	: "${PARALLEL_:=`which parallel 2>/dev/null`}"
	: "${RPATOOL:=`which rpatool 2>/dev/null || which rpatool.py 2>/dev/null`}"
	: "${MVDECRYPTOR:=`which MvDecryptor 2>/dev/null`}"
}

function assert-executable {
	if [ ! -x "$1" ] || [ -d "$1" ]; then
		echo "    ! Error: Cannot locate $2 executable, unable to continue."
		echo "             Please install ${3:-$2} package, or specify path to $2 executable via ${2^^} variable."
		echo
		read -p "             Press Enter to exit..."
		exit 1
	fi
}

function init {
	MODE_=$MODE
	case "$MODE" in
	    RenPy)
		: "${GAMEDIR:=./game}"
		NAME=RenCruncher
		MODE_="Ren'Py"
		;;
	    RPGM)
		: "${GAMEDIR:=`locate './www'`}"
		NAME=RPGMCruncher
		;;
	    Ace)
		: "${GAMEDIR:=.}"
		NAME=AceCruncher
		MODE_="RPGM VX Ace"
		;;
	    *)
		: "${GAMEDIR:=.}"
		NAME=ResourceCruncher
		;;
	esac
	: "${FFMPEG:=`which ffmpeg`}"
	: "${CONVERT:=`which convert`}"
	: "${QUALITY:=75}"
	: "${OPACITY:=10000}"
	: "${THREADS:=8}"
	[ "$PRETEND" = true ] || PRETEND=false
	[ "$ACCEL" = false ] || ACCEL=true
	[ "$LOGLEVEL" = - ] && LOGLEVEL="" || : "${LOGLEVEL:=-hide_banner -loglevel quiet -nostats}"
	#[ "$LOGLEVEL" = - ] && LOGLEVEL="" || : "${LOGLEVEL:=-hide_banner -loglevel warning -stats}"
	COUNT=( )
	clear
	set-term-title "$NAME"
	cat <<-END
	
	      $NAME - ${MODE_} Game Compactor v0.4.1 BETA
	
	                 by bas @ f95zone.to
	  ffmpeg v6.0 by mpekas @ f95zone.to
	 ----------------------------------------------------
	END
	$PRETEND && echo -e "  Pretend mode: only counting files\n"
	if [ ! -d "$GAMEDIR" ]; then
		cat <<-END
		   ! Error: Cannot locate ${MODE_} game folder, unable to continue.
		            Please run this script in the same folder as game executable.
		
		END
		read -p "            Press Enter to exit..."
		exit 1
	fi
	assert-executable "$FFMPEG"  ffmpeg
	assert-executable "$CONVERT" convert "imagemagick or graphicsmagick"
	[[ "$MODE" != RenPy || ! -x "$RPATOOL" || -d "$RPATOOL" ]] && RPATOOL=""
	[[ "$MODE" != RPGM || ! -x "$MVDECRYPTOR" || -d "$MVDECRYPTOR" ]] && MVDECRYPTOR=""
	[[ "$PARALLEL_" = '-' || ! -x "$PARALLEL_" || -d "$PARALLEL_" ]] && PARALLEL_=""
}

function menu {
	cat <<-END
	
	   *** THIS TOOL OVERWRITES FILES IN THE ${MODE_^^} GAME ***
	  *** IT DOES NOT MAKE BACKUPS OR SEND TO RECYCLE BIN ***
	          ** CLOSE NOW IF YOU DON'T WANT THIS **
	
	  Please select files to process:
	    1) Image Files
	    2) Audio Files
	    3) Video Files
	
	    6) All of the above
	
	END
	[ "$RPATOOL" ] && cat <<-END
	    x) Extract *.rpa archives
	    c) Repack *.rpa/ to archives
	
	END
	[ "$MVDECRYPTOR" ] && cat <<-END
	    d) Decrypt MV resource files
	    D) Decrypt MV resource files (autodetect key)
	
	END
	cat <<-END
	    Press 0 to Exit
	
	END
	read -p "  Enter a number/letter: "
	echo -e "\n ----------------------------------------------------\n"
	case "$REPLY" in
	    1)
		images
		report image
		;;
	    2)
		audio
		report audio
		;;
	    3)
		video
		report video
		;;
	    6)
		images
		audio
		video
		report image audio video
		;;
	    x) if [ "$RPATOOL" ]; then rpa unpack; fi ;;
	    c) if [ "$RPATOOL" ]; then rpa repack; fi ;;
	    d) if [ "$MVDECRYPTOR" ]; then "$MVDECRYPTOR"; fi ;;
	    D) if [ "$MVDECRYPTOR" ]; then DETECT=yes "$MVDECRYPTOR"; fi ;;
	    0) return 1 ;;
	esac
	return $?
}

function rpa {
	pushd "$GAMEDIR" >/dev/null
	local FILES=( **/*.rpa )
	local COUNTER=0
	for FILE in "${FILES[@]}"; do
		[ -e "$FILE" ] || continue
		[[ "$1" = unpack && ! -d "$FILE" ]] || [[ "$1" = repack && -d "$FILE" ]] || continue
		echo "$FILE" && let COUNTER++
		if [ "$1" = unpack ]; then
			"$RPATOOL" -xo "$FILE~/" "$FILE" || exit 1
		else
			pushd "$FILE" >/dev/null
			"$RPATOOL" -c "../$(basename "$FILE")~" * || exit 1
			popd >/dev/null
		fi
		rm -r "$FILE"
		mv "$FILE"{~,}
	done
	popd >/dev/null
	read -p "${1^}ed $COUNTER archives. Press Enter to continue..."
}

function filesize        { stat -c%s "$1"; }
function hex-bytes       { for i in "$@"; do printf '\\x%02x' $((0xFF & i)); done; }
function little-endian   { echo -ne `hex-bytes $1 $(($1>>8)) $(($1>>16)) $(($1>>24))`; }
function webp-header-fix { little-endian $((`filesize "$1"` - 8)) | dd of="$1" obs=1 seek=4 conv=notrunc; }
export -f filesize hex-bytes little-endian webp-header-fix

function convert-image    { "$CONVERT" -verbose "$1" -quality "$QUALITY" -define webp:method=5 WEBP:"$1~"; }
function convert-parallax { "$CONVERT" -verbose "$1" -define webp:lossless=true WEBP:"$1~"; }
function convert-jpgpng   {
	if (( $("$CONVERT" "$1" -alpha extract -format '%[min]' info:) < "$OPACITY" )); then	# check for transparent pixels
		"$CONVERT" -verbose "$1" -quality 95 PNG:"$1~" # for PNG: -quality {compression-level}{filter-type}
	else
		"$CONVERT" -verbose "$1" -quality "$QUALITY" JPEG:"$1~"
	fi
}
export -f convert-parallax convert-image convert-jpgpng

function convert-audio { "$FFMPEG" -i "$1" -map_metadata -1 -codec:a libopus -vn -b:a 32k -compression_level 10 -vbr on -f ogg "$1~" $LOGLEVEL; }
function convert-mp3   { "$FFMPEG" -i "$1" -map_metadata -1 -codec:a libmp3lame -qscale:a 7 -vn -f mp3 "$1~" $LOGLEVEL; }
function convert-ogg   { "$FFMPEG" -i "$1" -codec:a libvorbis -qscale:a 3 -f ogg "$1~" $LOGLEVEL; }
export -f convert-audio convert-mp3 convert-ogg

function convert-webp {
	if $ACCEL; then
		"$FFMPEG" -y -i "$1" -vaapi_device /dev/dri/renderD128 -vf 'format=nv12,hwupload' -c:v vp9_vaapi -threads "$THREADS" -pass 1 -b:v 0 \
		    -global_quality:v 150 -g 240 -fps_mode vfr -an \
		    -passlogfile "$1" -f "${2:-webm}" /dev/null $LOGLEVEL
		"$FFMPEG" -i "$1" -vaapi_device /dev/dri/renderD128 -vf 'format=nv12,hwupload' -c:v vp9_vaapi -threads "$THREADS" -pass 2 -b:v 0 \
		    -global_quality:v 150 -g 240 -fps_mode vfr -c:a libopus -vbr on -compression_level:a 10 -frame_duration 60 -application audio -b:a 32k \
		    -passlogfile "$1" -f "${2:-webm}" "$1~" $LOGLEVEL
	else
		"$FFMPEG" -y -i "$1" -c:v libvpx-vp9 -threads "$THREADS" -row-mt 1 -quality good -pass 1 -b:v 0 -crf 40 -speed 4 \
		    -tile-columns 4 -frame-parallel 1 \
		    -g 240 -fps_mode vfr -an \
		    -passlogfile "$1" -f "${2:-webm}" /dev/null $LOGLEVEL
		"$FFMPEG" -i "$1" -c:v libvpx-vp9 -threads "$THREADS" -row-mt 1 -quality good -pass 2 -b:v 0 -crf 40 -speed 1 \
		    -tile-columns 4 -frame-parallel 1 -auto-alt-ref 1 -lag-in-frames 25 \
		    -g 240 -fps_mode vfr -c:a libopus -vbr on -compression_level 10 -frame_duration 60 -application audio -b:a 32k \
		    -passlogfile "$1" -f "${2:-webm}" "$1~" $LOGLEVEL
	fi
	rm "$1"-0.log
}
function convert-ogv {
	"$FFMPEG" -y -i "$1" -c:v libtheora -threads "$THREADS" -pass 1 -g 240 -vsync vfr -an \
	    -passlogfile "$1" -f ogv /dev/null $LOGLEVEL
	"$FFMPEG" -i "$1" -c:v libtheora -threads "$THREADS" -pass 2 -g 240 -vsync vfr -c:a libvorbis -compression_level 10 \
	    -passlogfile "$1" -f ogv "$1~" $LOGLEVEL
	rm "$1"-0.log
}
export -f convert-webp convert-ogv

function images {
	if [ "$MODE" = Ace ]; then
		for EXT in jpg jpeg png; do process image $EXT jpgpng; done
	else
		for EXT in webp jpg jpeg bmp png gif; do process image $EXT; done
	fi
}
function audio  {
	if [[ "$MODE" = @(RPGM|Ace) ]]; then
		process audio mp3 mp3
		process audio wav mp3
		process audio ogg ogg
	else
		for EXT in mp3 wav ogg opus; do process audio $EXT; done
	fi
}
function video  {
	[ "$MODE" = Ace ] && FMT=ogv || FMT=webp
	for EXT in webm mp4 mkv m4v mpg mov ogv avi; do process video $EXT $FMT; done
}

function show-progress {
	local PERCENT=$(echo "scale=2;$1 / $2 * 100" | bc -l)
	set-term-title "$NAME: converting ${3^^} | $1/$2 files (${PERCENT%.*}%) ${*:4}"
	printf "%s|\e[48;5;15m%0${#2}d/%d %${PERCENT%.*}s\e[31m${PERCENT%.*}%%\e[0m| %s\n\n" "$3" "$1" "$2" "" "${*:4}"
}
function apply-conversion {
	local TYPE=$1
	local EXT=$2
	local FILE=$3
	echo -e "Now processing $GAMEDIR/$FILE...\n"
	chmod +w "$FILE"
	if [ "$MODE" = RenPy ] && [ "$EXT" = webp ]; then
		webp-header-fix "$FILE"
		echo -e "webp header rewritten, continuing...\n"
	fi
	if [ "$MODE" = RPGM ] && [[ "${FILE,,}" = img/parallaxes/* ]]; then
		convert-parallax "$FILE"
	else
		convert-"$4" "$FILE"
	fi
	if [ ! -s "$FILE"~ ]; then
		echo "ERROR - New filesize is 0 bytes - reverting..."
		rm -f "$FILE"~
	elif (( `filesize "$FILE"~` >= `filesize "$FILE"` )); then
		echo "SKIPPING - New filesize is larger than the old one..."
		rm -f "$FILE"~
	else
		mv -v "$FILE"{~,}
	fi
	echo "Processing of $FILE completed."
}
export -f apply-conversion

function process {
	local TYPE=$1
	local EXT=$2
	$PRETEND || echo -e "\n Looking for $EXT $TYPE files to process..."
	local TOTAL=`find "$GAMEDIR" -iname "*.$EXT" | wc -l`
	if (( TOTAL <= 0 )); then
		$PRETEND || echo -e "\nNo $EXT $TYPE files found.\n"
	else
		cd "$GAMEDIR"
		local FILES=( **/*."$EXT" )
		if ! $PRETEND; then
			local START=`date +%s.%N`
			set-term-title "$NAME: converting ${EXT^^} | $TOTAL $TYPE files"
			if $ACCEL && [ "${3:-$TYPE}" = webp ] || [ ! "$TYPE" = video ] && [ "$PARALLEL_" ]; then
				export GAMEDIR MODE CONVERT FFMPEG QUALITY OPACITY THREADS LOGLEVEL ACCEL
				"$PARALLEL_" --bar apply-conversion "$TYPE" "$EXT" {} "${3:-$TYPE}" ';' echo ::: "${FILES[@]}"
			else
				local COUNTER=0
				show-progress $COUNTER $TOTAL $EXT
				for FILE in "${FILES[@]}"; do
					let COUNTER++
					apply-conversion "$TYPE" "$EXT" "$FILE" "${3:-$TYPE}"
					local ETA=`echo $(date +%s.%N) $START $TOTAL $COUNTER | awk '{print int(0.5 + ($1 - $2) * ($3 - $4) / $4)}'`
					show-progress $COUNTER $TOTAL $EXT "ETA=$((( ETA < 60 )) && echo $ETA || printf '%dm%02d' `expr $ETA / 60` `expr $ETA % 60`)s"
				done
			fi
			local DURATION=`echo $(date +%s.%N) $START | awk '{print int(0.5 + 10 * ($1 - $2)) / 10}'`
		fi
		cd - >/dev/null
		COUNT[$EXT]="${#FILES[@]}"
		$PRETEND || echo -e "Done - processed ${#FILES[@]} $EXT $TYPE files (in $DURATION seconds).\n"
	fi
}

function count { echo "${COUNT[$1]:-0} $1"; }
function report {
	$PRETEND || set-term-title "$NAME: Finished"
	$PRETEND || cat <<-END
	 ----------------------------------------------------
	  Summary:
	
	END
	$PRETEND && ACTION=Found || ACTION=Processed
	[[ " $@ " =~ " image " ]] && echo "  $ACTION $(count webp), $(count jpg), $(count jpeg), $(count bmp), $(count png), and $(count gif) image files."
	[[ " $@ " =~ " audio " ]] && echo "  $ACTION $(count mp3), $(count ogg), $(count opus), and $(count wav) audio files."
	[[ " $@ " =~ " video " ]] && echo "  $ACTION $(count webm), $(count mp4), $(count mkv), $(count m4v), $(count mpg), $(count mov), $(count ogv), and $(count avi) video files."
	if $PRETEND; then
		return 1
	else
		cat <<-END
		 ----------------------------------------------------
		
		  Finished!
		
		  Enter "1" to go back to the menu, or any other
		END
		read -p "  key to exit: "
		echo -e "\n ----------------------------------------------------\n"
		[[ "$REPLY" = 1 ]];  return $?
	fi
}

if [[ "${BASH_SOURCE[0]}" = "$0" ]]; then	# won't be executed when sourcing this file
	parse-args "$@"
	while :; do
		init
		PRETEND=true images
		PRETEND=true audio
		PRETEND=true video
		PRETEND=true report image audio video
		if [ "$MODE" = RPGM ]; then
			echo -e "\n  Encryption key in game config/scripts [must match or not be hardcoded]:"
			grep --colour -oEH '"encryptionKey":\s*"[^"]*"' "$(locate "$GAMEDIR/data/System.json")"
			grep --colour -oEH '[_[:alnum:]]+\._encryptionKey\s*=[^;]*;?' -R "$(locate "$GAMEDIR/js")/"
		fi
		! $PRETEND && menu || break
	done
fi
$PRETEND || set-term-title "${PWD##*/}"
