#!/usr/bin/env bash

# release.sh generates an addon zip file from a Git, SVN, or Mercurial checkout.
#
# This is free and unencumbered software released into the public domain.
#
# Anyone is free to copy, modify, publish, use, compile, sell, or
# distribute this software, either in source code form or as a compiled
# binary, for any purpose, commercial or non-commercial, and by any
# means.
#
# In jurisdictions that recognize copyright laws, the author or authors
# of this software dedicate any and all copyright interest in the
# software to the public domain. We make this dedication for the benefit
# of the public at large and to the detriment of our heirs and
# successors. We intend this dedication to be an overt act of
# relinquishment in perpetuity of all present and future rights to this
# software under copyright law.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
# For more information, please refer to <http://unlicense.org/>

## USER OPTIONS

# Secrets for uploading
cf_token=
github_token=
wowi_token=
wago_token=

# Variables set via command-line options
slug=
addonid=
wagoid=
topdir=
releasedir=
overwrite=
nolib=
line_ending="dos"
skip_copying=
skip_externals=
skip_localization=
skip_zipfile=
skip_upload=
skip_cf_upload=
pkgmeta_file=
game_version=
game_type=
file_type=
file_name="{package-name}-{project-version}{nolib}{classic}"

wowi_markup="bbcode"

## END USER OPTIONS

if [[ ${BASH_VERSINFO[0]} -lt 4 ]] || [[ ${BASH_VERSINFO[0]} -eq 4 && ${BASH_VERSINFO[1]} -lt 3 ]]; then
	echo "ERROR! bash version 4.3 or above is required. Your version is ${BASH_VERSION}." >&2
	exit 1
fi

# Game versions for uploading
declare -A game_flavors=( ["retail"]="mainline" ["classic"]="classic" ["bcc"]="bcc" )
declare -A game_versions
toc_version=

# Script return code
exit_code=0

# Escape a string for use in sed substitutions.
escape_substr() {
	local s="$1"
	s=${s//\\/\\\\}
	s=${s//\//\\/}
	s=${s//&/\\&}
	echo "$s"
}

# File name templating
filename_filter() {
	local classic alpha beta invalid
	[ -n "$skip_invalid" ] && invalid="&" || invalid="_"
	if [[ "$game_type" != "retail" ]] && [[ "$game_type" != "classic" || "${si_project_version,,}" != *"-classic"* ]] && [[ "$game_type" != "bcc" || "${si_project_version,,}" != *"-bcc"* ]]; then
		# only append the game type if the tag doesn't include it
		classic="-$game_type"
	fi
	[ "$file_type" == "alpha" ] && alpha="-alpha"
	[ "$file_type" == "beta" ] && beta="-beta"
	sed \
		-e "s/{package-name}/$( escape_substr "$package" )/g" \
		-e "s/{project-revision}/$si_project_revision/g" \
		-e "s/{project-hash}/$si_project_hash/g" \
		-e "s/{project-abbreviated-hash}/$si_project_abbreviated_hash/g" \
		-e "s/{project-author}/$( escape_substr "$si_project_author" )/g" \
		-e "s/{project-date-iso}/$si_project_date_iso/g" \
		-e "s/{project-date-integer}/$si_project_date_integer/g" \
		-e "s/{project-timestamp}/$si_project_timestamp/g" \
		-e "s/{project-version}/$( escape_substr "$si_project_version" )/g" \
		-e "s/{game-type}/${game_type}/g" \
		-e "s/{release-type}/${file_type}/g" \
		-e "s/{alpha}/${alpha}/g" \
		-e "s/{beta}/${beta}/g" \
		-e "s/{nolib}/${nolib:+-nolib}/g" \
		-e "s/{classic}/${classic}/g" \
		-e "s/\([^A-Za-z0-9._-]\)/${invalid}/g" \
		<<< "$1"
}

toc_filter() {
	local keyword="$1"
	local remove="$2"
	if [ -z "$remove" ]; then
		# "active" build type: remove comments (keep content), remove non-blocks (remove all)
		sed \
			-e "/#@\(end-\)\{0,1\}${keyword}@/d" \
			-e "/#@non-${keyword}@/,/#@end-non-${keyword}@/d"
	else
		# "non" build type: remove blocks (remove content), uncomment non-blocks (remove tags)
		sed \
			-e "/#@${keyword}@/,/#@end-${keyword}@/d" \
			-e "/#@non-${keyword}@/,/#@end-non-${keyword}@/s/^#[[:blank:]]\{1,\}//" \
			-e "/#@\(end-\)\{0,1\}non-${keyword}@/d"
	fi
}


# Process command-line options
usage() {
	cat <<-'EOF' >&2
	Usage: release.sh [options]
	  -c               Skip copying files into the package directory.
	  -d               Skip uploading.
	  -e               Skip checkout of external repositories.
	  -l               Skip @localization@ keyword replacement.
	  -L               Only do @localization@ keyword replacement (skip upload to CurseForge).
	  -o               Keep existing package directory, overwriting its contents.
	  -s               Create a stripped-down "nolib" package.
	  -u               Use Unix line-endings.
	  -z               Skip zip file creation.
	  -t topdir        Set top-level directory of checkout.
	  -r releasedir    Set directory containing the package directory. Defaults to "$topdir/.release".
	  -p curse-id      Set the project id used on CurseForge for localization and uploading. (Use 0 to unset the TOC value)
	  -w wowi-id       Set the addon id used on WoWInterface for uploading. (Use 0 to unset the TOC value)
	  -a wago-id       Set the project id used on Wago Addons for uploading. (Use 0 to unset the TOC value)
	  -g game-version  Set the game version to use for uploading.
	  -m pkgmeta.yaml  Set the pkgmeta file to use.
	  -n package-name  Set the package zip file name. Use "-n help" for more info.
	EOF
}

OPTIND=1
while getopts ":celLzusop:dw:a:r:t:g:m:n:" opt; do
	case $opt in
		c) skip_copying="true" ;; # Skip copying files into the package directory
		z) skip_zipfile="true" ;; # Skip creating a zip file
		e) skip_externals="true" ;; # Skip checkout of external repositories
		l) skip_localization="true" ;; # Skip @localization@ keyword replacement
		L) skip_cf_upload="true" ;; # Skip uploading to CurseForge
		d) skip_upload="true" ;; # Skip uploading
		u) line_ending="unix" ;; # Use LF instead of CRLF as the line ending for all text files
		o) overwrite="true" ;; # Don't delete existing directories in the release directory
		p) slug="$OPTARG" ;; # Set CurseForge project id
		w) addonid="$OPTARG" ;; # Set WoWInterface addon id
		a) wagoid="$OPTARG" ;; # Set Wago Addons project id
		r) releasedir="$OPTARG" ;; # Set the release directory
		t) # Set the top-level directory of the checkout
			if [ ! -d "$OPTARG" ]; then
				echo "Invalid argument for option \"-t\" - Directory \"$OPTARG\" does not exist." >&2
				usage
				exit 1
			fi
			topdir="$OPTARG"
			;;
		s) # Create a nolib package without externals
			nolib="true"
			skip_externals="true"
			;;
		g) # Set the game type or version
			OPTARG="${OPTARG,,}"
			case "$OPTARG" in
				retail|classic|bcc) game_type="$OPTARG" ;; # game_version from toc
				mainline) game_type="retail" ;;
				bc)
					echo "Invalid argument for option \"-g\" ($OPTARG)" >&2
					echo "" >&2
					echo "The \"bc\" game type has been changed to \"bcc\" to match Blizzard." >&2
					echo "This affects TOC interface lines (Interface-BC -> Interface-BCC) and" >&2
					echo "build keywords (version-bc -> version-bcc)." >&2
					echo "" >&2
					exit 1
					;;
				*)
					# Set game version (x.y.z)
					# Build game type set from the last value if a list
					IFS=',' read -ra V <<< "$OPTARG"
					for i in "${V[@]}"; do
						if [[ ! "$i" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)[a-z]?$ ]]; then
							echo "Invalid argument for option \"-g\" ($i)" >&2
							usage
							exit 1
						fi
						if [[ ${BASH_REMATCH[1]} == "1" && ${BASH_REMATCH[2]} == "13" ]]; then
							game_type="classic"
						elif [[ ${BASH_REMATCH[1]} == "2" && ${BASH_REMATCH[2]} == "5" ]]; then
							game_type="bcc"
						else
							game_type="retail"
						fi
						# Only one version per game type is allowed
						if [ -n "${game_versions[$game_type]}" ]; then
							echo "Invalid argument for option \"-g\" ($i) - Only one version per game type is supported." >&2
							usage
							exit 1
						fi
						game_versions[$game_type]="$i"
					done
					game_version="$OPTARG"
			esac
			;;
		m) # Set the pkgmeta file
			if [ ! -f "$OPTARG" ]; then
				echo "Invalid argument for option \"-m\" - File \"$OPTARG\" does not exist." >&2
				usage
				exit 1
			fi
			pkgmeta_file="$OPTARG"
			;;
		n) # Set the package file name
			if [ "$OPTARG" = "help" ]; then
				cat <<-'EOF' >&2
				Set the package zip file name. There are several string substitutions you can
				use to include version control and build type infomation in the file name.

				The default file name is "{package-name}-{project-version}{nolib}{classic}".

				Tokens: {package-name}{project-revision}{project-hash}{project-abbreviated-hash}
				        {project-author}{project-date-iso}{project-date-integer}{project-timestamp}
				        {project-version}{game-type}{release-type}

				Flags:  {alpha}{beta}{nolib}{classic}

				Tokens are always replaced with their value. Flags are shown prefixed with a dash
				depending on the build type.
				EOF
				exit 0
			fi
			file_name="$OPTARG"
			if skip_invalid=true filename_filter "$file_name" | grep -q '[{}]'; then
				tokens=$( skip_invalid=true filename_filter "$file_name" | sed -e '/^[^{]*{\|}[^{]*{\|}[^{]*/s//}{/g' -e 's/^}\({.*}\){$/\1/' )
				echo "Invalid argument for option \"-n\" - Invalid substitutions: $tokens" >&2
				exit 1
			fi
			;;
		:)
			echo "Option \"-$OPTARG\" requires an argument." >&2
			usage
			exit 1
			;;
		\?)
			if [ "$OPTARG" = "?" ] || [ "$OPTARG" = "h" ]; then
				usage
				exit 0
			fi
			echo "Unknown option \"-$OPTARG\"" >&2
			usage
			exit 1
			;;
	esac
done
shift $((OPTIND - 1))

