#!/usr/bin/env bash
# This script lets you change the colors of folders in the Papirus icon theme
#
# @author: Sergei Eremenko (https://github.com/SmartFinn)
# @license: MIT license (MIT)
# @link: https://github.com/PapirusDevelopmentTeam/papirus-folders

if test -z "$BASH_VERSION"; then
	printf "Error: this script only works in bash.\n" >&2
	exit 1
fi

if (( BASH_VERSINFO[0] * 10 + BASH_VERSINFO[1] < 40 )); then
	printf "Error: this script requires bash version >= 4.0\n" >&2
	exit 1
fi

# set -x  # Uncomment to debug this shell script
set -o errexit \
	-o noclobber \
	-o pipefail

readonly THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
readonly PROGNAME="$(basename "${BASH_SOURCE[0]}")"
readonly VERSION="1.14.0"
readonly -a ARGS=("$@")

msg() {
	printf "%s: %b\n" "$PROGNAME" "$*"
}

verbose() {
	[ -t 4 ] || return 0
	msg "$@" >&4
}

err() {
	msg "Error:" "$*" >&2
}

_exit() {
	msg "$*" "Exiting ..."
	exit 0
}

fatal() {
	err "$*"
	exit 1
}

usage() {
	cat <<- EOF
	USAGE
	  $ $PROGNAME [options] -t <theme> {-C --color} <color>
	  $ $PROGNAME [options] -t <theme> {-D --default}
	  $ $PROGNAME [options] -t <theme> {-R --restore}

	OPERATIONS
	  -C --color <color>  change color of folders
	  -D --default        back to the default color
	  -R --restore        restore the last used color from the config file

	OPTIONS
	  -l --list           show available colors
	  -o --once           do not save the changes to the config file
	  -t --theme <theme>  make changes to the specified theme (Default: Papirus)
	  -u --update-caches  update icon caches for Papirus and siblings
	  -V --version        print $PROGNAME version and exit
	  -v --verbose        be verbose
	  -h --help           show this help
	EOF

	exit "${1:-0}"
}

_is_root_user() {
	if [ "$(id -u)" -eq 0 ]; then
		return 0
	fi

	return 1
}

_is_user_dir() {
	[ -n "$USER_HOME" ] || return 1

	# if $THEME_DIR is placed in home dir
	if [ -z "${THEME_DIR##"$USER_HOME"/*}" ]; then
		return 0
	fi

	return 1
}

_is_writable() {
	if [ -w "$THEME_DIR/48x48/places/folder.svg" ]; then
		return 0
	fi

	return 1
}

_is_valid_color() {
	local color="$1"

	eval "$(declare_colors)"

	for i in "${colors[@]}"; do
		[ "$i" == "$color" ] || continue
		return 0
	done

	return 1
}

declare_colors() {
	local -a colors=()

	for f in "$THEME_DIR/48x48/places/folder-"*"-documents.svg"; do
		[ -e "$f" ] || continue

		# Extract color from the path
		if [[ $f =~ .*/folder-(.+)-documents.svg ]]; then
			colors=( "${colors[@]}" "${BASH_REMATCH[1]}" )
		fi
	done

	# return array of colors
	declare -p colors
}

declare_current_color() {
	local icon_file icon_name current_color=''

	icon_file=$(readlink -f "$THEME_DIR/48x48/places/folder.svg")
	icon_name=$(basename "$icon_file" .svg)
	current_color="${icon_name##*-}"

	declare -p current_color
}

get_theme_dir() {
	local data_dir icons_dir
	local -a data_dirs=()
	local -a icons_dirs=(
		"$USER_HOME/.icons"
		"${XDG_DATA_HOME:-$USER_HOME/.local/share}/icons"
	)

	# Get data directories from XDG_DATA_DIRS variable and
	# convert colon-separated list into bash array
	IFS=: read -ra data_dirs <<< "${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"

	for data_dir in "${data_dirs[@]}"; do
		[ -d "$data_dir/icons" ] || continue
		icons_dirs=( "${icons_dirs[@]}" "${data_dir%/}/icons" )
	done

	for icons_dir in "${icons_dirs[@]}"; do
		[ -f "$icons_dir/$THEME_NAME/index.theme" ] || continue
		printf '%s' "$icons_dir/$THEME_NAME"
		verbose "'$THEME_NAME' is found in '$icons_dir'."
		return 0
	done

	return 1
}

get_real_user() {
	# return name of the user that runs the script
	local user=''

	if [ -n "$PKEXEC_UID" ]; then
		user="$(id -nu "$PKEXEC_UID")"
	elif [ -n "$SUDO_USER" ]; then
		user="$SUDO_USER"
	else
		user="$(id -nu)"
	fi

	printf '%s' "$user"
}

get_user_home() {
	local user="$1"

	getent passwd "$user" | awk -F: '{print $(NF-1)}'
}

