#!/usr/bin/env bash
set -e;
# This script takes all the jpg and mp4 files in a directory and
# organises them into a folder structure based on the month and year
# they were last modified (taken).
# 
# Licence: Mozilla Public License 2.0
# Full text: https://www.mozilla.org/en-US/MPL/2.0/
# Summary: https://tldrlegal.com/license/mozilla-public-license-2.0-(mpl-2)

# Sources and References
#  - Bash controlled paralellisation - http://unix.stackexchange.com/a/216475/64687

op_script_path="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )/organise-photos";
export op_script_path;

###############################################################################

temp_dir="$(mktemp --tmpdir -d "organise-photos-XXXXXXX")";
on_exit() {
	rm -rf "${temp_dir}";
}
trap on_exit EXIT;

for name in original gps filename mtime ctime mtime_exif; do
	mkdir -p "${temp_dir}/date-${name}";
done

###############################################################################

###
# Misc. utilities
###

# $1	The tag to log.
log_date_loc() {
	tag="${1}";
	if [[ -z "${tag}" ]]; then
		echo "Error: empty tag specified.";
		return 1;
	fi
	mktemp --tmpdir="${temp_dir}/date-${tag}" >/dev/null 2>&1;
}

command_exists() {
	which $1 >/dev/null 2>&1;
	return "$?";
}

check_command_alternatives() {
	thing_name="${1}"; shift;
	
	cmd_name_first="${1}";
	
	while [[ ! -z "${1}" ]]; do
		cmd_name="${1}";
		message="Checking ${cmd_name} to provide ${thing_name} -";
		if command_exists "${cmd_name}"; then
			[[ -z "${QUIET_OP}" ]] && echo "${message} success" >&2;
			echo "${cmd_name}";
			exit 0;
		fi
		
		[[ -z "${QUIET_OP}" ]] && echo "${message} failed" >&2;
		shift;
	done
	
	echo "No more alternatives to try - please install at least 1 of the above to continue (${cmd_name_first} is recommended)" >&2;
	exit 4;
}

# Checks to make sure a specified command is present.
# Adapted from the lantern build engine: https://gitlab.com/sbrl/lantern-build-engine
# $1 - Command name to check for
check_command() {
	if [[ -z "${QUIET_OP}" ]]; then echo -n "Checking for ${1} - "; fi
	if ! command_exists "${1}"; then
		echo "failed! Couldn't locate ${1}. Make sure it's installed and in your path.";
		exit 2;
	fi
	
	if [[ -z "${QUIET_OP}" ]]; then echo "success"; fi
}