# Set $topdir to top-level directory of the checkout.
if [ -z "$topdir" ]; then
	dir=$( pwd )
	if [ -d "$dir/.git" ] || [ -d "$dir/.svn" ] || [ -d "$dir/.hg" ]; then
		topdir=.
	else
		dir=${dir%/*}
		topdir=".."
		while [ -n "$dir" ]; do
			if [ -d "$topdir/.git" ] || [ -d "$topdir/.svn" ] || [ -d "$topdir/.hg" ]; then
				break
			fi
			dir=${dir%/*}
			topdir="$topdir/.."
		done
		if [ ! -d "$topdir/.git" ] && [ ! -d "$topdir/.svn" ] && [ ! -d "$topdir/.hg" ]; then
			echo "No Git, SVN, or Hg checkout found." >&2
			exit 1
		fi
	fi
fi

# Handle folding sections in CI logs
start_group() { echo "$1"; }
end_group() { echo; }

# Check for Travis CI
if [ -n "$TRAVIS" ]; then
	# Don't run the packager for pull requests
	if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
		echo "Not packaging pull request."
		exit 0
	fi
	if [ -z "$TRAVIS_TAG" ]; then
		# Don't run the packager if there is a tag pending
		check_tag=$( git -C "$topdir" tag --points-at HEAD )
		if [ -n "$check_tag" ]; then
			echo "Found future tag \"${check_tag}\", not packaging."
			exit 0
		fi
		# Only package master, classic, or develop
		if [ "$TRAVIS_BRANCH" != "master" ] && [ "$TRAVIS_BRANCH" != "classic" ] && [ "$TRAVIS_BRANCH" != "develop" ]; then
			echo "Not packaging \"${TRAVIS_BRANCH}\"."
			exit 0
		fi
	fi
	# https://github.com/travis-ci/travis-build/tree/master/lib/travis/build/bash
	start_group() {
		echo -en "travis_fold:start:$2\\r\033[0K"
		# release_timer_id="$(printf %08x $((RANDOM * RANDOM)))"
		# release_timer_start_time="$(date -u +%s%N)"
		# echo -en "travis_time:start:${release_timer_id}\\r\033[0K"
		echo "$1"
	}
	end_group() {
		# local release_timer_end_time="$(date -u +%s%N)"
		# local duration=$((release_timer_end_time - release_timer_start_time))
		# echo -en "\\ntravis_time:end:${release_timer_id}:start=${release_timer_start_time},finish=${release_timer_end_time},duration=${duration}\\r\033[0K"
		echo -en "travis_fold:end:$1\\r\033[0K"
	}
fi

# Check for GitHub Actions
if [ -n "$GITHUB_ACTIONS" ]; then
	# Prevent duplicate builds
	if [[ "$GITHUB_REF" == "refs/heads"* ]]; then
		check_tag=$( git -C "$topdir" tag --points-at HEAD )
		if [ -n "$check_tag" ]; then
			echo "Found future tag \"${check_tag}\", not packaging."
			exit 0
		fi
	fi
	start_group() { echo "##[group]$1"; }
	end_group() { echo "##[endgroup]"; }
fi
unset check_tag

# Load secrets
if [ -f "$topdir/.env" ]; then
	# shellcheck disable=1090
	. "$topdir/.env"
elif [ -f ".env" ]; then
	. ".env"
fi
[ -z "$cf_token" ] && cf_token=$CF_API_KEY
[ -z "$github_token" ] && github_token=$GITHUB_OAUTH
[ -z "$wowi_token" ] && wowi_token=$WOWI_API_TOKEN
[ -z "$wago_token" ] && wago_token=$WAGO_API_TOKEN

# Set $releasedir to the directory which will contain the generated addon zipfile.
if [ -z "$releasedir" ]; then
	releasedir="$topdir/.release"
fi

# Set $basedir to the basename of the checkout directory.
basedir=$( cd "$topdir" && pwd )
case $basedir in
/*/*)
	basedir=${basedir##/*/}
	;;
/*)
	basedir=${basedir##/}
	;;
esac

# Set $repository_type to "git" or "svn" or "hg".
repository_type=
if [ -d "$topdir/.git" ]; then
	repository_type=git
elif [ -d "$topdir/.svn" ]; then
	repository_type=svn
elif [ -d "$topdir/.hg" ]; then
	repository_type=hg
else
	echo "No Git, SVN, or Hg checkout found in \"$topdir\"." >&2
	exit 1
fi

# $releasedir must be an absolute path or inside $topdir.
case $releasedir in
/*)			;;
$topdir/*)	;;
*)
	echo "The release directory \"$releasedir\" must be an absolute path or inside \"$topdir\"." >&2
	exit 1
	;;
esac

# Create the staging directory.
mkdir -p "$releasedir" 2>/dev/null || {
	echo "Unable to create the release directory \"$releasedir\"." >&2
	exit 1
}

# Expand $topdir and $releasedir to their absolute paths for string comparisons later.
topdir=$( cd "$topdir" && pwd )
releasedir=$( cd "$releasedir" && pwd )

###
### set_info_<repo> returns the following information:
###
si_repo_type= # "git" or "svn" or "hg"
si_repo_dir= # the checkout directory
si_repo_url= # the checkout url
si_tag= # tag for HEAD
si_previous_tag= # previous tag
si_previous_revision= # [SVN|Hg] revision number for previous tag

si_project_revision= # Turns into the highest revision of the entire project in integer form, e.g. 1234, for SVN. Turns into the commit count for the project's hash for Git.
si_project_hash= # [Git|Hg] Turns into the hash of the entire project in hex form. e.g. 106c634df4b3dd4691bf24e148a23e9af35165ea
si_project_abbreviated_hash= # [Git|Hg] Turns into the abbreviated hash of the entire project in hex form. e.g. 106c63f
si_project_author= # Turns into the last author of the entire project. e.g. ckknight
si_project_date_iso= # Turns into the last changed date (by UTC) of the entire project in ISO 8601. e.g. 2008-05-01T12:34:56Z
si_project_date_integer= # Turns into the last changed date (by UTC) of the entire project in a readable integer fashion. e.g. 2008050123456
si_project_timestamp= # Turns into the last changed date (by UTC) of the entire project in POSIX timestamp. e.g. 1209663296
si_project_version= # Turns into an approximate version of the project. The tag name if on a tag, otherwise it's up to the repo. SVN returns something like "r1234", Git returns something like "v0.1-873fc1"

si_file_revision= # Turns into the current revision of the file in integer form, e.g. 1234, for SVN. Turns into the commit count for the file's hash for Git.
si_file_hash= # Turns into the hash of the file in hex form. e.g. 106c634df4b3dd4691bf24e148a23e9af35165ea
si_file_abbreviated_hash= # Turns into the abbreviated hash of the file in hex form. e.g. 106c63
si_file_author= # Turns into the last author of the file. e.g. ckknight
si_file_date_iso= # Turns into the last changed date (by UTC) of the file in ISO 8601. e.g. 2008-05-01T12:34:56Z
si_file_date_integer= # Turns into the last changed date (by UTC) of the file in a readable integer fashion. e.g. 20080501123456
si_file_timestamp= # Turns into the last changed date (by UTC) of the file in POSIX timestamp. e.g. 1209663296

# SVN date helper function
strtotime() {
	local value="$1" # datetime string
	local format="$2" # strptime string
	if [[ "${OSTYPE,,}" == *"darwin"* ]]; then # bsd
		date -j -f "$format" "$value" "+%s" 2>/dev/null
	else # gnu
		date -d "$value" +%s 2>/dev/null
	fi
}

set_info_git() {
	si_repo_dir="$1"
	si_repo_type="git"
	si_repo_url=$( git -C "$si_repo_dir" remote get-url origin 2>/dev/null | sed -e 's/^git@\(.*\):/https:\/\/\1\//' )
	if [ -z "$si_repo_url" ]; then # no origin so grab the first fetch url
		si_repo_url=$( git -C "$si_repo_dir" remote -v | awk '/(fetch)/ { print $2; exit }' | sed -e 's/^git@\(.*\):/https:\/\/\1\//' )
	fi

	# Populate filter vars.
	si_project_hash=$( git -C "$si_repo_dir" show --no-patch --format="%H" 2>/dev/null )
	si_project_abbreviated_hash=$( git -C "$si_repo_dir" show --no-patch --abbrev=7 --format="%h" 2>/dev/null )
	si_project_author=$( git -C "$si_repo_dir" show --no-patch --format="%an" 2>/dev/null )
	si_project_timestamp=$( git -C "$si_repo_dir" show --no-patch --format="%at" 2>/dev/null )
	si_project_date_iso=$( TZ='' printf "%(%Y-%m-%dT%H:%M:%SZ)T" "$si_project_timestamp" )
	si_project_date_integer=$( TZ='' printf "%(%Y%m%d%H%M%S)T" "$si_project_timestamp" )
	# XXX --depth limits rev-list :\ [ ! -s "$(git rev-parse --git-dir)/shallow" ] || git fetch --unshallow --no-tags
	si_project_revision=$( git -C "$si_repo_dir" rev-list --count "$si_project_hash" 2>/dev/null )

	# Get the tag for the HEAD.
	si_previous_tag=
	si_previous_revision=
	_si_tag=$( git -C "$si_repo_dir" describe --tags --always --abbrev=7 2>/dev/null )
	si_tag=$( git -C "$si_repo_dir" describe --tags --always --abbrev=0 2>/dev/null )
	# Set $si_project_version to the version number of HEAD. May be empty if there are no commits.
	si_project_version=$si_tag
	# The HEAD is not tagged if the HEAD is several commits past the most recent tag.
	if [ "$si_tag" = "$si_project_hash" ]; then
		# --abbrev=0 expands out the full sha if there was no previous tag
		si_project_version=$_si_tag
		si_previous_tag=
		si_tag=
	elif [ "$_si_tag" != "$si_tag" ]; then
		# not on a tag
		si_project_version=$( git -C "$si_repo_dir" describe --tags --abbrev=7 --exclude="*[Aa][Ll][Pp][Hh][Aa]*" 2>/dev/null )
		si_previous_tag=$( git -C "$si_repo_dir" describe --tags --abbrev=0 --exclude="*[Aa][Ll][Pp][Hh][Aa]*" 2>/dev/null )
		si_tag=
	else # we're on a tag, just jump back one commit
		if [[ ${si_tag,,} != *"beta"* && ${si_tag,,} != *"alpha"* ]]; then
			# full release, ignore beta tags
			si_previous_tag=$( git -C "$si_repo_dir" describe --tags --abbrev=0 --exclude="*[Aa][Ll][Pp][Hh][Aa]*" --exclude="*[Bb][Ee][Tt][Aa]*" HEAD~ 2>/dev/null )
		else
			si_previous_tag=$( git -C "$si_repo_dir" describe --tags --abbrev=0 --exclude="*[Aa][Ll][Pp][Hh][Aa]*" HEAD~ 2>/dev/null )
		fi
	fi
}

set_info_svn() {
	si_repo_dir="$1"
	si_repo_type="svn"

	# Temporary file to hold results of "svn info".
	_si_svninfo="${si_repo_dir}/.svn/release_sh_svninfo"
	svn info -r BASE "$si_repo_dir" 2>/dev/null > "$_si_svninfo"

	if [ -s "$_si_svninfo" ]; then
		_si_root=$( awk '/^Repository Root:/ { print $3; exit }' < "$_si_svninfo" )
		_si_url=$( awk '/^URL:/ { print $2; exit }' < "$_si_svninfo" )
		_si_revision=$( awk '/^Last Changed Rev:/ { print $NF; exit }' < "$_si_svninfo" )
		si_repo_url=$_si_root

		case ${_si_url#${_si_root}/} in
		tags/*)
			# Extract the tag from the URL.
			si_tag=${_si_url#${_si_root}/tags/}
			si_tag=${si_tag%%/*}
			si_project_revision="$_si_revision"
			;;
		*)
			# Check if the latest tag matches the working copy revision (/trunk checkout instead of /tags)
			_si_tag_line=$( svn log --verbose --limit 1 "$_si_root/tags" 2>/dev/null | awk '/^   A/ { print $0; exit }' )
			_si_tag=$( echo "$_si_tag_line" | awk '/^   A/ { print $2 }' | awk -F/ '{ print $NF }' )
			_si_tag_from_revision=$( echo "$_si_tag_line" | sed -e 's/^.*:\([0-9]\{1,\}\)).*$/\1/' ) # (from /project/trunk:N)

			if [ "$_si_tag_from_revision" = "$_si_revision" ]; then
				si_tag="$_si_tag"
				si_project_revision=$( svn info "$_si_root/tags/$si_tag" 2>/dev/null | awk '/^Last Changed Rev:/ { print $NF; exit }' )
			else
				# Set $si_project_revision to the highest revision of the project at the checkout path
				si_project_revision=$( svn info --recursive "$si_repo_dir" 2>/dev/null | awk '/^Last Changed Rev:/ { print $NF }' | sort -nr | head -n1 )
			fi
			;;
		esac

		if [ -n "$si_tag" ]; then
			si_project_version="$si_tag"
		else
			si_project_version="r$si_project_revision"
		fi

		# Get the previous tag and it's revision
		_si_limit=$((si_project_revision - 1))
		_si_tag=$( svn log --verbose --limit 1 "$_si_root/tags" -r $_si_limit:1 2>/dev/null | awk '/^   A/ { print $0; exit }' | awk '/^   A/ { print $2 }' | awk -F/ '{ print $NF }' )
		if [ -n "$_si_tag" ]; then
			si_previous_tag="$_si_tag"
			si_previous_revision=$( svn info "$_si_root/tags/$_si_tag" 2>/dev/null | awk '/^Last Changed Rev:/ { print $NF; exit }' )
		fi

		# Populate filter vars.
		si_project_author=$( awk '/^Last Changed Author:/ { print $0; exit }' < "$_si_svninfo" | cut -d" " -f4- )
		_si_timestamp=$( awk '/^Last Changed Date:/ { print $4,$5; exit }' < "$_si_svninfo" )
		si_project_timestamp=$( strtotime "$_si_timestamp" "%F %T" )
		si_project_date_iso=$( TZ='' printf "%(%Y-%m-%dT%H:%M:%SZ)T" "$si_project_timestamp" )
		si_project_date_integer=$( TZ='' printf "%(%Y%m%d%H%M%S)T" "$si_project_timestamp" )
		# SVN repositories have no project hash.
		si_project_hash=
		si_project_abbreviated_hash=

		rm -f "$_si_svninfo" 2>/dev/null
	fi
}

set_info_hg() {
	si_repo_dir="$1"
	si_repo_type="hg"
	si_repo_url=$( hg --cwd "$si_repo_dir" paths -q default )
	if [ -z "$si_repo_url" ]; then # no default so grab the first path
		si_repo_url=$( hg --cwd "$si_repo_dir" paths | awk '{ print $3; exit }' )
	fi

	# Populate filter vars.
	si_project_hash=$( hg --cwd "$si_repo_dir" log -r . --template '{node}' 2>/dev/null )
	si_project_abbreviated_hash=$( hg --cwd "$si_repo_dir" log -r . --template '{node|short}' 2>/dev/null )
	si_project_author=$( hg --cwd "$si_repo_dir" log -r . --template '{author}' 2>/dev/null )
	si_project_timestamp=$( hg --cwd "$si_repo_dir" log -r . --template '{date}' 2>/dev/null | cut -d. -f1 )
	si_project_date_iso=$( TZ='' printf "%(%Y-%m-%dT%H:%M:%SZ)T" "$si_project_timestamp" )
	si_project_date_integer=$( TZ='' printf "%(%Y%m%d%H%M%S)T" "$si_project_timestamp" )
	si_project_revision=$( hg --cwd "$si_repo_dir" log -r . --template '{rev}' 2>/dev/null )

	# Get tag info
	si_tag=
	# I'm just muddling through revsets, so there is probably a better way to do this
	# Ignore tag commits, so v1.0-1 will package as v1.0
	if [ "$( hg --cwd "$si_repo_dir" log -r '.-filelog(.hgtags)' --template '{rev}' 2>/dev/null )" == "" ]; then
		_si_tip=$( hg --cwd "$si_repo_dir" log -r 'last(parents(.))' --template '{rev}' 2>/dev/null )
	else
		_si_tip=$( hg --cwd "$si_repo_dir" log -r . --template '{rev}' 2>/dev/null )
	fi
	si_previous_tag=$( hg --cwd "$si_repo_dir" log -r "$_si_tip" --template '{latesttag}' 2>/dev/null )
	# si_project_version=$( hg --cwd "$si_repo_dir" log -r "$_si_tip" --template "{ ifeq(changessincelatesttag, 0, latesttag, '{latesttag}-{changessincelatesttag}-m{node|short}') }" 2>/dev/null ) # git style
	si_project_version=$( hg --cwd "$si_repo_dir" log -r "$_si_tip" --template "{ ifeq(changessincelatesttag, 0, latesttag, 'r{rev}') }" 2>/dev/null ) # svn style
	if [ "$si_previous_tag" = "$si_project_version" ]; then
		# we're on a tag
		si_tag=$si_previous_tag
		si_previous_tag=$( hg --cwd "$si_repo_dir" log -r "last(parents($_si_tip))" --template '{latesttag}' 2>/dev/null )
	fi
	si_previous_revision=$( hg --cwd "$si_repo_dir" log -r "$si_previous_tag" --template '{rev}' 2>/dev/null )
}

set_info_file() {
	if [ "$si_repo_type" = "git" ]; then
		_si_file=${1#si_repo_dir} # need the path relative to the checkout
		# Populate filter vars from the last commit the file was included in.
		si_file_hash=$( git -C "$si_repo_dir" log --max-count=1 --format="%H" "$_si_file" 2>/dev/null )
		si_file_abbreviated_hash=$( git -C "$si_repo_dir" log --max-count=1 --abbrev=7 --format="%h" "$_si_file" 2>/dev/null )
		si_file_author=$( git -C "$si_repo_dir" log --max-count=1 --format="%an" "$_si_file" 2>/dev/null )
		si_file_timestamp=$( git -C "$si_repo_dir" log --max-count=1 --format="%at" "$_si_file" 2>/dev/null )
		si_file_date_iso=$( TZ='' printf "%(%Y-%m-%dT%H:%M:%SZ)T" "$si_file_timestamp" )
		si_file_date_integer=$( TZ='' printf "%(%Y%m%d%H%M%S)T" "$si_file_timestamp" )
		si_file_revision=$( git -C "$si_repo_dir" rev-list --count "$si_file_hash" 2>/dev/null ) # XXX checkout depth affects rev-list, see set_info_git
	elif [ "$si_repo_type" = "svn" ]; then
		_si_file="$1"
		# Temporary file to hold results of "svn info".
		_sif_svninfo="${si_repo_dir}/.svn/release_sh_svnfinfo"
		svn info "$_si_file" 2>/dev/null > "$_sif_svninfo"
		if [ -s "$_sif_svninfo" ]; then
			# Populate filter vars.
			si_file_revision=$( awk '/^Last Changed Rev:/ { print $NF; exit }' < "$_sif_svninfo" )
			si_file_author=$( awk '/^Last Changed Author:/ { print $0; exit }' < "$_sif_svninfo" | cut -d" " -f4- )
			_si_timestamp=$( awk '/^Last Changed Date:/ { print $4,$5,$6; exit }' < "$_sif_svninfo" )
			si_file_timestamp=$( strtotime "$_si_timestamp" "%F %T %z" )
			si_file_date_iso=$( TZ='' printf "%(%Y-%m-%dT%H:%M:%SZ)T" "$si_file_timestamp" )
			si_file_date_integer=$( TZ='' printf "%(%Y%m%d%H%M%S)T" "$si_file_timestamp" )
			# SVN repositories have no project hash.
			si_file_hash=
			si_file_abbreviated_hash=

			rm -f "$_sif_svninfo" 2>/dev/null
		fi
	elif [ "$si_repo_type" = "hg" ]; then
		_si_file=${1#si_repo_dir} # need the path relative to the checkout
		# Populate filter vars.
		si_file_hash=$( hg --cwd "$si_repo_dir" log --limit 1 --template '{node}' "$_si_file" 2>/dev/null )
		si_file_abbreviated_hash=$( hg --cwd "$si_repo_dir" log --limit 1 --template '{node|short}' "$_si_file" 2>/dev/null )
		si_file_author=$( hg --cwd "$si_repo_dir" log --limit 1 --template '{author}' "$_si_file" 2>/dev/null )
		si_file_timestamp=$( hg --cwd "$si_repo_dir" log --limit 1 --template '{date}' "$_si_file" 2>/dev/null | cut -d. -f1 )
		si_file_date_iso=$( TZ='' printf "%(%Y-%m-%dT%H:%M:%SZ)T" "$si_file_timestamp" )
		si_file_date_integer=$( TZ='' printf "%(%Y%m%d%H%M%S)T" "$si_file_timestamp" )
		si_file_revision=$( hg --cwd "$si_repo_dir" log --limit 1 --template '{rev}' "$_si_file" 2>/dev/null )
	fi
}

# Set some version info about the project
case $repository_type in
git)	set_info_git "$topdir" ;;
svn)	set_info_svn "$topdir" ;;
hg) 	set_info_hg  "$topdir" ;;
esac

tag=$si_tag
project_version=$si_project_version
previous_version=$si_previous_tag
project_hash=$si_project_hash
project_revision=$si_project_revision
previous_revision=$si_previous_revision
project_timestamp=$si_project_timestamp
project_github_url=
project_github_slug=
if [[ "$si_repo_url" == "https://github.com"* ]]; then
	project_github_url=${si_repo_url%.git}
	project_github_slug=${project_github_url#https://github.com/}
fi
project_site=

# Bare carriage-return character.
carriage_return=$( printf "\r" )

# Returns 0 if $1 matches one of the colon-separated patterns in $2.
match_pattern() {
	_mp_file=$1
	_mp_list="$2:"
	while [ -n "$_mp_list" ]; do
		_mp_pattern=${_mp_list%%:*}
		_mp_list=${_mp_list#*:}
		# shellcheck disable=2254
		case $_mp_file in
			$_mp_pattern)
				return 0
				;;
		esac
	done
	return 1
}

# Simple .pkgmeta YAML processor.
yaml_keyvalue() {
	yaml_key=${1%%:*}
	yaml_value=${1#$yaml_key:}
	yaml_value=${yaml_value#"${yaml_value%%[! ]*}"} # trim leading whitespace
	yaml_value=${yaml_value#[\'\"]} # trim leading quotes
	yaml_value=${yaml_value%[\'\"]} # trim trailing quotes
}

yaml_listitem() {
	yaml_item=${1#-}
	yaml_item=${yaml_item#"${yaml_item%%[! ]*}"} # trim leading whitespace
}

###
### Process .pkgmeta to set variables used later in the script.
###

if [ -z "$pkgmeta_file" ]; then
	pkgmeta_file="$topdir/.pkgmeta"
	# CurseForge allows this so check for it
	if [ ! -f "$pkgmeta_file" ] && [ -f "$topdir/pkgmeta.yaml" ]; then
		pkgmeta_file="$topdir/pkgmeta.yaml"
	fi
fi

# Variables set via .pkgmeta.
package=
manual_changelog=
changelog=
changelog_markup="text"
enable_nolib_creation=
ignore=
contents=
nolib_exclude=
wowi_gen_changelog="true"
wowi_archive="true"
wowi_convert_changelog="true"
declare -A relations=()

parse_ignore() {
	pkgmeta="$1"
	[ -f "$pkgmeta" ] || return 1

	checkpath="$topdir" # paths are relative to the topdir
	copypath=""
	if [ "$2" != "" ]; then
		checkpath=$( dirname "$pkgmeta" )
		copypath="$2/"
	fi

	yaml_eof=
	while [ -z "$yaml_eof" ]; do
		IFS='' read -r yaml_line || yaml_eof="true"
		# Skip commented out lines.
		if [[ $yaml_line =~ ^[[:space:]]*\# ]]; then
			continue
		fi
		# Strip any trailing CR character.
		yaml_line=${yaml_line%$carriage_return}

		case $yaml_line in
		[!\ ]*:*)
			# Split $yaml_line into a $yaml_key, $yaml_value pair.
			yaml_keyvalue "$yaml_line"
			# Set the $pkgmeta_phase for stateful processing.
			pkgmeta_phase=$yaml_key
			;;
		[\ ]*"- "*)
			yaml_line=${yaml_line#"${yaml_line%%[! ]*}"} # trim leading whitespace
			# Get the YAML list item.
			yaml_listitem "$yaml_line"
			if [ "$pkgmeta_phase" = "ignore" ]; then
				pattern=$yaml_item
				if [ -d "$checkpath/$pattern" ]; then
					pattern="$copypath$pattern/*"
				elif [ ! -f "$checkpath/$pattern" ]; then
					# doesn't exist so match both a file and a path
					pattern="$copypath$pattern:$copypath$pattern/*"
				else
					pattern="$copypath$pattern"
				fi
				if [ -z "$ignore" ]; then
					ignore="$pattern"
				else
					ignore="$ignore:$pattern"
				fi
			fi
			;;
		esac
	done < "$pkgmeta"
}

if [ -f "$pkgmeta_file" ]; then
	if grep -q $'^[ ]*\t\+[[:blank:]]*[[:graph:]]' "$pkgmeta_file"; then
		# Try to cut down on some troubleshooting pain.
		echo "ERROR! Your pkgmeta file contains a leading tab. Only spaces are allowed for indentation in YAML files." >&2
		grep -n $'^[ ]*\t\+[[:blank:]]*[[:graph:]]' "$pkgmeta_file" | sed $'s/\t/^I/g'
		exit 1
	fi

	yaml_eof=
	while [ -z "$yaml_eof" ]; do
		IFS='' read -r yaml_line || yaml_eof="true"
		# Skip commented out lines.
		if [[ $yaml_line =~ ^[[:space:]]*\# ]]; then
			continue
		fi
		# Strip any trailing CR character.
		yaml_line=${yaml_line%$carriage_return}

		case $yaml_line in
		[!\ ]*:*)
			# Split $yaml_line into a $yaml_key, $yaml_value pair.
			yaml_keyvalue "$yaml_line"
			# Set the $pkgmeta_phase for stateful processing.
			pkgmeta_phase=$yaml_key

			case $yaml_key in
			enable-nolib-creation)
				if [ "$yaml_value" = "yes" ]; then
					enable_nolib_creation="true"
				fi
				;;
			manual-changelog)
				changelog=$yaml_value
				manual_changelog="true"
				;;
			changelog-title)
				project="$yaml_value"
				;;
			package-as)
				package=$yaml_value
				;;
			wowi-create-changelog)
				if [ "$yaml_value" = "no" ]; then
					wowi_gen_changelog=
				fi
				;;
			wowi-convert-changelog)
				if [ "$yaml_value" = "no" ]; then
					wowi_convert_changelog=
				fi
				;;
			wowi-archive-previous)
				if [ "$yaml_value" = "no" ]; then
					wowi_archive=
				fi
				;;
			esac
			;;
		" "*)
			yaml_line=${yaml_line#"${yaml_line%%[! ]*}"} # trim leading whitespace
			case $yaml_line in
			"- "*)
				# Get the YAML list item.
				yaml_listitem "$yaml_line"
				case $pkgmeta_phase in
				ignore)
					pattern=$yaml_item
					if [ -d "$topdir/$pattern" ]; then
						pattern="$pattern/*"
					elif [ ! -f "$topdir/$pattern" ]; then
						# doesn't exist so match both a file and a path
						pattern="$pattern:$pattern/*"
					fi
					if [ -z "$ignore" ]; then
						ignore="$pattern"
					else
						ignore="$ignore:$pattern"
					fi
					;;
				tools-used)
					relations["$yaml_item"]="tool"
					;;
				required-dependencies)
					relations["$yaml_item"]="requiredDependency"
					;;
				optional-dependencies)
					relations["$yaml_item"]="optionalDependency"
					;;
				embedded-libraries)
					relations["$yaml_item"]="embeddedLibrary"
					;;
				esac
				;;
			*:*)
				# Split $yaml_line into a $yaml_key, $yaml_value pair.
				yaml_keyvalue "$yaml_line"
				case $pkgmeta_phase in
				manual-changelog)
					case $yaml_key in
					filename)
						changelog=$yaml_value
						manual_changelog="true"
						;;
					markup-type)
						if [ "$yaml_value" = "markdown" ] || [ "$yaml_value" = "html" ]; then
							changelog_markup=$yaml_value
						else
							changelog_markup="text"
						fi
						;;
					esac
					;;
				esac
				;;
			esac
			;;
		esac
	done < "$pkgmeta_file"
fi

# Add untracked/ignored files to the ignore list
if [ "$repository_type" = "git" ]; then
	OLDIFS=$IFS
	IFS=$'\n'
	for _vcs_ignore in $( git -C "$topdir" ls-files --others --directory ); do
		if [ -d "$topdir/$_vcs_ignore" ]; then
			_vcs_ignore="$_vcs_ignore*"
		fi
		if [ -z "$ignore" ]; then
			ignore="$_vcs_ignore"
		else
			ignore="$ignore:$_vcs_ignore"
		fi
	done
	IFS=$OLDIFS
elif [ "$repository_type" = "svn" ]; then
	# svn always being difficult.
	OLDIFS=$IFS
	IFS=$'\n'
	for _vcs_ignore in $( cd "$topdir" && svn status --no-ignore --ignore-externals | awk '/^[?IX]/' | cut -c9- | tr '\\' '/' ); do
		if [ -d "$topdir/$_vcs_ignore" ]; then
			_vcs_ignore="$_vcs_ignore/*"
		fi
		if [ -z "$ignore" ]; then
			ignore="$_vcs_ignore"
		else
			ignore="$ignore:$_vcs_ignore"
		fi
	done
	IFS=$OLDIFS
elif [ "$repository_type" = "hg" ]; then
	_vcs_ignore=$( hg --cwd "$topdir" status --ignored --unknown --no-status --print0 | tr '\0' ':' )
	if [ -n "$_vcs_ignore" ]; then
		_vcs_ignore=${_vcs_ignore:0:-1}
		if [ -z "$ignore" ]; then
			ignore="$_vcs_ignore"
		else
			ignore="$ignore:$_vcs_ignore"
		fi
	fi
fi

###
### Process TOC file
###

# Set the package name from a TOC file name
if [[ -z "$package" ]]; then
	package=$( cd "$topdir" && find *.toc -maxdepth 0 2>/dev/null | head -n1 )
	if [[ -z "$package" ]]; then
		echo "Could not find an addon TOC file. In another directory? Set 'package-as' in .pkgmeta" >&2
		exit 1
	fi
	package=${package%.toc}
	if [[ $package =~ ^(.*)-(Mainline|Classic|BCC)$ ]]; then
		package="${BASH_REMATCH[1]}"
	fi
fi

toc_path="$package.toc"

# Handle having the main addon in a sub dir
if [[ ! -f "$topdir/$toc_path" && -f "$topdir/$package/$toc_path" ]]; then
	toc_path="$package/$toc_path"
fi

if [[ ! -f "$topdir/$toc_path" ]]; then
	echo "Could not find an addon TOC file. In another directory? Make sure it matches the 'package-as' in .pkgmeta" >&2
	exit 1
fi

# Get the interface version for setting the upload version.
toc_file=$(
	# remove bom and cr and apply some non-version toc filters
	[ "$file_type" != "alpha" ] && _tf_alpha="true"
	sed -e $'1s/^\xEF\xBB\xBF//' -e $'s/\r//g' "$topdir/$toc_path" | toc_filter alpha ${_tf_alpha} | toc_filter debug true
)
root_toc_version=$( awk '/^## Interface:/ { print $NF; exit }' <<< "$toc_file" )
toc_version="$root_toc_version"
if [[ -n "$toc_version" && -z "$game_type" ]]; then
	# toc -> game type
	case $toc_version in
		113*) game_type="classic" ;;
		205*) game_type="bcc" ;;
		*) game_type="retail"
	esac
else
	# game type -> toc
	game_type_toc_version=$( awk 'tolower($0) ~ /^## interface-'${game_type:-retail}':/ { print $NF; exit }' <<< "$toc_file" )
	if [[ -z "$game_type" ]]; then
		# default to retail
		game_type="retail"
	elif [[ -n "$game_type_toc_version" ]]; then
		# use the game version value if set
		toc_version="$game_type_toc_version"
	fi
	# Check for other interface lines
	if [[ -z "$toc_version" ]] || \
		 [[ "$game_type" == "classic" && "$toc_version" != "113"* ]] || \
		 [[ "$game_type" == "bcc" && "$toc_version" != "205"* ]] || \
		 [[ "$game_type" == "retail" && ("$toc_version" == "113"* || "$toc_version" == "205"*) ]]
	then
		toc_version="$game_type_toc_version"
		if [[ -z "$toc_version" ]]; then
			# Check @non-@ blocks
			case $game_type in
				classic) toc_version=$( sed -n '/@non-[-a-z]*@/,/@end-non-[-a-z]*@/{//b;p}' <<< "$toc_file" | awk '/#[[:blank:]]*## Interface:[[:blank:]]*(113)/ { print $NF; exit }' ) ;;
				bcc) toc_version=$( sed -n '/@non-[-a-z]*@/,/@end-non-[-a-z]*@/{//b;p}' <<< "$toc_file" | awk '/#[[:blank:]]*## Interface:[[:blank:]]*(205)/ { print $NF; exit }' ) ;;
			esac
			# This becomes the actual interface version after string replacements
			root_toc_version="$toc_version"
		fi
	fi
	if [[ -z "$toc_version" ]]; then
		echo "Addon TOC interface version is not compatible with the game version \"${game_type}\" or was not found." >&2
		exit 1
	fi
	if [[ "${toc_version,,}" == "incompatible" ]]; then
		echo "Addon TOC interface version is set as incompatible for game version \"${game_type}\"." >&2
		exit 1
	fi
fi
if [ -z "$game_version" ]; then
	printf -v game_version "%d.%d.%d" ${toc_version:0:1} ${toc_version:1:2} ${toc_version:3:2} 2>/dev/null || {
		echo "Addon TOC interface version \"${toc_version}\" is invalid." >&2
		exit 1
	}
	game_versions[$game_type]="$game_version"
fi

# Get the title of the project for using in the changelog.
if [ -z "$project" ]; then
	project=$( awk '/^## Title:/ { print $0; exit }' <<< "$toc_file" | sed -e 's/|c[0-9A-Fa-f]\{8\}//g' -e 's/|r//g' -e 's/|T[^|]*|t//g' -e 's/## Title[[:space:]]*:[[:space:]]*\(.*\)/\1/' -e 's/[[:space:]]*$//' )
fi
# Grab CurseForge ID and WoWI ID from the TOC file if not set by the script.
if [ -z "$slug" ]; then
	slug=$( awk '/^## X-Curse-Project-ID:/ { print $NF; exit }' <<< "$toc_file" )
fi
if [ -z "$addonid" ]; then
	addonid=$( awk '/^## X-WoWI-ID:/ { print $NF; exit }' <<< "$toc_file" )
fi
if [ -z "$wagoid" ]; then
	wagoid=$( awk '/^## X-Wago-ID:/ { print $NF; exit }' <<< "$toc_file" )
fi
unset toc_file

# unset project ids if they are set to 0
[ "$slug" = "0" ] && slug=
[ "$addonid" = "0" ] && addonid=
[ "$wagoid" = "0" ] && wagoid=

# Automatic file type detection based on CurseForge rules
# 1) Untagged commits will be marked as an alpha.
# 2) Tagged commits will be marked as a release with the following exceptions:
#    - If the tag contains the word "alpha", it will be marked as an alpha file.
#    - If instead the tag contains the word "beta", it will be marked as a beta file.
if [ -n "$tag" ]; then
	if [[ "${tag,,}" == *"alpha"* ]]; then
		file_type="alpha"
	elif [[ "${tag,,}" == *"beta"* ]]; then
		file_type="beta"
	else
		file_type="release"
	fi
else
	file_type="alpha"
fi

echo
echo "Packaging $package"
if [ -n "$project_version" ]; then
	echo "Current version: $project_version"
fi
if [ -n "$previous_version" ]; then
	echo "Previous version: $previous_version"
fi
(
	[ "$game_type" = "retail" ] && retail="retail" || retail="non-retail version-${game_type}"
	[ "$file_type" = "alpha" ] && alpha="alpha" || alpha="non-alpha"
	echo "Build type: ${retail} ${alpha} non-debug${nolib:+ nolib}"
	echo "Game version: ${game_version}"
	echo
)
if [[ "$slug" =~ ^[0-9]+$ ]]; then
	project_site="https://wow.curseforge.com"
	echo "CurseForge ID: $slug${cf_token:+ [token set]}"
fi
if [ -n "$addonid" ]; then
	echo "WoWInterface ID: $addonid${wowi_token:+ [token set]}"
fi
if [ -n "$wagoid" ]; then
	echo "Wago ID: $wagoid${wago_token:+ [token set]}"
fi
if [ -n "$project_github_slug" ]; then
	echo "GitHub: $project_github_slug${github_token:+ [token set]}"
fi
if [ -n "$project_site" ] || [ -n "$addonid" ] || [ -n "$wagoid" ] || [ -n "$project_github_slug" ]; then
	echo
fi
echo "Checkout directory: $topdir"
echo "Release directory: $releasedir"
echo

# Set $pkgdir to the path of the package directory inside $releasedir.
pkgdir="$releasedir/$package"
if [ -d "$pkgdir" ] && [ -z "$overwrite" ]; then
	#echo "Removing previous package directory: $pkgdir"
	rm -fr "$pkgdir"
fi
if [ ! -d "$pkgdir" ]; then
	mkdir -p "$pkgdir"
fi

# Set the contents of the addon zipfile.
contents="$package"

###
### Create filters for pass-through processing of files to replace repository keywords.
###

# Filter for simple repository keyword replacement.
vcs_filter() {
	sed \
		-e "s/@project-revision@/$si_project_revision/g" \
		-e "s/@project-hash@/$si_project_hash/g" \
		-e "s/@project-abbreviated-hash@/$si_project_abbreviated_hash/g" \
		-e "s/@project-author@/$( escape_substr "$si_project_author" )/g" \
		-e "s/@project-date-iso@/$si_project_date_iso/g" \
		-e "s/@project-date-integer@/$si_project_date_integer/g" \
		-e "s/@project-timestamp@/$si_project_timestamp/g" \
		-e "s/@project-version@/$( escape_substr "$si_project_version" )/g" \
		-e "s/@file-revision@/$si_file_revision/g" \
		-e "s/@file-hash@/$si_file_hash/g" \
		-e "s/@file-abbreviated-hash@/$si_file_abbreviated_hash/g" \
		-e "s/@file-author@/$( escape_substr "$si_file_author" )/g" \
		-e "s/@file-date-iso@/$si_file_date_iso/g" \
		-e "s/@file-date-integer@/$si_file_date_integer/g" \
		-e "s/@file-timestamp@/$si_file_timestamp/g"
}

# Find URL of localization api.
set_localization_url() {
	localization_url=
	if [ -n "$slug" ] && [ -n "$cf_token" ] && [ -n "$project_site" ]; then
		localization_url="${project_site}/api/projects/$slug/localization/export"
	fi
	if [ -z "$localization_url" ] && find "$topdir" -path '*/.*' -prune -o -name "*.lua" -print0 | xargs -0 grep -q "@localization"; then
		echo "Skipping localization! Missing CurseForge API token and/or project id is invalid."
		echo
	fi
}

