#!/bin/bash

# rust create_release v0.6.3
# 2025-09-20

STAR_LINE='****************************************'
CWD=$(pwd)

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
PURPLE='\033[0;35m'
RESET='\033[0m'

# $1 string - error message
error_close() {
	echo -e "\n${RED}ERROR - EXITED: ${YELLOW}$1${RESET}\n"
	exit 1
}

# Check that dialog is installed
if ! [ -x "$(command -v dialog)" ]; then
	error_close "dialog is not installed"
fi

# $1 string - question to ask
# $1 question to ask
# Ask a yes no question, only accepts `y` or `n` as a valid answer, returns 0 for yes, 1 for no
ask_yn() {
	while true; do
		printf "\n%b%s? [y/N]:%b " "${GREEN}" "$1" "${RESET}"
		read -r answer
		if [[ "$answer" == "y" ]]; then
			return 0
		elif [[ "$answer" == "n" ]]; then
			return 1
		else
			echo -e "${RED}\nPlease enter 'y' or 'n'${RESET}"
		fi
	done
}

# ask continue, or quit
ask_continue() {
	if ! ask_yn "continue"; then
		exit
	fi
}

# ask continue, or quit
ask_build_deb() {
	if ! ask_yn "build deb"; then
		exit
	fi
}

# semver major update
update_major() {
	local bumped_major
	bumped_major=$((MAJOR + 1))
	echo "${bumped_major}.0.0"
}

# semver minor update
update_minor() {
	local bumped_minor
	bumped_minor=$((MINOR + 1))
	MINOR=bumped_minor
	echo "${MAJOR}.${bumped_minor}.0"
}

# semver patch update
update_patch() {
	local bumped_patch
	bumped_patch=$((PATCH + 1))
	PATCH=bumped_patch
	echo "${MAJOR}.${MINOR}.${bumped_patch}"
}

# Get the url of the github repo, strip .git from the end of it
get_git_remote_url() {
	GIT_REPO_URL="$(git config --get remote.origin.url | sed 's/\.git$//')"
}

# Check that git status is clean
check_git_clean() {
	GIT_CLEAN=$(git status --porcelain)
	if [[ -n $GIT_CLEAN ]]; then
		error_close "git dirty"
	fi
}

# Check currently on dev branch
check_git() {
	CURRENT_GIT_BRANCH=$(git branch --show-current)
	check_git_clean
	if [[ ! "$CURRENT_GIT_BRANCH" =~ ^dev$ ]]; then
		error_close "not on dev branch"
	fi
}

# Ask user if current changelog is acceptable
ask_changelog_update() {
	echo "${STAR_LINE}"
	RELEASE_BODY_TEXT=$(sed '/# <a href=/Q' CHANGELOG.md)
	printf "%s" "$RELEASE_BODY_TEXT"
	printf "\n%s\n" "${STAR_LINE}"

	if ask_yn "accept release body"; then
		update_release_body_and_changelog "$RELEASE_BODY_TEXT"
	else
		exit
	fi
}

# Edit the release-body to include new lines from changelog
# add commit urls to changelog
# $1 RELEASE_BODY
update_release_body_and_changelog() {
	echo -e
	DATE_SUBHEADING="### $(date +'%Y-%m-%d')\n\n"
	RELEASE_BODY_ADDITION="${DATE_SUBHEADING}$1"

	# Put new changelog entries into release-body, add link to changelog
	echo -e "${RELEASE_BODY_ADDITION}\n\nsee <a href='${GIT_REPO_URL}/blob/main/CHANGELOG.md'>CHANGELOG.md</a> for more details" >.github/release-body.md

	# Add subheading with release version and date of release
	echo -e "# <a href='${GIT_REPO_URL}/releases/tag/${NEW_TAG_WITH_V}'>${NEW_TAG_WITH_V}</a>\n${DATE_SUBHEADING}${CHANGELOG_ADDITION}$(cat CHANGELOG.md)" >CHANGELOG.md

	# Update changelog to add links to commits [hex:8](url_with_full_commit)
	# "[aaaaaaaaaabbbbbbbbbbccccccccccddddddddd]" -> "[aaaaaaaa](https:/www.../commit/aaaaaaaaaabbbbbbbbbbccccccccccddddddddd)"
	sed -i -E "s=(\s)\[([0-9a-f]{8})([0-9a-f]{32})\]= [\2](${GIT_REPO_URL}/commit/\2\3)=g" CHANGELOG.md

	# Update changelog to add links to closed issues
	# "closes #1" -> "closes [#1](https:/www.../issues/1)""
	sed -i -r -E "s=closes \#([0-9]+)=closes [#\1](${GIT_REPO_URL}/issues/\1)=g" CHANGELOG.md

	# Update changelog to add links to merged PR's
	# "merges #1" -> "merges [#1](https:/www.../pull/1)""
	sed -i -r -E "s=merges \#([0-9]+)=merges [#\1](${GIT_REPO_URL}/pull/\1)=g" CHANGELOG.md
}

