#! /usr/bin/env bash

set -e

export LANG='C.UTF-8'
export LANGUAGE="${LANG}"

_sed () {
	case "${system_name}" in
		'macos'|'freebsd')
			gsed "${@}"
			;;
		*)
			sed "${@}"
			;;
	esac
}

_cp () {
	case "${system_name}" in
		'macos'|'freebsd')
			gcp -R --preserve=timestamps -H -L "${1}" "${2}"
			;;
		*)
			cp -R --preserve=timestamps -H -L "${1}" "${2}"
			;;
	esac
}

Common::noOp () {
	true
}

Common::getPath () {
	local file_path="${1}"

	if command -v cygpath >/dev/null
	then
		if [ "${file_path}" = '-' ]
		then
			tr '\n' '\0' \
			| xargs -0 -P1 -I{} \
				cygpath --unix '{}'
		else
			cygpath --unix "${file_path}"
		fi
	else
		if [ "${file_path}" = '-' ]
		then
			cat
		else
			printf '%s\n' "${file_path}"
		fi
	fi \
	| _sed -e 's|/*$||'
}

Common::grepLdd () {
	case "${system_name}" in
		'macos')
			egrep '^\t/'
			;;
		*)
			egrep ' => '
			;;
	esac
}

Common::stripLdd () {
	case "${system_name}" in
		'macos')
			_sed -e 's/^\t\(.*\) (compatibility version .*/\1/'
			;;
		*)
			_sed -e 's/ (0x[0-9a-f]*)$//;s/^.* => //'
			;;
	esac
}

Multi::excludeLdd () {
	case "${system_name}" in
		'linux'|'freebsd')
			# - always bundle built-in libraries
			# - always rely on up-to-date x11 and gl libraries, bundling them will break on future distros
			# - gtk is not easily bundlable on linux because it looks for harcoded system path to optional
			#   shared libraries like image codecs, theme engines, sound notification system, etc.
			#   so expect user to install gtk first
			# - since we ask user to instal gtk, we can also ask them to install gtkglext,
			#   which is likely to pull gtk itself, x11 and gl dependencies
			# - old fontconfig does not work correctly if newer fontconfig configuration is installed
			# - if gtk and fontconfig is installed, pango and freetype are
			local ldd_line
			while read ldd_line
			do
				if echo "${ldd_line}" | egrep '/builtins/'
				then
					echo "${ldd_line}"
				elif echo "${ldd_line}" \
					| egrep -q '/libc\.|/libstdc\+\+\.|/libdl\.|/libm\.|/libX|/libxcb|/libGL|/libICE\.|/libSM\.|/libpthread\.'
				then
					Common::noOp
				elif echo "${ldd_line}" \
					| egrep -q '/libatk|/libgdk|/libgtk|/libgio|/libglib|/libgmodule|/libgobject|/libcairo|/libpango|/libfontconfig|/libfreetype'
				then
					Common::noOp
				# FreeBSD specific
				elif echo "${ldd_line}" \
					| egrep -q '/libc\+\+|/libgxxrt'
				then
					Common::noOp
				else
					echo "${ldd_line}"
				fi
			done
			;;
		'windows')
			egrep -i '\.dll => [A-Z]:\\msys64\\'
			;;
		'macos')
			egrep -v '^\t/System/|^\t/usr/lib/'
			;;
	esac
}

Multi::filterLib () {
	Common::grepLdd \
	| Multi::excludeLdd \
	| Common::stripLdd \
	| Common::getPath -
}

Multi::printLdd () {
	local exe_file="${1}"

	case "${system_name}" in
		'linux'|'freebsd')
			ldd "${exe_file}"
			;;
		'windows')
			ntldd --recursive "${exe_file}"
			;;
		'macos')
			otool -L "${exe_file}"
	esac
}

Multi::getGtkThemeName () {
	case "${system_name}" in
		'linux'|'freebsd')
			echo 'Adwaita'
			;;
		'windows')
			echo 'MS-Windows'
			;;
		*)
			echo 'Raleigh'
			;;
	esac
}

Multi::getGtkLibName () {
	case "${system_name}" in
		'linux'|'freebsd')
			echo 'libgtk-x11-2.0.so.0'
			;;
		'windows')
			echo 'libgtk-win32-2.0-0.dll'
			;;
		'macos')
			echo 'libgtk-quartz-2.0.0.dylib'
			;;
	esac
}

Multi::getRootPrefix () {
	local lib_file="${1}"

	case "${system_name}" in
		'linux')
			echo 'usr'
			;;
		'freebsd'|'macos')
			echo 'usr/local'
			;;
		'windows')
			basename "${lib_file}" \
			| xargs -n1 -P1 which \
			| cut -f2 -d'/'
			;;
	esac
}