# From https://github.com/dylanaraps/pure-bash-bible#use-regex-on-a-string
regex() {
	# Usage: regex "string" "regex"
	[[ $1 =~ $2 ]] && printf '%s\n' "${BASH_REMATCH[1]}"
}
# From https://github.com/dylanaraps/pure-bash-bible#trim-leading-and-trailing-white-space-from-string
# Usage: trim_string "   example   string    "
trim_string() {
	: "${1#"${1%%[![:space:]]*}"}"
	: "${_%"${_##*[![:space:]]}"}"
	printf '%s\n' "$_"
}

## Returns the size of a file in bytes.
# $1	The path to the file to look at.
filesize() {
	stat --printf="%s" "${1}";
}

###############################################################################


#  ██████ ██      ██
# ██      ██      ██
# ██      ██      ██
# ██      ██      ██
#  ██████ ███████ ██

check_command grep;
exif_reader="$(check_command_alternatives "exif manipulator" exiftool jhead)";
check_command jpegoptim; jpeg_optimiser="jpegoptim"; # Anyone got a better one we could use?
png_optimiser="$(check_command_alternatives "png optimiser" oxipng optipng)";
check_command find;
check_command xargs;
check_command mogrify;
check_command mkdir;
check_command mv;
check_command cut;
imagemagick_convert="convert";
if ! command_exists "${imagemagick_convert}"; then
	echo "Warning: ImageMagick's convert command not found, thumbnail generation will be unavailable" >&2;
	imagemagick_convert="";
fi
# For buffering jpegoptim output to make it neater & easier to understand
sponge="sponge";
if ! command_exists "${sponge}"; then
	sponge="cat";
fi

if [[ "${exif_reader}" != "exiftool" ]]; then
	echo "Warning: exiftool not found, using ${exif_reader} (limits reading exif data to jpeg images)" >&2;
fi
if [[ "${png_optimiser}" != "oxipng" ]]; then
	echo "Warning: oxipng not found, using ${png_optimiser} (better lossless compression can be achieved with oxipng; oxipng is not multithreaded)";
fi

if [[ -z "${DRY_RUN}" ]]; then
	export DRY_RUN=false;
fi

export mode="MAIN"; # MAIN, FILE
filename="";

while test "$#" -gt 0
do
	case "$1" in
		--help)
			echo organise-photos
			echo "    by Starbeamrainbowlabs <feedback@starbeamrainbowlabs.com>"
			echo "This script takes all the jpg and mp4 files in a directory and organises them into a folder structure based on the month and year they were last modified (taken)."
			echo "Subcommands:"
			echo "    file <path/to/file>         Process only the specified file (does NOT preprocess it - e.g. autorotate, optimise, thumbnail, etc)"
			echo "    thumbnail <path/to/file>    Write optimised thumbnail for the specified file if possible"
			echo "    optimise <path/to/file>     Optimise a specified png or jpeg"
			echo "    optimise_dir                Optimise all the pngs and jpegs in the current directory recursively";
			echo "    optimise_dir_single         Optimise all the pngs and jpegs in directly in the current directory";
			echo 
			echo "Options:"
			echo "    --help"
			echo "         Show this help message"
			echo "    --dry-run"
			echo "         Do a dry run - don't actually move any files."
			exit 
			;;
		--dry-run)
			echo Activating dry run mode.
			export DRY_RUN=true;
			;;
		
		# Thumbnails
		file)
			export mode="FILE";
			shift;
			filename="${1}";
			;;
		
		thumbnail )
			export mode="THUMBNAIL";
			shift;
			filename="${1}";
			;;
			
		optimise )
			export mode="OPTIMISE_FILE";
			shift;
			filename="${1}";
			;;
		
		optimise_dir_single )
			export mode="OPTIMISE_DIR";
			;;
		optimise_dir )
			export mode="OPTIMISE_DIR_RECURSIVE";
			;;
	esac
	shift
done

# Controls the maximum allowed processes to run in parallel.
# Useful if your system has limited RAM but many CPU cores (e.g. Raspberry Pi 4 1GB RAM)
export MAX_PARALLEL="${MAX_PARALLEL}";

if [[ ! -z "${MAX_PARALLEL}" ]] && [[ "${mode}" == "MAIN" ]]; then
	echo ">>> Max parallel processes is ${MAX_PARALLEL}" >&2;
fi

# $1	The number of CPU cores requested (defaults to $(nproc))
parallel() {
	nproc="$1";
	if [[ -z "${nproc}" ]]; then
		nproc="$(nproc)";
	fi
	if [[ ! -z "${MAX_PARALLEL}" ]] && [[ "${nproc}" -gt "${MAX_PARALLEL}" ]]; then
		nproc="${MAX_PARALLEL}";
	fi
	echo "${nproc}";
}

###############################################################################

# ██████  ██████  ███████ ██████  ██████   ██████   ██████ ███████ ███████ ███████
# ██   ██ ██   ██ ██      ██   ██ ██   ██ ██    ██ ██      ██      ██      ██
# ██████  ██████  █████   ██████  ██████  ██    ██ ██      █████   ███████ ███████
# ██      ██   ██ ██      ██      ██   ██ ██    ██ ██      ██           ██      ██
# ██      ██   ██ ███████ ██      ██   ██  ██████   ██████ ███████ ███████ ███████

autorotate_jpegs() {
	if ! command_exists mogrify; then
		echo "Warning: Imagemagick not installed (specifically the mogrify command), can't autorotate jpegs" >&2;
		return 0;
	fi
	find . -maxdepth 1 \( -iname "*.jpg" -o -iname "*.jpeg" \) -print0 | xargs -0 -P "$(parallel)" -I {} sh -c 'filename="{}"; mogrify -auto-orient "${filename}"; exitcode=$?; if [ "${exitcode}" -ne 0 ]; then echo "mogrify: exit code ${exitcode} for ${filename}"; fi; true';
}