config() {
	# usage: config [{-n --new}] {-s --set} key=value... | {-g --get} key...
	local config_dir
	local config_file

	if _is_user_dir; then
		config_dir="${XDG_CONFIG_HOME:-$USER_HOME/.config}/$PROGNAME"
	else
		config_dir="/var/lib/$PROGNAME"
	fi

	config_file="$config_dir/keep"

	while (( "$#" )); do
		case "$1" in
			-g|--get) shift;
				[ -f "$config_file" ] || return 1

				for key; do
					[ -n "$key" ] || continue
					awk -F= -v key="$key" '
					$1 == key {
						print $2
						exit
					}
					' "$config_file"
				done

				break
				;;
			-n|--new) shift;
				rm -f "$config_file"
				;;
			-e|--exists) shift;
				# return 1 if test config_file not exist or empty
				if [ -f "$config_file" ] && [ -s "$config_file" ]; then
					return 0
				else
					return 1
				fi
				;;
			-s|--set) shift;
				[ "$ONCE" -eq "1" ] && break
				[ -d "$config_dir"  ] || mkdir -p "$config_dir"
				[ -f "$config_file" ] || touch "$config_file"

				verbose "Saving params to '$config_file' ..."
				cat >> "$config_file" <<- EOF
				$(for key_value; do echo "$key_value"; done)
				EOF

				break
				;;
			*)
				err "illegal option -- '$1'"
				return 1
		esac
	done

	return 0
}

change_color() {
	local color="${1:?${FUNCNAME[-1]}: color is not set}"
	local size prefix file_path file_name symlink_path
	local -a sizes=(22x22 24x24 32x32 48x48 64x64)
	local -a prefixes=("folder-$color" "user-$color")

	for size in "${sizes[@]}"; do
		for prefix in "${prefixes[@]}"; do
			for file_path in "$THEME_DIR/$size/places/$prefix"{-*,}.svg; do
				[ -f "$file_path" ] || continue  # is a file
				[ -L "$file_path" ] && continue  # is not a symlink

				file_name="${file_path##*/}"
				symlink_path="${file_path/-$color/}"  # remove color suffix

				ln -sf "$file_name" "$symlink_path" || {
					fatal "Fail to create '$symlink_path' symlink"
				}
			done
		done
	done
}

list_colors() {
	local color='' prefix=''

	eval "$(declare_colors)"
	eval "$(declare_current_color)"

	for color in "${colors[@]}"; do
		if [ "$current_color" == "$color" ]; then
			prefix='>'
		else
			prefix=''
		fi

		printf '%2s %s\n' "$prefix" "$color"
	done
}

do_change_color() {
	_is_valid_color "$SELECTED_COLOR" || {
		fatal "Unable to find '$SELECTED_COLOR' color in '$THEME_NAME'"
	}

	verify_privileges

	msg "Changing color of folders to '$SELECTED_COLOR' for '$THEME_NAME' ..."
	change_color "$SELECTED_COLOR"
	config --new --set "theme=$THEME_NAME" "color=$SELECTED_COLOR"

	case "$THEME_NAME" in
		Papirus-Dark|Papirus-Light)
			update_icon_caches
			;;
		*)
			update_icon_cache
			;;
	esac
}

do_revert_default() {
	verify_privileges

	msg "Restoring default folder color for '$THEME_NAME' ..."
	change_color "${DEFAULT_COLORS[$THEME_NAME]:-blue}"
	config --new

	case "$THEME_NAME" in
		Papirus-Dark|Papirus-Light)
			update_icon_caches
			;;
		*)
			update_icon_cache
			;;
	esac
}

do_restore_color() {
	local saved_color=''

	if config --exists; then
		THEME_NAME="$(config --get theme)"
		saved_color="$(config --get color)"
	else
		_exit "Unable to find config file."
	fi

	THEME_DIR="$(get_theme_dir)" || {
		_exit "Unable to find '$THEME_NAME' icon theme."
	}

	_is_valid_color "$saved_color" || {
		_exit "Unable to find '$saved_color' color in '$THEME_NAME'."
	}

	verify_privileges

	change_color "$saved_color"
	msg "'$saved_color' color of the folders has been restored."
}

delete_icon_caches() {
	local icon_cache real_user='' real_home=''

	real_user="$(get_real_user)"
	real_home="$(get_user_home "$real_user")"

	declare -a icon_caches=(
		# KDE 5 icon caches
		"$real_home/.cache/icon-cache.kcache"
		"/var/tmp/kdecache-$real_user/icon-cache.kcache"
	)

	verbose "Deleting icon caches ..."
	for icon_cache in "${icon_caches[@]}"; do
		[ -e "$icon_cache" ] || continue
		rm -f "$icon_cache"
	done
}

update_icon_cache() {
	[ -z "$DISABLE_UPDATE_ICON_CACHE" ] || return 0

	delete_icon_caches

	verbose "Rebuilding icon cache for '$THEME_NAME' ..."
	gtk-update-icon-cache -qf "$THEME_DIR" || true
}