# Filter to handle @localization@ repository keyword replacement.
# https://authors.curseforge.com/knowledge-base/projects/531-localization-substitutions/
declare -A unlocalized_values=( ["english"]="ShowPrimary" ["comment"]="ShowPrimaryAsComment" ["blank"]="ShowBlankAsComment" ["ignore"]="Ignore" )
localization_filter() {
	_ul_eof=
	while [ -z "$_ul_eof" ]; do
		IFS='' read -r _ul_line || _ul_eof="true"
		# Strip any trailing CR character.
		_ul_line=${_ul_line%$carriage_return}
		case $_ul_line in
		*@localization\(*\)@*)
			_ul_lang=
			_ul_namespace=
			_ul_singlekey=
			_ul_tablename="L"
			# Get the prefix of the line before the comment.
			_ul_prefix=${_ul_line%%@localization(*}
			_ul_prefix=${_ul_prefix%%--*}
			# Strip everything but the localization parameters.
			_ul_params=${_ul_line#*@localization(}
			_ul_params=${_ul_params%)@}
			# Sanitize the params a bit. (namespaces are restricted to [a-zA-Z0-9_], separated by [./:])
			_ul_params=${_ul_params// /}
			_ul_params=${_ul_params//,/, }
			# Pull the locale language first (mainly for warnings).
			_ul_lang="enUS"
			if [[ $_ul_params == *"locale=\""* ]]; then
				_ul_lang=${_ul_params##*locale=\"}
				_ul_lang=${_ul_lang:0:4}
				_ul_lang=${_ul_lang%%\"*}
			else
				echo "    Warning! No locale set, using enUS." >&2
			fi
			# Generate a URL parameter string from the localization parameters.
			# https://authors.curseforge.com/knowledge-base/projects/529-api
			_ul_url_params=""
			set -- ${_ul_params}
			for _ul_param; do
				_ul_key=${_ul_param%%=*}
				_ul_value=${_ul_param#*=}
				_ul_value=${_ul_value%,*}
				_ul_value=${_ul_value#*\"}
				_ul_value=${_ul_value%\"*}
				case ${_ul_key} in
					escape-non-ascii)
						if [ "$_ul_value" = "true" ]; then
							_ul_url_params="${_ul_url_params}&escape-non-ascii-characters=true"
						fi
						;;
					format)
						if [ "$_ul_value" = "lua_table" ]; then
							_ul_url_params="${_ul_url_params}&export-type=Table"
						fi
						;;
					handle-unlocalized)
						if [ "$_ul_value" != "english" ] && [ -n "${unlocalized_values[$_ul_value]}" ]; then
							_ul_url_params="${_ul_url_params}&unlocalized=${unlocalized_values[$_ul_value]}"
						fi
						;;
					handle-subnamespaces)
						if [ "$_ul_value" = "concat" ]; then # concat with /
							_ul_url_params="${_ul_url_params}&concatenante-subnamespaces=true"
						elif [ "$_ul_value" = "subtable" ]; then
							echo "    ($_ul_lang) Warning! ${_ul_key}=\"${_ul_value}\" is not supported. Include each full subnamespace, comma delimited." >&2
						fi
						;;
					key)
						# _ul_params was stripped of spaces, so reparse the line for the key
						_ul_singlekey=${_ul_line#*@localization(}
						_ul_singlekey=${_ul_singlekey#*key=\"}
						_ul_singlekey=${_ul_singlekey%%\",*}
						_ul_singlekey=${_ul_singlekey%%\")@*}
						;;
					locale)
						_ul_lang=$_ul_value
						;;
					namespace)
						# reparse to get all namespaces if multiple
						_ul_namespace=${_ul_params##*namespace=\"}
						_ul_namespace=${_ul_namespace%%\"*}
						_ul_namespace=${_ul_namespace//, /,}
						_ul_url_params="${_ul_url_params}&namespaces=${_ul_namespace}"
						_ul_namespace="/${_ul_namespace}"
						;;
					namespace-delimiter)
						if [ "$_ul_value" != "/" ]; then
							echo "    ($_ul_lang) Warning! ${_ul_key}=\"${_ul_value}\" is not supported." >&2
						fi
						;;
					prefix-values)
						echo "    ($_ul_lang) Warning! \"${_ul_key}\" is not supported." >&2
						;;
					same-key-is-true)
						if [ "$_ul_value" = "true" ]; then
							_ul_url_params="${_ul_url_params}&true-if-value-equals-key=true"
						fi
						;;
					table-name)
						if [ "$_ul_value" != "L" ]; then
							_ul_tablename="$_ul_value"
							_ul_url_params="${_ul_url_params}&table-name=${_ul_value}"
						fi
						;;
				esac
			done

			if [ -z "$_cdt_localization" ] || [ -z "$localization_url" ]; then
				echo "    Skipping localization (${_ul_lang}${_ul_namespace})" >&2

				# If the line isn't a TOC entry, print anything before the keyword.
				if [[ $_ul_line != "## "* ]]; then
					if [ -n "$_ul_eof" ]; then
						echo -n "$_ul_prefix"
					else
						echo "$_ul_prefix"
					fi
				fi
			else
				_ul_url="${localization_url}?lang=${_ul_lang}${_ul_url_params}"
				echo "    Adding ${_ul_lang}${_ul_namespace}" >&2

				if [ -z "$_ul_singlekey" ]; then
					# Write text that preceded the substitution.
					echo -n "$_ul_prefix"

					# Fetch the localization data, but don't output anything if there is an error.
					curl -s -H "x-api-token: $cf_token" "${_ul_url}" | awk -v url="$_ul_url" '/^{"error/ { o="    Error! "$0"\n           "url; print o >"/dev/stderr"; exit 1 } /<!DOCTYPE/ { print "    Error! Invalid output\n           "url >"/dev/stderr"; exit 1 } /^'"$_ul_tablename"' = '"$_ul_tablename"' or \{\}/ { next } { print }'

					# Insert a trailing blank line to match CF packager.
					if [ -z "$_ul_eof" ]; then
						echo ""
					fi
				else
					# Parse out a single phrase. This is kind of expensive, but caching would be way too much effort to optimize for what is basically an edge case.
					_ul_value=$( curl -s -H "x-api-token: $cf_token" "${_ul_url}" | awk -v url="$_ul_url" '/^{"error/ { o="    Error! "$0"\n           "url; print o >"/dev/stderr"; exit 1 } /<!DOCTYPE/ { print "    Error! Invalid output\n           "url >"/dev/stderr"; exit 1 } { print }' | sed -n '/L\["'"$_ul_singlekey"'"\]/p' | sed 's/^.* = "\(.*\)"/\1/' )
					if [ -n "$_ul_value" ] && [ "$_ul_value" != "$_ul_singlekey" ]; then
						# The result is different from the base value so print out the line.
						echo "${_ul_prefix}${_ul_value}${_ul_line##*)@}"
					fi
				fi
			fi
			;;
		*)
			if [ -n "$_ul_eof" ]; then
				echo -n "$_ul_line"
			else
				echo "$_ul_line"
			fi
		esac
	done
}

lua_filter() {
	local level
	case $1 in
		alpha) level="=" ;;
		debug) level="==" ;;
		retail|version-*) level="====" ;;
		*) level="==="
	esac
	sed \
		-e "s/--@$1@/--[${level}[@$1@/g" \
		-e "s/--@end-$1@/--@end-$1@]${level}]/g" \
		-e "s/--\[===\[@non-$1@/--@non-$1@/g" \
		-e "s/--@end-non-$1@\]===\]/--@end-non-$1@/g"
}