Multi::getLibPrefix () {
	local lib_file="${1}"

	case "${system_name}" in
		'linux'|'freebsd')
			dirname "${lib_file}" \
			| cut -f3- -d'/'
			;;
		'windows')
			echo 'lib'
			;;
		'macos')
			echo 'lib'
			;;
	esac
}

Multi::getGtkDeps () {
	local lib_prefix="${1}"
	local gtk_theme_name="${2}"

	case "${system_name}" in
		'linux'|'freebsd'|'windows')
			cat <<-EOF
			share/themes/${gtk_theme_name}/gtk-2.0
			share/icons/hicolor
			${lib_prefix}/gdk-pixbuf-2.0
			${lib_prefix}/gtk-2.0
			EOF
			;;
		'macos')
			cat <<-EOF
			etc/fonts
			share/themes/${gtk_theme_name}/gtk-2.0
			share/fontconfig
			share/icons/hicolor
			share/locale
			${lib_prefix}/gdk-pixbuf-2.0
			${lib_prefix}/gtk-2.0
			EOF
			;;
	esac

	case "${system_name}" in
		'linux'|'freebsd')
			cat <<-EOF
			${lib_prefix}/libatk-bridge-2.0.so.0
			${lib_prefix}/libcanberra-0.30
			${lib_prefix}/libcanberra.so.0
			${lib_prefix}/libcanberra-gtk.so.0
			EOF
			;;
	esac
}

Multi::rewriteLoadersCache () {
	local bundle_component_path="${1}"
	local cache_file

	find "${bundle_component_path}" \
		-type f \
		\( \
		-name 'loaders.cache' \
		-o -name 'immodules.cache' \
		\) \
	| while read cache_file
	do
		_sed \
			-e 's|^"/[^"]*/lib/|"lib/|;s| "/[^"]*/share/| "share/|;/^# ModulesPath = /d;/^# Created by /d;/^#$/d' \
			-i "${cache_file}"
	done
}

Multi::bundleGtkDepsFromFile () {
	local lib_file="${1}"
	local component_dir
	local real_component_dir
	local bundle_component_dir

	lib_basename="$(basename "${lib_file}")"

	gtk_lib_name="$(Multi::getGtkLibName)"
	if [ "${lib_basename}" = "${gtk_lib_name}" ]
	then
		root_prefix="$(Multi::getRootPrefix "${lib_file}")"
		lib_prefix="$(Multi::getLibPrefix "${lib_file}")"
		gtk_theme_name="$(Multi::getGtkThemeName)"

		for component_dir in $(Multi::getGtkDeps "${lib_prefix}" "${gtk_theme_name}")
		do
			bundle_component_dir="$(echo "${component_dir}" | _sed -e 's|^'"${lib_prefix}"'|lib|')"
			if ! [ -e "${bundle_dir}/${bundle_component_dir}" ]
			then
				real_component_dir="$(realpath "/${root_prefix}/${component_dir}")"

				mkdir -p "${bundle_dir}/$(dirname "${bundle_component_dir}")"

				_cp \
					"${real_component_dir}" \
					"${bundle_dir}/${bundle_component_dir}"

				Multi::rewriteLoadersCache "${bundle_dir}/${bundle_component_dir}"
			fi
		done
	fi
}

Multi::bundleLibFromFile () {
	local exe_file="${1}"
	local lib_file

	Multi::printLdd "${exe_file}" \
	| Multi::filterLib \
	| while read lib_file
	do
		if [ "${lib_file}" = 'not found' ]
		then
			printf 'ERROR: library not found while bundling %s (but link worked)\n' "${exe_file}" >&2
			Multi::printLdd "${exe_file}" | grep 'not found'
			exit 1
		fi
		lib_basename="$(basename "${lib_file}")"

		if [ -f "${lib_dir}/${lib_basename}" ]
		then
			continue
		fi

		_cp \
			"${lib_file}" \
			"${lib_dir}/${lib_basename}"

		Multi::bundleGtkDepsFromFile "${lib_file}"

		case "${system_name}" in
			'macos')
				Multi::bundleLibFromFile "${lib_file}"
				;;
		esac
	done
}

Multi::cleanUp () {
	# Remove from bundle things that useless to be distributed,
	# like headers or static libraries, also remove
	# empty directories.
	find "${bundle_dir}/lib" \
		-type f \
		-name '*.a' \
		-exec rm -f {} \;

	find "${bundle_dir}/lib" \
		-type f \
		-name '*.h' \
		-exec rm -f {} \;

	find "${bundle_dir}/lib" \
		-depth \
		-type d \
		-empty \
		-exec rmdir {} \;
}

