#!/bin/sh

# Copyright 2013-2021 Arx Libertatis Team (see the AUTHORS file)
#
# This file is part of Arx Libertatis.
#
# Arx Libertatis is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Arx Libertatis is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Arx Libertatis.  If not, see <http://www.gnu.org/licenses/>.

##########################################################################################
# Install script for Arx Fatalis data files to be used with Arx Libertatis
# Usage: just run the damned script, maybe check --help

# This scripts targets Linux and FreeBSD, but may also work on other UNIX-like systems.

# Is this a multi-thousand-line bas^H^H^HPOSIX shell script?
#  Sure looks like it.
# Am I mad?
#  Most likely.

# If you want to edit the required files and checksums, scroll to the end.


##########################################################################################
# Colors

disable_color() {
	red='' ; green='' ; yellow='' ; blue='' ; pink='' ; cyan='' ; white=''
	dim_red='' ; dim_green='' ; dim_yellow='' ; dim_blue='' ; dim_pink=''
	dim_cyan='' ; dim_white='' ; reset=''
}
disable_color
if [ -z "${NO_COLOR:-1}" ] && [ -t 1 ] && [ "$(tput colors 2> /dev/null)" != -1 ] ; then
	
	       red="$(printf '\033[1;31m')"
	     green="$(printf '\033[1;32m')"
	    yellow="$(printf '\033[1;33m')"
	      blue="$(printf '\033[1;34m')"
	      pink="$(printf '\033[1;35m')"
	      cyan="$(printf '\033[1;36m')"
	     white="$(printf '\033[1;37m')"
	
	   dim_red="$(printf '\033[0;31m')"
	 dim_green="$(printf '\033[0;32m')"
	dim_yellow="$(printf '\033[0;33m')"
	  dim_blue="$(printf '\033[0;34m')"
	  dim_pink="$(printf '\033[0;35m')"
	  dim_cyan="$(printf '\033[0;36m')"
	 dim_white="$(printf '\033[0;37m')"
	
	     reset="$(printf '\033[m')"
fi


##########################################################################################
# Constants

# Name and download locations for the 1.21 patch
patch_ver='1.21'
patch_name="ArxFatalis_${patch_ver}_MULTILANG.exe"
patch_name_localized="ArxFatalis_${patch_ver}_%s.exe"
patch_url_path="arxfatalis/patches/${patch_ver}/${patch_name}"
patch_url_master="https://cdn.bethsoft.com/${patch_url_path}"
patch_urls="https://arx.vg/${patch_name} ${patch_url_master}"
patch_urls="$patch_urls https://web.archive.org/web/${patch_url_master}"

# Name and download locations for the Japanese 1.02j patch
patch_jp_ver='1.02j'
patch_jp_name="arx_jpn_patch_${patch_jp_ver}.exe"
patch_jp_url_master="http://www.capcom.co.jp/pc/arx/patch/${patch_jp_name}"
patch_jp_urls="https://arx.vg/${patch_jp_name}" # master URL is no longer available
patch_jp_urls="$patch_jp_urls https://web.archive.org/web/${patch_jp_url_master}"

# Name and store page for the GOG.com download
gog_names='setup_arx_fatalis.exe setup_arx_fatalis_2.0.0.7.exe'
gog_url='https://www.gog.com/gamecard/arx_fatalis'

# Store page for the Steam download
steam_appid='1700'
steam_url="https://store.steampowered.com/app/$steam_appid/Arx_Fatalis/"
steam_install_dir="Arx Fatalis"

# Name and wiki page for the demo download
demo_names="arx_demo_english.zip arxdemoenglish.zip arx_demo_english.exe"
demo_names="$demo_names arx_fatalis_demo_fr.zip arx_demo_german.exe arx_jpn_demo.exe"
demo_url='https://arx.vg/demo-data'

bug_report_url='https://arx.vg/bug'

cabextract_url='https://cabextract.org.uk/'
innoextract_url='https://constexpr.org/innoextract/'


##########################################################################################
# Standard directories

user_pwd="$PWD"
user_pwd="${user_pwd%/}"
platform="$(uname)"
command="$(basename "$0")"
scommand="$(printf '%s' "$command" | tr - _)"
if [ "$platform" = 'Darwin' ] ; then
	# macOS
	data_dirs='/Applications'
	data_home="$HOME/Library/Application Support"
	config_home="$HOME/Library/Application Support"
	data_dir_suffixes='ArxLibertatis'
	user_dir_suffixes='ArxLibertatis'
	config_dir_suffixes='ArxLibertatis'
	downloads_dir="$HOME/Downloads"
else
	# Linux, FreeBSD, ...
	data_dirs="${XDG_DATA_DIRS:-"/usr/local/share/:/usr/share/"}:/opt"
	data_home="${XDG_DATA_HOME:-"$HOME/.local/share"}"
	config_home="${XDG_CONFIG_HOME:-"$HOME/.config"}"
	data_dir_suffixes='games/arx:arx'
	user_dir_suffixes='arx'
	config_dir_suffixes='arx'
	[ -f "${config_home}/user-dirs.dirs" ] && . "${config_home}/user-dirs.dirs"
	downloads_dir="${XDG_DOWNLOAD_DIR:-"$HOME/Downloads"}"
fi
downloads_dir="${downloads_dir%/}"
tempdir="${TMPDIR:-"/tmp"}"
tempdir="${tempdir%/}"
[ -d "$tempdir" ] || tempdir="$PWD"
eval "data_path=\"\$${scommand}_PATH\""
[ -z "$data_path" ] && data_path="$arx_PATH"


##########################################################################################
# Helper functions

exec 4>&2  # fd to the original stderr (we redirect output to a log file in some cases)
logfile='' # log file receiving sdout and stderr

true=0  # Return value / exit status that evaluates to true
false=1 # Return value / exit status that evaluates to false

# 1 if the script is being run as root, false otherwise
if [ "$(id -u)" = 0 ] ; then is_root=1 ; else is_root=0 ; fi

# Print one line of text, without escape codes or other shell-specific shenanigans.
# Seriously, shells, you can't even agree on a consistent implementation of echo?
# Usage: print <text>
print() {
	printf '%s\n' "$1"
}

puts() {
	printf '%s' "$1"
}

disabled_commands=' ' # List of commands that should not be used, even if they exist

# Make `have` return false for a command
# Usage: disable_command <command>
disable_command() {
	disabled_commands="$disabled_commands$1 "
}

# Check if a command is available.
# Usage: have <command>
# Return: $true if the command is available, $false otherwise
have() {
	case "$disabled_commands" in *" $1 "*) return $false ; esac
	command -v "$1" > /dev/null 2>&1
}