## Returns the command used to optimise a given JPEG image.
# $1	Optional. If set to "strip", then all metadata is stripped from the image.
__cmd_optimise_jpeg() {
	local flags="${1}";
	local command="";
	
	case "${jpeg_optimiser}" in
		jpegoptim )
			command="jpegoptim --preserve --all-progressive";
			# If QUIET_OP=notoptimise, then everyone but us should be quiet
			if [[ ! -z "${QUIET_OP}" ]] && [[ "${QUIET_OP}" != "notoptimise" ]];
				then command="${command} --quiet";
			fi
			if [[ "${flags}" == "strip" ]]; then command="${command} --strip-all"; fi
			;;
		* ) echo "No jpeg optimiser set" >&2; exit 5; ;;
	esac
	echo "${command}";
}
optimise_jpegs() {
	if [[ "$(find . -maxdepth 1 -regextype egrep -iregex '^.*\.jpe?g$' | wc -l)" -eq "0" ]]; then return 0; fi
	
	local command
	command="$(__cmd_optimise_jpeg)";
	
	find . -maxdepth 1 -regextype egrep -iregex '^.*\.jpe?g$' -print0 | xargs -0 -I {} -P "$(parallel)" sh -c "${command} '{}' | ${sponge}; true";
}

## Returns the command used to optimise a given PNG images.
# PNGS smaller than 25MiB: -Z
# PNGS larger than 25 MiB: -D
# $1	The path to the png image. Used to determine which optimisation method to use.
# $2	Optional. If set to "strip", then all metadata is stripped from the image.
__cmd_optimise_png() {
	local filepath="${1}"
	
	local command="";
	case "${png_optimiser}" in
		optipng )
			command="optipng -o7 -preserve";
			# If QUIET_OP=notoptimise, then everyone but us should be quiet
			if [[ ! -z "${QUIET_OP}" ]] && [[ "${QUIET_OP}" != "notoptimise" ]]; then
				command="${command} -quiet";
			fi
			if [[ "${flags}" == "strip" ]]; then command="${command} -strip all"; fi
			;;
		oxipng )
			command="oxipng -omax -a";
			# 26214400 = 25 MiB
			if [[ ! -z "${filepath}" ]] && [[ "$(filesize "${filepath}")" -gt 26214400 ]]; then
				command="${command} -D"; # -Z is way too slow for large images, but -D is still ok
			else
				command="${command} -Z"; # Slow, but slightly better compression
			fi
			if [[ "${flags}" == "strip" ]]; then
				command="${command} -s"; # Strip safe
			else
				command="${command} -p"; # Preserve
			fi
			# If QUIET_OP=notoptimise, then everyone but us should be quiet
			if [[ ! -z "${QUIET_OP}" ]] && [[ "${QUIET_OP}" != "notoptimise" ]]; then
				command="${command} --quiet";
			fi
			;;
		* ) echo "No png optimiser set" >&2; exit 5; ;;
	esac
	
	echo "${command}";
}

optimise_pngs() {
	if [[ "$(find . -maxdepth 1 -iname "*.png" | wc -l)" -eq "0" ]]; then return 0; fi
	
	local command
	command="$(__cmd_optimise_png)";
	
	find . -maxdepth 1 -iname "*.png" -print0 | xargs -0 -P "$(parallel)" -n1 ${command};
}

## Optimises a single image file (currently only png or jpeg).
# $1	The path to the image file to optimise.
# $2	Optional. Any flags to alter the way the command functions. Currently supported: strip (strip all metadata)
optimise_file() {
	local filepath="${1}" flags="${2}" extension command;
	extension="$(echo "${filepath##*.}" | tr '[:upper:]' '[:lower:]')";
	
	if [[ ! -r "${filepath}" ]]; then
		echo "Error: '${filepath}' is not readable (does it exist and the read permission set?)" >&2;
		return 6;
	fi
	
	local local_sponge="${sponge}";
	
	case "${extension}" in
		png )
			command="$(__cmd_optimise_png "${filepath}" "${flags}")";
			# PNGs take longer, so it's useful to know which one we're on, but
			# only in interactive mode. When running non-interactively
			# (e.g. via cron), then we don't care how long it takes.
			# Ref https://www.cyberciti.biz/faq/linux-unix-bash-check-interactive-shell/
			if [[ $- == *i* ]]; then local_sponge="cat"; fi
			;;
		jpeg | jpg ) command="$(__cmd_optimise_jpeg "${flags}")"; ;;
	esac
	
	${command} "${filepath}" | ${local_sponge};
}