# update version in cargo.toml, to match selected current version
update_version_number_in_files() {
	sed -i "s|^version = .*|version = \"${MAJOR}.${MINOR}.${PATCH}\"|" Cargo.toml
}

# Work out the current version, based on git tags
# create new semver version based on user input
# Set MAJOR MINOR PATCH
check_tag() {
	LATEST_TAG=$(git describe --tags "$(git rev-list --tags --max-count=1)")
	echo -e "\nCurrent tag: ${PURPLE}${LATEST_TAG}${RESET}\n"
	echo -e "${YELLOW}Choose new tag version:${RESET}\n"
	if [[ $LATEST_TAG =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ ]]; then
		IFS="." read -r MAJOR MINOR PATCH <<<"${LATEST_TAG:1}"
	else
		MAJOR="0"
		MINOR="0"
		PATCH="0"
	fi
	OP_MAJOR="major___v$(update_major)"
	OP_MINOR="minor___v$(update_minor)"
	OP_PATCH="patch___v$(update_patch)"
	OPTIONS=("$OP_MAJOR" "$OP_MINOR" "$OP_PATCH")
	select choice in "${OPTIONS[@]}"; do
		case $choice in
		"$OP_MAJOR")
			MAJOR=$((MAJOR + 1))
			MINOR=0
			PATCH=0
			break
			;;
		"$OP_MINOR")
			MINOR=$((MINOR + 1))
			PATCH=0
			break
			;;
		"$OP_PATCH")
			PATCH=$((PATCH + 1))
			break
			;;
		*)
			error_close "invalid option $REPLY"
			;;
		esac
	done
}

# run all tests
cargo_test() {
	cargo test -- --test-threads=1
	ask_continue
}

# Simulate publishing to crates.io
cargo_publish_dry_run() {
	echo -e "${PURPLE}cargo publish --dry-run${RESET}"
	cargo publish --dry-run
	ask_continue
}

# Check to see if cross is installed - if not then install
check_cross() {
	if ! [ -x "$(command -v cross)" ]; then
		echo -e "${GREEN}cargo install cross${RESET}"
		cargo install cross
	fi
}

# Build, using cross-rs, for linux x86 musl
cross_build_x86_linux() {
	skip_confirm=$1
	check_cross
	echo -e "${YELLOW}cross build --target x86_64-unknown-linux-musl --release${RESET}"
	cross build --target x86_64-unknown-linux-musl --release
	[ "$skip_confirm" -ne 1 ] && ask_build_deb
	cargo deb --target=x86_64-unknown-linux-musl --no-build
}

# Build, using cross-rs, for linux arm64 musl
cross_build_aarch64_linux() {
	skip_confirm=$1
	check_cross
	echo -e "${YELLOW}cross build --target aarch64-unknown-linux-musl --release${RESET}"
	cross build --target aarch64-unknown-linux-musl --release
	[ "$skip_confirm" -ne 1 ] && ask_build_deb
	cargo deb --target=aarch64-unknown-linux-musl --no-build
}

# Build, using cross-rs, for linux armv6 musl
cross_build_armv6_linux() {
	skip_confirm=$1
	check_cross
	echo -e "${YELLOW}cross build --target arm-unknown-linux-musleabihf --release${RESET}"
	cross build --target arm-unknown-linux-musleabihf --release
	[ "$skip_confirm" -ne 1 ] && ask_build_deb
	cargo deb --target arm-unknown-linux-musleabihf --no-build
}

# Build, using cross-rs, for windows x86
cross_build_x86_windows() {
	check_cross
	echo -e "${YELLOW}cross build --target x86_64-pc-windows-gnu --release${RESET}"
	cross build --target x86_64-pc-windows-gnu --release
}