# Make a path absolute no matter if it is relative or not
# Usage: abspath <path>
# Too bad we can't just use readlink -m
abspath() {
	case "$1" in
		/*) print "$1" ;;
		 *) print "$PWD/$1" ;;
	esac
}

# Get the canonical representation of an existing path
# Usage: canonicalize <path>
# Too bad we can't just use readlink -f
if have realpath ; then
	canonicalize() { realpath "$1" ; }
elif have grealpath ; then
	canonicalize() { grealpath "$1" ; }
elif have greadlink ; then
	canonicalize() { greadlink -f "$1" ; }
else
	canonicalize() {
		_canonicalize_old_pwd="$PWD"
		_canonicalize_file="$1"
		while true ; do
			cd "$(dirname "$_canonicalize_file")"
			_canonicalize_file="$(basename "$_canonicalize_file")"
			[ -L "$_canonicalize_file" ] || break;
			_canonicalize_file="$(readlink "$_canonicalize_file")"
		done
		echo "$(pwd -P)/$_canonicalize_file"
		cd "$_canonicalize_old_pwd"
	}
fi

cleanup_functions='' # List of functions to be run on exit

# Add a function to run on exit.
# Functions are run in the order they are added.
# Usage: on_exit <code>
# Cleanup functions will receive one argument: the exit message if any or an empty string.
on_exit() {
	[ -z "$cleanup_functions" ] || cleanup_functions=" $cleanup_functions"
	cleanup_functions="$1$cleanup_functions"
}

# Run exit runctions.
cleanup() {
	_cleanup_functions="$cleanup_functions" ; cleanup_functions=''
	[ -z "$_cleanup_functions" ] && return
	eval "for _cleanup_func in $_cleanup_functions ; do \"\$_cleanup_func\" \"\$@\" ; done"
}

# Register our cleanup handler.
trap "cleanup" EXIT
# Some shells don't have their own (non-libc) SIGINT handler, but the EXIT trap
# won't trigger if there is none!
trap 'print >&4 ; quit 1' INT

# Run cleanup functions with a possible message and then exit.
# Usage: quit <status> [<message>]
quit() {
	cleanup "$2"
	exit $1
}

# Exit with a non-zero status and optionally print a message.
# Usage: die [<message>...]
die() {
	_die_message=''
	if [ $# -gt 0 ] ; then
		_die_message="$1" ; shift
		for _die_arg ; do _die_message="$_die_message $_die_arg" ; done
		_die_message="$_die_message

If you think this is a bug in the install script
please report the complete output at
  $bug_report_url"
		if [ -n "$logfile" ] && [ -f "$logfile" ] ; then
			_die_message="$_die_message

Also attach the contents of
  $logfile"
			logfile='' # so that we don't remove it on exit
			printf "${red}%s${reset}\\n" "$_die_message" >&4 # also print to priginal stdout
			printf '\n%s\n' 'Preserving log file.' >&4
		fi
	
		printf "${red}%s${reset}\\n" "$_die_message"
	fi
	quit 1 "$_die_message"
}

# Escape a string from stdin for use in a whitespace-separated list.
# Usage: print <string> | escape_pipe
escape_pipe() {
	sed "s:[^a-zA-Z0-9/_.$1]:\\\\&:g"
}

# Escape a string for use in a whitespace-separated list.
# Usage: escape <string>
escape() {
	print "$1" | escape_pipe "$2"
}

# Convert a colon-separated list into an escaped whitespace-separated list.
# Usage: to_list <colon-list>
to_list() {
	escape "$1" | sed 's/\\:/ /g'
}

# Line-based output into a list
# Usage: ls | lines_to_list
lines_to_list() {
	escape_pipe | tr '\n' ' '
}

# Check if a whitespace separated list contains a string.
# Usage: list_contains <list-var> <needle>
list_contains() {
	eval "_list_contents=\"\$$1\""
	[ -z "$_list_contents" ] && return $false
	eval "for _list_contains_entry in $_list_contents ; do" \
		" [ \"\$_list_contains_entry\" = \"\$2\" ] && return \$true ; done"
	return $false
}

# Append a string to a whitespace separated list.
# Usage: list_append <list-var> <string> [comment]
# Whitespace separated lists can be loaded into the argument list using:
#  eval "set -- $var"
list_append() {
	_list_entry="$(escape "$2")"
	eval "_list_contents=\"\$$1\""
	if [ -z "$_list_contents" ]
		then eval "$1=\"\$_list_entry\""
		else eval "$1=\"\$_list_contents \$_list_entry\""
	fi
	eval "[ -z \"\$$1__list_count\" ] && $1__list_count=0"
	eval "_list_count=\$$1__list_count"
	eval "$1__list_comment_$_list_count=\"\$3\""
	eval "$1__list_count=\$(($1__list_count + 1))"
}

# Append one list to another, preserving comments.
# Usage: list_merge <list-var> <append-list-var>
list_merge() {
	eval "_list_append=\"\$$2\""
	[ -z "$_list_append" ] && return
	eval "
		_list_merge_i=0
		for _list_merge_entry in $_list_append ; do
			list_append $1 \"\$_list_merge_entry\" \"\$(list_comment $2 \$_list_merge_i)\"
			_list_merge_i=\$((_list_merge_i + 1))
		done
	"
}

# Get a comment associated with alist entry
# Usage: list_comment <list-var> <index>
list_comment() {
	eval "print \"\$$1__list_comment_$2\""
}

# Set a comment associated with alist entry
# Usage: list_comment <list-var> <index> <comment>
set_list_comment() {
	eval "$1__list_comment_$2=\"\$3\""
}

# Append a string to a whitespace separated list if it isn't already in the list.
# Usage: set_append <list-var> <string> [comment]
set_append() {
	if ! list_contains "$1" "$2" ; then
		list_append "$1" "$2" "$3"
	fi
}

# Check if a directory contains a file while ignoring case differences.
# Usage: icontains <dir> <filename>
icontains() {
	[ -n "$(find "$1" -mindepth 1 -maxdepth 1 -iname "$2")" ]
}

# Check if a directory or file is writable or can be created.
# Usage: is_writable <path>
is_writable() {
	[ -w "$1" ] && return $true
	[ ! -e "$1" ] && is_writable "$(dirname "$1")"
}

# Create a directory and die with a message on error.
# Usage: create_dir <path> <type>
create_dir() {
	mkdir -p "$1" || die "Could not create $2 directory: $1"
}

probe_file_dirs=''
set_append probe_file_dirs "$user_pwd"
set_append probe_file_dirs "$downloads_dir"
set_append probe_file_dirs "$HOME"
set_append probe_file_dirs "$tempdir"

# Find a file in standard directories.
# Usage: probe_file <command> <filename> [comment]
# Will call `command <file>` for each file found.
probe_file() {
	eval "for _probe_file_d in $probe_file_dirs ; do [ -f \"\$_probe_file_d/\$2\" ] && \$1 \"\$_probe_file_d/\$2\" \"\$3\" && return \$true ; done"
}

# Find files in standard directories.
# Usage: probe_file <command> <list> [comment]
# Will call `command <file>` for each file found.
probe_files() {
	[ -z "$2" ] && return $false
	eval "for _probe_files_file in $2 ; do probe_file \"\$1\" \"\$_probe_files_file\" \"\$3\" && return \$true ; done"
	return $false
}


##########################################################################################
# Parse command-line arguments

extract_zip_reqs=''
list_append extract_zip_reqs 'bsdtar' 'libarchive'
list_append extract_zip_reqs 'unzip'
list_append extract_zip_reqs '7za'
list_append extract_zip_reqs '7z' 'p7zip'
extract_ms_cab_reqs=''
list_append extract_ms_cab_reqs 'bsdtar' 'with libarchive 3.1+'
list_append extract_ms_cab_reqs 'cabextract' "$cabextract_url"
list_append extract_ms_cab_reqs '7za'
list_append extract_ms_cab_reqs '7z' 'p7zip'
extract_installshield_reqs=''
list_append extract_installshield_reqs 'unshield'
extract_rar_reqs=''
list_append extract_rar_reqs 'unrar'
extract_ace_reqs=''
list_append extract_ace_reqs 'unace'
extract_innosetup_reqs=''
list_append extract_innosetup_reqs 'innoextract' "$innoextract_url"
mount_cdrom_reqs=''
list_append mount_cdrom_reqs 'fuseiso'
extract_iso_reqs=''
list_append extract_iso_reqs 'isoinfo'
list_append extract_iso_reqs 'bsdtar' 'libarchive'
list_append extract_iso_reqs '7z' 'p7zip'
extract_cdrom_reqs=''
list_merge extract_cdrom_reqs mount_cdrom_reqs
list_merge extract_cdrom_reqs extract_iso_reqs
download_reqs=''
list_append download_reqs 'wget'
list_append download_reqs 'curl'
list_append download_reqs 'fetch' 'FreeBSD'

printf '%s %s\n' "${white}Welome to the ${green}Arx Fatalis${white} ${patch_ver} data" \
     "install script for UNIX-like systems!${reset}"

patchfile=''      # Main patch file
patchfile_jp=''   # Japanese patch file
sourcefile=''     # Source file or directory
datadir=''        # Output data directory
batch=0           # Never wait for user input
gui=0             # Display a graphical user interface (command-line interface otherwise)
install=1         # Install new non-patch files
selected_stuff=0  # Has the user made a selection
installed_stuff=0 # Have we already installed anything?
patch=1           # Install patch files if needed
probe_patch=1     # Look for patch files in standard locations and download if needed
redirect_log=1    # Redirect standard output/error output to a log file in GUI mode

# Enable compatibility with old install-* scripts.
# Usage: enable_compat_mode <help-flag> <sourcefile> <patchfile> <datadir>
enable_compat_mode() {
	print \
		"${yellow}Enabling compatibility mode for ${pink}$command${yellow}.${reset}

${dim_yellow}The individual ${dim_pink}install-*${dim_yellow} scripts have been merged.
Rename this script to something else (like ${dim_pink}arx-install-data${dim_yellow}) to unlock its full power!${reset}
" >&2
	batch=1
	probe_patch=0
	if [ -z "$1" ] || [ "$1" = '--help' ] || [ "$1" = '-h' ] ; then
		printf '%s\n\n%s\n' "$5" \
			"${yellow}More options are available in the non-compatibility mode.${reset}"
		exit $false
	fi
	if [ -z "$2" ] ; then install=0           ; else sourcefile="$2" ; fi
	if [ -z "$3" ] ; then patch=0             ; else patchfile="$3"  ; fi
	if [ -z "$4" ] ; then datadir="$user_pwd" ; else datadir="$4"    ; fi
}

case "$command" in

install-cd)
[ "$1" = "--no-progress" ] && shift # ignore - not supported
enable_compat_mode "$1" "$1" "$2" "$3" "\
Usage: $command path/to/mount/point/ path/to/ArxFatalis_1.21_MULTILANG.exe [output_dir]
or     $command path/to/cd.iso path/to/ArxFatalis_1.21_MULTILANG.exe [output_dir]" ;;

install-copy)
enable_compat_mode "$1" "$1" '' "$2" "\
Usage: $command path/to/ArxFatalis/ [output_dir]" ;;

install-demo)
enable_compat_mode "$1" "$1" '' "$2" "\
Usage: $command path/to/arx_demo_english.zip [output_dir]" ;;

install-gog)
[ "$1" = "--no-progress" ] && shift # ignore - not supported
enable_compat_mode "$1" "$1" '' "$2" "\
Usage: $command path/to/setup_arx_fatalis.exe [output_dir]" ;;

install-verify)
enable_compat_mode "$1" '' '' "$1" "\
Usage: $command [directory]" ;;

*) # non-compatibility mode

# Print elements in a list, joined by ' or '
# Usage: print_help_or <list-var> [color]
print_help_or() {
	_print_help_or_var=$1
	eval "_print_help_or_list=\"\$$1\""
	_print_help_or_color="$2"
	[ -z "$1" ] && return
	eval "
		_print_help_or_i=0
		for _print_help_or_entry in $_print_help_or_list ; do
			[ \$_print_help_or_i = 0 ] || puts ' or '
			printf '%s%s' \"\$_print_help_or_color\" \"\$_print_help_or_entry\"
			[ -z \"\$_print_help_or_color\" ] || puts \"\$reset\"
			_print_help_or_comment=\"\$(list_comment \$_print_help_or_var \$_print_help_or_i)\"
			[ -z \"\$_print_help_or_comment\" ] || printf ' (%s)' \"\$_print_help_or_comment\"
			_print_help_or_i=\$((_print_help_or_i + 1))
		done
	"
}

# Print elements in a list, one per line.
# Usage: print_help_list <prefix-format> <list-var>
# prefi-format will receive one argument: the list index starting at 1
print_help_list() {
	_print_help_list_prefix="$1"
	_print_help_list_var=$2
	eval "_print_help_list_list=\"\$$2\""
	[ -z "$1" ] && return
	eval "
		_print_help_list_i=0
		for _print_help_list_entry in $_print_help_list_list ; do
			case \"\$_print_help_list_prefix\" in
				*%*) printf \"\$_print_help_list_prefix\" \$((_print_help_list_i + 1)) ;;
				*)   puts \"\$_print_help_list_prefix\"
			esac
			printf \"%s\${reset}\" \"\$_print_help_list_entry\"
			_print_help_list_comment=\"\$(list_comment \$_print_help_list_var \$_print_help_list_i)\"
			[ -z \"\$_print_help_list_comment\" ] || printf ' (%s)' \"\$_print_help_list_comment\"
			printf '\n'
			_print_help_list_i=\$((_print_help_list_i + 1))
		done
	"
}

# Print help output.
# Usage: print_help [<error-message>]
print_help() {
	[ -z "${1-}" ] || ( printf '%s\n\n' "${red}$1${reset}" )
	print "
${white}Start the script without any arguments to select paths interactively:
       \$ $command${reset}

Usage: $command [--source] source [--patch patchfile] [[--data-dir] datadir]
       $command [--patch patchfile] [--data-dir datadir]
       $command --verify [[--data-dir] datadir]

 ${green}-s, --source PATH${reset}   Path to the source file or directory
 ${cyan}-d, --data-dir DIR${reset}  Where to install the data
 ${blue}-p, --patch FILE${reset}    Path to the ${patch_ver} patch file
 --patch-jp FILE     Path to the ${patch_jp_ver} Japanese patch file
 -v, --verify        Only verify the files in the data-dir, don't install new ones,
                     except for patch files.
 -n, --no-patch      Don't use a patch file unless explicitly specified.
 -h, --help          Print this message and maybe more
 -b, --batch         Never ask the user questions
 -g, --gui           Show a GUI asking the user what to do
                     Requires ${dim_pink}KDialog${reset}, ${dim_pink}Zenity${reset}, or ${dim_pink}Xdialog${reset}.
                     If none of them are available the script is re-launched
                     in a terminal emulator.
 -c, --cli           Interactively ask the user to select files/directories (no GUI)
 --no-redirect-log   Don't redirect output to a log file when in GUI mode
 --disable-COMMAND   Don't use the given tool, even if it exists.
                     Valid values are 7z, 7za, aterm, bsdtar, cabextract, curl,
                     dcop, fetch, fuseiso, fusermount, gnome-terminal, greadlink,
                     grealpath, gtkterm, gxmessage, innoextract, isoinfo, kdialog,
                     konsole, md5, md5sum, mount, qdbus, realpath, rxvt, umount,
                     unace, unrar, unshield, unzip, urxvt, wget, Xdialog,
                     xmessage, xterm, x-terminal-emulator, zenity.

--gui is enabled by default if there are no arguments *and* stdin, stdout or stderr is not a terminal
"
	[ -n "${1-}" ] && exit $false
	help_innosetup="$(print_help_or extract_innosetup_reqs "$dim_pink")"
	help_cdrom="$(print_help_or extract_cdrom_reqs "$dim_pink") or root access"
	help_cab="$(print_help_or extract_ms_cab_reqs "$dim_pink")"
	help_zip="$(print_help_or extract_zip_reqs "$dim_pink")"
	help_unshield="$(print_help_or extract_installshield_reqs "$dim_pink")"
	help_rar="$(print_help_or extract_rar_reqs "$dim_pink")"
	help_ace="$(print_help_or extract_ace_reqs "$dim_pink")"
	help_download="$(print_help_or download_reqs "$dim_pink")"
	help_optpatch="may use the 1.21 patch file and require ${help_innosetup} if not already patched"
	help_probe_file_dirs="
   a) the current working directory  (\$PWD):              $user_pwd
   b) the user's downloads directory (\$XDG_DOWNLOAD_DIR): $downloads_dir
   c) the user's home directory      (\$HOME):             $HOME
   d) the temp directory             (\$TMPDIR):           $tempdir"
  help_probe_file_dirs_patch="${help_probe_file_dirs}
   e) the directory containing the source file"
  help_probed_files="$gog_names $demo_names"
	print "
The ${pink}dependencies${reset} required by the ${command} script depend on the source files.
However, you always need either ${dim_pink}md5sum${reset} or ${dim_pink}md5${reset}.


The ${green}source${reset} can be one of many things:

 * ${white}Mounted Arx Fatalis ${green}cdrom${reset}
   requires:
    - ${help_cab}
    - ${help_innosetup}
   needs the 1.21 patch file

 * ${white}Arx Fatalis cdrom ${green}ISO${white} image / device file${reset}
   requires:
    - ${help_cdrom}
    - ${help_cab}
    - ${help_innosetup}
   needs the 1.21 patch file

 * ${white}Arx Fatalis installer from ${green}GOG.com${white}${reset} ($(print_help_or gog_names))
   requires:
    - ${help_innosetup}
   never uses the 1.21 patch file
   get it from ${dim_green}${gog_url}${reset}

 * ${green}Installed${white} copy of Arx Fatalis${reset} (for example from ${green}Steam${reset})
   ${help_optpatch}
   get it from ${dim_green}${steam_url}${reset}

 * ${white}Arx Fatalis ${green}demo${reset} (one of the following)
$(print_help_list "    - " demo_names)
   requires:
    - ${help_zip} [english/french .zip]
    - ${help_cab}
    - ${help_rar} [english .exe]
    - ${help_ace} [german .exe]
    - ${help_unshield} [japanese .exe]
   never uses the 1.21 patch file
   get it from ${dim_green}${demo_url}${reset}

 * ${white}Extracted Arx Fatalis demo installer${reset}
   requires:
    - ${help_cab}
   never uses the 1.21 patch file

 * ${white}Installed copy of the Arx Fatalis demo${reset}
   never uses the 1.21 patch file

If no source is specified, these files will be probed:
1. The following files in${help_probe_file_dirs}
$(print_help_list "   1.%d ${green}" help_probed_files)
2. The user's ${green}Steam${reset} library, if available
3. If \$WINEPREFIX is set, any installation in there
4. Any installation in the default WINEPREFIX (${green}~/.wine${reset})
5. Any mounted ${green}cdrom${reset} or ISO file


If no ${blue}patch${reset} file is specified, but is needed and
the --no-patch option wasn't specified:
1. Try to find the following files in${help_probe_file_dirs_patch}
   1.1. ${blue}${patch_name}${reset}
   1.2. $(printf "$patch_name_localized" '<LANG>')
        Where <LANG> is one of EN, ES, FR, GE, IT, RU,
        depending on the language of the data files.
2. Downloaded from:
$(print_help_list " - ${dim_blue}" patch_urls)
Downloading the patch file requires ${help_download}.
Extracting the ${patch_ver} patch file requires ${help_innosetup}.

For the Japanese version, if no ${blue}patch-jp${reset} file is specified,
but is needed and the --no-patch option wasn't specified specified:
1. Try to find ${blue}${patch_jp_name}${reset} in${help_probe_file_dirs_patch}
2. Downloaded from:
$(print_help_list " - ${dim_blue}" patch_jp_urls)
Downloading the patch file requires ${help_download}.
Extracting the Japanese patch file requires:
    - ${help_unshield}
    - ${help_cab}.


If no ${cyan}data-dir${reset} to install into is specified,
one is automatically selected similarly to how Arx Libertatis would:
If --verify and --no-patch are give, use the first existing directory
of the following, otherwise, use the first existing writable directory
or, if none exists, the first directory that can be created:
1. Any path in \$${scommand}_PATH or \$arx_PATH (for use in wrapper scripts)
2. \"\${XDG_DATA_DIRS:-\"/usr/local/share/:/usr/share/\"}:/opt\" / \"$data_dir_suffixes\":"
	i=1
	eval "set -- $(to_list "$data_dirs")"
	for prefix in "$@" ; do
		eval "set -- $(to_list "$data_dir_suffixes")"
		for suffix ; do
			printf "   2.%d. ${dim_cyan}%s${reset}\\n" $i "$prefix/$suffix"
			i=$((i + 1))
		done
	done
print "3. \"\${XDG_DATA_HOME:-\"\$HOME/.local/share\"}\" / \"$user_dir_suffixes\""
	i=1
	eval "set -- $(to_list "$user_dir_suffixes")"
	for suffix ; do
		printf "   3.%d. ${dim_cyan}%s${reset}\\n" $i "$data_home/$suffix"
			i=$((i + 1))
	done
	print
	exit $true
}

user_is_sane=1
if [ ! -t 0 ] || [ ! -t 1 ] || [ ! -t 2 ] ; then
	[ $# = 0 ] && gui=1
fi
while [ $# -gt 0 ] ; do
	case "$1" in
		--source=*)               sourcefile="${1#--source=}"   ; install=1 ;;
		-s|--source)      shift ; sourcefile="$1"               ; install=1 ;;
		--data-dir=*)             datadir="${1#--data-dir=}"                ;;
		-d|--data-dir)    shift ; datadir="$1"                              ;;
		--patch=*)                patchfile="${1#--patch=}"       ; patch=1 ;;
		-p|--patch)       shift ; patchfile="$1"                  ; patch=1 ;;
		--patch-jp=*)             patchfile_jp="${1#--patch-jp=}" ; patch=1 ;;
		--patch-jp)       shift ; patchfile_jp="$1"               ; patch=1 ;;
		-v|--verify)                                      install=0 patch=0 ;;
		-n|--no-patch)    [ -z "$patchfile" ] && [ -z "$patchfile_jp" ] && patch=0
		                  probe_patch=0   ;;
		-b|--batch)       batch=1         ;;
		-g|--gui)                   gui=1 ;;
		-c|--cui|--cli)   batch=0 ; gui=0 ;;
		--no-redirect-log) redirect_log=0 ;;
		--i-am-insane)    user_is_sane=0  ;;
		--disable-*)              disable_command "${1#--disable-}" ;;
		--disable)        shift ; disable_command "${1#--disable-}" ;;
		-h|--help)        print_help                     ;;
		-*)               print_help "Unknown option: $1" ;;
		*)
			if [ -z "${sourcefile-}" ] && [ $install = 1 ] ; then sourcefile="$1"
			elif [ -z "${datadir-}"    ] ; then datadir="$1"
			else print_help "Too many options: $1" ; fi
	esac
	[ -z "${1-}" ] && print_help "Expected more options"
	shift;
done

print "See \`${dim_pink}$command --help${reset}\` for available options."

esac

# Make user-provided paths absolute
[ -n "$sourcefile" ] && sourcefile="$(abspath "$sourcefile")"
[ -n "$datadir" ]    && datadir="$(abspath "$datadir")"
[ -n "$patchfile" ]  && patchfile="$(abspath "$patchfile")"

# Sanity check
[ $install = 1 ] && [ $batch = 1 ] && [ -z "$sourcefile" ] && [ $user_is_sane = 1 ] \
	&& die "You have used --batch without providing a source file!
This would just pick the first source file found, which is a bad idea™.
If you really want this, add the --i-am-insane option."


##########################################################################################
# User interface abstraction

_dialog_title="Arx Fatalis ${patch_ver} data installer"

# Handle magic environment variable to tell the script that it has been launched
# in its own terminal and should not try to create a GUI.
if [ $batch = 0 ] && [ "$_arx_install_data_force_cli" = 1 ] ; then
	trap '_arx_install_data_force_cli=0 ; quit 1' INT
	printf "\n${yellow}%s${reset}\n\n" \
		'Note: Install KDialog, Zenity or Xdialog for a better GUI'
	wait_exit() {
		[ "$_arx_install_data_force_cli" = 1 ] && print 'Press enter to exit...' && read f
		exit $false
	}
	on_exit wait_exit
	gui=0
	for var in batch install patch probe_patch sourcefile datadir \
		patchfile patchfile_jp disabled_commands; do
		eval "$var=\"\$_arx_install_data_force_$var\""
	done
fi

# Select the dialog backend to use
if [ $gui = 1 ] ; then
	
	# Detect if we are running in a KDE session
	is_kde=0
	case "$DESKTOP_SESSION" in *kde*|*KDE*) is_kde=1 ; esac
	[ -z "$KDE_FULL_SESSION" ]           || is_kde=1
	[ -z "$KDE_SESSION_UID" ]            || is_kde=1
	[ -z "$KDE_SESSION_VERSION" ]        || is_kde=1
	
	# Select the GUI backend, prefer kdialog for KDE sessions, zenity otherwise
	if [ $is_kde = 1 ] ; then preferred=kdialog ; else preferred=zenity ; fi
	for backend in $preferred zenity kdialog Xdialog ; do
		have $backend && gui=$backend && break
	done
	
	if [ $gui = 1 ] ; then
		
		# No dialog backend available
		# Try opening a graphical terminal and launching the script in there.
		print 'No GUI dialog backend is available - trying to launch a terminal emulator'
		term_cmd="$(abspath "$(command -v "$0" 2> /dev/null)")"
		# Not all terminals accept command arguments in the same way.
		# Instead of hacking terminal-specific code, use a magic environment
		# variable to tell the sub-process how to behave.
		_arx_install_data_force_cli=1
		export _arx_install_data_force_cli
		for var in batch install patch probe_patch sourcefile datadir \
			patchfile patchfile_jp disabled_commands ; do
			eval "_arx_install_data_force_$var=\"\$$var\""
			eval "export _arx_install_data_force_$var"
		done
		if [ $is_kde = 1 ] ; then preferred=konsole ; else preferred=x-terminal-emulator ; fi
		for backend in x-terminal-emulator $preferred \
			aterm urxvt rxvt konsole xterm gnome-terminal
		do
			if have $backend ; then
				$backend -e "$term_cmd" || continue
				exit $true
			fi
		done
		
		# Hm, that didn't work either - bail
		message="No GUI dialog backend is available"
		message="$message - install KDialog, Zenity or Xdialog, or use the --cli option."
		# Final attempt to let the user know what happened
		for backend in gxmessage xmessage ; do
			if have $backend ; then
				$backend -center -buttons OK "$_dialog_title

$message"
				break
			fi
		done
		die "$message"
		
	fi
	
	# We don't need colors for the UI, but they may cause problems - get rid of them
	disable_color
	
	if [ $redirect_log = 1 ] ; then
		# Redirect all further output into a log file
		logfile="$(abspath "$(mktemp "$tempdir/arx-install-data.log.XXXXX")")"
		clean_logfile() {
			[ -z "$logfile" ] || rm -f "$logfile"
		}
		on_exit clean_logfile
		print "Enabling GUI mode, standard output/error saved to $logfile"
		print "Use the --cli option for an interactive command-line interface."
		exec > "$logfile" 2>&1
	else
		print "Enabling GUI mode..."
		print "Use the --cli option for an interactive command-line interface."
	fi
	
elif [ ! "$_arx_install_data_force_cli" = 1 ] ; then
	
	print "Enabling CLI mode, use the --gui option for a graphical interface."
	
fi

#----------------------------------------------------------------------------------------#
# Functions for controlling an asynchronous process via stdin

pipe_file=''
pipe_pid=0

# Run a command in the background and open a pipe to pass commands to it
# Usage: pipe_create <command> [<args>...]
pipe_create() {
	
	pipe_destroy
	
	# Pipe commands via a FIFO or, if that fails, via a regular file
	pipe_file="$(mktemp -u "$tempdir/arx-install-data.pipe.XXXXX")"
	if mkfifo -m 600 "$pipe_file" 2> /dev/null ; then
		# Fast communication via a FIFO
		"$@" < "$pipe_file" &
		pipe_pid="$!"
	else
		# Fallback via regular file, may use polling
		pipe_file="$(mktemp "$tempdir/arx-install-data.pipe.XXXXX")"
		[ -z "$pipe_file" ] && return $false
		tail -f "$pipe_file" | "$@" &
		pipe_pid="$!"
	fi
	
	# Open fd 3 for writing into the pipe
	exec 3> "$pipe_file"
	
	return $true
}

# Kill the program created via pipe_create and cleanup files
# Usage: pipe_destroy
pipe_destroy() {
	
	# Close fd pointing to the pipe
	exec 3<&-
	
	# Remove the FIFO or temp file
	[ -z "$pipe_file" ] || rm -f "$pipe_file" > /dev/null 2>&1
	pipe_file=''
	
	# Terminate the remote process
	[ "$pipe_pid" = 0 ] || kill "$pipe_pid" > /dev/null 2>&1
	pipe_pid=0
	
	return $true
}

# Check if the remote process is running
# Usage: pipe_exists || print 'oh noes'
pipe_exists() {
	[ -z "$pipe_file" ] && return $false
	[ "$pipe_pid" = 0 ] && return $false
	kill -s 0 "$pipe_pid" > /dev/null 2>&1
}

# Send a message to the remote process
# Usage: pipe_write <command>
pipe_write() {
	[ -z "$pipe_file" ] || print "$1" >&3
}

#----------------------------------------------------------------------------------------#
# Code for the different GUI/CLI implementations
# Each implementation exposes dialog_* primitives that are used by generic functions.

case $gui in

#----------------------------------------------------------------------------------------#
zenity)

# Helper functions

# Run Zenity
# Usage: zenity_run <title-prefix> <dialog-type> [<args>...]
zenity_run() {
	_zenity_run_t="$1" ; shift
	[ -z "$_zenity_run_t" ] || _zenity_run_t="$_zenity_run_t - "
	zenity --title "$_zenity_run_t$_dialog_title" "$@"
}

# Dialog abstraction

# Create the main progress window.
# Usage: dialog_create
dialog_create() {
	pipe_create zenity --title "$_dialog_title" --width 450 --progress
}

# Destroy the main progress window.
# Usage: dialog_destroy
dialog_destroy() {
	pipe_destroy
}

# Show an error dialog.
# Usage: dialog_error <message>
dialog_error() {
	zenity_run 'Error' --error --no-wrap --text="$1"
}

# Show a message box.
# Usage: dialog_message <message>
dialog_message() {
	zenity_run 'Status' --info --no-wrap --text="$1"
}

# Ask a yes/no question.
# Usage: dialog_ask <question>
dialog_ask() {
	zenity_run 'Confirm' --question --no-wrap --ok-label=Yes --cancel-label=No --text="$1"
}

# Has the user quested to cancel the operation?
# Usage: dialog_cancelled && print "cancelled"
dialog_cancelled() {
	! pipe_exists
}

# Set the status text.
# Usage: dialog_set_text <text>
dialog_set_text() {
	pipe_write "#$1"
}

# Set if the progress bar should continuously animate instead of showing the value.
# Usage: dialog_set_pulsate <enable>
dialog_set_pulsate() {
	if [ $1 = 1 ]
		then pipe_write "pulsate:true"
		else pipe_write "pulsate:false"
	fi
}

# Set the current progress value.
# Usage: dialog_set_value <percentage>
dialog_set_value() {
	pipe_write "$1"
}

# Select an entry in a list.
# Usage dialog_select_entry <var> <label> <tag1> <item1> [ <tag2> < item2> ... ]
dialog_select_entry() {
	_zenity_select_entry_v="$1" ; shift
	_zenity_select_entry_t="$1" ; shift
	_zenity_select_entry_r="$(
		zenity_run 'Select path' --width 550 --height 300 \
			--list --text="$_zenity_select_entry_t" \
			--column '#'  --column 'Path' --hide-column=1 "$@" --hide-header
	)"
	[ -z "$_zenity_select_entry_r" ] && return $false
	eval "$_zenity_select_entry_v=\"\$_zenity_select_entry_r\""
	return $true
}

# dialog_select_path does not support the --any flag
dialog_select_path_any=0

# Let the user select a path.
# Usage: dialog_select_path (--file|--dir|--any) <result-var> <label>
# Any is only supported if $dialog_select_path_any is 1.
dialog_select_path() {
	case "$1" in
		--any) die 'not implemented' ;;
		--file) _zenity_select_path_f='--file-selection' ;;
		--dir)  _zenity_select_path_f='--file-selection --directory' ;;
	esac
	_zenity_select_path="$(
		eval "zenity_run \"\$3\" $_zenity_select_path_f" 2> /dev/null
	)"
	[ -z "$_zenity_select_path" ] && return $false
	eval "$2=\"\$_zenity_select_path\""
	return $true
}

dialog_retry() {
	zenity_run 'Error' --question --no-wrap --text="$1" \
		--ok-label='Retry' --cancel-label='Ignore'
	case $? in
		0) dialog_retry_choice='retry' ;;
		1) dialog_retry_choice='ignore' ;;
		*) dialog_retry_choice='abort' ;;
	esac
}

;;

#----------------------------------------------------------------------------------------#
kdialog)

# Helper functions

kdialog_handle='' # dbus/dcop handle for the main progress window

# Send a message to the main KDialog instance non-_q variants hide all output
kdialog_qdbus_q() { have qdbus && eval "qdbus $kdialog_handle \"\$@\"" 2> /dev/null ; }
kdialog_qdbus()   { kdialog_qdbus_q "$@" > /dev/null                                ; }
kdialog_dcop_q()  { have dcop  && eval "dcop  $kdialog_handle \"\$@\"" 2> /dev/null ; }
kdialog_dcop()    { kdialog_dcop_q  "$@" > /dev/null                                ; }
kdialog_cmd_q()   { kdialog_qdbus_q "$@" || kdialog_dcop_q "$@"                     ; }
kdialog_cmd()     { kdialog_cmd_q "$@" > /dev/null                                  ; }

# Run KDialog
# Usage: kdialog_run <title-prefix> <dialog-type> [<args>...]
kdialog_run() {
	_kdialog_run_t="$1" ; shift
	[ -z "$_kdialog_run_t" ] || _kdialog_run_t="$_kdialog_run_t - "
	kdialog --icon arx-libertatis --title "$_kdialog_run_t$_dialog_title" "$@"
}

# Dialog abstraction

# Create the main progress window.
# Usage: dialog_create
dialog_create() {
	dialog_destroy
	_kdialog_force_width='WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW'
	kdialog_handle="$(kdialog_run '' --progressbar "$_kdialog_force_width" 0)"
	[ -z "$kdialog_handle" ] && return $false
	kdialog_cmd showCancelButton true
	return $true
}

# Destroy the main progress window.
# Usage: dialog_destroy
dialog_destroy() {
	[ -z "$kdialog_handle" ] && return $true
	kdialog_cmd close
	kdialog_handle=''
}

# Show an error dialog.
# Usage: dialog_error <message>
dialog_error() {
	kdialog_run 'Error' --error "$1" > /dev/null 2> /dev/null
}

# Show a message box.
# Usage: dialog_message <message>
dialog_message() {
	kdialog_run 'Status' --msgbox "$1" > /dev/null 2> /dev/null
}

# Ask a yes/no question.
# Usage: dialog_ask <question>
dialog_ask() {
	kdialog_run 'Confirm' --warningyesno "$1" > /dev/null 2> /dev/null
}

# Has the user quested to cancel the operation?
# Usage: dialog_cancelled && print "cancelled"
dialog_cancelled() {
	[ "$(kdialog_cmd_q wasCancelled || print true)" = true ]
}

# Set the status text.
# Usage: dialog_set_text <text>
dialog_set_text() {
	kdialog_qdbus setLabelText "$1" || kdialog_dcop setLabel "$1"
}

# Set if the progress bar should continuously animate instead of showing the value.
# Usage: dialog_set_pulsate <enable>
dialog_set_pulsate() {
	_kdialog_max=100
	[ $1 = 1 ] && _kdialog_max=0
	kdialog_qdbus Set "" maximum $_kdialog_max || kdialog_dcop setMaximum $_kdialog_max
}

# Set the current progress value.
# Usage: dialog_set_value <percentage>
dialog_set_value() {
	kdialog_qdbus Set "" value "$1" || kdialog_dcop setProgress "$1"
	[ $1 = 100 ] && kdialog_cmd showCancelButton true
}

# Select an entry in a list.
# Usage dialog_select_entry <var> <label> <tag1> <item1> [ <tag2> < item2> ... ]
dialog_select_entry() {
	_kdialog_select_entry_v="$1" ; shift
	_kdialog_select_entry_t="$1" ; shift
	_kdialog_select_entry_w="                                   "
	_kdialog_select_entry_w="$_kdialog_select_entry_w$_kdialog_select_entry_w"
	_kdialog_select_entry_r="$(
		kdialog_run 'Select path' \
			--menu "$_kdialog_select_entry_t$_kdialog_select_entry_w" "$@" 2> /dev/null
	)"
	[ -z "$_kdialog_select_entry_r" ] && return $false
	eval "$_kdialog_select_entry_v=\"\$_kdialog_select_entry_r\""
	return $true
}

# dialog_select_path does not support the --any flag
dialog_select_path_any=0

# Let the user select a path.
# Usage: dialog_select_path (--file|--dir|--any) <result-var> <label>
# Any is only supported if $dialog_select_path_any is 1.
dialog_select_path() {
	case "$1" in
		--any) die 'not implemented' ;;
		--file) _kdialog_select_path_f=--getopenfilename ;;
		--dir)  _kdialog_select_path_f=--getexistingdirectory ;;
	esac
	_kdialog_select_path="$(
		kdialog_run "$3" $_kdialog_select_path_f "$HOME" 2> /dev/null
	)"
	[ -z "$_kdialog_select_path" ] && return $false
	eval "$2=\"\$_kdialog_select_path\""
	return $true
}

dialog_retry() {
	kdialog_run 'Error' \
		--yes-label 'Retry' --no-label 'Ignore' --cancel-label 'Abort' \
		--warningyesnocancel "$1" 2>&1
	case $? in
		0) dialog_retry_choice='retry' ;;
		1) dialog_retry_choice='ignore' ;;
		*) dialog_retry_choice='abort' ;;
	esac
}

;;

#----------------------------------------------------------------------------------------#
Xdialog)

# Helper functions

# Run Xdialog
# Usage: Xdialog_run <title-prefix> <dialog-type> [<args>...]
Xdialog_run() {
	_Xdialog_run_t="$1" ; shift
	[ -z "$_Xdialog_run_t" ] || _Xdialog_run_t="$_Xdialog_run_t - "
	Xdialog --left --title "$_Xdialog_run_t$_dialog_title" "$@"
}

# Dialog abstraction

# Create the main progress window.
# Usage: dialog_create
dialog_create() {
	_Xdialog_width='WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW'
	pipe_create Xdialog --left --title "$_dialog_title" --gauge "$_Xdialog_width" 0 0
}

# Destroy the main progress window.
# Usage: dialog_destroy
dialog_destroy() {
	pipe_destroy
}

# Show an error dialog.
# Usage: dialog_error <message>
dialog_error() {
	dialog_message "$1" # no dedicated error box for Xdialog
}

# Show a message box.
# Usage: dialog_message <message>
dialog_message() {
	Xdialog_run 'Status' --msgbox "$1" 0 0
}

# Ask a yes/no question.
# Usage: dialog_ask <question>
dialog_ask() {
	Xdialog_run 'Confirm' --yesno "$1" 0 0
}

# Has the user quested to cancel the operation?
# Usage: dialog_cancelled && print "cancelled"
dialog_cancelled() {
	! pipe_exists
}

# Set the status text.
# Usage: dialog_set_text <text>
dialog_set_text() {
	pipe_write 'XXX'
	pipe_write "$1"
	pipe_write 'XXX'
}

# Set if the progress bar should continuously animate instead of showing the value.
# Usage: dialog_set_pulsate <enable>
dialog_set_pulsate() {
	true # Pulsate is not supported by Xdialog
}

# Set the current progress value.
# Usage: dialog_set_value <percentage>
dialog_set_value() {
	pipe_write "$1"
}

# Select an entry in a list.
# Usage dialog_select_entry <var> <label> <tag1> <item1> [ <tag2> < item2> ... ]
dialog_select_entry() {
	_Xdialog_select_entry_v="$1" ; shift
	_Xdialog_select_entry_t="$1" ; shift
	_Xdialog_select_entry_r="$(
		Xdialog_run 'Select path' \
			--menubox "$_Xdialog_select_entry_t" 20 80 10 "$@" 2>&1
	)"
	[ -z "$_Xdialog_select_entry_r" ] && return $false
	eval "$_Xdialog_select_entry_v=\"\$_Xdialog_select_entry_r\""
	return $true
}

# dialog_select_path does not support the --any flag
dialog_select_path_any=0

# Let the user select a path.
# Usage: dialog_select_path (--file|--dir|--any) <result-var> <label>
# Any is only supported if $dialog_select_path_any is 1.
dialog_select_path() {
	case "$1" in
		--any) die 'not implemented' ;;
		--file) _Xdialog_select_path_f=--fselect ;;
		--dir)  _Xdialog_select_path_f=--dselect ;;
	esac
	_Xdialog_select_path="$(
		Xdialog_run "$3" $_Xdialog_select_path_f "$HOME" 0 0 2>&1
	)"
	[ -z "$_Xdialog_select_path" ] && return $false
	eval "$2=\"\$_Xdialog_select_path\""
	return $true
}

dialog_retry() {
	Xdialog_run 'Error' --ok-label='Retry' --cancel-label='Ignore' --yesno "$1" 0 0
	case $? in
		0) dialog_retry_choice='retry' ;;
		1) dialog_retry_choice='ignore' ;;
		*) dialog_retry_choice='abort' ;;
	esac
}

;;

#----------------------------------------------------------------------------------------#
0) # command-line

# Dialog abstraction

# Create the main progress window.
# Usage: dialog_create
dialog_create() {
	true
}

# Destroy the main progress window.
# Usage: dialog_destroy
dialog_destroy() {
	true
}

# Show an error dialog.
# Usage: dialog_error <message>
dialog_error() {
	true # error messages are always printed to stdout
}

# Show a message box.
# Usage: dialog_message <message>
dialog_message() {
	true
}

# Ask a yes/no question.
# Usage: dialog_ask <question>
dialog_ask() {
	die 'unimplemented'
}

# Has the user quested to cancel the operation?
# Usage: dialog_cancelled && print "cancelled"
dialog_cancelled() {
	false # never cancelled SIGINT is not trapped
}

# Set the status text.
# Usage: dialog_set_text <text>
dialog_set_text() {
	true
}

# Set if the progress bar should continuously animate instead of showing the value.
# Usage: dialog_set_pulsate <enable>
dialog_set_pulsate() {
	true
}

# Set the current progress value.
# Usage: dialog_set_value <percentage>
dialog_set_value() {
	true
}

# Select an entry in a list.
# Usage dialog_select_entry <var> <label> <tag1> <item1> [ <tag2> < item2> ... ]
dialog_select_entry() {
	_cli_select_entry_var="$1" ; shift
	
	# Print a list for the user to select from
	print "$1:" ; shift
	_cli_select_entry_min=$1
	_cli_select_entry_max=$1
	_cli_select_entry_f=' [default]'
	while [ $# -gt 0 ] ; do
		_cli_select_entry_i=$1 ; shift
		_cli_select_entry_t="$1" ; shift
		if [ $_cli_select_entry_i -lt $_cli_select_entry_min ] ; then
			_cli_select_entry_min=$_cli_select_entry_i
		fi
		if [ $_cli_select_entry_i -gt $_cli_select_entry_max ] ; then
			_cli_select_entry_max=$_cli_select_entry_i
		fi
		printf ' %d) %s%s\n' $_cli_select_entry_i \
			"$_cli_select_entry_t" "$_cli_select_entry_f"
		_cli_select_entry_f=''
	done
	
	# Read a number (or empty string for the first entry)
	while true ; do
		
		puts '> #'
		read -r _cli_select_entry_r
		
		[ -z "$_cli_select_entry_r" ] && _cli_select_entry_r=1
		
		case "$_cli_select_entry_r" in
			'quit') ;; 'q') ;; 'exit') ;; 'abort') ;;
			*)
			if [ ! "$_cli_select_entry_r" -lt $_cli_select_entry_min ] 2> /dev/null \
			   && [ ! "$_cli_select_entry_r" -gt $_cli_select_entry_max ] 2> /dev/null
			then
				eval "$_cli_select_entry_var=\"\$_cli_select_entry_r\""
				return $true
			else
				printf "Please enter a number between %d and %d.\n" \
					$_cli_select_entry_min $_cli_select_entry_max
				continue
			fi
		esac
		die
		
	done
	
	return $true
}

# dialog_select_path supports the --any flag
dialog_select_path_any=1

# Let the user select a path.
# Usage: dialog_select_path (--file|--dir|--any) <result-var> <label>
# Any is only supported if $dialog_select_path_any is 1.
dialog_select_path() {
	_cli_select_path_var="$2"
	print "$3:" ; shift
	puts '> '
	read -r _cli_select_path_r
	[ -z "$_cli_select_path_r" ] && return $false
	eval "$_cli_select_path_var=\"\$_cli_select_path_r\""
	return $true
}

dialog_retry() {
	printf '\n%s\n' "${red}Error:${reset} $1"
	while true ; do
		print "Abort / [Retry] / Ignore"
		puts '> '
		read -r _cli_dialog_retry_r
		case "$_cli_dialog_retry_r" in
			a|A|abort|Abort|ABORT)    dialog_retry_choice='abort'  ; return ;;
			''|r|R|retry|Retry|RETRY) dialog_retry_choice='retry'  ; return ;;
			i|I|ignore|Ignore|IGNORE) dialog_retry_choice='ignore' ; return ;;
		esac
	done
}

esac


##########################################################################################
# Common user interface implementation

# Ask the user if the setup should really be cancelled.
handle_cancel() {
	_handle_cancel_message="Are you sure you want to exit the Arx Fatalis data installer?"
	if [ $installed_stuff = 1 ] ; then
		_handle_cancel_message="$_handle_cancel_message

Already installed files will not be removed!"
	fi
	if [ $installed_stuff = 1 ] || [ $selected_stuff = 1 ] ; then
		dialog_ask "$_handle_cancel_message" || return $true
	fi
	print 'Aborted by user' && die
}

# Update the status.
# Usage: status (<percent>|--temp) [<message>]
_status_text="Initializing..."
_status_cur=''
_status_pulsate=default
status() {
	
	if [ "$1" = '--temp' ] ; then
		_status_value=0
		_status_temp=1
	else
		_status_value=$1
		_status_temp=0
	fi
	_status_new="${2:-$_status_text}"
	
	# Handle the cancel and close buttons
	if dialog_cancelled ; then
		handle_cancel
		dialog_create || die "Could not re-create progress window."
		_status_cur=''
		_status_pulsate=default
	fi
	
	# Update the progress text if one was provided
	if [ ! "$_status_cur" = "$_status_new" ] ; then
		_status_cur="$_status_new"
		print "$_status_new"
		dialog_set_text "$_status_new"
	fi
	[ $_status_temp = 0 ] && _status_text="$_status_new"
	
	# Set the maximum progress value
	if [ $_status_value = 0 ] ; then _new_status_pulsate=1 ; else _new_status_pulsate=0 ; fi
	if [ ! "$_status_pulsate" = $_new_status_pulsate ] ; then
		_status_pulsate="$_new_status_pulsate"
		dialog_set_pulsate $_status_pulsate
	fi
	
	# Update the progress value
	[ $_status_pulsate = 0 ] && dialog_set_value $_status_value
	
}

# Print an error message and show an error dialog if we have a GUI/
# Usage: error <message>
error() {
	print "${dim_red}$1${reset}"
	dialog_error "$1"
}

# Let the user select an item from a list or enter a custom one.
# In batch mode, select the first one if --first is given, die otherwise.
# Usage: user_select_entry (--existing|--writable) (--any|--file|--dir) \
#                          <list> <result-var> <desc> <desc-color> <list-color> <verb>
user_select_entry() {
	
	_user_select_entry_access="$1"
	_user_select_entry_t="$2"
	_user_select_entry_lname="$3"
	eval "_user_select_entry_list=\"\$$_user_select_entry_lname\""
	_user_select_entry_var="$4"
	_user_select_entry_desc="$5"
	_user_select_entry_color1="$6"
	_user_select_entry_color2="$7"
	_user_select_entry_verb="$8"
	
	# Select the first element if in batch mode
	eval "_user_select_entry_current=\"\$$_user_select_entry_var\""
	if [ $batch = 1 ] || [ -n "$_user_select_entry_current" ] ; then
		_user_select_entry_=''
		eval "set -- $_user_select_entry_list"
		for _user_select_entry ; do
			_user_select_entry_="$_user_select_entry"
			break
		done
		[ -z "$_user_select_entry_" ] && die "Missing $_user_select_entry_desc!"
		eval "$_user_select_entry_var=\"\$_user_select_entry_\""
		return $true
	fi
	
	if [ $gui = 0 ]
		then print
		else status --temp "Select a ${_user_select_entry_desc}"
	fi
	
	_user_select_entry_i=1
	_user_select_entry_nolist=0
	if [ -z "$_user_select_entry_list" ] \
	&& [ ! $_user_select_entry_t = --any ] \
	&& [ ! $dialog_select_path_any = 1 ] ; then
		
		# No entries detected - directly prompt the user
		_user_select_entry_num=1
		_user_select_entry_nolist=1
		
	else
		
		_user_select_entry_tlist=''
		
		# Format the list entries in a user friendly way
		eval "set -- $_user_select_entry_list"
		for _user_select_entry ; do
			
			# Use 'command (file)' if comment available or 'file' otherwise
			_user_select_entry_comment="$(
				list_comment "$_user_select_entry_lname" "$((_user_select_entry_i - 1))"
			)"
			if [ -z "$_user_select_entry_comment" ] ; then
				_user_select_entry_label="$_user_select_entry_color2$_user_select_entry$reset"
			else
				_user_select_entry_label="$_user_select_entry_color2$_user_select_entry_comment$reset"
				_user_select_entry_label="$_user_select_entry_label: $_user_select_entry"
			fi
			
			# Add the tag and label to the arguments
			list_append _user_select_entry_tlist $_user_select_entry_i
			list_append _user_select_entry_tlist "$_user_select_entry_label"
			
			# Remember the file for the tag
			eval "_user_select_entry_$_user_select_entry_i=\"\$_user_select_entry\""
			
			_user_select_entry_i=$((_user_select_entry_i + 1))
		done
		
		# Add entries for custom files/diectories
		if [ $_user_select_entry_t = --any ] && [ $dialog_select_path_any = 1 ] ; then
			list_append _user_select_entry_tlist $_user_select_entry_i
			list_append _user_select_entry_tlist "Select file or directory to $_user_select_entry_verb.."
		else
			_user_select_entry_ii=$_user_select_entry_i
			if [ $_user_select_entry_t = --any ] || [ $_user_select_entry_t = --file ] ; then
				list_append _user_select_entry_tlist $_user_select_entry_ii
				list_append _user_select_entry_tlist "Select file to $_user_select_entry_verb..."
				_user_select_entry_ii=$((_user_select_entry_ii + 1))
			fi
			if [ $_user_select_entry_t = --any ] || [ $_user_select_entry_t = --dir ] ; then
				list_append _user_select_entry_tlist $_user_select_entry_ii
				list_append _user_select_entry_tlist "Select directory to $_user_select_entry_verb..."
				_user_select_entry_ii=$((_user_select_entry_ii + 1))
			fi
		fi
		
	fi
	
	# Loop until we have selected a value
	while true ; do
		
		if [ $_user_select_entry_nolist = 0 ] ; then
			
			# Ask the user to select an entry
			eval "set -- $_user_select_entry_tlist"
			while true ; do
				if dialog_select_entry _user_select_entry_num \
					"${_user_select_entry_color1}Select a ${_user_select_entry_desc}${reset}" "$@"
				then
					break
				else
					handle_cancel
				fi
			done
			
		fi
		
		if [ $_user_select_entry_num -lt $_user_select_entry_i ] ; then
			
			# User selected an entry -- return that
			eval "$_user_select_entry_var=\"\$_user_select_entry_$_user_select_entry_num\""
			selected_stuff=1
			return $true
			
		fi
		
		# Loop until we have selected a path of the correct type
		while true ; do
			
			# Adjust type based on user selection
			if [ $_user_select_entry_t = --any ] && [ ! $dialog_select_path_any = 1 ] ; then
				if [ $_user_select_entry_num = $_user_select_entry_i ]
					then _user_select_entry_type=--file
					else _user_select_entry_type=--dir
				fi
			else
				_user_select_entry_type=$_user_select_entry_t
			fi
			
			_user_select_entry_c="Select a custom source " 
			
			if ! dialog_select_path "$_user_select_entry_type" _user_select_entry_path \
				"${_user_select_entry_color1}Choose a custom ${_user_select_entry_desc}${reset}" \
				"$@" ; then
				
				# User cancelled the dialog - return to the main selection, unless there is none
				if [ $_user_select_entry_nolist = 1 ] ; then
					handle_cancel
					continue # let the user try again
				fi
				break # return to list selection
				
			fi
			
			_user_select_entry_path="$(abspath "$_user_select_entry_path")"
			
			# The user selected a path, now check if it meets our criteria.
			case "$_user_select_entry_access" in
				--existing)
				if [ ! -e "$_user_select_entry_path" ]  ; then
					error "$_user_select_entry_path does not exist!"
					continue # let the user try again
				fi ;;
				--writable)
				if ! is_writable "$_user_select_entry_path" ; then
					error "$_user_select_entry_path is not writable!"
					continue # let the user try again
				fi ;;
			esac
			case "$_user_select_entry_t" in
				--any) ;; # anything goes
				--file)
				if [ -e "$_user_select_entry_path" ] && [ -d "$_user_select_entry_path" ] ; then
					error "$_user_select_entry_path is is a directory, but we need a file!"
					continue
				fi ;;
				--dir)
				if [ -e "$_user_select_entry_path" ] && [ ! -d "$_user_select_entry_path" ] ; then
					error "$_user_select_entry_path is is a file, but we need a directory!"
					continue
				fi ;;
			esac
			
			# Everything went better than expected - save the result
			eval "$_user_select_entry_var=\"\$_user_select_entry_path\""
			selected_stuff=1
			return $true
			
		done
		
	done
	
}

# Show error message and close windows on exit
ui_cleanup() {
	[ -z "$1" ] || dialog_error "$1"
	dialog_destroy
}
on_exit ui_cleanup

# Initialize the UI
dialog_create || die "
Could not create main window.

You could try the --cli option.
"
[ $gui = 0 ] || status --temp "$_status_text"


##########################################################################################
# Autodetect source file/dir

sourcefiles='' # List of source file/directory candidates

# Add a source file or directory to the list of candidates.
# Usage: found_source_file <file> [comment]
# Return: $true if more source files should be probed, $false otherwise.
found_source_file() {
	set_append sourcefiles "$(canonicalize "$1")" "$2"
	if [ $batch = 1 ] ; then return $true ; else return $false ; fi
}

# Get a subsection in a .vdf file
# Usage: cat config | steam_get_section <section>
steam_get_section() {
	# Abuse indentation to find the section end
	grep -Pzoi "\\n(\\s*)\"$1\"\\n\\s+\\{(\\n.*)*?\\n\\1}"
}

# Get a subsection in a .vdf file - and everything after it
# Usage: cat config | steam_get_section_tail <section>
steam_get_section_tail() {
	# Abuse indentation to find the section end
	grep -Pzoi "\\n\\s*\"$1\"\\n\\s+\\{(\\n.*)*"
}

# Remove backtick escapes
# Usage: echo <escaped_string> | unescape
steam_unescape() {
	sed 's/\\\(.\)/\1/g'
}

# Get an entry in a .vdf file, or the first one if there are multiple
# Usage: cat config | steam_get_entry <key>
steam_get_entry() {
	grep -Pi "^\\s*\"$1\"\s*\".*\"\\s*$" \
	| head -1 \
	| sed 's/^[^"]*"[^"]*"[^"]*"\(.*\)"[^"]*$/\1/' \
	| steam_unescape
}

# Get the app information section for an appid
# Usage: steam_get_appinfo <config_file> <appid>
steam_get_appinfo() {
	_steam_config="$1"
	_steam_appid="$2"
	
	_steam_appinfo="$(
		  steam_get_section 'Software' < "$_steam_config" \
		| steam_get_section 'Valve' \
		| steam_get_section 'Steam' \
		| steam_get_section 'apps' \
		| steam_get_section "$_steam_appid"
	)"
	
	# Fallback if the tree structure changed
	[ -z "$_steam_appinfo" ] && _steam_appinfo="$(
		steam_get_section "$_steam_appid" < "$_steam_config"
	)"
	
	# Fallback if the indentation changed
	[ -z "$_steam_appinfo" ] && _steam_appinfo="$(
		steam_get_section_tail "$_steam_appid" < "$_steam_config"
	)"
	
	print "$_steam_appinfo"
}

# Get a config entry for an appid
# Usage: steam_get_app_config <config_file> <appid>
steam_get_installdir() {
	steam_get_appinfo "$1" "$2" | steam_get_entry 'installdir'
}

# Find possible source directories from a Steam library
# Usage: probe_steam_library <steamlibrary>
# Return: $true if more source files should be probed, $false otherwise.
probe_steam_library() {
	_steam_library="$1"
	for _steam_appdir in "$_steam_library/SteamApps" "$_steam_library/steamapps" ; do
		_steam_installdir="$_steam_appdir/common/$steam_install_dir"
		if [ -d "$_steam_installdir" ] ; then
			found_source_file "$_steam_installdir" "Steam" && return $true
		fi
	done
	return $false
}

# Find possible source directories from a Steam installation
# Usage: probe_steam_root <steamroot>
# Return: $true if more source files should be probed, $false otherwise.
probe_steam_root() {
	_steam_root="$1"
	
	_steam_config="$_steam_root/config/config.vdf"
	
	if [ -f "$_steam_config" ] ; then
		
		# Strategy 1: Read install directory from Steam config file
		_steam_installdir="$(steam_get_installdir "$_steam_config" "$steam_appid")"
		if [ -d "$_steam_installdir" ] ; then
			found_source_file "$_steam_installdir" "Steam" && return $true
		fi
		
		# Strategy 2: Look for known install directory in steam libraries
		_steam_libraries="$(
			sed -n '/BaseInstallFolder/s/^[^\"]*\"[^\"]*\"[^\"]*\"\([^\"]*\)\".*/\1/p' \
			< "$_steam_config" \
			| sed 's:[^a-zA-Z0-9/_.\n]:\\&:g' \
			| lines_to_list
		)"
		_steam_library_call='probe_steam_library "$_steam_library" && return $true'
		eval "for _steam_library in $_steam_libraries ; do $_steam_library_call ; done"
		
	fi
	
	# Strategy 3: Look for known install directory in the default library
	probe_steam_library "$_steam_root" && return $true
	
	return $false
}