Linux::getRpath () {
	local exe_file="${1}"

	local exe_dir="$(dirname "${exe_file}")"
	local path_start="$(printf '%s' "${bundle_dir}" | wc -c)"
	path_start="$((${path_start} + 1))"

	local exe_subdir="$(echo "${exe_dir}" | cut -c "${path_start}-" | _sed -e 's|//*|/|;s|^/||')"

	local rpath_origin='$ORIGIN'

	if [ "${exe_subdir}" = '' ]
	then
		printf '%s/lib\n' "${rpath_origin}"
	else
		if [ "${exe_subdir}" = 'lib' ]
		then
			printf '%s\n' "${rpath_origin}"
		else
			local num_parent_dir="$(echo "${exe_subdir}" | tr '/' '\n' | wc -l)"
			local rpath_subdir
			local i=0
			while [ "${i}" -lt "${num_parent_dir}" ]
			do
				rpath_subdir="${rpath_subdir}/.."
				i="$((${i} + 1))"
			done
			printf '%s%s/lib\n' "${rpath_origin}" "${rpath_subdir}"
		fi
	fi
}

Linux::patchExe () {
	local exe_file="${1}"

	local linux_rpath_string=$"$(Linux::getRpath "${exe_file}")"
	chmod u+w,go-w "${exe_file}"
	patchelf --set-rpath "${linux_rpath_string}" "${exe_file}"
}

Linux::patchLib () {
	local lib_dir="${1}"
	local exe_file

	find "${lib_dir}" \
		-type f \
		-name '*.so*' \
	| while read exe_file
	do
		Linux::patchExe "${exe_file}"
		chmod ugo-x "${exe_file}"
	done
}

Darwin::patchExe () {
	local exe_file="${1}"

	Multi::printLdd "${exe_file}" \
	| Multi::filterLib \
	| while read lib_file
	do
		new_path="$(echo "${lib_file}" | _sed -e 's|^/.*/lib/|@executable_path/lib/|')"
		id_name="$(echo "${lib_file}" | _sed -e 's|.*/||g')"
		chmod u+w,go-w "${exe_file}"
		install_name_tool -change "${lib_file}" "${new_path}" "${exe_file}"
		install_name_tool -id "${id_name}" "${exe_file}"
	done
}

Darwin::patchLib () {
	local lib_dir="${1}"
	local exe_file

	find "${lib_dir}" \
		-type f \
		\( \
		-name '*.dylib' \
		-o -name '*.so' \
		\) \
	| while read exe_file
	do
		Darwin::patchExe "${exe_file}"
		chmod ugo-x "${exe_file}"
	done
}

Windows::listLibForManifest () {
	local lib_dir="${1}"

	find "${lib_dir}" \
		-maxdepth 1 \
		-type f \
		-name '*.dll' \
		-exec basename {} \; \
	| tr '\n' '\0' \
	| xargs -0 -P1 -I{} \
		printf '  <file name="{}"/>\n'
}

Windows::writeManifest () {
	local lib_dir="${1}"

	cat > "${manifest_file}" <<-EOF
	<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
	  <assemblyIdentity type="win32" name="lib" version="1.0.0.0"/>
	$(Windows::listLibForManifest "${lib_dir}")
	</assembly>
	EOF
}

system_name="${1}"; shift
bundle_dir="${1}"; shift

if ! [ -z "${1}" ]
then
	exe_file="${1}"; shift
fi

bundle_dir="$(Common::getPath "${bundle_dir}")"
registry_dir="${bundle_dir}/registry"
lib_dir="${bundle_dir}/lib"

manifest_file="${lib_dir}/lib.manifest"

exe_action='Common::noOp'
lib_action='Common::noOp'

case "${system_name}" in
	'register')
		mkdir -p "${registry_dir}"
		Common::getPath "${exe_file}" > "${registry_dir}/$(uuidgen)"
		exit
		;;
	'linux'|'freebsd')
		exe_action='Linux::patchExe'
		lib_action='Linux::patchLib'
		;;
	'windows')
		lib_action='Windows::writeManifest'
		;;
	'macos')
		exe_action='Darwin::patchExe'
		lib_action='Darwin::patchLib'
		;;
	*)
		printf 'ERROR: unsupported system: %s\n' "${system_name}" >&2
		exit 1
		;;
esac

mkdir -p "${lib_dir}"

if [ -d "${registry_dir}" ]
then
	for registry_entry in "${registry_dir}"/*
	do
		exe_file="$(cat "${registry_entry}")"

		Multi::bundleLibFromFile "${exe_file}"

		"${exe_action}" "${exe_file}"

		rm "${registry_entry}"

		"${exe_action}" "${exe_file}"
	done

	rmdir "${registry_dir}"
fi

"${lib_action}" "${lib_dir}"

Multi::cleanUp