# Build, using zig-build, for Apple silicon
zig_build_aarch64_apple() {
	# mkdir /workspace/oxker/target
	echo -e "${YELLOW}docker run --rm -v $(pwd):/io -w /io ghcr.io/rust-cross/cargo-zigbuild bash -e -c 'rustup update stable && rustup default stable && rustup target add aarch64-apple-darwin && cargo zigbuild --release --target aarch64-apple-darwin${RESET}"

	docker run --rm -v "$(pwd):/io" -w /io \
	ghcr.io/rust-cross/cargo-zigbuild \
	bash -ec 'rustup update stable && rustup default stable && rustup target add aarch64-apple-darwin && cargo zigbuild --release --target aarch64-apple-darwin'

	if ask_yn "sudo chown $(pwd)/target"; then
		echo -e "${YELLOW}sudo chown -R vscode:vscode $(pwd)/target${RESET}"
		sudo chown -R vscode:vscode "$(pwd)/target"
	fi
}

cargo_clean() {
	echo -e "${YELLOW}cargo clean${RESET}"
	cargo clean
}

# Build all releases that GitHub workflow would
# This will download GB's of docker images
# $1 is 0 or 1, if 1 won't run ask_continue
cross_build_all() {
	if ask_yn "cargo clean"; then
		cargo_clean
	fi
	skip_confirm=$1
	cross_build_armv6_linux "${skip_confirm}"
	 [ "$skip_confirm" -ne 1 ] && ask_continue
	cross_build_aarch64_linux "${skip_confirm}"
	 [ "$skip_confirm" -ne 1 ] && ask_continue
	cross_build_x86_linux "${skip_confirm}"
	 [ "$skip_confirm" -ne 1 ] && ask_continue
	cross_build_x86_windows
	 [ "$skip_confirm" -ne 1 ] && ask_continue
	zig_build_aarch64_apple
	 [ "$skip_confirm" -ne 1 ] && ask_continue
}

# $1 text to colourise
release_continue() {
	echo -e "\n${PURPLE}$1${RESET}"
	ask_continue
}

# Check repository for typos
check_typos() {
	echo -e "\n${PURPLE}check typos${RESET}"
	typos
	ask_continue
}

# Make sure the unused lint isn't used
check_allow_unused() {
	matches_any=$(find . -type d \( -name .git -o -name target \) -prune -o -type f -exec grep -lE '^#!\[allow\(unused\)\]$' {} +)
	matches_cargo=$(grep "^unused = \"allow\"" ./Cargo.toml)
	if [ -n "$matches_any" ]; then
		echo "\"#[allow(unused)]\" in ${matches_any}"
		ask_continue
	elif [ -n "$matches_cargo" ]; then
		echo "\"unused = \"allow\"\" in Cargo.toml"
		ask_continue
	fi
}

# build container for amd64 platform
build_container_amd64() {
	echo -e "${YELLOW}docker build  --platform linux/amd64 --no-cache -t oxker_amd64 --no-cache -f containerised/Dockerfile .; docker save -o /tmp/oxker_amd64.tar oxker_amd64${RESET}"
	docker build --platform linux/amd64 --no-cache -t oxker_amd64 -f containerised/Dockerfile .
	docker save -o /tmp/oxker_amd64.tar oxker_amd64
}

# build container for aarm64 platform
build_container_arm64() {
	echo -e "${YELLOW}docker build  --platform linux/arm64 --no-cache -t oxker_arm64 --no-cache -f containerised/Dockerfile .; docker save -o /tmp/oxker_arm64.tar oxker_arm64${RESET}"
	docker build --platform linux/arm64 --no-cache -t oxker_arm64 -f containerised/Dockerfile .
	docker save -o /tmp/oxker_arm64.tar oxker_arm64
}

# build container for armv6 platform
build_container_armv6() {
	echo -e "${YELLOW}docker build  --platform linux/arm/v6 --no-cache -t oxker_armv6 --no-cache -f containerised/Dockerfile .; docker save -o /tmp/oxker_armv6.tar oxker_armv6${RESET}"
	docker build --platform linux/arm/v6 --no-cache -t oxker_armv6 -f containerised/Dockerfile .
	docker save -o /tmp/oxker_armv6.tar oxker_armv6
}

# Build all the containers, this get executed in the github action
# $1 is 0 or 1, if 1 won't run ask_continue
build_container_all() {
	skip_confirm=$1
	build_container_amd64 
	[ "$skip_confirm" -ne 1 ] && ask_continue
	build_container_arm64 
	[ "$skip_confirm" -ne 1 ] && ask_continue
	build_container_armv6 
	[ "$skip_confirm" -ne 1 ] && ask_continue
}