# Find possible source directories from Steam
# Usage: probe_steam
# Return: $true if more source files should be probed, $false otherwise.
probe_steam() {
	_steam_roots=''
	for _steam_root in "$HOME/.steam/root" "$HOME/.steam/steam" "$data_home/Steam" ; do
		[ -d "$_steam_root" ] || continue
		set_append _steam_roots "$(canonicalize "$_steam_root")"
	done
	_steam_root_call='probe_steam_root "$_steam_root" && return $true'
	[ -z "$_steam_roots" ] && return $false
	eval "for _steam_root in $_steam_roots ; do $_steam_root_call ; done"
	return $false
}

# Find a match for a case insensitive path containing arx.exe
# Usage: probe_wine_path <baseprefix> <ipath>
probe_wine_path() {
	[ -d "$1" ] || return $false
	if [ -z "$2" ] ; then
		if icontains "$1" 'arx.exe' ; then
			found_source_file "$1" "Wine"
		fi
		return $false
	fi
	_wine_path_dir="${2%%\\*}"
	if [ "$_wine_path_dir" = "$2" ]
		then _wine_path=''
		else _wine_path="${2#*\\}"
	fi
	_wine_paths="$(
		find "$1/" -mindepth 1 -maxdepth 1 -iname "$(escape "$_wine_path_dir")" -print \
			| lines_to_list
	)"
	[ -z "$_wine_paths" ] && return $false
	_wine_path_call='probe_wine_path "$_wine_path_prefix" "$_wine_path" && return $true'
	eval "for _wine_path_prefix in $_wine_paths ; do $_wine_path_call ; done"
	return $false
}