toc_interface_filter() {
	# Always remove BOM so ^ works
	if [ "$root_toc_version" != "$toc_version" ]; then
		# toc version isn't what is set in the toc file
		if [ -n "$root_toc_version" ]; then # rewrite
			sed -e $'1s/^\xEF\xBB\xBF//' -e 's/^## Interface:.*$/## Interface: '"$toc_version"'/' -e '/^## Interface-/d'
		else # add
			sed -e $'1s/^\xEF\xBB\xBF//' -e '1i\
## Interface: '"$toc_version" -e '/^## Interface-/d'
		fi
	else # cleanup
		sed -e $'1s/^\xEF\xBB\xBF//' -e '/^## Interface-/d'
	fi
}

xml_filter() {
	sed \
		-e "s/<!--@$1@-->/<!--@$1@/g" \
		-e "s/<!--@end-$1@-->/@end-$1@-->/g" \
		-e "s/<!--@non-$1@/<!--@non-$1@-->/g" \
		-e "s/@end-non-$1@-->/<!--@end-non-$1@-->/g"
}

do_not_package_filter() {
	case $1 in
		lua) sed '/--@do-not-package@/,/--@end-do-not-package@/d' ;;
		toc) sed '/#@do-not-package@/,/#@end-do-not-package@/d' ;;
		xml) sed '/<!--@do-not-package@-->/,/<!--@end-do-not-package@-->/d' ;;
	esac
}