# Full flow to create a new release
release_flow() {
	check_allow_unused
	check_typos

	check_git
	get_git_remote_url

	cargo_test
	cross_build_all 0
	build_container_all 0
	cargo_publish_dry_run

	cd "${CWD}" || error_close "Can't find ${CWD}"
	check_tag

	NEW_TAG_WITH_V="v${MAJOR}.${MINOR}.${PATCH}"
	printf "\nnew tag chosen: %s\n\n" "${NEW_TAG_WITH_V}"

	RELEASE_BRANCH=release-$NEW_TAG_WITH_V
	echo -e
	ask_changelog_update

	release_continue "checkout ${RELEASE_BRANCH}"
	git checkout -b "$RELEASE_BRANCH"

	release_continue "update_version_number_in_files"
	update_version_number_in_files

	echo -e "\ncargo fmt"
	cargo fmt

	echo -e "\n${PURPLE}cargo check${RESET}\n"
	cargo check

	release_continue "git add ."
	git add .

	release_continue "git commit -m \"chore: release \"${NEW_TAG_WITH_V}\""
	git commit -m "chore: release ${NEW_TAG_WITH_V}"

	release_continue "git checkout main"
	git checkout main

	echo -e "${PURPLE}git pull origin main${RESET}"
	git pull origin main

	echo -e "${PURPLE}git merge --no-ff \"${RELEASE_BRANCH}\" -m \"chore: merge ${RELEASE_BRANCH} into main\"${RESET}"
	git merge --no-ff "$RELEASE_BRANCH" -m "chore: merge ${RELEASE_BRANCH} into main"

	echo -e "\n${PURPLE}cargo check${RESET}\n"
	cargo check

	release_continue "git tag -am \"${RELEASE_BRANCH}\" \"$NEW_TAG_WITH_V\""
	git tag -am "${RELEASE_BRANCH}" "$NEW_TAG_WITH_V"

	release_continue "git push --atomic origin main \"$NEW_TAG_WITH_V\""
	git push --atomic origin main "$NEW_TAG_WITH_V"

	release_continue "git checkout dev"
	git checkout dev

	release_continue "git merge --no-ff main -m \"chore: merge main into dev\""
	git merge --no-ff main -m "chore: merge main into dev"

	release_continue "git push origin dev"
	git push origin dev

	release_continue "git branch -d \"$RELEASE_BRANCH\""
	git branch -d "$RELEASE_BRANCH"
}

build_choice() {
	cmd=(dialog --backtitle "Choose option" --keep-tite --radiolist "choose" 14 80 16)
	options=(
		1 "x86 musl linux" off
		2 "aarch64 musl linux" off
		3 "armv6 musl linux" off
		4 "aarch64 apple" off
		5 "x86 windows" off
		6 "all" off
		7 "all automatic" off
	)
	choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty)
	exitStatus=$?
	if [ $exitStatus -ne 0 ]; then
		exit
	fi
	for choice in $choices; do
		case $choice in
		0)
			exit
			;;
		1)
			cross_build_x86_linux 0
			exit
			;;
		2)
			cross_build_aarch64_linux 0
			exit
			;;
		3)
			cross_build_armv6_linux 0
			exit
			;;
		4)
			zig_build_aarch64_apple
			exit
			;;
		5)
			cross_build_x86_windows
			exit
			;;
		6)
			cross_build_all 0
			exit
			;;
		7)
			cross_build_all 1
			exit
			;;
		esac
	done
}

build_container_choice() {
	cmd=(dialog --backtitle "Choose option" --keep-tite --radiolist "choose" 14 80 16)
	options=(
		1 "x86 " off
		2 "aarch64" off
		3 "armv6" off
		4 "all" off
		5 "all automatic" off
	)
	choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty)
	exitStatus=$?
	if [ $exitStatus -ne 0 ]; then
		exit
	fi
	for choice in $choices; do
		case $choice in
		0)
			exit
			;;
		1)
			build_container_amd64 0
			exit
			;;
		2)
			build_container_arm64 0
			exit
			;;
		3)
			build_container_armv6 0
			exit
			;;
		4)
			build_container_all 0
			exit
			;;
		5)
			build_container_all 1
			exit
			;;
		esac
	done

}

main() {
	cmd=(dialog --backtitle "Choose option" --keep-tite --radiolist "choose" 14 80 16)
	options=(
		1 "test" off
		2 "release" off
		3 "build" off
		4 "docker builds" off
	)
	choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty)
	exitStatus=$?
	if [ $exitStatus -ne 0 ]; then
		exit
	fi
	for choice in $choices; do
		case $choice in
		0)
			exit
			;;
		1)
			cargo_test
			main
			break
			;;
		2)
			release_flow
			break
			;;
		3)
			build_choice
			main
			break
			;;
		4)
			build_container_choice
			main
			break
			;;
		esac
	done
}

main