# Find possible source directories from a registry key.
# Usage: probe_wine_registry <key> <variable>
# Return: $true if more source files should be probed, $false otherwise.
probe_wine_registry() {
	
	# This is intentionally implemented without calling wine as we don't want to
	# modify the source.
	
	# Get candidate paths from the registry file
	_wine_pattern='Software\\\\(Wow6432Node\\\\)?'
	_wine_pattern="$_wine_pattern$(print "$1" | sed 's/\\/\\\\/g' | escape_pipe '()|')"
	_wine_paths="$(
		# The --after is just an arbitrary limit
		# We verify the paths by checking for arx.exe, but for efficiency we still want
		# to avoid false positives.
		cat "$_wine_prefix"/*.reg \
			| grep -iPA 20 "^\\[$_wine_pattern\\]" 2> /dev/null \
			| grep -iP "^\"?$2\"?=" 2> /dev/null \
			| sed 's/^[^=]*="\([^"]*\)".*$/\1/;s/\\\(.\)/\1/g' \
			| lines_to_list
	)"
	
	# For each candidate, find a case-insensitive match and check if it contains arx.exe
	eval "set -- $_wine_paths"
	for _wine_path ; do
		probe_wine_path "$_wine_prefix/dosdevices" "$_wine_path" && return $true
	done
	
	return $false
}

# Find possible source directories from uninstall registry entries.
# Usage: probe_wine_uninstall_info <id>
# Return: $true if more source files should be probed, $false otherwise.
probe_wine_uninstall_info() {
	probe_wine_registry "Microsoft\\Windows\\CurrentVersion\\Uninstall\\$1" \
	                    'InstallLocation'
}

# Find possible source directories in a WINEPREFIX.
# Usage: probe_wineprefix <wineprefix>
# Return: $true if more source files should be probed, $false otherwise.
probe_wineprefix() {
	
	[ -d "$1" ] || return $false
	[ -e "$1/system.reg" ] || [ -e "$1/user.reg" ] || return $false
	_wine_prefix="$1"
	
	# Normal install
	probe_wine_registry 'Arkane Studios\Installed Apps\Arx Fatalis' 'Folder' && return $true
	
	# GOG version
	probe_wine_registry 'GOG.com\GOGARXFATALIS' 'PATH' && return $true
	
	# Probe uninstall entries - combined for efficiency
	_wine_uninstall='Microsoft\Windows\CurrentVersion\Uninstall\'
	# Steam
	_wine_steam="Steam App $steam_appid"
	# Original game
	_wine_orig='{96443F45-13E2-11D6-AC87-00D0B7A9E540}'
	# 1.21 patch
	_wine_patch='{171251E0-4EED-4EA1-A46D-3213A226F2B3}_is1'
	probe_wine_registry "$_wine_uninstall($_wine_steam|$_wine_orig|$_wine_patch)" \
		'InstallLocation' && return $true
	
}

# Check a mounte point if it's an Arx Fatalis cd
# Usage: probe_cd <mountpoint> <fstype>
# Return: $true if more source files should be probed, $false otherwise
probe_cd() {
	node="$1"
	mountpoint="$2"
	fstype="$3"
	
	case "$fstype" in
		cd9660) ;; iso9660) ;; udf) ;; *fuseiso) ;;
		*) return $true
	esac
	
	if [ -d "$mountpoint/bin" ] && icontains "$mountpoint/bin" 'arx.ttf' 2> /dev/null ; then
		found_source_file "$mountpoint" "CDROM at $node" && return $true
	fi
	
}

# Find mounted Arx Fatalis CDs
# Usage: probe_cdrom
# Return: $true if more source files should be probed, $false otherwise
probe_cdrom() {
	
	_probe_cdrom_mountpoints="$(
		[ -f /proc/mounts ] && lines_to_list < /proc/mounts # Linux - use /proc/mounts
		mount -p 2> /dev/null | lines_to_list               # Hope that mount has a -p option
	)"
	
	eval "set -- $_probe_cdrom_mountpoints"
	for _probe_cdrom_line ; do
		eval "probe_cd $(escape "$_probe_cdrom_line" ' ' | sed 's/\\\\\040/\\ /g')"  # space-separated
		eval "probe_cd $(print "$_probe_cdrom_line" | tr '\t' '\n' | lines_to_list)" # tab-separated
	done
	
}

# Find possible source files/directories
# Usage: probe_source_files
# If sourcefile is already set, uses that.
probe_source_files() {
	
	if [ -n "$sourcefile" ] ; then
		
		# Trust the user
		found_source_file "$sourcefile" && return $true
		
		# But also allow to refile the dir in non-batch mode if it is a wineprefix
		[ -d "$sourcefile" ] && probe_wineprefix "$sourcefile"
		
		[ "$sourcefiles" = "$sourcefile" ] || sourcefile=''
		
		return $true
	fi
	
	status 5 "Searching for source files..."
	
	# Find by filename
	probe_files found_source_file "$gog_names" 'GOG.com setup'
	probe_files found_source_file "$demo_names" 'Demo'
	
	probe_steam && return $true
	
	# Find an Arx Fatalis installation in $WINEPREFIX
	[ -n "${WINEPREFIX-}" ] && probe_wineprefix "$WINEPREFIX" && return $true
	
	# Find an Arx Fatalis installation in ~/.wine
	[ ! "${WINEPREFIX-}" = "$HOME/.wine" ] && probe_wineprefix "$HOME/.wine" && return $true
	
	# Find an Arx Fatalis cdrom
	probe_cdrom && return $true
	
	# Just in case it will ever be installable under non-Windows systems
	steam_source_path="$HOME/.steam/root/SteamApps/common/Arx Fatalis"
	[ -d "$steam_source_path" ] && found_source_file "$steam_source_path" 'Steam'
	
}


##########################################################################################
# Autodetect destination dir

datadirs='' # List of destination data directory candidate

# Add a destination data directory to the list of candidates.
# Usage: found_data_dir <dir> [comment]
# Return: $true if more data directories should be probed, $false otherwise.
found_data_dir() {
	set_append datadirs "$(abspath "$1")" "$2"
	if [ $batch = 1 ] ; then return $true ; else return $false ; fi
}

# Add a destination data directory to the list of candidates if it is writable.
# In verify mode, also existing read-only directories are added.
# Usage: probe_data_dir <must-exists> <dir> [comment]
# Return: $true if more data directories should be probed, $false otherwise.
probe_data_dir() {
	[ "$1" = 1 ] && [ ! -e "$2" ] && return $false
	[ $install = 0 ] && [ ! -e "$2" ] && return $false
	if [ $install = 1 ] || [ $patch = 1 ] ; then is_writable "$2" || return $false ; fi
	found_data_dir "$2" "$3"
}

# Find possible destination data directories
# Usage: probe_data_dirs
# If datadir is already set, uses that.
probe_data_dirs() {
	
	if [ -n "$datadir" ] ; then
		found_data_dir "$datadir"
		return $true
	fi
	
	status 10 "Searching for destination directories..."
	
	for _probe_data_dirs_existing in 1 0 ; do
		
		# Try paths supplied by wrapper scripts
		if [ -n "$data_path" ] ; then
			eval "set -- $(to_list "$data_path")"
			for _probe_data_dirs_path ; do
				if [ -f "$_probe_data_dirs_path/data" ] ; then
					eval "set -- $(lines_to_list < "$_probe_data_dirs_path/data")"
					for _probe_data_dirs_line ; do
						case "$_probe_data_dirs_line" in
							/*) ;; *) _probe_data_dirs_line="$(
							 canonicalize "$_probe_data_dirs_path/$_probe_data_dirs_line"
							)"
						esac
						probe_data_dir $_probe_data_dirs_existing "$_probe_data_dirs_line" \
							"portable" && return $true
					done
				elif [ -d "$_probe_data_dirs_path/data" ] ; then
					probe_data_dir $_probe_data_dirs_existing "$_probe_data_dirs_path/data" \
						"portable" && return $true
				else
					probe_data_dir $_probe_data_dirs_existing "$_probe_data_dirs_path" "portable" \
						&& return $true
				fi
			done
		fi
		
		# Try system paths
		eval "set -- $(to_list "$data_dirs")"
		for _probe_data_dirs_prefix in "$@" ; do
			eval "set -- $(to_list "$data_dir_suffixes")"
			for _probe_data_dirs_suffix ; do
				_probe_data_dirs_force=$_probe_data_dirs_existing
				[ -e "$_probe_data_dirs_prefix/$_probe_data_dirs_suffix" ] && \
					_probe_data_dirs_force=0
				probe_data_dir $_probe_data_dirs_force \
					"$_probe_data_dirs_prefix/$_probe_data_dirs_suffix/data" "system" \
					&& return $true
				probe_data_dir $_probe_data_dirs_existing \
					"$_probe_data_dirs_prefix/$_probe_data_dirs_suffix" "system" \
					&& return $true
			done
		done
		
		# Try user paths
		if [ $is_root = 0 ] ; then
			eval "set -- $(to_list "$user_dir_suffixes")"
			for _probe_data_dirs_suffix ; do
				_probe_data_dirs_force=$_probe_data_dirs_existing
				[ -e "$data_home/$_probe_data_dirs_suffix" ] && \
					_probe_data_dirs_force=0
				probe_data_dir $_probe_data_dirs_force \
					"$data_home/$_probe_data_dirs_suffix/data" "user" \
					&& return $true
				[ $install = 0 ] && probe_data_dir $_probe_data_dirs_existing \
					"$data_home/$_probe_data_dirs_suffix" "user" \
					&& return $true
			done
		fi
		
	done
	
}


##########################################################################################
# Extract helpers and other utility abstractions

# Calculate the MD5 checksum of a file.
# Usage: checksum <result-var> <file>
checksum() {
	
	if have md5sum ; then
		_checksum_result="$(md5sum -b "$2" | sed 's/ .*//g')"
		eval "$1=\"\$_checksum_result\""
		return $true
	fi
	
	if have md5 ; then
		_checksum_result="$(md5 -q "$2")"
		eval "$1=\"\$_checksum_result\""
		return $true
	fi
	
	die "You need either md5sum or md5."
}