line_ending_filter() {
	local _lef_eof _lef_line
	while [ -z "$_lef_eof" ]; do
		IFS='' read -r _lef_line || _lef_eof="true"
		# Strip any trailing CR character.
		_lef_line=${_lef_line%$carriage_return}
		if [ -n "$_lef_eof" ]; then
			# Preserve EOF not preceded by newlines.
			echo -n "$_lef_line"
		else
			case $line_ending in
				dos) printf "%s\r\n" "$_lef_line" ;; # Terminate lines with CR LF.
				unix) printf "%s\n" "$_lef_line"  ;; # Terminate lines with LF.
			esac
		fi
	done
}

###
### Copy files from the working directory into the package directory.
###

# Copy of the contents of the source directory into the destination directory.
# Dotfiles and any files matching the ignore pattern are skipped.  Copied files
# are subject to repository keyword replacement.
#
copy_directory_tree() {
	_cdt_alpha=
	_cdt_debug=
	_cdt_ignored_patterns=
	_cdt_localization=
	_cdt_nolib=
	_cdt_do_not_package=
	_cdt_unchanged_patterns=
	_cdt_classic=
	_cdt_external=
	OPTIND=1
	while getopts :adi:lnpu:c:e _cdt_opt "$@"; do
		# shellcheck disable=2220
		case $_cdt_opt in
			a)	_cdt_alpha="true" ;;
			d)	_cdt_debug="true" ;;
			i)	_cdt_ignored_patterns=$OPTARG ;;
			l)	_cdt_localization="true"
					set_localization_url
					;;
			n)	_cdt_nolib="true" ;;
			p)	_cdt_do_not_package="true" ;;
			u)	_cdt_unchanged_patterns=$OPTARG ;;
			c)	_cdt_classic=$OPTARG ;;
			e)	_cdt_external="true" ;;
		esac
	done
	shift $((OPTIND - 1))
	_cdt_srcdir=$1
	_cdt_destdir=$2

	if [ -z "$_cdt_external" ]; then
		start_group "Copying files into ${_cdt_destdir#$topdir/}:" "copy"
	else # don't nest groups
		echo "Copying files into ${_cdt_destdir#$topdir/}:"
	fi
	if [ ! -d "$_cdt_destdir" ]; then
		mkdir -p "$_cdt_destdir"
	fi
	# Create a "find" command to list all of the files in the current directory, minus any ones we need to prune.
	_cdt_find_cmd="find ."
	# Prune everything that begins with a dot except for the current directory ".".
	_cdt_find_cmd+=" \( -name \".*\" -a \! -name \".\" \) -prune"
	# Prune the destination directory if it is a subdirectory of the source directory.
	_cdt_dest_subdir=${_cdt_destdir#${_cdt_srcdir}/}
	case $_cdt_dest_subdir in
		/*)	;;
		*)	_cdt_find_cmd+=" -o -path \"./$_cdt_dest_subdir\" -prune" ;;
	esac
	# Print the filename, but suppress the current directory ".".
	_cdt_find_cmd+=" -o \! -name \".\" -print"
	( cd "$_cdt_srcdir" && eval "$_cdt_find_cmd" ) | while read -r file; do
		file=${file#./}
		if [ -f "$_cdt_srcdir/$file" ]; then
			# Check if the file should be ignored.
			skip_copy=
			# Prefix external files with the relative pkgdir path
			_cdt_check_file=$file
			if [ -n "${_cdt_destdir#$pkgdir}" ]; then
				_cdt_check_file="${_cdt_destdir#$pkgdir/}/$file"
			fi
			# Skip files matching the colon-separated "ignored" shell wildcard patterns.
			if [ -z "$skip_copy" ] && match_pattern "$_cdt_check_file" "$_cdt_ignored_patterns"; then
				skip_copy="true"
			fi
			# Never skip files that match the colon-separated "unchanged" shell wildcard patterns.
			unchanged=
			if [ -n "$skip_copy" ] && match_pattern "$file" "$_cdt_unchanged_patterns"; then
				skip_copy=
				unchanged="true"
			fi
			# Copy unskipped files into $_cdt_destdir.
			if [ -n "$skip_copy" ]; then
				echo "  Ignoring: $file"
			else
				dir=${file%/*}
				if [ "$dir" != "$file" ]; then
					mkdir -p "$_cdt_destdir/$dir"
				fi
				# Check if the file matches a pattern for keyword replacement.
				if [ -n "$unchanged" ] || ! match_pattern "$file" "*.lua:*.md:*.toc:*.txt:*.xml"; then
					echo "  Copying: $file (unchanged)"
					cp "$_cdt_srcdir/$file" "$_cdt_destdir/$dir"
				else
					# Set the filters for replacement based on file extension.
					_cdt_filters="vcs_filter"
					case $file in
						*.lua)
							[ -n "$_cdt_do_not_package" ] && _cdt_filters+="|do_not_package_filter lua"
							[ -n "$_cdt_alpha" ] && _cdt_filters+="|lua_filter alpha"
							[ -n "$_cdt_debug" ] && _cdt_filters+="|lua_filter debug"
							if [ -n "$_cdt_classic" ]; then
								_cdt_filters+="|lua_filter retail"
								_cdt_filters+="|lua_filter version-retail"
								[ "$_cdt_classic" = "classic" ] && _cdt_filters+="|lua_filter version-bcc"
								[ "$_cdt_classic" = "bcc" ] && _cdt_filters+="|lua_filter version-classic"
							else
								_cdt_filters+="|lua_filter version-classic"
								_cdt_filters+="|lua_filter version-bcc"
							fi
							[ -n "$_cdt_localization" ] && _cdt_filters+="|localization_filter"
							;;
						*.xml)
							[ -n "$_cdt_do_not_package" ] && _cdt_filters+="|do_not_package_filter xml"
							[ -n "$_cdt_nolib" ] && _cdt_filters+="|xml_filter no-lib-strip"
							[ -n "$_cdt_alpha" ] && _cdt_filters+="|xml_filter alpha"
							[ -n "$_cdt_debug" ] && _cdt_filters+="|xml_filter debug"
							if [ -n "$_cdt_classic" ]; then
								_cdt_filters+="|xml_filter retail"
								_cdt_filters+="|xml_filter version-retail"
								[ "$_cdt_classic" = "classic" ] && _cdt_filters+="|xml_filter version-bcc"
								[ "$_cdt_classic" = "bcc" ] && _cdt_filters+="|xml_filter version-classic"
							else
								_cdt_filters+="|xml_filter version-classic"
								_cdt_filters+="|xml_filter version-bcc"
							fi
							;;
						*.toc)
							_cdt_filters+="|do_not_package_filter toc"
							[ -n "$_cdt_nolib" ] && _cdt_filters+="|toc_filter no-lib-strip true" # leave the tokens in the file normally
							_cdt_filters+="|toc_filter alpha ${_cdt_alpha}"
							_cdt_filters+="|toc_filter debug ${_cdt_debug}"
							_cdt_filters+="|toc_filter retail ${_cdt_classic:+true}"
							_cdt_filters+="|toc_filter version-retail ${_cdt_classic:+true}"
							_cdt_filters+="|toc_filter version-classic $([[ -z "$_cdt_classic" || "$_cdt_classic" == "bcc" ]] && echo "true")"
							_cdt_filters+="|toc_filter version-bcc $([[ -z "$_cdt_classic" || "$_cdt_classic" == "classic" ]] && echo "true")"
							[[ -z "$_cdt_external" && ! $file =~ -(Mainline|Classic|BCC).toc$ ]] && _cdt_filters+="|toc_interface_filter"
							[ -n "$_cdt_localization" ] && _cdt_filters+="|localization_filter"
							;;
					esac

					# Set the filter for normalizing line endings.
					_cdt_filters+="|line_ending_filter"

					# Set version control values for the file.
					set_info_file "$_cdt_srcdir/$file"

					echo "  Copying: $file"
					eval < "$_cdt_srcdir/$file" "$_cdt_filters" > "$_cdt_destdir/$file"
				fi
			fi
		fi
	done
	if [ -z "$_external_dir" ]; then
		end_group "copy"
	fi
}

if [ -z "$skip_copying" ]; then
	cdt_args="-dp"
	[ "$file_type" != "alpha" ] && cdt_args+="a"
	[ -z "$skip_localization" ] && cdt_args+="l"
	[ -n "$nolib" ] && cdt_args+="n"
	[ "$game_type" != "retail" ] && cdt_args+=" -c $game_type"
	[ -n "$ignore" ] && cdt_args+=" -i \"$ignore\""
	[ -n "$changelog" ] && cdt_args+=" -u \"$changelog\""
	eval copy_directory_tree "$cdt_args" "\"$topdir\"" "\"$pkgdir\""
fi

# Reset ignore and parse pkgmeta ignores again to handle ignoring external paths
ignore=
parse_ignore "$pkgmeta_file"

###
### Process .pkgmeta again to perform any pre-move-folders actions.
###

retry() {
	local result=0
	local count=1
	while [[ "$count" -le 3 ]]; do
		[[ "$result" -ne 0 ]] && {
			echo -e "\033[01;31mRetrying (${count}/3)\033[0m" >&2
		}
		"$@" && { result=0 && break; } || result="$?"
		count="$((count + 1))"
		sleep 3
	done
	return "$result"
}

# Checkout the external into a ".checkout" subdirectory of the final directory.
checkout_external() {
	_external_dir=$1
	_external_uri=$2
	_external_tag=$3
	_external_type=$4
	# shellcheck disable=2034
	_external_slug=$5 # unused until we can easily fetch the project id
	_external_checkout_type=$6

	_cqe_checkout_dir="$pkgdir/$_external_dir/.checkout"
	mkdir -p "$_cqe_checkout_dir"
	if [ "$_external_type" = "git" ]; then
		if [ -z "$_external_tag" ]; then
			echo "Fetching latest version of external $_external_uri"
			retry git clone -q --depth 1 "$_external_uri" "$_cqe_checkout_dir" || return 1
		elif [ "$_external_tag" != "latest" ]; then
			echo "Fetching $_external_checkout_type \"$_external_tag\" from external $_external_uri"
			if [ "$_external_checkout_type" = "commit" ]; then
				retry git clone -q "$_external_uri" "$_cqe_checkout_dir" || return 1
				git -C "$_cqe_checkout_dir" checkout -q "$_external_tag" || return 1
			else
				git -c advice.detachedHead=false clone -q --depth 1 --branch "$_external_tag" "$_external_uri" "$_cqe_checkout_dir" || return 1
			fi
		else # [ "$_external_tag" = "latest" ]; then
			retry git clone -q --depth 50 "$_external_uri" "$_cqe_checkout_dir" || return 1
			_external_tag=$( git -C "$_cqe_checkout_dir" for-each-ref refs/tags --sort=-creatordate --format=%\(refname:short\) --count=1 )
			if [ -n "$_external_tag" ]; then
				echo "Fetching tag \"$_external_tag\" from external $_external_uri"
				git -C "$_cqe_checkout_dir" checkout -q "$_external_tag" || return 1
			else
				echo "Fetching latest version of external $_external_uri"
			fi
		fi

		# pull submodules
		git -C "$_cqe_checkout_dir" submodule -q update --init --recursive || return 1

		set_info_git "$_cqe_checkout_dir"
		echo "Checked out $( git -C "$_cqe_checkout_dir" describe --always --tags --abbrev=7 --long )" #$si_project_abbreviated_hash
	elif [ "$_external_type" = "svn" ]; then
		if [[ $external_uri == *"/trunk" ]]; then
			_cqe_svn_trunk_url=$_external_uri
			_cqe_svn_subdir=
		else
			_cqe_svn_trunk_url="${_external_uri%/trunk/*}/trunk"
			_cqe_svn_subdir=${_external_uri#${_cqe_svn_trunk_url}/}
		fi

		if [ -z "$_external_tag" ]; then
			echo "Fetching latest version of external $_external_uri"
			retry svn checkout -q "$_external_uri" "$_cqe_checkout_dir" || return 1
		else
			_cqe_svn_tag_url="${_cqe_svn_trunk_url%/trunk}/tags"
			if [ "$_external_tag" = "latest" ]; then
				_external_tag=$( svn log --verbose --limit 1 "$_cqe_svn_tag_url" 2>/dev/null | awk '/^   A \/tags\// { print $2; exit }' | awk -F/ '{ print $3 }' )
				if [ -z "$_external_tag" ]; then
					_external_tag="latest"
				fi
			fi
			if [ "$_external_tag" = "latest" ]; then
				echo "No tags found in $_cqe_svn_tag_url"
				echo "Fetching latest version of external $_external_uri"
				retry svn checkout -q "$_external_uri" "$_cqe_checkout_dir" || return 1
			else
				_cqe_external_uri="${_cqe_svn_tag_url}/$_external_tag"
				if [ -n "$_cqe_svn_subdir" ]; then
					_cqe_external_uri="${_cqe_external_uri}/$_cqe_svn_subdir"
				fi
				echo "Fetching tag \"$_external_tag\" from external $_cqe_external_uri"
				retry svn checkout -q "$_cqe_external_uri" "$_cqe_checkout_dir" || return 1
			fi
		fi
		set_info_svn "$_cqe_checkout_dir"
		echo "Checked out r$si_project_revision"
	elif [ "$_external_type" = "hg" ]; then
		if [ -z "$_external_tag" ]; then
			echo "Fetching latest version of external $_external_uri"
			retry hg clone -q "$_external_uri" "$_cqe_checkout_dir" || return 1
		elif [ "$_external_tag" != "latest" ]; then
			echo "Fetching $_external_checkout_type \"$_external_tag\" from external $_external_uri"
			retry hg clone -q --updaterev "$_external_tag" "$_external_uri" "$_cqe_checkout_dir" || return 1
		else # [ "$_external_tag" = "latest" ]; then
			retry hg clone -q "$_external_uri" "$_cqe_checkout_dir" || return 1
			_external_tag=$( hg --cwd "$_cqe_checkout_dir" log -r . --template '{latesttag}' )
			if [ -n "$_external_tag" ]; then
				echo "Fetching tag \"$_external_tag\" from external $_external_uri"
				hg --cwd "$_cqe_checkout_dir" update -q "$_external_tag"
			else
				echo "Fetching latest version of external $_external_uri"
			fi
		fi
		set_info_hg "$_cqe_checkout_dir"
		echo "Checked out r$si_project_revision"
	else
		echo "Unknown external: $_external_uri" >&2
		return 1
	fi
	# Copy the checkout into the proper external directory.
	(
		cd "$_cqe_checkout_dir" || return 1
		# Set the slug for external localization, if needed.
		# Note: We don't actually do localization since we need the project id and
		# the only way to convert slug->id would be to scrape the project page :\
		slug= #$_external_slug
		project_site=
		if [[ "$_external_uri" == *"wowace.com"* || "$_external_uri" == *"curseforge.com"* ]]; then
			project_site="https://wow.curseforge.com"
		fi
		# If a .pkgmeta file is present, process it for an "ignore" list.
		parse_ignore "$_cqe_checkout_dir/.pkgmeta" "$_external_dir"
		copy_directory_tree -dnpe -i "$ignore" "$_cqe_checkout_dir" "$pkgdir/$_external_dir"
	)
	# Remove the ".checkout" subdirectory containing the full checkout.
	if [ -d "$_cqe_checkout_dir" ]; then
		rm -fr "$_cqe_checkout_dir"
	fi
}

external_pids=()

external_dir=
external_uri=
external_tag=
external_type=
external_slug=
external_checkout_type=
process_external() {
	if [ -n "$external_dir" ] && [ -n "$external_uri" ] && [ -z "$skip_externals" ]; then
		# convert old curse repo urls
		case $external_uri in
			*git.curseforge.com*|*git.wowace.com*)
				external_type="git"
				# git://git.curseforge.com/wow/$slug/mainline.git -> https://repos.curseforge.com/wow/$slug
				external_uri=${external_uri%/mainline.git}
				external_uri="https://repos${external_uri#*://git}"
				;;
			*svn.curseforge.com*|*svn.wowace.com*)
				external_type="svn"
				# svn://svn.curseforge.com/wow/$slug/mainline/trunk -> https://repos.curseforge.com/wow/$slug/trunk
				external_uri=${external_uri/\/mainline/}
				external_uri="https://repos${external_uri#*://svn}"
				;;
			*hg.curseforge.com*|*hg.wowace.com*)
				external_type="hg"
				# http://hg.curseforge.com/wow/$slug/mainline -> https://repos.curseforge.com/wow/$slug
				external_uri=${external_uri%/mainline}
				external_uri="https://repos${external_uri#*://hg}"
				;;
			svn:*)
				# just in case
				external_type="svn"
				;;
			*)
				if [ -z "$external_type" ]; then
					external_type="git"
				fi
				;;
		esac

		if [[ $external_uri == "https://repos.curseforge.com/wow/"* || $external_uri == "https://repos.wowace.com/wow/"* ]]; then
			if [ -z "$external_slug" ]; then
				external_slug=${external_uri#*/wow/}
				external_slug=${external_slug%%/*}
			fi

			# check if the repo is svn
			_svn_path=${external_uri#*/wow/$external_slug/}
			if [[ "$_svn_path" == "trunk"* ]]; then
				external_type="svn"
			elif [[ "$_svn_path" == "tags/"* ]]; then
				external_type="svn"
				# change the tag path into the trunk path and use the tag var so it gets logged as a tag
				external_tag=${_svn_path#tags/}
				external_tag=${external_tag%%/*}
				external_uri="${external_uri%/tags*}/trunk${_svn_path#tags/$external_tag}"
			fi
		fi

		if [ -n "$external_slug" ]; then
			relations["$external_slug"]="embeddedLibrary"
		fi

		echo "Fetching external: $external_dir"
		checkout_external "$external_dir" "$external_uri" "$external_tag" "$external_type" "$external_slug" "$external_checkout_type" &> "$releasedir/.$BASHPID.externalout" &
		external_pids+=($!)
	fi
	external_dir=
	external_uri=
	external_tag=
	external_type=
	external_slug=
	external_checkout_type=
}

# Don't leave extra files around if exited early
kill_externals() {
	rm -f "$releasedir"/.*.externalout
	kill 0
}
trap kill_externals INT

if [ -z "$skip_externals" ] && [ -f "$pkgmeta_file" ]; then
	yaml_eof=
	while [ -z "$yaml_eof" ]; do
		IFS='' read -r yaml_line || yaml_eof="true"
		# Skip commented out lines.
		if [[ $yaml_line =~ ^[[:space:]]*\# ]]; then
			continue
		fi
		# Strip any trailing CR character.
		yaml_line=${yaml_line%$carriage_return}

		case $yaml_line in
		[!\ ]*:*)
			# Started a new section, so checkout any queued externals.
			process_external
			# Split $yaml_line into a $yaml_key, $yaml_value pair.
			yaml_keyvalue "$yaml_line"
			# Set the $pkgmeta_phase for stateful processing.
			pkgmeta_phase=$yaml_key
			;;
		" "*)
			yaml_line=${yaml_line#"${yaml_line%%[! ]*}"} # trim leading whitespace
			case $yaml_line in
			"- "*)
				;;
			*:*)
				# Split $yaml_line into a $yaml_key, $yaml_value pair.
				yaml_keyvalue "$yaml_line"
				case $pkgmeta_phase in
				externals)
					case $yaml_key in
					url) external_uri=$yaml_value ;;
					tag)
						external_tag=$yaml_value
						external_checkout_type=$yaml_key
						;;
					branch)
						external_tag=$yaml_value
						external_checkout_type=$yaml_key
						;;
					commit)
						external_tag=$yaml_value
						external_checkout_type=$yaml_key
						;;
					type) external_type=$yaml_value ;;
					curse-slug) external_slug=$yaml_value ;;
					*)
						# Started a new external, so checkout any queued externals.
						process_external

						external_dir=$yaml_key
						nolib_exclude="$nolib_exclude $pkgdir/$external_dir/*"
						if [ -n "$yaml_value" ]; then
							external_uri=$yaml_value
							# Immediately checkout this fully-specified external.
							process_external
						fi
						;;
					esac
					;;
				esac
				;;
			esac
			;;
		esac
	done < "$pkgmeta_file"
	# Reached end of file, so checkout any remaining queued externals.
	process_external

	if [ ${#external_pids[*]} -gt 0 ]; then
		echo
		echo "Waiting for externals to finish..."
		echo

		while [ ${#external_pids[*]} -gt 0 ]; do
			wait -n
			for i in ${!external_pids[*]}; do
				pid=${external_pids[i]}
				if ! kill -0 $pid 2>/dev/null; then
					_external_output="$releasedir/.$pid.externalout"
					if ! wait $pid; then
						_external_error=1
						# wrap each line with a bright red color code
						awk '{ printf "\033[01;31m%s\033[0m\n", $0 }' "$_external_output"
						echo
					else
						start_group "$( head -n1 "$_external_output" )" "external.$pid"
						tail -n+2 "$_external_output"
						end_group "external.$pid"
					fi
					rm -f "$_external_output" 2>/dev/null
					unset 'external_pids[i]'
				fi
			done
		done

		if [ -n "$_external_error" ]; then
			echo
			echo "There was an error fetching externals :(" >&2
			exit 1
		fi
	fi
fi

# Restore the signal handlers
trap - INT

###
### Create the changelog of commits since the previous release tag.
###

if [ -z "$project" ]; then
	project="$package"
fi

# Create a changelog in the package directory if the source directory does
# not contain a manual changelog.
if [ -n "$manual_changelog" ] && [ -f "$topdir/$changelog" ]; then
	start_group "Using manual changelog at $changelog" "changelog"
	head -n7 "$topdir/$changelog"
	[ "$( wc -l < "$topdir/$changelog" )" -gt 7 ] && echo "..."
	end_group "changelog"

	# Convert Markdown to BBCode (with HTML as an intermediary) for sending to WoWInterface
	# Requires pandoc (http://pandoc.org/)
	if [ "$changelog_markup" = "markdown" ] && [ -n "$wowi_convert_changelog" ] && hash pandoc &>/dev/null; then
		wowi_changelog="$releasedir/WOWI-$project_version-CHANGELOG.txt"
		pandoc -f commonmark -t html "$topdir/$changelog" | sed \
			-e 's/<\(\/\)\?\(b\|i\|u\)>/[\1\2]/g' \
			-e 's/<\(\/\)\?em>/[\1i]/g' \
			-e 's/<\(\/\)\?strong>/[\1b]/g' \
			-e 's/<ul[^>]*>/[list]/g' -e 's/<ol[^>]*>/[list="1"]/g' \
			-e 's/<\/[ou]l>/[\/list]\n/g' \
			-e 's/<li><p>/[*]/g' -e 's/<li>/[*]/g' -e 's/<\/p><\/li>//g' -e 's/<\/li>//g' \
			-e 's/\[\*\]\[ \] /[*]☐ /g' -e 's/\[\*\]\[[xX]\] /[*]☒ /g' \
			-e 's/<h1[^>]*>/[size="6"]/g' -e 's/<h2[^>]*>/[size="5"]/g' -e 's/<h3[^>]*>/[size="4"]/g' \
			-e 's/<h4[^>]*>/[size="3"]/g' -e 's/<h5[^>]*>/[size="3"]/g' -e 's/<h6[^>]*>/[size="3"]/g' \
			-e 's/<\/h[1-6]>/[\/size]\n/g' \
			-e 's/<blockquote>/[quote]/g' -e 's/<\/blockquote>/[\/quote]\n/g' \
			-e 's/<div class="sourceCode"[^>]*><pre class="sourceCode lua"><code class="sourceCode lua">/[highlight="lua"]/g' -e 's/<\/code><\/pre><\/div>/[\/highlight]\n/g' \
			-e 's/<pre><code>/[code]/g' -e 's/<\/code><\/pre>/[\/code]\n/g' \
			-e 's/<code>/[font="monospace"]/g' -e 's/<\/code>/[\/font]/g' \
			-e 's/<a href=\"\([^"]\+\)\"[^>]*>/[url="\1"]/g' -e 's/<\/a>/\[\/url]/g' \
			-e 's/<img src=\"\([^"]\+\)\"[^>]*>/[img]\1[\/img]/g' \
			-e 's/<hr \/>/_____________________________________________________________________________\n/g' \
			-e 's/<\/p>/\n/g' \
			-e '/^<[^>]\+>$/d' -e 's/<[^>]\+>//g' \
			-e 's/&quot;/"/g' \
			-e 's/&amp;/&/g' \
			-e 's/&lt;/</g' \
			-e 's/&gt;/>/g' \
			-e "s/&#39;/'/g" \
			| line_ending_filter > "$wowi_changelog"
	fi
else
	if [ -n "$manual_changelog" ]; then
		echo "Warning! Could not find a manual changelog at $topdir/$changelog"
		manual_changelog=
	fi
	changelog="CHANGELOG.md"
	changelog_markup="markdown"

	if [ -n "$wowi_gen_changelog" ] && [ -z "$wowi_convert_changelog" ]; then
		wowi_markup="markdown"
	fi

	start_group "Generating changelog of commits into $changelog" "changelog"

	_changelog_range=
	if [ "$repository_type" = "git" ]; then
		changelog_url=
		changelog_version=
		changelog_previous="[Previous Releases](${project_github_url}/releases)"
		changelog_url_wowi=
		changelog_version_wowi=
		changelog_previous_wowi="[url=${project_github_url}/releases]Previous Releases[/url]"
		if [ -z "$previous_version" ] && [ -z "$tag" ]; then
			# no range, show all commits up to ours
			changelog_url="[Full Changelog](${project_github_url}/commits/${project_hash})"
			changelog_version="[${project_version}](${project_github_url}/tree/${project_hash})"
			changelog_url_wowi="[url=${project_github_url}/commits/${project_hash}]Full Changelog[/url]"
			changelog_version_wowi="[url=${project_github_url}/tree/${project_hash}]${project_version}[/url]"
			_changelog_range="$project_hash"
		elif [ -z "$previous_version" ] && [ -n "$tag" ]; then
			# first tag, show all commits upto it
			changelog_url="[Full Changelog](${project_github_url}/commits/${tag})"
			changelog_version="[${project_version}](${project_github_url}/tree/${tag})"
			changelog_url_wowi="[url=${project_github_url}/commits/${tag}]Full Changelog[/url]"
			changelog_version_wowi="[url=${project_github_url}/tree/${tag}]${project_version}[/url]"
			_changelog_range="$tag"
		elif [ -n "$previous_version" ] && [ -z "$tag" ]; then
			# compare between last tag and our commit
			changelog_url="[Full Changelog](${project_github_url}/compare/${previous_version}...${project_hash})"
			changelog_version="[$project_version](${project_github_url}/tree/${project_hash})"
			changelog_url_wowi="[url=${project_github_url}/compare/${previous_version}...${project_hash}]Full Changelog[/url]"
			changelog_version_wowi="[url=${project_github_url}/tree/${project_hash}]${project_version}[/url]"
			_changelog_range="$previous_version..$project_hash"
		elif [ -n "$previous_version" ] && [ -n "$tag" ]; then
			# compare between last tag and our tag
			changelog_url="[Full Changelog](${project_github_url}/compare/${previous_version}...${tag})"
			changelog_version="[$project_version](${project_github_url}/tree/${tag})"
			changelog_url_wowi="[url=${project_github_url}/compare/${previous_version}...${tag}]Full Changelog[/url]"
			changelog_version_wowi="[url=${project_github_url}/tree/${tag}]${project_version}[/url]"
			_changelog_range="$previous_version..$tag"
		fi
		# lazy way out
		if [ -z "$project_github_url" ]; then
			changelog_url=
			changelog_version=$project_version
			changelog_previous=
			changelog_url_wowi=
			changelog_version_wowi="[color=orange]${project_version}[/color]"
			changelog_previous_wowi=
		elif [ -z "$github_token" ]; then
			# not creating releases :(
			changelog_previous=
			changelog_previous_wowi=
		fi
		changelog_date=$( TZ='' printf "%(%Y-%m-%d)T" "$project_timestamp" )

		cat <<- EOF | line_ending_filter > "$pkgdir/$changelog"
		# $project

		## $changelog_version ($changelog_date)
		$changelog_url $changelog_previous

		EOF
		git -C "$topdir" log "$_changelog_range" --pretty=format:"###%B" \
			| sed -e 's/^/    /g' -e 's/^ *$//g' -e 's/^    ###/- /g' -e 's/$/  /' \
			      -e 's/\([a-zA-Z0-9]\)_\([a-zA-Z0-9]\)/\1\\_\2/g' \
			      -e 's/\[ci skip\]//g' -e 's/\[skip ci\]//g' \
			      -e '/git-svn-id:/d' -e '/^[[:space:]]*This reverts commit [0-9a-f]\{40\}\.[[:space:]]*$/d' \
			      -e '/^[[:space:]]*$/d' \
			| line_ending_filter >> "$pkgdir/$changelog"

		# WoWI uses BBCode, generate something usable to post to the site
		# the file is deleted on successful upload
		if [ -n "$addonid" ] && [ -n "$tag" ] && [ -n "$wowi_gen_changelog" ] && [ "$wowi_markup" = "bbcode" ]; then
			wowi_changelog="$releasedir/WOWI-$project_version-CHANGELOG.txt"
			cat <<- EOF | line_ending_filter > "$wowi_changelog"
			[size=5]${project}[/size]
			[size=4]${changelog_version_wowi} (${changelog_date})[/size]
			${changelog_url_wowi} ${changelog_previous_wowi}
			[list]
			EOF
			git -C "$topdir" log "$_changelog_range" --pretty=format:"###%B" \
				| sed -e 's/^/    /g' -e 's/^ *$//g' -e 's/^    ###/[*]/g' \
				      -e 's/\[ci skip\]//g' -e 's/\[skip ci\]//g' \
				      -e '/git-svn-id:/d' -e '/^[[:space:]]*This reverts commit [0-9a-f]\{40\}\.[[:space:]]*$/d' \
				      -e '/^[[:space:]]*$/d' \
				| line_ending_filter >> "$wowi_changelog"
			echo "[/list]" | line_ending_filter >> "$wowi_changelog"
		fi

	elif [ "$repository_type" = "svn" ]; then
		if [ -n "$previous_revision" ]; then
			_changelog_range="-r$project_revision:$previous_revision"
		else
			_changelog_range="-rHEAD:1"
		fi
		changelog_date=$( TZ='' printf "%(%Y-%m-%d)T" "$project_timestamp" )

		cat <<- EOF | line_ending_filter > "$pkgdir/$changelog"
		# $project

		## $project_version ($changelog_date)

		EOF
		svn log "$topdir" "$_changelog_range" --xml \
			| awk '/<msg>/,/<\/msg>/' \
			| sed -e 's/<msg>/###/g' -e 's/<\/msg>//g' \
			      -e 's/^/    /g' -e 's/^ *$//g' -e 's/^    ###/- /g' -e 's/$/  /' \
			      -e 's/\([a-zA-Z0-9]\)_\([a-zA-Z0-9]\)/\1\\_\2/g' \
			      -e 's/\[ci skip\]//g' -e 's/\[skip ci\]//g' \
			      -e '/^[[:space:]]*$/d' \
			| line_ending_filter >> "$pkgdir/$changelog"

		# WoWI uses BBCode, generate something usable to post to the site
		# the file is deleted on successful upload
		if [ -n "$addonid" ] && [ -n "$tag" ] && [ -n "$wowi_gen_changelog" ] && [ "$wowi_markup" = "bbcode" ]; then
			wowi_changelog="$releasedir/WOWI-$project_version-CHANGELOG.txt"
			cat <<- EOF | line_ending_filter > "$wowi_changelog"
			[size=5]${project}[/size]
			[size=4][color=orange]${project_version}[/color] (${changelog_date})[/size]

			[list]
			EOF
			svn log "$topdir" "$_changelog_range" --xml \
				| awk '/<msg>/,/<\/msg>/' \
				| sed -e 's/<msg>/###/g' -e 's/<\/msg>//g' \
				      -e 's/^/    /g' -e 's/^ *$//g' -e 's/^    ###/[*]/g' \
				      -e 's/\[ci skip\]//g' -e 's/\[skip ci\]//g' \
				      -e '/^[[:space:]]*$/d' \
				| line_ending_filter >> "$wowi_changelog"
			echo "[/list]" | line_ending_filter >> "$wowi_changelog"
		fi

	elif [ "$repository_type" = "hg" ]; then
		if [ -n "$previous_revision" ]; then
			_changelog_range="::$project_revision - ::$previous_revision - filelog(.hgtags)"
		else
			_changelog_range="."
		fi
		changelog_date=$( TZ='' printf "%(%Y-%m-%d)T" "$project_timestamp" )

		cat <<- EOF | line_ending_filter > "$pkgdir/$changelog"
		# $project

		## $project_version ($changelog_date)

		EOF
		hg --cwd "$topdir" log -r "$_changelog_range" --template '- {fill(desc|strip, 76, "", "  ")}\n' | line_ending_filter >> "$pkgdir/$changelog"

		# WoWI uses BBCode, generate something usable to post to the site
		# the file is deleted on successful upload
		if [ -n "$addonid" ] && [ -n "$tag" ] && [ -n "$wowi_gen_changelog" ] && [ "$wowi_markup" = "bbcode" ]; then
			wowi_changelog="$releasedir/WOWI-$project_version-CHANGELOG.txt"
			cat <<- EOF | line_ending_filter > "$wowi_changelog"
			[size=5]${project}[/size]
			[size=4][color=orange]${project_version}[/color] (${changelog_date})[/size]

			[list]
			EOF
			hg --cwd "$topdir" log "$_changelog_range" --template '[*]{desc|strip|escape}\n' | line_ending_filter >> "$wowi_changelog"
			echo "[/list]" | line_ending_filter >> "$wowi_changelog"
		fi
	fi

	echo "$(<"$pkgdir/$changelog")"
	end_group "changelog"
fi

###
### Process .pkgmeta to perform move-folders actions.
###

if [ -f "$pkgmeta_file" ]; then
	yaml_eof=
	while [ -z "$yaml_eof" ]; do
		IFS='' read -r yaml_line || yaml_eof="true"
		# Skip commented out lines.
		if [[ $yaml_line =~ ^[[:space:]]*\# ]]; then
			continue
		fi
		# Strip any trailing CR character.
		yaml_line=${yaml_line%$carriage_return}

		case $yaml_line in
		[!\ ]*:*)
			# Split $yaml_line into a $yaml_key, $yaml_value pair.
			yaml_keyvalue "$yaml_line"
			# Set the $pkgmeta_phase for stateful processing.
			pkgmeta_phase=$yaml_key
			;;
		" "*)
			yaml_line=${yaml_line#"${yaml_line%%[! ]*}"} # trim leading whitespace
			case $yaml_line in
			"- "*)
				;;
			*:*)
				# Split $yaml_line into a $yaml_key, $yaml_value pair.
				yaml_keyvalue "$yaml_line"
				case $pkgmeta_phase in
				move-folders)
					srcdir="$releasedir/$yaml_key"
					destdir="$releasedir/$yaml_value"
					if [[ -d "$destdir" && -z "$overwrite" && "$srcdir" != "$destdir/"* ]]; then
						rm -fr "$destdir"
					fi
					if [ -d "$srcdir" ]; then
						if [ ! -d "$destdir" ]; then
							mkdir -p "$destdir"
						fi
						echo "Moving $yaml_key to $yaml_value"
						mv -f "$srcdir"/* "$destdir" && rm -fr "$srcdir"
						contents="$contents $yaml_value"
						# Check to see if the base source directory is empty
						_mf_basedir=${srcdir%$(basename "$yaml_key")}
						if [ ! "$( ls -A "$_mf_basedir" )" ]; then
							echo "Removing empty directory ${_mf_basedir#$releasedir/}"
							rm -fr "$_mf_basedir"
						fi
					fi
					# update external dir
					nolib_exclude=${nolib_exclude//$srcdir/$destdir}
					;;
				esac
				;;
			esac
			;;
		esac
	done < "$pkgmeta_file"
	if [ -n "$srcdir" ]; then
		echo
	fi
fi

###
### Create the final zipfile for the addon.
###

if [ -z "$skip_zipfile" ]; then
	archive_version="$project_version"
	archive_name="$( filename_filter "$file_name" ).zip"
	archive_label="$archive_version"
	if [[ "${file_name}" == *"{game-type}"* ]] || [[ "$game_type" != "retail" && "${file_name}" == *"{classic}"* ]]; then
		# append the game-type for clarity
		archive_label="$archive_version-$game_type"
		if [[ "$game_type" == "classic" && "${project_version,,}" == *"-classic"* ]] || [[ "$game_type" == "bcc" && "${project_version,,}" == *"-bcc"* ]]; then
			# this is mostly for BigWigs projects that tag classic separately (eg, v10-classic)
			# to prevent the extra -classic without changing all our workflows
			archive_label="$archive_version"
		fi
	fi
	archive="$releasedir/$archive_name"

	if [ -n "$GITHUB_ACTIONS" ]; then
		echo "::set-output name=archive_path::${archive}"
	fi

	nolib_archive_version="${project_version}-nolib"
	nolib_archive_name="$( nolib=true filename_filter "$file_name" ).zip"
	if [ "$archive_name" = "$nolib_archive_name" ]; then
		# someone didn't include {nolib} and they're forcing nolib creation
		nolib_archive_name="${nolib_archive_name#.zip}-nolib.zip"
	fi
	nolib_archive_label="${archive_label}-nolib"
	nolib_archive="$releasedir/$nolib_archive_name"

	if [ -n "$nolib" ]; then
		archive_version="$nolib_archive_version"
		archive_name="$nolib_archive_name"
		archive_label="$nolib_archive_label"
		archive="$nolib_archive"
		nolib_archive=
	fi

	start_group "Creating archive: $archive_name" "archive"
	if [ -f "$archive" ]; then
		rm -f "$archive"
	fi
	#( cd "$releasedir" && zip -X -r "$archive" $contents )
	( cd "$releasedir" && 7z a -bso0 -sns- -r "$archive" $contents )

	if [ ! -f "$archive" ]; then
		exit 1
	fi
	end_group "archive"

	# Create nolib version of the zipfile
	if [ -n "$enable_nolib_creation" ] && [ -z "$nolib" ] && [ -n "$nolib_exclude" ]; then
		# run the nolib_filter
		find "$pkgdir" -type f \( -name "*.xml" -o -name "*.toc" \) -print | while read -r file; do
			case $file in
				*.toc) _filter="toc_filter no-lib-strip true" ;;
				*.xml) _filter="xml_filter no-lib-strip" ;;
			esac
			$_filter < "$file" > "$file.tmp" && mv "$file.tmp" "$file"
		done

		# make the exclude paths relative to the release directory
		nolib_exclude=${nolib_exclude//$releasedir\//}

		start_group "Creating no-lib archive: $nolib_archive_name" "archive.nolib"
		if [ -f "$nolib_archive" ]; then
			rm -f "$nolib_archive"
		fi
		# set noglob so each nolib_exclude path gets quoted instead of expanded
		( set -f; cd "$releasedir" && zip -X -r -q "$nolib_archive" $contents -x $nolib_exclude )

		if [ ! -f "$nolib_archive" ]; then
			exit_code=1
		fi
		end_group "archive.nolib"
	fi

	###
	### Deploy the zipfile.
	###

	upload_curseforge=$( [[ -z "$skip_upload" && -z "$skip_cf_upload" && -n "$slug" && -n "$cf_token" && -n "$project_site" ]] && echo true )
	upload_wowinterface=$( [[ -z "$skip_upload" && -n "$tag" && -n "$addonid" && -n "$wowi_token" ]] && echo true )
	upload_wago=$( [[ -z "$skip_upload" && -n "$wagoid" && -n "$wago_token" ]] && echo true )
	upload_github=$( [[ -z "$skip_upload" && -n "$tag" && -n "$project_github_slug" && -n "$github_token" ]] && echo true )

	if [[ -n "$upload_curseforge" || -n "$upload_wowinterface" || -n "$upload_github" || -n "$upload_wago" ]] && ! hash jq &>/dev/null; then
		echo "Skipping upload because \"jq\" was not found."
		echo
		upload_curseforge=
		upload_wowinterface=
		upload_wago=
		upload_github=
		exit_code=1
	fi

	if [ -n "$upload_curseforge" ]; then
		_cf_versions=$( curl -s -H "x-api-token: $cf_token" $project_site/api/game/versions )
		if [ -n "$_cf_versions" ]; then
			_cf_game_version="$game_version"
			if [ -n "$_cf_game_version" ]; then
				_cf_game_version_id=$( echo "$_cf_versions" | jq -c --argjson v "[\"${game_version//,/\",\"}\"]" 'map(select(.name as $x | $v | index($x)) | .id) | select(length > 0)' 2>/dev/null )
				if [ -n "$_cf_game_version_id" ]; then
					# and now the reverse, since an invalid version will just be dropped
					_cf_game_version=$( echo "$_cf_versions" | jq -r --argjson v "$_cf_game_version_id" 'map(select(.id as $x | $v | index($x)) | .name) | join(",")' 2>/dev/null )
				fi
			fi
			if [ -z "$_cf_game_version_id" ]; then
				case $game_type in
					retail) _cf_game_type_id=517 ;;
					classic) _cf_game_type_id=67408 ;;
					bcc) _cf_game_type_id=73246 ;;
				esac
				_cf_game_version_id=$( echo "$_cf_versions" | jq -c --argjson v "$_cf_game_type_id" 'map(select(.gameVersionTypeID == $v)) | max_by(.id) | [.id]' 2>/dev/null )
				_cf_game_version=$( echo "$_cf_versions" | jq -r --argjson v "$_cf_game_type_id" 'map(select(.gameVersionTypeID == $v)) | max_by(.id) | .name' 2>/dev/null )
			fi
		fi
		if [ -z "$_cf_game_version_id" ]; then
			echo "Error fetching game version info from $project_site/api/game/versions"
			echo
			echo "Skipping upload to CurseForge."
			echo
			upload_curseforge=
			exit_code=1
		fi
	fi

	# Upload to CurseForge.
	if [ -n "$upload_curseforge" ]; then
		_cf_payload=$( cat <<-EOF
		{
		  "displayName": "$archive_label",
		  "gameVersions": $_cf_game_version_id,
		  "releaseType": "$file_type",
		  "changelog": $( jq --slurp --raw-input '.' < "$pkgdir/$changelog" ),
		  "changelogType": "$changelog_markup"
		}
		EOF
		)
		_cf_payload_relations=
		for i in "${!relations[@]}"; do
			_cf_payload_relations="$_cf_payload_relations{\"slug\":\"$i\",\"type\":\"${relations[$i]}\"},"
		done
		if [[ -n $_cf_payload_relations ]]; then
			_cf_payload_relations="{\"relations\":{\"projects\":[${_cf_payload_relations%,}]}}"
			_cf_payload=$( echo "$_cf_payload $_cf_payload_relations" | jq -s -c '.[0] * .[1]' )
		fi

		echo "Uploading $archive_name ($_cf_game_version $file_type) to $project_site/projects/$slug"
		resultfile="$releasedir/cf_result.json"
		result=$( echo "$_cf_payload" | curl -sS --retry 3 --retry-delay 10 \
				-w "%{http_code}" -o "$resultfile" \
				-H "x-api-token: $cf_token" \
				-F "metadata=<-" \
				-F "file=@$archive" \
				"$project_site/api/projects/$slug/upload-file"
		) && {
			case $result in
				200) echo "Success!" ;;
				302)
					echo "Error! ($result)"
					# don't need to ouput the redirect page
					exit_code=1
					;;
				404)
					echo "Error! No project for \"$slug\" found."
					exit_code=1
					;;
				*)
					echo "Error! ($result)"
					if [ -s "$resultfile" ]; then
						echo "$(<"$resultfile")"
					fi
					exit_code=1
					;;
			esac
		} || {
			exit_code=1
		}
		echo

		rm -f "$resultfile" 2>/dev/null
	fi

	if [ -n "$upload_wowinterface" ]; then
		_wowi_game_version=
		_wowi_versions=$( curl -s -H "x-api-token: $wowi_token" https://api.wowinterface.com/addons/compatible.json )
		if [ -n "$_wowi_versions" ]; then
			# Multiple versions, match on game version
			if [[ "$game_version" == *","* ]]; then
				_wowi_game_version=$( echo "$_wowi_versions" | jq -r --argjson v "[\"${game_version//,/\",\"}\"]" 'map(select(.id as $x | $v | index($x)) | .id) | join(",")' 2>/dev/null )
			fi
			# TOC matching
			if [ -z "$_wowi_game_version" ]; then
				_wowi_game_version=$( echo "$_wowi_versions" | jq -r --arg toc "$toc_version" '.[] | select(.interface == $toc and .default == true) | .id' 2>/dev/null )
			fi
			if [ -z "$_wowi_game_version" ]; then
				_wowi_game_version=$( echo "$_wowi_versions" | jq -r --arg toc "$toc_version" 'map(select(.interface == $toc))[0] | .id // empty' 2>/dev/null )
			fi
			# Handle delayed support (probably don't really need this anymore)
			if [ -z "$_wowi_game_version" ] && [ "$game_type" != "retail" ]; then
				_wowi_game_version=$( echo "$_wowi_versions" | jq -r --arg toc $((toc_version - 1)) '.[] | select(.interface == $toc) | .id' 2>/dev/null )
			fi
			if [ -z "$_wowi_game_version" ]; then
				_wowi_game_version=$( echo "$_wowi_versions" | jq -r '.[] | select(.default == true) | .id' 2>/dev/null )
			fi
		fi
		if [ -z "$_wowi_game_version" ]; then
			echo "Error fetching game version info from https://api.wowinterface.com/addons/compatible.json"
			echo
			echo "Skipping upload to WoWInterface."
			echo
			upload_wowinterface=
			exit_code=1
		fi
	fi

	# Upload tags to WoWInterface.
	if [ -n "$upload_wowinterface" ]; then
		_wowi_args=()
		if [ -f "$wowi_changelog" ]; then
			_wowi_args+=("-F changelog=<$wowi_changelog")
		elif [ -n "$manual_changelog" ] || [ "$wowi_markup" = "markdown" ]; then
			_wowi_args+=("-F changelog=<$pkgdir/$changelog")
		fi
		if [ -z "$wowi_archive" ]; then
			_wowi_args+=("-F archive=No")
		fi

		echo "Uploading $archive_name ($_wowi_game_version) to https://www.wowinterface.com/downloads/info$addonid"
		resultfile="$releasedir/wi_result.json"
		result=$( curl -sS --retry 3 --retry-delay 10 \
				-w "%{http_code}" -o "$resultfile" \
				-H "x-api-token: $wowi_token" \
				-F "id=$addonid" \
				-F "version=$archive_version" \
				-F "compatible=$_wowi_game_version" \
				"${_wowi_args[@]}" \
				-F "updatefile=@$archive" \
				"https://api.wowinterface.com/addons/update"
		) && {
			case $result in
				202)
					echo "Success!"
					if [ -f "$wowi_changelog" ]; then
						rm -f "$wowi_changelog" 2>/dev/null
					fi
					;;
				401)
					echo "Error! No addon for id \"$addonid\" found or you do not have permission to upload files."
					exit_code=1
					;;
				403)
					echo "Error! Incorrect api key or you do not have permission to upload files."
					exit_code=1
					;;
				*)
					echo "Error! ($result)"
					if [ -s "$resultfile" ]; then
						echo "$(<"$resultfile")"
					fi
					exit_code=1
					;;
			esac
		} || {
			exit_code=1
		}
		echo

		rm -f "$resultfile" 2>/dev/null
	fi

	# Upload to Wago
	if [ -n "$upload_wago" ] ; then
		_wago_support_property=""
		for type in "${!game_versions[@]}"; do
			if [[ "$type" == "bcc" ]]; then
				_wago_support_property+="\"supported_bc_patch\": \"${game_versions[$type]}\", "
			else
				_wago_support_property+="\"supported_${type}_patch\": \"${game_versions[$type]}\", "
			fi
		done

		_wago_stability="$file_type"
		if [ "$file_type" = "release" ]; then
			_wago_stability="stable"
		fi

		_wago_payload=$( cat <<-EOF
		{
		  "label": "$archive_label",
		  $_wago_support_property
		  "stability": "$_wago_stability",
		  "changelog": $( jq --slurp --raw-input '.' < "$pkgdir/$changelog" )
		}
		EOF
		)

		echo "Uploading $archive_name ($game_version $file_type) to Wago"
		resultfile="$releasedir/wago_result.json"
		result=$( echo "$_wago_payload" | curl -sS --retry 3 --retry-delay 10 \
				-w "%{http_code}" -o "$resultfile" \
				-H "authorization: Bearer $wago_token" \
				-H "accept: application/json" \
				-F "metadata=<-" \
				-F "file=@$archive" \
				"https://addons.wago.io/api/projects/$wagoid/version"
		) && {
			case $result in
				200|201) echo "Success!" ;;
				302)
					echo "Error! ($result)"
					# don't need to ouput the redirect page
					exit_code=1
					;;
				404)
					echo "Error! No Wago project for id \"$wagoid\" found."
					exit_code=1
					;;
				*)
					echo "Error! ($result)"
					if [ -s "$resultfile" ]; then
						echo "$(<"$resultfile")"
					fi
					exit_code=1
					;;
			esac
		} || {
			exit_code=1
		}
		echo

		rm -f "$resultfile" 2>/dev/null
	fi

	# Create a GitHub Release for tags and upload the zipfile as an asset.
	if [ -n "$upload_github" ]; then
		upload_github_asset() {
			_ghf_release_id=$1
			_ghf_file_name=$2
			_ghf_file_path=$3
			_ghf_resultfile="$releasedir/gh_asset_result.json"
			_ghf_content_type="application/${_ghf_file_name##*.}" # zip or json

			# check if an asset exists and delete it (editing a release)
			asset_id=$( curl -sS \
					-H "Accept: application/vnd.github.v3+json" \
					-H "Authorization: token $github_token" \
					"https://api.github.com/repos/$project_github_slug/releases/$_ghf_release_id/assets" \
				| jq --arg file "$_ghf_file_name"  '.[] | select(.name? == $file) | .id'
			)
			if [ -n "$asset_id" ]; then
				curl -s \
					-X DELETE \
					-H "Accept: application/vnd.github.v3+json" \
					-H "Authorization: token $github_token" \
					"https://api.github.com/repos/$project_github_slug/releases/assets/$asset_id" &>/dev/null
			fi

			echo -n "Uploading $_ghf_file_name... "
			result=$( curl -sS --retry 3 --retry-delay 10 \
					-w "%{http_code}" -o "$_ghf_resultfile" \
					-H "Accept: application/vnd.github.v3+json" \
					-H "Authorization: token $github_token" \
					-H "Content-Type: $_ghf_content_type" \
					--data-binary "@$_ghf_file_path" \
					"https://uploads.github.com/repos/$project_github_slug/releases/$_ghf_release_id/assets?name=$_ghf_file_name"
			) && {
				if [ "$result" = "201" ]; then
					echo "Success!"
				else
					echo "Error ($result)"
					if [ -s "$_ghf_resultfile" ]; then
						echo "$(<"$_ghf_resultfile")"
					fi
					exit_code=1
				fi
			} || {
				exit_code=1
			}

			rm -f "$_ghf_resultfile" 2>/dev/null
			return 0
		}

		_gh_metadata='{ "filename": "'"$archive_name"'", "nolib": false, "metadata": ['
		for type in "${!game_versions[@]}"; do
			_gh_metadata+='{ "flavor": "'"${game_flavors[$type]}"'", "interface": '"$toc_version"' },'
		done
		_gh_metadata=${_gh_metadata%,}
		_gh_metadata+='] }'
		if [ -f "$nolib_archive" ]; then
			_gh_metadata+=',{ "filename": "'"$nolib_archive_name"'", "nolib": true, "metadata": ['
			for type in "${!game_versions[@]}"; do
				_gh_metadata+='{ "flavor": "'"${game_flavors[$type]}"'", "interface": '"$toc_version"' },'
			done
			_gh_metadata=${_gh_metadata%,}
			_gh_metadata+='] }'
		fi
		_gh_metadata='{ "releases": ['"$_gh_metadata"'] }'

		versionfile="$releasedir/release.json"
		jq -c '.' <<< "$_gh_metadata" > "$versionfile" || echo "There was an error creating release.json" >&2

		_gh_payload=$( cat <<-EOF
		{
		  "tag_name": "$tag",
		  "name": "$tag",
		  "body": $( jq --slurp --raw-input '.' < "$pkgdir/$changelog" ),
		  "draft": false,
		  "prerelease": $( [[ "$file_type" != "release" ]] && echo true || echo false )
		}
		EOF
		)
		resultfile="$releasedir/gh_result.json"

		release_id=$( curl -sS \
				-H "Accept: application/vnd.github.v3+json" \
				-H "Authorization: token $github_token" \
				"https://api.github.com/repos/$project_github_slug/releases/tags/$tag" \
			| jq '.id // empty'
		)
		if [ -n "$release_id" ]; then
			echo "Updating GitHub release: https://github.com/$project_github_slug/releases/tag/$tag"
			_gh_release_url="-X PATCH https://api.github.com/repos/$project_github_slug/releases/$release_id"

			# combine version info
			_gh_metadata_url=$( curl -sS \
					-H "Accept: application/vnd.github.v3+json" \
					-H "Authorization: token $github_token" \
					"https://api.github.com/repos/$project_github_slug/releases/$release_id/assets" \
				| jq -r '.[] | select(.name? == "release.json") | .url // empty'
			)
			if [ -n "$_gh_metadata_url" ]; then
				_gh_previous_metadata=$( curl -sSL --fail \
						-H "Accept: application/octet-stream" \
						-H "Authorization: token $github_token" \
						"$_gh_metadata_url"
				) && {
					jq -sc '.[0].releases + .[1].releases | unique_by(.filename) | { releases: [.[]] }' <<< "${_gh_previous_metadata} ${_gh_metadata}" > "$versionfile"
				} || {
					echo "Warning: Unable to update release.json ($?)"
				}
			fi
		else
			echo "Creating GitHub release: https://github.com/$project_github_slug/releases/tag/$tag"
			_gh_release_url="https://api.github.com/repos/$project_github_slug/releases"
		fi
		result=$( echo "$_gh_payload" | curl -sS --retry 3 --retry-delay 10 \
				-w "%{http_code}" -o "$resultfile" \
				-H "Accept: application/vnd.github.v3+json" \
				-H "Authorization: token $github_token" \
				-d @- \
				$_gh_release_url
		) && {
			if [ "$result" = "200" ] || [ "$result" = "201" ]; then # edited || created
				if [ -z "$release_id" ]; then
					release_id=$( jq '.id' < "$resultfile" )
				fi
				upload_github_asset "$release_id" "$archive_name" "$archive"
				if [ -f "$nolib_archive" ]; then
					upload_github_asset "$release_id" "$nolib_archive_name" "$nolib_archive"
				fi
				if [ -s "$versionfile" ]; then
					upload_github_asset "$release_id" "release.json" "$versionfile"
				fi
			else
				echo "Error! ($result)"
				if [ -s "$resultfile" ]; then
					echo "$(<"$resultfile")"
				fi
				exit_code=1
			fi
		} || {
			exit_code=1
		}

		rm -f "$resultfile" 2>/dev/null
		[ -z "$CI" ] && rm -f "$versionfile" 2>/dev/null
		echo
	fi
fi

# All done.

echo
echo "Packaging complete."
echo

exit $exit_code