## Determines if the given file is an image or not.
# Returns exit code 0 if it is, and 1 if it is not.
# $1	The path to the file to inspect.
is_image() {
	local filepath="${1}" mime;
	mime="$(file --brief --mime-type "${filepath}")";
	
	case "${mime}" in
		image/* ) return 0; ;;
		* ) return 1; ;;
	esac
}

###############################################################################

# ███████ ██   ██ ██ ███████     ██ ███    ██ ███████  ██████
# ██       ██ ██  ██ ██          ██ ████   ██ ██      ██    ██
# █████     ███   ██ █████       ██ ██ ██  ██ █████   ██    ██
# ██       ██ ██  ██ ██          ██ ██  ██ ██ ██      ██    ██
# ███████ ██   ██ ██ ██          ██ ██   ████ ██       ██████

# Extracts the exif daata from the given filename.
# $1	The path to the file to examine
get_exif_info() {
	local filepath="${1}" result exit_code command=""
	case "${exif_reader}" in
		exiftool ) command="exiftool -dateFormat %Y-%m-%dT%H:%M:%S"; ;;
		jhead ) command="jhead"; ;;
	esac
	
	set +e;
	result="$(${command} "${filepath}")";
	exit_code="${?}"; set -e;
	
	if [[ "${exif_reader}" == "exiftool" ]] \
		&& [[ "${exit_code}" -ne 0 ]] \
		&& command_exists "jhead"; then
		set +e;
		result="$(jhead "${filepath}")";
		exit_code="${?}";
		set -e;
	fi
	
	if [[ "${exit_code}" -eq 0 ]]; then
		echo "${result}";
		return 0;
	fi
	return 1;
}

# Extracts the value of a specific field name from some exif data.
get_exif_field() {
	local data="${1}";
	local field_name="${2}";
	
	row="$(echo "${data}" | grep --max-count=1 -i "${field_name}")";
	
	if [[ "$(trim_string "${row}" | wc -c)" -eq 0 ]]; then return; fi
	
	value="$(echo "${row}" | cut -d ':' -f 2- )";
	
	echo "$(trim_string "${value}")";
}

# Sets the date taken (DateTimeOriginal) for the given filename (usually an image).
# $1	The path to the file to update
# $2	The new date.
set_datetimeoriginal() {
	filepath="${1}";
	new_date="${2}";
	
	case "${exif_reader}" in
		exiftool )
			if [[ "${DRY_RUN}" == "true" ]]; then return 0; fi
			set +e;
			exiftool "-DateTimeOriginal=$(exiftool_date "${new_date}")" "${filepath}" >&2;
			set -e;
			;;
		jhead )
			if [[ "${DRY_RUN}" == "true" ]]; then return 0; fi
			set +e;
			jhead -ts"$(jhead_date "${new_date}")" "${filepath}" >&2;
			set -e;
			;;
		* )
			echo "set_datetimeoriginal Warning: Unsupported exif manipulator ${exif_reader}" >&2;
			return 1;
			;;
	esac
}


###############################################################################

# ██████   █████  ████████ ███████      ██  ████████ ██ ███    ███ ███████
# ██   ██ ██   ██    ██    ██          ██      ██    ██ ████  ████ ██
# ██   ██ ███████    ██    █████      ██       ██    ██ ██ ████ ██ █████
# ██   ██ ██   ██    ██    ██        ██        ██    ██ ██  ██  ██ ██
# ██████  ██   ██    ██    ███████  ██         ██    ██ ██      ██ ███████
# 
# ██   ██  █████  ███    ██ ██████  ██      ██ ███    ██  ██████
# ██   ██ ██   ██ ████   ██ ██   ██ ██      ██ ████   ██ ██
# ███████ ███████ ██ ██  ██ ██   ██ ██      ██ ██ ██  ██ ██   ███
# ██   ██ ██   ██ ██  ██ ██ ██   ██ ██      ██ ██  ██ ██ ██    ██
# ██   ██ ██   ██ ██   ████ ██████  ███████ ██ ██   ████  ██████

# Standardises a date/time string to be in the RFC3339 format.
# Also converts the date/time to UTC and strips the timezone.
# $1	The date to standardise.
standardise_date() {
	source="${1}";
	
	date --date="${source}" --utc --rfc-3339=seconds | cut -d+ -f 1;
}
# Converts a date to the format accepted by exiftool.
# $1	The date to convert.
exiftool_date() {
	source="${1}";
	
	date --date="${source}" --utc +"%Y:%m:%d %H:%M:%S";
}
# Converts a date to the format accepted by jhead.
# $1	The date to convert.
jhead_date() {
	source="${1}";
	
	date --date="${source}" --utc +"%Y:%m:%d-%H:%M:%S";
}
# Converts a date to the format we use for year directory names.
# $1	The date to convert.
directory_year_date() {
	source="${1}";
	
	date --date="${source}" --utc +"%Y";
}
# Converts a date to the format we use for month directory names.
# $1	The date to convert.
directory_month_date() {
	source="${1}";
	
	date --date="${source}" --utc +"%m-%B";
}
directorypath_yearmonth_date() {
	source="${1}";
	echo "$(directory_year_date "${source}")/$(directory_month_date "${source}")";
}

# Finds the date from a file.
# Finds and extracts the following in preferential order:
# 1. Date taken from exif data (many filetypes support this - not just JPEG)
# 2. Date in filename
# 3. File metadata: EITHER creation time (ctime) OR last modification time (mtime), whichever is earlier
# 
# exiftool is preferred, but if it's not installed or crashes, jhead is tried instead.
# 
# If the date is not found in the date taken EXIF field (DateTimeOriginal),
# then we attempt to set the DateTimeOriginal EXIF field for the file in
# question. 
# 
# $1	The path to the file to investigate
find_file_date() {
	filepath="${1}";
	exif="$(get_exif_info "${filepath}")"; exit_code="$?";
	# echo -e "DEBUG exif_info\n$exif" >&2;
	
	if [[ ! -z "${exif}" ]]; then
		# 1: Try the date taken (DateTimeOriginal)
		date_original="$(get_exif_field "${exif}" "Date/Time Original")";
		if [[ "${exif_reader}" == "jhead" ]] && [[ -z "${date_original}" ]]; then
			date_original="$(get_exif_field "${exif}" "Date/Time")";
		fi
		if [[ ! -z "${date_original}" ]]; then
			log_date_loc "original";
			echo "${date_original}"; return 0;
		fi
		
		# 2: Try the GPS date/time
		date_gps="$(get_exif_field "${exif}" "GPS Date/Time")";
		if [[ ! -z "${date_gps}" ]]; then
			set +e; set_datetimeoriginal "${filepath}" "${date_gps}"; set -e;
			log_date_loc "gps";
			echo "$(standardise_date "${date_gps}")";
			return 0;
		fi
		
		# Other fields we DON'T check:
		# Profile Date Time		This is the date/time of the ICC profile, not the image itself (ref https://photo.stackexchange.com/a/82496)
	fi
	
	# 3: Filename
	# Look for a subtring in the basename like EITHER 20210817 OR 2021-08-17
	# with NO numbers either side
	date_filename="$(basename "${filepath}" | grep -iPoh '(?<![0-9])[0-9]{8}(?![0-9])')";
	if [[ -z "${date_filename}" ]]; then
		date_filename="$(basename "${filepath}" | grep -iPoh '(?<![0-9])[0-9]{4}-[0-1][0-9]-[0-3][0-9](?![0-9])' | tr -d '-')";
	fi
	
	# Only process it if the date found was valid
	if [[ ! -z "${date_filename}" ]] && ! standardise_date "${date_filename}" >/dev/null 2>&1; then
		if [[ ! -z "${exif}" ]]; then
			set +e; set_datetimeoriginal "${filepath}" "${date_filename}"; set -e;
		fi
		log_date_loc "filename";
		echo "$(standardise_date "${date_filename}")";
		return 0;
	fi
	
	# 4: EITHER mtime OR ctime OR mtime_exif, whichever is earlier
	date_mtime="$(standardise_date "$(stat -c "%y" "${filepath}")")";
	date_ctime="$(get_exif_field "${exif}" "Create Date")";
	date_mtime_exif="$(get_exif_field "${exif}" "Modify Date")";
	if [[ ! -z "${date_ctime}" ]]; then
		date_ctime="$(standardise_date "${date_ctime}")";
	fi
	if [[ ! -z "${date_mtime_exif}" ]]; then
		date_mtime_exif="$(standardise_date "${date_mtime_exif}")";
	fi
	
	date_exif="${date_ctime}";
	choice="ctime";
	# Pick the best out of 
	if [[ -z "${date_exif}" ]] || [[ "$(date --date="${date_ctime}" +%s)" -lt "$(date --date="${date_mtime_exif}" +%s)" ]]; then
		choice="mtime_exif";
		date_exif="${date_mtime_exif}";
	fi
	
	if [[ -z "${date_exif}" ]] || [[ "$(date --date="${date_mtime}" +%s)" -lt "$(date --date="${date_exif}" +%s)" ]]; then
		# The mtime is before the ctime
		if [[ ! -z "${exif}" ]]; then
			set +e; set_datetimeoriginal "${filepath}" "${date_mtime}"; set -e;
		fi
		log_date_loc "mtime";
		echo "${date_mtime}";
	else
		if [[ ! -z "${exif}" ]]; then
			set +e; set_datetimeoriginal "${filepath}" "${date_exif}"; set -e;
		fi
		log_date_loc "${choice}";
		echo "${date_exif}";
	fi
}


###############################################################################


# ████████ ██   ██ ██    ██ ███    ███ ██████  ███    ██  █████  ██ ██
#    ██    ██   ██ ██    ██ ████  ████ ██   ██ ████   ██ ██   ██ ██ ██
#    ██    ███████ ██    ██ ██ ████ ██ ██████  ██ ██  ██ ███████ ██ ██
#    ██    ██   ██ ██    ██ ██  ██  ██ ██   ██ ██  ██ ██ ██   ██ ██ ██
#    ██    ██   ██  ██████  ██      ██ ██████  ██   ████ ██   ██ ██ ███████
# 
#  ██████  ███████ ███    ██ ███████ ██████   █████  ████████ ██  ██████  ███    ██
# ██       ██      ████   ██ ██      ██   ██ ██   ██    ██    ██ ██    ██ ████   ██
# ██   ███ █████   ██ ██  ██ █████   ██████  ███████    ██    ██ ██    ██ ██ ██  ██
# ██    ██ ██      ██  ██ ██ ██      ██   ██ ██   ██    ██    ██ ██    ██ ██  ██ ██
#  ██████  ███████ ██   ████ ███████ ██   ██ ██   ██    ██    ██  ██████  ██   ████

make_thumbnail() {
	local filepath_source="${1}";
	local source_extension="${filepath_source##*.}" target_extension exiftool_args_extra;
	target_extension="$(echo "${source_extension}" | tr '[:upper:]' '[:lower:]')";
	
	# We don't handle anything but images
	# Technically mp4s for example can contain thumbnails too, but handling
	# them correctly would first require an ffmpeg call to extract a frame to
	# work with first. Doable, but currently out-of-scope.
	if ! is_image "${filepath_source}"; then
		return 0;
	fi
	
	if [[ "${exif_reader}" != "exiftool" ]]; then
		if [[ -z "$QUIET_OP" ]]; then echo "Can't create thumbnails because exiftool is not the current exif handling program (current: ${exif_reader})." >&2; fi
		return 1;
	fi
	
	# Thumbnail images for PNGs are actually JPEGs believe it or not
	# If you try and set a PNG as the thumbnail, you get this error:
	# Warning: [Minor] Not a valid image for Olympus:ThumbnailImage
	# 	0 image files updated
	# 	1 image files unchanged
	if [[ "${target_extension}" == "png" ]]; then target_extension="jpeg"; fi
	
	filepath_thumbnail="$(mktemp --tmpdir="${temp_dir}" --suffix=".${target_extension}")";
	
	if [[ ! -z "${QUIET_OP}" ]]; then exiftool_args_extra="${exiftool_args_extra} -quiet"; fi
	
	convert "${filepath_source}" -distort Resize 100x100 "${filepath_thumbnail}";
	
	optimise_file "${filepath_thumbnail}" strip; # This helps reduce filesize significantly, and most metadata is completely useless in a thumbnail as far as I'm aware
	
	exiftool "-ThumbnailImage<=${filepath_thumbnail}" "${filepath_source}";
	exit_code="${?}";
	if [[ "${exit_code}" -ne 0 ]]; then
		return "${exit_code}";
	fi
	
	rm "${filepath_source}_original";
}

make_thumbnails() {
	find . -maxdepth 1 \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.gif' -o -iname '*.tiff' -o -iname '*.png' \) -print0 | xargs -0 -I {} -P "$(parallel)" sh -c 'QUIET_OP=true "${op_script_path}" thumbnail "{}"; exitcode=$?; if [ "${exitcode}" -ne 0 ]; then echo "exit code ${exitcode}"; fi; true';
}


###############################################################################

# ██   ██  █████  ███    ██ ██████  ██      ███████         ███████ ██ ██      ███████
# ██   ██ ██   ██ ████   ██ ██   ██ ██      ██              ██      ██ ██      ██
# ███████ ███████ ██ ██  ██ ██   ██ ██      █████           █████   ██ ██      █████
# ██   ██ ██   ██ ██  ██ ██ ██   ██ ██      ██              ██      ██ ██      ██
# ██   ██ ██   ██ ██   ████ ██████  ███████ ███████ ███████ ██      ██ ███████ ███████

handle_file() {
	local filename="$1";
	
	if [[ -z "${filename}" ]]; then
		echo "Error: Empty filename specified." >&2;
		return 10;
	fi
	
	if [[ ! -s "${filename}" ]]; then
		echo "Error: Refusing to operate on empty file at '${filename}'." >&2;
		return 11;
	fi
	
	message="Processing ${filename} -";
		
	date_filename="$(find_file_date "${filename}")";
	
	if [[ -z "${date_filename}" ]]; then
		echo -e "${message} error, failed to find datetime (this is a bug)" >&2;
		return 12;
	fi
	
	new_directory="$(directorypath_yearmonth_date "${date_filename}")";
	new_filename="${new_directory}/$(basename "${filename}")";
	
	if [[ -z "${new_directory}" ]] || [[ "${new_directory}" == "/" ]]; then
		echo -e "${message} error, failed to calculate new directory name" >&2;
		return 13;
	fi
	
	message="${message} filing in ${new_directory}";
	
	if [[ "${DRY_RUN}" == true ]] ; then
		echo dry run
		return;
	fi
	
	if [[ ! -d "${new_directory}" ]]; then mkdir -p "${new_directory}"; fi
	mv "${filename}" "${new_filename}";
	exit_code="$?";
	if [[ "${exit_code}" -ne 0 ]]; then
		echo -e "${message}error, mv exited with code $?)" >&2;
		return "${exit_code}";
	fi
	
	# Delete exiftool backup files, if any
	if [[ -r "${filename}_original" ]]; then
		rm "${filename}_original";
	fi
	
	echo -e "${message}";
}


###############################################################################

# ███    ███  █████  ██ ███    ██
# ████  ████ ██   ██ ██ ████   ██
# ██ ████ ██ ███████ ██ ██ ██  ██
# ██  ██  ██ ██   ██ ██ ██  ██ ██
# ██      ██ ██   ██ ██ ██   ████

do_main() {
	if [[ "${DRY_RUN}" == false ]]; then
		echo -e "*** Automatically rotating pictures [1 / 4] ***";
		# Automatically rotate the images according to their exif data
		autorotate_jpegs;
		echo -e "*** done ***";
		echo -e "*** Writing thumbnails [2 / 4] ***";
		# Generate thumbnails - useful for fast previewing in Windows File Explorer
		make_thumbnails;
		echo -e "*** done ***";
		
		echo -e "*** Optimising images [3 / 4] ***";
		echo -e " * Optimising jpegs";
		optimise_jpegs;
		echo -e " * Optimising pngs";
		optimise_pngs
		echo -e "*** done ***";
	fi
	
	
	# No need to wrap this in a dry run if, since we do it on a per-file level
	# i=0; # N is set above
	echo -e "*** Filing images [4 / 4] ***";
	# find . -maxdepth 1 \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.gif" -o -iname "*.mp4" -o -iname "*.avi" -o -iname "*.mov" -o -iname "*.webm" -o -iname "*.png" \) -print0 | while read -r -d '' filename
	# do
	# 	set +e;
	# 	((i=i%N)); ((i++==0)) && wait -n; # Wait for the next job to complete
	# 	set -e;
	# 	handle_file "${filename}" &
	# done
	export QUIET_OP="yes";
	# find . -maxdepth 1 \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.gif" -o -iname "*.mp4" -o -iname "*.avi" -o -iname "*.mov" -o -iname "*.webm" -o -iname "*.png" \) -print0 | xargs -0 -P "$(parallel)" -I {} sh -c 'organise-photos --file "{}"; exitcode=$?; if [ "${exitcode}" -ne 0 ]; then echo "exit code ${exitcode}"; fi; true';
	find . -maxdepth 1 \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.gif" -o -iname "*.mp4" -o -iname "*.avi" -o -iname "*.mov" -o -iname "*.webm" -o -iname "*.png" \) -print0 | xargs -0 -I {} -P "$(parallel)" sh -c '"${op_script_path}" file "{}"; exitcode=$?; if [ "${exitcode}" -ne 0 ]; then echo "exit code ${exitcode}"; fi; true';
	
	wait
	
	# Doesn't work, because images are handled by a subprocess which has a different
	# temporary directory
	# count_date_original="$(ls "${temp_dir}/date-original" | wc -l)";
	# count_date_gps="$(ls "${temp_dir}/date-gps" | wc -l)";
	# count_date_filename="$(ls "${temp_dir}/date-filename" | wc -l)";
	# count_date_mtime="$(ls "${temp_dir}/date-mtime" | wc -l)";
	# count_date_ctime="$(ls "${temp_dir}/date-ctime" | wc -l)";
	# count_date_mtime_exif="$(ls "${temp_dir}/date-mtime_exif" | wc -l)";
	# 
	# echo "date taken stats: original: ${count_date_original} gps: ${count_date_gps} filename: ${count_date_filename} mtime: ${count_date_mtime} ctime: ${count_date_ctime} mtime_exif: ${count_date_mtime_exif}";
	
	echo "*** Complete! ***"
}

# Handles a single file. Note that it does NOT optimise it, as that's done all
# at once in the main function as then it's easier to avoid over-saturating the CPU.
# $1 - The path to the file to operate on.
do_file() {
	filename="${1}";
	
	handle_file "${filename}";
	return "${?}";
}

case "${mode}" in
	MAIN )
		do_main;
		;;
	
	FILE )
		do_file "${filename}";
		;;
	
	THUMBNAIL )
		make_thumbnail "${filename}";
		;;
	
	OPTIMISE_FILE )
		optimise_file "${filename}";
		;;
		
	OPTIMISE_DIR )
		echo -e "*** Optimising images ***";
		echo -e " * Optimising jpegs [ 1 / 2 ]";
		optimise_jpegs;
		echo -e " * Optimising pngs [ 2 / 2 ]";
		optimise_pngs
		echo -e "*** done ***";
		;;
	
	OPTIMISE_DIR_RECURSIVE )
		echo -e "*** Optimising jpegs [ 1 / 2 ] ***";
		find . -regextype egrep -iregex '^.*\.jpe?g$' -print0 | xargs -0 -I{} -P "$(parallel)" sh -c 'QUIET_OP=notoptimise "${op_script_path}" optimise "{}"; exitcode=$?; if [ "${exitcode}" -ne 0 ]; then echo "exit code ${exitcode}"; fi; true';
		
		echo -e "*** Optimising pngs [ 2 / 2 ] ***";
		# Calulate the number of images we should optimise in parallel to saturate the CPU
		png_parallel="$(($(nproc) / 6))"; # oxipng -D / oxipng -Z only run 6 tests at once
		if [[ "${png_parallel}" -gt 1 ]]; then png_parallel="$((png_parallel+1))"; fi
		# optipng is single threaded :-/
		if [[ "${png_optimiser}" == "optipng" ]]; then png_parallel="$(nproc)"; fi
		png_parallel="$(parallel "${png_parallel}")";
		
		find . -iname "*.png" -print0 | xargs -0 -I {} -P "${png_parallel}" sh -c 'QUIET_OP=true "${op_script_path}" optimise "{}"; exitcode=$?; if [ "${exitcode}" -ne 0 ]; then echo "exit code ${exitcode}"; fi; true'
		echo -e "*** done ***";
		;;
	
esac

exit 0