have_run() {
	eval "_have_run_p=\"\$${1}_reqs\""
	eval "for _have_run_program in $_have_run_p ; do have \$_have_run_program && return $true ; done"
	return $false
}

# Extract an archive file to the current directory.
# Usage: extract <file> <types>...
have_extract() {
	have_run "extract_$1"
}
extract() {
	_extract_file="$1" ; shift
	
	while true ; do
		
		_extract_missing=''
		for _extract_type ; do
			
			if "have_extract_${_extract_type}" ; then
				"extract_${_extract_type}" "$_extract_file" && return "$true"
			else
				list_merge _extract_missing extract_${_extract_type}_reqs
			fi
			
		done
		
		_extract_msg="${white}Could not extract $(basename "$_extract_file")${reset}"
		[ -z "$_extract_missing" ] \
			|| _extract_msg="$_extract_msg

Please install one or more of the following:
$(print_help_list " - $dim_pink" _extract_missing)"
		
		[ $batch = 1 ] && die "Error: $_extract_msg"
		
		dialog_retry "$_extract_msg"
		case $dialog_retry_choice in
			abort)  die "Error extracting files" ;;
			retry)  continue ;;
			ignore) return $true ;;
		esac
		
	done
	
}

# Extract a .zip file to the current directory.
# Usage: extract_zip <zipfile>
have_extract_zip() { have_extract zip ; }
extract_zip() {
	
	if have bsdtar ; then
		printf 'Extracting %s using bsdtar\n' "$1"
		bsdtar xvf "$1"
		return $?
	fi
	
	if have unzip ; then
		puts 'unzip: '
		unzip "$1"
		return $?
	fi
	
	for _extract_zip_sz in 7za 7z ; do
		if have $_extract_zip_sz ; then
			$_extract_zip_sz x "$1"
			return $?
		fi
	done
	
	die "no program to extract $1"
}