update_icon_caches() {
	local theme=''

	delete_icon_caches

	for theme in "${!DEFAULT_COLORS[@]}"; do
		[ -f "$THEME_DIR/../$theme/index.theme" ] || continue
		verbose "Rebuilding icon cache for '$theme' ..."
		gtk-update-icon-cache -qf "$THEME_DIR/../$theme" || true
	done
}

verify_privileges() {
	_is_root_user && return 0
	_is_user_dir  && return 0
	_is_writable  && return 0

	verbose "This operation requires root privileges."

	if command -v sudo > /dev/null; then
		exec sudo USER_HOME="$USER_HOME" XDG_DATA_DIRS="$XDG_DATA_DIRS" \
			"$THIS_SCRIPT" "${ARGS[@]}"
	else
		fatal "You need to be root to run this command."
	fi
}

parse_args() {
	local arg='' opt=''
	local -a args=()

	# Show help if no argument is passed
	if [ -z "$1" ]; then
		usage 2
	fi

	# Translate --gnu-long-options to -g (short options)
	for arg; do
		case "$arg" in
			--help)           args+=( -h ) ;;
			--list)           args+=( -l ) ;;
			--once)           args+=( -o ) ;;
			--theme)          args+=( -t ) ;;
			--update-caches)  args+=( -u ) ;;
			--verbose)        args+=( -v ) ;;
			--color|--colour) args+=( -C ) ;;
			--default)        args+=( -D ) ;;
			--restore)        args+=( -R ) ;;
			--version)        args+=( -V ) ;;
			--[0-9a-zA-Z]*)
				err "illegal option -- '$arg'"
				usage 2
				;;
			*) args+=("$arg")
		esac
	done

	# Reset the positional parameters to the short options
	set -- "${args[@]}"

	while getopts ":C:DRlot:uvVh" opt; do
		case "$opt" in
			C ) OPERATIONS+=("change-color")
				SELECTED_COLOR="$OPTARG"
				;;
			D ) OPERATIONS+=("revert-default") ;;
			R ) OPERATIONS+=("restore-color") ;;
			l ) OPERATIONS+=("list-colors") ;;
			o ) ONCE=1 ;;
			t ) THEME_NAME="$OPTARG" ;;
			u ) OPERATIONS+=("update-icon-caches") ;;
			v ) VERBOSE=1 ;;
			V ) printf "%s %s\n" "$PROGNAME" "$VERSION"
				exit 0
				;;
			h ) usage 0 ;;
			: ) err "option requires an argument -- '-$OPTARG'"
				usage 2
				;;
			\?) err "illegal option -- '-$OPTARG'"
				usage 2
				;;
		esac
	done

	shift $((OPTIND-1))

	# Return an error if any positional parameters are found
	if [ -n "$1" ]; then
		err "illegal parameter -- '$1'"
		usage 2
	fi
}

main() {
	# default values of options
	declare THEME_NAME="${THEME_NAME:-Papirus}"
	declare -i VERBOSE="${VERBOSE:-0}"
	declare -i ONCE="${ONCE:-0}"
	declare -A DEFAULT_COLORS=(
		['Papirus']='blue'
		['Papirus-Dark']='blue'
		['Papirus-Light']='blue'
	)

	declare SELECTED_COLOR=''
	declare -a OPERATIONS=()

	parse_args "${ARGS[@]}"

	if [ "$VERBOSE" -eq "1" ]; then
		# open a file descriptor for verbose messages
		exec 4>&1
		# close the file descriptor before exiting
		trap 'exec 4>&-' EXIT HUP INT TERM
	fi

	# set USER_HOME variable instead HOME to prevent changing user's icons
	# when running with sudo
	[ -n "$USER_HOME" ] || USER_HOME="$(get_user_home "$(id -nu)")"

	if [ -f "$THEME_NAME/index.theme" ]; then
		# THEME_NAME is a path to an icon theme
		THEME_DIR="$(readlink -f "$THEME_NAME")"
		THEME_NAME="$(basename "$THEME_DIR")"
		verbose "The path to '$THEME_DIR' theme is specified."
	else
		THEME_DIR="$(get_theme_dir)" || {
			fatal "Fail to find '$THEME_NAME' icon theme."
		}
	fi

	for operation in "${OPERATIONS[@]}"; do
		case "$operation" in
			change-color)
				do_change_color
				;;
			revert-default)
				do_revert_default
				;;
			restore-color)
				do_restore_color
				;;
			list-colors)
				if [ -t 1 ]; then
					cat <<- EOF
					List of available colors:

					$(list_colors)

					EOF
				else
					list_colors
				fi
				;;
			update-icon-caches)
				verify_privileges
				update_icon_caches
				;;
		esac
	done

	verbose "Done!"

	exit 0
}

main

exit 1