# Mount a .iso file or CDROM using fuseiso if available  or normal mount if root.
# Usage: mount_cdrom <cdromfile> <mountpoint>
have_mount_cdrom() {
	have fuseiso && have fusermount && return $true
	have mount && have umount && [ $is_root = 1 ] && return $true
	return $false
}
mount_cdrom() {
	
	if have fuseiso && have fusermount &&  fuseiso "$1" "$2" ; then
		printf 'Mounted %s at %s using fuseiso\n' "$1" "$2"
		return $true
	fi
	
	if have mount && have umount && [ $is_root = 1 ] && mount -o loop,ro "$1" "$2" ; then
		printf 'Mounted %s at %s\n' "$1" "$2"
		return $true
	fi
	
	die "no program to extract $1"
}

# Unmount a CDROM that was mounted using mount_cdrom.
# Usage: unmount_cdrom <mountpoint>
unmount_cdrom() {
	have fusermount && fusermount -u "$1" > /dev/null 2>&1
	have umount && [ $is_root = 1 ] && umount "$1" > /dev/null 2>&1
}

# isoinfo wrapper to extract all files to the current directory
extract_isoinfo() {
	_extract_isoinfo_file="$1"
	
	# Get a list of all files in the ISO image
	_extract_isoinfo_files="$(
		isoinfo -i "$_extract_isoinfo_file" -J -f | grep ';1$' | lines_to_list
	)"
	[ -z "$_extract_isoinfo_files" ] && return $false
	
	eval "set -- $_extract_isoinfo_files"
	for _extract_isoinfo_e ; do
		
		# Remove leading / and trailing ;1 from filenames
		_extract_isoinfo_f="$(print "$_extract_isoinfo_e" | sed 's:^/*::;s:;1$::')"
		[ -z "$_extract_isoinfo_f" ] && continue
		printf ' - %s\n' "$_extract_isoinfo_f"
		
		# Create subdirectories as needed
		_extract_isoinfo_d="$(dirname "$_extract_isoinfo_f")"
		[ -z "$_extract_isoinfo_d" ] || mkdir -p "$_extract_isoinfo_d" || return $false
		
		# Extract the file
		isoinfo -i "$_extract_isoinfo_file" -J -x "$_extract_isoinfo_e" \
			> "$_extract_isoinfo_f" || return $false
		
		# Don't rely on isoinfo setting a non-zero return code, check that we got something
		[ -s "$_extract_isoinfo_f" ] || return $false
		
	done
	
	return $true
}

# Extract a .iso file or CDROM to the current directory.
# Usage: extract_iso <cdromfile>
have_extract_iso() { have_extract iso ; }
extract_iso() {
	
	if have isoinfo ; then
		printf 'Extracting %s using isoinfo\n' "$1"
		extract_isoinfo "$1"
		return $?
	fi
	
	ret=$false
	
	if have bsdtar ; then
		printf 'Extracting %s using bsdtar\n' "$1"
		bsdtar xvf "$1"
		ret="$?"
		
		# Older versions of bsdtar don't always get the names right - fix them
		_extrac_cdrom_f="$(find "$PWD" -depth -iname '*;1' | lines_to_list)"
		if [ -n "$_extrac_cdrom_f" ] ; then
			eval "for _extract_iso_f in $_extrac_cdrom_f ; do mv -f \"\$_extract_iso_f\" \"\$(print \"\$_extract_iso_f\" | sed 's:;1$::')\" ; done"
		fi
		
	elif have 7z ; then
		7z x -tiso "$1"
		ret="$?"
	fi
	
	# For some iso files bsdtar just does nothing - at least let the user know
	if [ ! -d "$PWD/bin" ] || ! icontains "$PWD/bin" 'arx.ttf' ; then
		_extract_iso_err="${yellow}It looks like bsdtar/p7zip didn't do what it was supposed to - this will likely fail!${reset}

You might have better luck with ${dim_pink}isoinfo${reset} or ${dim_pink}fuseiso${reset}, or by manually mounting the CD/ISO."
		printf '%s\n' "$_extract_iso_err"
		[ $batch = 0 ] && dialog_message "$_extract_iso_err"
	fi
	
	return $ret
}

# Extract a microsoft .cab or .exe file to the current directory.
# Usage: extract_ms_cab <cabfile>
extract_cab_check_bsdtar() {
	if have bsdtar ; then
		case "$(bsdtar --version)" in
			# These versions have bugs that cause corrupted files
			'') ;;
			*'libarchive 1.'*) ;;
			*'libarchive 2.'*) ;;
			*'libarchive 3.0') ;;
			*'libarchive 3.0.'*) ;;
			# Newer versions should work fine
			*)
			return $true
		esac
	fi
	return $false
}
have_extract_ms_cab() {
	extract_cab_check_bsdtar && return $true
	have cabextract || have 7za || have 7z
}
extract_ms_cab() {
	
	if extract_cab_check_bsdtar ; then
		printf 'Extracting %s using bsdtar\n' "$1"
		bsdtar xvf "$1"
		return $?
	fi
	
	if have cabextract ; then
		puts 'cabextract: '
		cabextract "$1"
		return $?
	fi
	
	for _extract_ms_cab_sz in 7za 7z ; do
		if have $_extract_ms_cab_sz ; then
			$_extract_ms_cab_sz x "$1"
			return $?
		fi
	done
	
	die "no program to extract $1"
}

# Extract an InstallShield .cab or .exe file to the current directory.
# Usage: extract_installshield <cabfile>
have_extract_installshield() { have_extract installshield ; }
extract_installshield() {
	
	if have unshield ; then
		puts 'unshield: '
		unshield x "$1"
		return $?
	fi
	
	die "no program to extract $1"
}

# Extract a RAR .rar or .exe file to the current directory.
# Usage: extract_rar <rarfile>
have_extract_rar() { have_extract rar ; }
extract_rar() {
	
	if have unrar ; then
		puts 'unrar: '
		unrar x -y -o "$1"
		return $?
	fi
	
	die "no program to extract $1"
}

# Extract an ACE .ace or .exe file to the current directory.
# Usage: extract_ace <acefile>
have_extract_ace() { have_extract ace ; }
extract_ace() {
	
	if have unace ; then
		puts 'unace '
		unace x -y -o "$1"
		return $?
	fi
	
	die "no program to extract $1"
}

# Extract an Inno Setup .exe file to the current directory.
# Usage: extract_innosetup <exefile>
have_extract_innosetup() { have innoextract ; }
innosetup_language=''
extract_innosetup() {
	
	if have innoextract ; then
		puts 'innoextract: '
		if [ -z "$innosetup_language" ]
			then innoextract --color=off "$1" ; return $?
			else innoextract --color=off --language="$innosetup_language" "$1" ; return $?
		fi
	fi
	
	die "no program to extract $1"
}

# Extract all .cab files in a directory.
# Usage: extract_cab_files <sourcedir> <destdir>
extract_cab_files() {
	
	_extract_cab_files_i=0
	_extract_cab_files_files="$(
		find "$1/" -mindepth 1 -type f -iname '*.cab' -print \
		| lines_to_list
	)"
	eval "set -- $_extract_cab_files_files"
	for _extract_cab_files_cabfile ; do
		[ -z "$_extract_cab_files_cabfile" ] && continue
		
		while true ; do
			_extract_cab_files_cabdir="$sourcedir/cab.$_extract_cab_files_i"
			if [ -e "$_extract_cab_files_cabdir" ] ; then
				_extract_cab_files_i=$((_extract_cab_files_i + 1))
				continue
			fi
			create_dir "$_extract_cab_files_cabdir" "cab #$i work"
			break
		done
		
		cd "$_extract_cab_files_cabdir"
		case "$(basename "$_extract_cab_files_cabfile")" in
			data*) extract "$_extract_cab_files_cabfile" installshield ms_cab ;;
			*)     extract "$_extract_cab_files_cabfile" ms_cab installshield ;;
		esac
		
		_extract_cab_files_i=$((_extract_cab_files_i + 1))
	done
	
}

# Download a file.
# Usage: download_file <url> <destination>
have_download() { have_run download ; }
download_impl() {
	
	if have wget ; then
		wget -O "$2" "$1"
		return $?
	fi
	
	if have curl ; then 
		curl --location --fail -o "$2" "$1"
		return $?
	fi
	
	if have fetch ; then
		fetch -o "$2" "$1"
		return $?
	fi
	
	die "no program to download $1"
}
download_file() {
	
	download_impl "$1" "$2" || return $false
	
	# Check that we got something useful
	case "$(file --dereference --brief --mime-type "$2" 2> /dev/null)" in
		*/html*) ;;
		*/xml*) ;;
		*) return $true
	esac
	
	rm -f "$2"
	return $false;
}

# Download a file from a list of mirrors.
# Usage: download <callback> <name> <names> <urls> <destdir>
download() {
	_download_callback="$1"
	_download_name="$2"
	_download_names="$3"
	_download_urls="$4"
	_download_dest="$5"
	
	while true ; do
		
		probe_files "$_download_callback" "$_download_names" && return $true
		
		_download_missing=''
		if have_download ; then
			eval "for _download_url in $_download_urls ; do download_file \"\$_download_url\" \"\$_download_dest\" && \$_download_callback \"\$_download_dest\" && return $true ; done"
		else
			list_merge _download_missing download_reqs
		fi
		
		_download_msg="${white}Could not download ${_download_name}${reset}"
		[ -z "$_download_missing" ] \
			|| _download_msg="$_download_msg

Please install one of the following:
$(print_help_list " - $dim_pink" _download_missing)"
		
		_download_msg="$_download_msg

You can download the file manually from
$(print_help_list " - $dim_blue" _download_urls)

and put it in one of these locations:
$(print_help_list " - $dim_white" probe_file_dirs)"
		
		[ $batch = 1 ] && die "Error: $_download_msg"
		
		dialog_retry "$_download_msg"
		case $dialog_retry_choice in
			abort)  die "Error downloading files" ;;
			retry)  continue ;;
			ignore) return $true ;;
		esac
		
	done
	
}


##########################################################################################
# Unpack source

workdir=''
cleanup_workdir() {
	[ -n "$workdir" ] && [ -e "$workdir" ] && rm -rf "$workdir"
}
# Create the work directory if it doesn't exist.
# Also register a cleanup function to remove the work directory on exit.
# Usage: create_workdir
create_workdir() {
	[ -z "$workdir" ] || return $true
	selected_stuff=1
	workdir="$datadir/$command-temp"
	cleanup_workdir
	on_exit cleanup_workdir
	create_dir "$workdir" 'work'
}

# Extract setup*.cab files from a source directory into $sourcedir/cab.*.
# Usage: extract_source_dir <sourcedir>
extract_source_dir() {
	extract_cab_files "$1" "$sourcedir"
}

# Extract a source executable into $sourcedir/exe.
# Usage: extract_source_exe <sourcefile>
extract_source_exe() {
	
	sourcedir_exe="$sourcedir/exe"
	create_dir "$sourcedir_exe" 'exe work'
	
	cd "$sourcedir_exe" || die
	case "$(basename "$1")" in
		arx_demo_english.exe)
			extract "$1" rar ace ms_cab installshield innosetup ;; # English demo
		arx_demo_german.exe)
			extract "$1" ace rar ms_cab installshield innosetup ;; # German demo
		arx_jpn_*.exe)
			extract "$1" ms_cab installshield innosetup rar ace ;; # Japanese demo
		*)
			extract "$1" innosetup ms_cab installshield rar ace ;; # GOG.com setup
	esac
	
	extract_source_dir "$sourcedir_exe"
	
}

# Wrap mount_cdrom et al so we can use them with extract()
extract_mount_cdrom_reqs=''
list_merge extract_mount_cdrom_reqs mount_cdrom_reqs
have_extract_mount_cdrom() { have_mount_cdrom ; }
extract_mount_cdrom() {
	# Ignore the current working directory, always mount to $cdromdir
	if mount_cdrom "$1" "$cdromdir" ; then
		# Pretent the mountpoint is the original source
		sourcefile="$cdromdir"
		sourcedir_cdrom="$cdromdir"
		return $true
	else
		return $false
	fi
}

cdromdir=''
cleanup_cdrom() {
	[ -n "$cdromdir" ] && [ -e "$cdromdir" ] && unmount_cdrom "$cdromdir"
}
# Mount a source CDROM/ISO and adjust $sourcefile or extract it into $sourcedir/cdrom.
# Usage: extract_source_cdrom <sourcefile>
extract_source_cdrom() {
	
	# Try to mount the cdrom to avoid unneeded copies
	# Keep the mount point out of $sourcedir so we don't try to mv files from it
	cdromdir="$workdir/cdrom"
	cleanup_cdrom
	on_exit cleanup_cdrom
	create_dir "$cdromdir" 'mount work'
	
	# Otherwise, extract the files from the CDROM if we have the required tools
	sourcedir_cdrom="$sourcedir/cdrom"
	create_dir "$sourcedir_cdrom" 'cdrom work'
	cd "$sourcedir_cdrom" || die
	
	extract "$1" mount_cdrom iso
	
	# Extract any cab files on the CDROM
	extract_source_dir "$sourcedir_cdrom"
}

# Extract a source executable into $sourcedir/zip.
# Also extracts contained setup*.cab files into $sourcedir/cab.*.
# Usage: extract_source_zip <sourcefile>
extract_source_zip() {
	
	sourcedir_zip="$sourcedir/zip"
	create_dir "$sourcedir_zip" 'zip work'
	
	cd "$sourcedir_zip" || die
	extract "$1" zip
	
	extract_source_dir "$sourcedir_zip"
}

sourcedir=''
# Extract the source file or directory if it hansn't been extracted already.
# Usage: extract_source
extract_source() {
	[ -z "$sourcedir" ] || return $true
	
	status --temp "${white}Extracting source...${reset}"
	
	create_workdir
	sourcedir="$workdir/source"
	create_dir "$sourcedir" 'source work'
	
	if [ -d "$sourcefile" ] ; then
		extract_source_dir "$sourcefile"
	elif [ -f "$sourcefile" ] ; then
		case "$sourcefile" in
			*.zip) extract_source_zip "$sourcefile" ;;
			*.exe) extract_source_exe "$sourcefile" ;;
			*.iso) extract_source_cdrom "$sourcefile" ;;
			*)
			case "$(file --dereference --brief --mime-type "$sourcefile" 2> /dev/null)" in
				application/zip)             extract_source_zip "$sourcefile" ;;
				application/x-dosexec)       extract_source_exe "$sourcefile" ;;
				application/x-iso9660-image) extract_source_cdrom "$sourcefile" ;;
				*) die "Unknown source file type: $sourcefile"
			esac
		esac
	else
		extract_source_cdrom "$sourcefile"
	fi
	
	print
}


##########################################################################################
# Detect data language

find_file_impl() {
	_patchable="$1"
	
	# Search for both file.ext and file_default.ext
	_file="$(escape "$(basename "$2")")"
	_file_d="$(print "$_file" | sed 's/^\(.*\)\(\.[^.]*\)$/\1_default\2/')"
	
	# Prefer files from the patch - if available, they are most likely the correct ones
	[ "$_patchable" = 1 ] && [ -n "$patchdir" ]  \
		&& find "$patchdir" \( -name "$_file" -o -name "$_file_d" \)  -print
	
	# Find the file in the source
	[ -n "$sourcedir" ] && find "$sourcedir" -iname "$_file" -print
	[ -d "$sourcefile" ]  && find "$sourcefile" -iname "$_file" -print
	# Find the _default.pak variant only after the .pak variant
	# This is required to detect the correct language from Steam installs
	# as the english version is always present as speech_default.pak and loc_default.pak
	[ -n "$sourcedir" ] && find "$sourcedir" -iname "$_file_d" -print
	[ -d "$sourcefile" ]  && find "$sourcefile" -iname "$_file_d" -print
	
	# Also find the file if it is already in the data directory, but don't ignore case
	find "$datadir" -path '*-temp' -prune \( -name "$_file" -o -name "$_file_d" \) -print
	
}
# Find a file in patch, source and data directories.
# Usage: find_file <is-patchable> <return-list-var> <filename/path>
find_file() {
	eval "$2=\"\$(find_file_impl \"\$1\" \"\$3\" | lines_to_list)\""
}

# Copy/move a file to the data diectory.
# Usage: use_file <file> <data-path>
# Action taken depends on the source directory.
use_file() {
	_in="$1"
	_out="$datadir/$2"
	
	# Don't change anything on verify-only mode
	[ $install = 0 ] && [ $patch = 0 ] && return $true
	
	# Don't try to copy/move a file onto itself
	[ "$_in" = "$_out" ] && return $true
	
	# Create directories as needed
	_outdir="$(dirname "$_out")"
	create_dir "$_outdir" 'output'
	
	# Copy or move the file
	if [ "${_in#"$sourcefile"}" = "$_in" ]
		then mv -f "$_in" "$_out" || die "Could not move $_in to $_out!"
		else cp -f "$_in" "$_out" || die "Could not copy $_in to $_out!"
	fi
	installed_stuff=1
	
	# Fix permissions
	chmod --reference="$_outdir" "$_out" > /dev/null 2>&1
	chmod -x "$_out" > /dev/null 2>&1
}

data_lang=''      # Data language/tipe ID
data_lang_desc='' # Friendly data language/type label

# Detect the data language and type
# Usage: detect_data_langauge <callback>
# <callback> receives a speech.pak checksum and sets data_lang and data_lang_desc
detect_data_langauge() {
	callback="$1"
	
	# TODO Add support for installing multiple languages at once
	
	if [ $install = 1 ]
		then _detect_data_langauge_status=40
		else _detect_data_langauge_status=10
	fi
	[ $gui = 0 ] || status $_detect_data_langauge_status 'Detecting data language...'
	puts "${white}Detecting data language..."
	
	_speech_checksums=''
	
	find_file 0 _speech_files 'speech.pak'
	eval "set -- $_speech_files"
	for _speech_file ; do
		
		checksum _speech_checksum "$_speech_file"
		
		data_lang=''
		"$callback" "$_speech_checksum"
		
		if [ -z "$data_lang" ] ; then
			list_append _speech_checksums "$_speech_checksum"
		else
			use_file "$_speech_file" 'speech.pak'
			break
		fi
		
	done
	
	if [ -z "$data_lang" ] ; then
		printf '\n'
		case "$_speech_checksums" in
			'') die "speech*.pak not found" ;;
			*)  die "Unsupported data language - speech*.pak checksum: $_speech_checksums" ;;
		esac
	fi
	
	printf " ${green}%s${reset}\n\n" "${data_lang_desc}"
	
}


##########################################################################################
# Get the patch file

# Warn if the user-supplied patch file has a suspicious name.
# Usage: patch_check_file_name <file> <expected-name-list>
patch_check_file_name() {
	_user_patch_name="$(basename "$1")"
	_patch_check_file_names="$2"
	if ! list_contains _patch_check_file_names "$_user_patch_name" ; then
		printf "${yellow}Warning: unexpected patch file name: %s\n" "$_user_patch_name" >&2
		printf "Expected %s${reset}\n" "$(print_help_or _patch_check_file_names)" >&2
	fi
}

# Find or download a patch file.
# Usage: probe_patch_file_impl <callback> <name> <user-supplied-file> \
#                              <expected-name-list> <url-list> \
# <callback> will be called with the patch file
probe_patch_file_impl() {
	
	_patch_found="$1"
	_patch_name="$2"
	_patch_file="$3"
	_patch_names="$4"
	_patch_urls="$5"
	
	if [ -n "$_patch_file" ] ; then
		
		# Check the filename of user-supplied patchse
		patch_check_file_name "$_patch_file" "$_patch_names"
		
		"$_patch_found" "$_patch_file"
		return $true
	fi
	
	[ $probe_patch = 0 ] && return $true
	
	# Probe local files now so we don't lie abut downloading it
	probe_files "$_patch_found" "$_patch_names" && return $true
	
	status --temp "${white}Downloading patch ${blue}${_patch_name}${reset}..."
	
	create_workdir
	download "$_patch_found" "$_patch_name" \
	         "$_patch_names" "$_patch_urls" "${workdir}/${_patch_name}" \
		&& return $true
	
}

# Callback for the japanese patch.
patch_jp_found() {
	patchfile_jp="$(abspath "$1")"
	printf "Using Japanese %s patch: ${blue}%s${reset}\n" \
		"$patch_jp_ver" "$patchfile_jp"
	return $true
}

# Callback for the main patch.
patch_found() {
	patchfile="$(abspath "$1")"
	printf "Using %s patch: ${blue}%s${reset}\n" "$patch_ver" "$patchfile"
	return $true
}

# Find the patch file(s) for a specific language.
# Usage: probe_patch_file <language>
# If patchfile is already set, uses that.
probe_patch_file() {
	
	_patch_file_lang=''
	case "$1" in
		'german')   _patch_file_lang='GE' ;;
		'english')  _patch_file_lang='EN' ;;
		'spanish')  _patch_file_lang='ES' ;;
		'french')   _patch_file_lang='FR' ;;
		'italian')  _patch_file_lang='IT' ;;
		'russian')  _patch_file_lang='RU' ;;
		'japanese')
			_patch_jp_names=''
			list_append _patch_jp_names "$patch_jp_name"
			probe_patch_file_impl patch_jp_found "$patch_jp_name" "$patchfile_jp" \
			                      "$_patch_jp_names" "$patch_jp_urls"
		;;
	esac
	_patch_names=''
	list_append _patch_names "$patch_name"
	if [ -n "$_patch_file_lang" ] ; then
		list_append _patch_names "$(printf "$patch_name_localized" "$_patch_file_lang")"
	fi
	
	probe_patch_file_impl patch_found "$patch_name" "$patchfile" \
	                      "$_patch_names" "$patch_urls"
	
}

patchdir='' # Directory where the patch file(s) are extracted

# Extract all patch files for a specific language.
# Usage: extract_patch <language>
# Does nothing if the patch files are already extracted.
extract_patch() {
	[ -z "$patchdir" ] || return $true
	_extract_patch_lang="$1"
	
	print
	
	# Search for and download the patch files if needed
	probe_patch_file "$_extract_patch_lang"
	
	status --temp "${white}Extracting patch...${reset}"
	
	create_workdir
	patchdir="$workdir/patch"
	create_dir "$patchdir" 'patch work'
	
	# Extract the main patch file
	if [ -n "$patchfile" ] ; then
		_patchdir_main="$patchdir/main"
		create_dir "$_patchdir_main" 'main patch work'
		cd "$_patchdir_main"
		innosetup_language="$_extract_patch_lang"
		extract "$patchfile" innosetup
		innosetup_language=''
	fi
	
	if [ -n "$patchfile_jp" ] ; then
		
		# Extract the Japanese patch file
		_patchdir_jp="$patchdir/main"
		create_dir "$_patchdir_jp" 'jp patch work'
		cd "$_patchdir_jp"
		extract "$patchfile_jp" ms_cab
		
		# Also extract contained files
		extract_cab_files "$_patchdir_jp" "$patchdir"
		
	fi
	
	print
	status --temp
}


##########################################################################################
# Copy and verify files

checksum_failed=0 # Was there any mismatched checksum or missing file so far?

# Handle a required file: find, compare checksum and copy/move if needed.
# Usage: required_file <is-patchable> <filepath> <checksums>
required_file() {
	
	_patchable="$1"
	_name="$2"
	_valid="$3"
	
	find_file 1 _files "$_name"
	eval "set -- $_files"
	_checksums=''
	for _file ; do
		checksum _checksum "$_file"
		
		if list_contains _valid "$_checksum" ; then
			# We found a match - use it
			printf ' - %s\n' "$_name"
			use_file "$_file" "$_name"
			return $true
		fi
		
		# Remember mismatched checksums so we can output debug info if none matched
		list_append _checksums "$_checksum"
		continue
		
	done
	
	# No matching file found!
	
	# If we didn't use the patch yet, fetch it and try again!
	if [ $patch = 1 ] && [ $_patchable = 1 ] && [ -z "$patchdir" ] ; then
		extract_patch "$data_lang"
		if [ -n "$patchdir" ] ; then
			required_file "$_patchable" "$_name" "$_valid"
			return $?
		fi
	fi
	
	# Let the user know that something is wrong!
	if [ -z "$_checksums" ] ; then
		printf "${red}Missing ${dim_red}%s${red}!${reset}\n" "$_name" >&2
	else
		printf "${red}Checksum failed for ${dim_red}%s${reset}:\n" "$_name" >&2
		printf "  expected: ${dim_red}%s${reset}\n" "$(print_help_or _valid)" >&2
		printf "  actual:   ${dim_red}%s${reset}\n" "$(print_help_or _checksums)" >&2
	fi
	
	# Be optimistic, copy the first result even if the checksum doesn't match!
	# We will display an error at the end (end exit with $false), but it may still work.
	eval "set -- $_files"
	for _file ; do
		use_file "$_file" "$_name"
		break
	done
	
	checksum_failed=1
	return $false
}

# Handle an optional file: find and copy/move if it exists.
# Usage: optional_file <filepath>
optional_file() {
	_name="$1"
	find_file 1 _files "$_name"
	eval "set -- $_files"
	for _file ; do
		# There is no checksum, just copy the first file
		printf ' - %s\n' "$_name"
		use_file "$_file" "$_name"
		break
	done
}


##########################################################################################
# Setup

# Select source file / directory
if [ $install = 1 ] ; then
	probe_source_files
	if [ $batch = 0 ] ; then
		list_append sourcefiles 'Patch existing install' ''
		list_append sourcefiles 'Verify existing install only' ''
	fi
	user_select_entry --existing --any sourcefiles sourcefile \
		"source file or directory to install from" "$green" "$dim_green" 'install from'
	case "$sourcefile" in
		'Patch existing install')       install=0 ; patch=1 ;;
		'Verify existing install only') install=0 ; patch=0 ;;
		*) [ -e "$sourcefile" ] || die "Missing source file: $sourcefile"
	esac
	set_append probe_file_dirs "$(dirname "$sourcefile")"
fi

# Select destination data directory
probe_data_dirs
if [ $install = 1 ] ; then
	verb='install to' ; access=--writable
elif [ $patch = 1 ] ; then
	verb='patch'      ; access=--existing
else
	verb='verify'     ; access=--existing
fi
user_select_entry $access --dir datadirs datadir \
	"data directory to $verb" "$cyan" "$dim_cyan" "$verb"
[ -z "$datadir" ] && die "Missing data dir."
if [ $install = 1 ] ; then
	create_dir "$datadir" 'data'
else
	[ -d "$datadir" ] || die "Missing data dir: $datadir"
fi

# Extract source files
if [ $install = 1 ] ; then
	printf "\nInstalling Arx Fatalis data files \nfrom %s\nto   %s\n\n" \
		"${green}$sourcefile${reset}" "${cyan}$datadir${reset}"
	extract_source
else
	printf "\nVerifying Arx Fatalis data files \nin %s\n\n" "${cyan}$datadir${reset}"
fi


##########################################################################################
# Required files

# Detect language
determine_language() {
	speech_checksum="$1" # speech.pak
	
	case "$speech_checksum" in
		'4e8f962d8204bcfd79ce6f3226d6d6de') data_lang='english'       ;;
		'4c3fdb1f702700255924afde49081b6e') data_lang='german'        ;;
		'ab8a93161688d793a7c78fbefd7d133e') data_lang='german'        ;;
		'2f88c67ae1537919e69386d27583125b') data_lang='spanish'       ;;
		'4edf9f8c799190590b4cd52cfa5f91b1') data_lang='french'        ;;
		'81f05dea47c52d43f01c9b44dd8fe962') data_lang='italian'       ;;
		'677163bc319cd1e9aa1b53b5fb3e9402') data_lang='russian'       ;;
		'5df8ba0d4ec58bd43d04307eb4c06d86') data_lang='russian'       ;;
		'235b86700fc80b3eb86731d748013a38') data_lang='japanese'      ;;
		'62ca7b1751c0615ee131a94f0856b389') data_lang='english-demo'  ;;
		'09038e43508232c44537c162f9e3ecde') data_lang='french-demo'   ;;
		'a424fcfc46dd4f11b04030efac15a668') data_lang='german-demo'   ;;
		'eeacbd9a845ecc00054934e82e9d7dd3') data_lang='japanese-demo' ;;
	esac
	
	case "$data_lang" in
		'english')       data_lang_desc='English' ;;
		'german')        data_lang_desc='German' ;;
		'spanish')       data_lang_desc='Spanish' ;;
		'french')        data_lang_desc='French' ;;
		'italian')       data_lang_desc='Italian' ;;
		'russian')       data_lang_desc='Russian' ;;
		'japanese')      data_lang_desc='Japanese' ;;
		'english-demo')  data_lang_desc='English (demo)' ;;
		'french-demo')   data_lang_desc='French (demo)' ;;
		'german-demo')   data_lang_desc='German (demo)' ;;
		'japanese-demo') data_lang_desc='Japanese (demo)' ;;
	esac
	
}
detect_data_langauge determine_language

if [ $install = 1 ] ; then
	progress=50
	status $progress "${white}Copying and verifying files...${reset}"
	case "$data_lang" in *-demo) increment=8 ;; *) increment=1 ;; esac
else
	progress=15
	status $progress "${white}Verifying files...${reset}"
	case "$data_lang" in *-demo) increment=14 ;; *) increment=2 ;; esac
fi
print " - speech.pak"

# Usage: f <is-patchable> <file> <checksums>...
f() {
	
	# Update progress bar
	progress=$((progress + increment))
	status $progress
	
	# Verify & copy file
	required_file "$@"
}

# speech.pak - already copied in detect_data_langauge

# loc.pak contains the localized text, so it's different for each language!
case "$data_lang" in
	german)        loc_checksum='31bc35bca48e430e108db1b8bcc2621d' ;;
	english)       loc_checksum='a47b192493afb5794e2161a62d35b69f' ;;
	spanish)       loc_checksum='121f99608814a2c9c5857cfadb665553' ;;
	french)        loc_checksum='f8fc448fea12469ed94f417c313fe5ea' ;;
	italian)       loc_checksum='a9e162f2916f5737a95bd8c5bd8a979e' ;;
	russian)       loc_checksum='a131bf2398ee70a9c22a2bbffd9d0d99' ;;
	japanese)      loc_checksum='9dcb0f5d7a517be4f1d9190419900892' ;;
	english-demo)  loc_checksum='2ae16d3925c597dca70f960f175def3a' ;;
	french-demo)   loc_checksum='4a8ac68341d4758a32d9cd04955b115e' ;;
	german-demo)   loc_checksum='87accec0658aa109a3efa8b41aab61df' ;;
	japanese-demo) loc_checksum='9d84cede805b13fdf7fce856ecc15b19' ;;
	*)             loc_checksum=''
esac
if [ -n "$loc_checksum" ] ; then
	f 1 'loc.pak' "$loc_checksum"
fi

# misc/arx.ttf is the same for everything except Japanese (and Russian for version 1.22)
# there are also separate misc/arx_russian.ttf and misc/arx_taiwanese.ttf handled later
font_checksum_default='9a95ff96795c034524ba1c2e94ea12c7'
font_checksum_japanese='58eab00842d8adea8d553ae1f66b0c9b'
font_checksum_russian='921561e83786efcd25f92147b60a13db'
f 1 'misc/arx.ttf' "$font_checksum_default $font_checksum_japanese $font_checksum_russian"

case "$data_lang" in
	
	english-demo)
	f 0 'data2.pak'                                        958b78f8f370b06d769843137138c461
	f 0 'data.pak'                                         5d7ba6e6c79ebf7fbb232eaced9e8ad9
	f 0 'misc/logo.bmp'                                    aa3dfbd4bc9c863d10a0c5345ae5a4c9
	f 0 'sfx.pak'                                          ea1b3e6d6f4906905d4a34f07e9a59ac
	;;
	
	french-demo)
	f 0 'data2.pak'                                        8dc1d1b3e85d4a41ae320aa3fa9c649a
	f 0 'data.pak'                                         5d7ba6e6c79ebf7fbb232eaced9e8ad9
	f 0 'misc/logo.bmp'                                    aa3dfbd4bc9c863d10a0c5345ae5a4c9
	f 0 'sfx.pak'                                          ea1b3e6d6f4906905d4a34f07e9a59ac
	;;
	
	german-demo)
	f 0 'data2.pak'                                        143ba491a357263a2dfad9936a66eeb6
	f 0 'data.pak'                                         5d7ba6e6c79ebf7fbb232eaced9e8ad9
	f 0 'misc/logo.bmp'                                    aa3dfbd4bc9c863d10a0c5345ae5a4c9
	f 0 'sfx.pak'                                          ea1b3e6d6f4906905d4a34f07e9a59ac
	;;
	
	japanese-demo)
	f 0 'data2.pak'                                        958b78f8f370b06d769843137138c461
	f 0 'data.pak'                                         903dfe1878a0cedff3b941fd3aa22ba9
	f 0 'misc/logo.bmp'                                    aa3dfbd4bc9c863d10a0c5345ae5a4c9
	f 0 'sfx.pak'                                          ea1b3e6d6f4906905d4a34f07e9a59ac
	;;
	
	*) # full game
	
	f 1 'graph/interface/misc/arkane.bmp'                  afff1099c01ffeb03b9a351f7b5966b6
	f 1 'graph/interface/misc/quit1.bmp'                   41445d3792a1f8818d950aca47254488
	f 1 'graph/obj3d/textures/fixinter_barrel.jpg'         8419274acbff7346c3661b18d6aad6dc
	f 1 'graph/obj3d/textures/fixinter_bell.bmp'           5743b9047c9ad65540c318dfcc98123a
	f 1 'graph/obj3d/textures/fixinter_metal_door.jpg'     f246eff6b19c9c710313b4a4dce96a69
	f 1 'graph/obj3d/textures/fixinter_public_notice.bmp'  f81394abbb9006ce0950843b7909db33
	f 1 'graph/obj3d/textures/item_bread.bmp'              544448f8eedc912aa231a6a04fffb7c5
	f 1 'graph/obj3d/textures/item_club.jpg'               7e26c4199ddaca494c8b369294306b0b
	f 1 'graph/obj3d/textures/item_long_sword.jpg'         3a6196fe9b7666c7d80d82be06f6de86
	f 1 'graph/obj3d/textures/item_mauld_sabre.jpg'        18492c25ebac02f83e2f0ebda61ecb00
	f 1 'graph/obj3d/textures/item_mauldsword.jpg'         503a5c2f23668040c675aefdde6dbbe5
	f 1 'graph/obj3d/textures/item_mirror.jpg'             c0a22b4f7a7a6461da68206e94928637
	f 1 'graph/obj3d/textures/item_ring_casting.bmp'       348f9add709bacee08556d1f8cf10f3f
	f 1 'graph/obj3d/textures/item_rope.bmp'               ff05de281c8b380ee98f6e123d3d51cb
	f 1 'graph/obj3d/textures/item_spell_sheet.jpg'        024ccbb520020f92fba5a5a4f0270cea
	f 1 'graph/obj3d/textures/item_torch2.jpg'             027951899b4829599ca611010ea3484f
	f 1 'graph/obj3d/textures/item_torch.jpg'              9ada166f23ddcb775ac20836e752187e
	f 1 'graph/obj3d/textures/item_zohark.bmp'             cd206a4027f86c6e57b7710c94049efa
	f 1 'graph/obj3d/textures/l7_dwarf_[wood]_board08.jpg' 79ccc81adb7c37b98f40b478ef1fccd4
	f 1 'graph/obj3d/textures/l7_dwarf_[wood]_board80.jpg' 691611087b13d38ef02bb9dfd6a2518e
	f 1 'graph/obj3d/textures/npc_dog.bmp'                 116bd374c14ae8c387a4da1899e1dca7
	f 1 'graph/obj3d/textures/npc_pig.bmp'                 b7a4d0d3d230b2d1470176909004e38b
	f 1 'graph/obj3d/textures/npc_pig_dirty.bmp'           76034d8d74056c8a982479d36321c228
	f 1 'graph/obj3d/textures/npc_rat_base.bmp'            00c585ec9ebe8006d7ca72993de7b51b
	f 1 'graph/obj3d/textures/npc_rat_base_cm.bmp'         cae38facbf77db742180b9e58d0eb42f
	f 1 'graph/obj3d/textures/npc_worm_body_part1.jpg'     0b220bffaedc89fa663f08d12630c342
	f 1 'graph/obj3d/textures/npc_worm_body_part2.bmp'     20797cb78f6393a0fb5405969ba9f805
	f 1 'graph/obj3d/textures/[wood]_light_door.jpg'       00d0b018e995e7d013d6e52e92126901
	f 1 'misc/logo.avi'                                    63ed31a4eb3d226c23e58cfaa974d484
	f 1 'misc/logo.bmp'                                    afff1099c01ffeb03b9a351f7b5966b6
	f 1 'data2.pak'                                        f7e0ce700bf963429ac535ca86f8a7b4
	
	f 0 'sfx.pak'                                          2efc9a74c517fd1ee9919900cf4091d2
	
	# data.pak is censored in some versions (presumably has less gore)
	# At least the original german and italian CDs have the censored version.
	# The censored version has different level files and a different
	# human_female_villager model.
	# There are also minor differences in the scripts, but those are
	# overwritten by data2.pak from the 1.21 patch.
	# A third data.pak variant can be found on the original French Arx Fatalis CD:
	# It is almost identical to the preceding censored version, but has different level
	# files for level 1 (.llf only) and 3 (.llf and .fts).
	data_checksum_original='a91a0b39a046233debbb10b4850e13eb'
	data_checksum_censored='a88d239dc7919ab113ff45483cb4ad46'
	data_checksum_frenchcd='7ae3632eef92700cd6c5e143aa0fe67b'
	data_checksum_russiancd='b297ab9ae41a593b13cbdd0ecaf1f999'
	f 0 'data.pak' "$data_checksum_original $data_checksum_censored $data_checksum_frenchcd $data_checksum_russiancd"
	
esac

# Optional files - we don't need them, but copy them anyway if available
optional_file 'manual.pdf'
optional_file 'map.pdf'
optional_file 'arx_handbuch.pdf'

# Not in version 1.22 (except for Russian for arx_russian.ttf and arx_taiwanese.ttf)
optional_file "misc/arx_default.ttf"
optional_file "misc/arx_russian.ttf"
optional_file "misc/arx_taiwanese.ttf"

# Only in version 1.22
optional_file "misc/arx_base.ttf"
optional_file "localisation\presence.ini"
optional_file "localisation\snd_armor.ini"
optional_file "localisation\snd_material.ini"
optional_file "localisation\snd_other.ini"
optional_file "localisation\snd_step.ini"
optional_file "localisation\snd_weapon.ini"
optional_file "localisation\ucredits_chinese.txt"
optional_file "localisation\ucredits_deutsch.txt"
optional_file "localisation\ucredits_english.txt"
optional_file "localisation\ucredits_francais.txt"
optional_file "localisation\ucredits_italiano.txt"
optional_file "localisation\ucredits_russian.txt"
optional_file "localisation\utext_francais.ini"

print


##########################################################################################
# Print a summary

if [ $install = 1 ] ; then verb='Installed' ; else verb='Verified' ; fi
printf "${white}%s Arx Fatalis %s data: ${green}%s${reset}\n" "$verb" \
	"$patch_ver" "$data_lang_desc"

if [ $checksum_failed = 1 ] ; then
	[ $gui = 0 ] || status 100 "Error!"
	die "There are wrong or missing files!${reset}

The game may run fine, or it may fail - good luck!" >&2
fi

status 100 "${dim_green}All good!${reset}"
if [ $install = 1 ] ; then verb='Installation' ; else verb='Verification' ; fi
dialog_message "$verb complete: $data_lang_desc

Have fun playing Arx Fatalis!"

quit $true
