#!/usr/bin/env sh
# httpmirror-cli - a CLI posix shell script to replace softwares' registry mirror

# MIT License
# 
# Copyright (c) 2023 Dongliang Mu <dzm91@hust.edu.cn>
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# 
# 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 OR COPYRIGHT HOLDERS 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.

set -e

script_version="v1.1.4"

domains="mirrors.hust.edu.cn"

gen_tag="Generated by hustmirror-cli"

build_time="2024-05-11 02:41:25 UTC"

print_logo() {
	r_echo ' _   _ _   _ ____ _____ __  __ ___ ____  ____   ___  ____  '
	r_echo '| | | | | | / ___|_   _|  \/  |_ _|  _ \|  _ \ / _ \|  _ \ '
	r_echo '| |_| | | | \___ \ | | | |\/| || || |_) | |_) | | | | |_) |'
	r_echo '|  _  | |_| |___) || | | |  | || ||  _ <|  _ <| |_| |  _ < '
	r_echo '|_| |_|\___/|____/ |_| |_|  |_|___|_| \_\_| \_\\___/|_| \_\'
	r_echo
	r_echo "hustmirror-cli ${script_version} build ${build_time}"
}

source_os_release() {
	# /etc/os-release does exist in most Linux distributions, and BSD variants
	test -e /etc/os-release && os_release='/etc/os-release' || os_release='/usr/lib/os-release'
	if [ -f "${os_release}" ]; then
		. "${os_release}"
	else
		NAME="Unknown"
	fi
}

is_root() {
	[ "$(id -u)" -eq 0 ] 
}

is_windows() {
	uname -a | grep -qiE "cygwin|mingw|msys"
}

has_command() {
	command -v "${1}" >/dev/null 2>&1
}

has_sudo() {
	has_command sudo
}

has_curl() {
	has_command curl
}

has_git() {
	has_command git
}

has_sed() {
	has_command sed
}

is_tty() {
	# cache it to avoid too many forks
	[ -z "$IS_TTY" ] && {
		IS_TTY=0
		# check if stdout is a tty, if is, then check if /dev/tty is readable
		[ -t 1 ] && sh -c "exec < /dev/tty" >/dev/null 2>&1 || IS_TTY=1
	} 
	return $IS_TTY
}

c_echo() {
	if has_command printf; then
		printf "$*\n"
	else
		echo "$*"
	fi
}

r_echo() {
	if has_command printf; then
		printf "%s\n" "$*"
	else
		echo "$*"
	fi
}

echo_red() {
	if is_tty; then
		c_echo "\033[0;31m${1}\033[0m"
	else
		c_echo "${1}"
	fi
}

echo_green() {
	if is_tty; then
		c_echo "\033[0;32m${1}\033[0m"
	else
		c_echo "${1}"
	fi
}

echo_yellow() {
	if is_tty; then
		c_echo "\033[0;33m${1}\033[0m"
	else
		c_echo "${1}"
	fi
}

echo_blue() {
	if is_tty; then
		c_echo "\033[0;34m${1}\033[0m"
	else
		c_echo "${1}"
	fi
}

print_error() {
	echo_red "[ERR] ${1}"
}

print_warning() {
	echo_yellow "[WARN] ${1}"
}

print_success() {
	echo_green "[+] ${1}"
}

print_info() {
	echo_green "[!] ${1}"
}

print_status() {
	echo_yellow "[*] ${1}"
}

print_question() {
	echo_yellow "[?] ${1}"
}

get_input() {
	if ! is_tty || [ "$silent_input" = "y" ]; then
		if [ -n "${2}" ]; then
			input="${2}"
			return 0
		fi
		return 1
	fi
	[ -n "${1}" ] && print_question "${1}"
	read -r -p "[>] " input </dev/tty
	if [ -z "${input}" ]; then
		input="${2}"
	fi
}

confirm() {
	# call with a prompt string or use a default
	get_input "${1:-Are you sure?} [y/N]" "n"
	case "${input}" in
		[yY][eE][sS]|[yY])
			true
			;;
		*)
			false
			;;
	esac
}

confirm_y() {
	# call with a prompt string or use a default
	get_input "${1:-Are you sure?} [Y/n]" "y"
	case "${input}" in
		[nN][oO]|[nN])
			false
			;;
		*)
			true
			;;
	esac
}

print_supported() {
	print_info "Supported softwares:"
	echo "$supported_softwares" | xargs echo "   " | fold -s -w 80
}

set_sudo(){
	sudo=''
	if ! is_root; then
		print_warning "You are not root, trying to use sudo..."
		has_sudo || {
			print_error "No sudo command found, please install sudo first."
			return 1
		}
		sudo='sudo'
	fi
}


# ask user to select a item from a menu
# $1 tip message
# $2 menu items
select_from_menu() {
	message=$1
	shift
	menu_items=$@
	menu_number=0
	# implement array in POSIX shell
	set -- $menu_items
	print_question "$message"
	for item in $menu_items
	do
		menu_number=$(expr $menu_number + 1)
		echo "    $menu_number:" "$item"
	done
	while true; do
		get_input "Input an item number from the list above."
		result=$(eval "echo \$$input")
		if [ -z "$result" ]; then
			print_error "Bad input!"
		else
			break
		fi
	done
}

set_default_domain() {
	set -- $*
	domain=$1
}

# help bootstrap
if [ "$0" = "sh" ] || [ "$0" = "bash" ]; then
	program="hust-mirror.sh"
else
	program=$(basename "$0")
fi

# help text, define _help_(topic) variable
_help_basic="A CLI posix shell script to generate a configuration file for software
repository on different distributions.

Usage: $program [options...]
       $program [command] [targets...]

Options:
   -h, --help                 Display this help message
   -i                         Enter interact mode

Commands: (use \`$program help [command]\` to get more information)
   help                       Display help message
   deploy                     Deploy the configuration file
   autodeploy                 Deploy suggested configuration file
   recover                    Recover the configuration file
   autorecover                Recover deployed and recoverable configuration file
   install                    Install this script to user's local bin

Commands alias:
   h                          help
   d                          deploy
   ad                         autodeploy
   r                          recover
   ar                         autorecover
   i|u|update                 install

Examples:
- Enter interact mode
   $program -i
- Deploy some configuration file
   $program deploy openeuler
- Get command help
   $program help ad

Environments: (optional)
   HM_HTTP                    protoal (http/https), default is http
   HM_DOMAIN                  domain name

"

_help_help="Get help of a command or a target topic.

Usage: $program help [command|topic]
       $program h [command|topic] (alias)

Examples:
- Get help of a command
   $program help deploy
- Get help of a topic
   $program help debian

"

_help_deploy="
Deploy the configuration file.

Usage: $program deploy [targets...]
       $program d [targets...] (alias)

Examples:
- Deploy the configuration file for openeuler and pypi
   $program deploy openeuler pypi

"

_help_autodeploy="
Check the system and deploy suggested configuration file.

Usage: $program autodeploy
       $program ad (alias)

Options: (optional)
   -y                         Answer default option to all questions

"

_help_recover="Recover the configuration file.

Usage: $program recover [targets...]
       $program r [targets...] (alias)

"

_help_install="Install (Update) this script online to user's local bin.

Usage: $program install
       $program i | u | update (alias)

Note: This command will install the script to ~/.local/bin, and add it to
      PATH in ~/.bashrc or ~/.zshrc.

"

_help_autorecover="Recover deployed and recoverable configuration file.

Usage: $program autorecover
       $program ar (alias)

Note: This command will only recover the configuration file that can be recovered,
      if you don't use this tool to deploy or have deployed some configurations not 
      support to recover, you can't recover it.

"

_help_d=${_help_deploy}
_help_ad=${_help_autodeploy}
_help_r=${_help_recover}
_help_ar=${_help_autorecover}
_help_i=${_help_install}
_help_u=${_help_install}
_help_h=${_help_help}
_help_update=${_help_install}

_help_debian="Debian mirror configuration.

Environments:
   DEBIAN_USE_SID            Use unstable instead of testing.

"

display_help() {
	echo

	if [ -z "$1" ]; then
		r_echo "$_help_basic"
		return
	fi

	eval help_text=\"\${_help_${1}}\"

	if [ -z "$help_text" ]; then
		print_error "No help information for $1"
	else
		r_echo "$help_text"
	fi
}

# expand tab for help text
# code style is noexpandtab

set_default_domain $domains
http="https"

# Not only to replace distributions but also softwares like pypi,
# npm registry, dockerhub, etc.
supported_softwares="alpine
anolis
archlinux
archlinuxcn
blackarch
crates
debian
deepin
kali
linuxmint
ohmyzsh
openeuler
openkylin
pypi
rustup
ubuntu"

# BEGIN MIRRORS
_blackarch_config_file="/etc/pacman.d/blackarch-mirrorlist"

_blackarch_check() {
	source_os_release
	[ "$NAME" = "Arch Linux" ]
}

_blackarch_install() {
	config_file=$_blackarch_config_file
	set_sudo

	$sudo cp ${config_file} ${config_file}.bak || {
		print_error "Failed to backup ${config_file}"
		return 1
	}

	old_config=$(cat ${config_file})
	{
		cat << EOF | $sudo tee ${config_file} > /dev/null
# ${gen_tag}
Server = ${http}://${domain}/blackarch/\$repo/os/\$arch
${old_config}
EOF
	} || {
		print_error "Failed to add mirror to ${config_file}"
		return 1
	}
}

_blackarch_is_deployed() {
	config_file=$_blackarch_config_file
	pattern="^[^#]*Server\s*=\s*${http}://${domain}/blackarch/\\\$repo/os/\\\$arch"
	grep -qE "${pattern}" ${config_file} 
}

_blackarch_can_recover() {
	bak_file=${_blackarch_config_file}.bak
	result=0
	test -f $bak_file || result=$?
	return $result
}

_blackarch_uninstall() {
	config_file=$_blackarch_config_file
	set_sudo
	$sudo mv ${config_file}.bak ${config_file} || {
		print_error "Failed to recover ${config_file}"
		return 1
	}
}

_debian_check() {
	source_os_release
	[ "$NAME" = "Debian GNU/Linux" ]
}

_debian_set_config_file() {
	config_file="/etc/apt/sources.list"
	if ! [ -f $config_file ]; then # rule for docker
		old_file="/etc/apt/sources.list.d/debian.sources"
	else
		old_file=$config_file
	fi
}

_debian_install() {
	_debian_set_config_file
	source_os_release
	codename=${VERSION_CODENAME}
	echo "$PRETTY_NAME" | grep "sid" > /dev/null && {
		if [ "$HM_DEBIAN_SID" = "true" ]; then
			codename="sid"
		else
			print_warning "hustmirror-cli cannot distinguish sid or testing"
			get_input "Please input codename (sid/testing): " "testing"
			codename="$input"
		fi
	}

	set_sudo

	if [ -f $config_file ]; then
		$sudo cp ${config_file} ${config_file}.bak || {
			print_error "Failed to backup ${config_file}"
			return 1
		}
	else
		print_warning "No ${config_file} found, creating new one"
	fi

	secure_url="${http}://${domain}/debian-security/"
	confirm_y "Use official secure source? (Strongly recommended)" && \
		secure_url="${http}://security.debian.org/debian-security"

	src_prefix="# "
	confirm "Use source code?" && \
		src_prefix=""


	security_appendix='-security'
	[ "$codename" = "buster" ] && security_appendix='/updates'

	NFW=''
	if [ "$codename" = "bookworm" ] || [ "$codename" = "sid" ] || [ "$codename" = "testing" ]; then
	  NFW=' non-free-firmware'	
	fi

	if [ "$codename" = "sid" ]; then
		sid_prefix="# "
	fi


	$sudo sh -e -c "cat << EOF > ${config_file}
# ${gen_tag}
deb ${http}://${domain}/debian ${codename} main contrib non-free${NFW}
${src_prefix}deb-src ${http}://${domain}/debian ${codename} main contrib non-free${NFW}

${sid_prefix}deb ${http}://${domain}/debian ${codename}-updates main contrib non-free${NFW}
${sid_prefix}${src_prefix}deb-src ${http}://${domain}/debian ${codename}-updates main contrib non-free${NFW}

${sid_prefix}deb ${http}://${domain}/debian ${codename}-backports main contrib non-free${NFW}
${sid_prefix}${src_prefix}deb-src ${http}://${domain}/debian ${codename}-backports main contrib non-free${NFW}

${sid_prefix}deb ${secure_url} ${codename}${security_appendix} main contrib non-free${NFW}
${sid_prefix}${src_prefix}deb-src ${http}://security.debian.org/debian-security ${codename}${security_appendix} main contrib non-free${NFW}

EOF" || {
		print_error "Failed to add mirror to ${config_file}"
		return 1
	}

	confirm_y "Do you want to apt update?" && {
		$sudo apt update || {
			print_error "apt update failed"
			return 1
		}
	}

	true
}

_debian_uninstall() {
	_debian_set_config_file
	set_sudo
	$sudo sh -c "rm ${config_file}; mv ${old_file}.bak ${old_file}" || {
		print_error "Failed to recover ${old_file}"
		return 1
	}
}

_debian_is_deployed() {
	_debian_set_config_file
	$sudo grep -q "${gen_tag}" ${config_file}
}

_debian_can_recover() {
	_debian_set_config_file
	bak_file="$old_file.bak"
	test -f $bak_file
}


_synonyms_arch="archlinuxcn"
_archlinuxcn_config_file="/etc/pacman.conf"

_archlinuxcn_check() {
	source_os_release
	[ "$NAME" = "Arch Linux" ]
}

_archlinuxcn_install() {
	config_file=$_archlinuxcn_config_file
	set_sudo

	$sudo cp ${config_file} ${config_file}.bak || {
		print_error "Failed to backup ${config_file}"
		return 1
	}

	old_config=$(cat ${config_file})
	{
		cat << EOF | $sudo tee ${config_file} > /dev/null
${old_config}
# ${gen_tag}
[archlinuxcn]
Server = ${http}://${domain}/archlinuxcn/\$arch
EOF
	} || {
		print_error "Failed to add mirror to ${config_file}"
		return 1
	}
}

_archlinuxcn_is_deployed() {
	config_file=$_archlinuxcn_config_file
	pattern="^[^#]*Server\s*=\s*${http}://${domain}/archlinuxcn/\\\$arch"
	grep -qE "${pattern}" ${config_file} 
}

_archlinuxcn_can_recover() {
	bak_file=${_archlinuxcn_config_file}.bak
	result=0
	test -f $bak_file || result=$?
	return $result
}

_archlinuxcn_uninstall() {
	config_file=$_archlinuxcn_config_file
	set_sudo
	$sudo mv ${config_file}.bak ${config_file} || {
		print_error "Failed to recover ${config_file}"
		return 1
	}
}


_linuxmint_config_file="/etc/apt/sources.list.d/official-package-repositories.list"

_linuxmint_check() {
	source_os_release
	[ "$NAME" = "Linux Mint" ]
}

_linuxmint_install() {
	config_file=$_linuxmint_config_file
	source_os_release

	codename=${VERSION_CODENAME}
	ubuntu_codename=${UBUNTU_CODENAME}
	set_sudo

	$sudo cp ${config_file} ${config_file}.bak || {
		print_error "Failed to backup ${config_file}"
		return 1
	}

	# Replace linuxmint source, remove lines related to Ubuntu at the same time
    new_file=$(sed -E -e "s|https?://([^/]+)/linuxmint|${http}://${domain}/linuxmint|;s|https?://packages\.linuxmint\.com|${http}://${domain}/linuxmint|;/ubuntu/d" $config_file)
	{
		cat << EOF | $sudo tee ${config_file} > /dev/null
# ${gen_tag}
${new_file}
EOF
	} || {
		print_error "Failed to add mirror to ${config_file}"
		return 1
	}

	# Append Ubuntu source to the end of the file
	print_info "Adding Ubuntu mirror to ${config_file}:"
	secure_url="${http}://${domain}/ubuntu/"
	confirm_y "Use official secure source? (Strongly recommended)" && \
		secure_url="http://security.ubuntu.com/ubuntu/"

	propoesd_prefix="# "
	confirm "Use proposed source?" && \
		propoesd_prefix=""

	src_prefix="# "
	confirm "Use source code?" && \
		src_prefix=""

	$sudo sh -e -c "cat <<EOF >> ${config_file}
# ${gen_tag}
deb ${http}://${domain}/ubuntu/ ${ubuntu_codename} main restricted universe multiverse
${src_prefix}deb-src ${http}://${domain}/ubuntu/ ${ubuntu_codename} main restricted universe multiverse
deb ${http}://${domain}/ubuntu/ ${ubuntu_codename}-updates main restricted universe multiverse
${src_prefix}deb-src ${http}://${domain}/ubuntu/ ${ubuntu_codename}-updates main restricted universe multiverse
deb ${secure_url} ${ubuntu_codename}-security main restricted universe multiverse
${src_prefix}deb-src ${secure_url} ${ubuntu_codename}-security main restricted universe multiverse

${propoesd_prefix}deb ${http}://${domain}/ubuntu/ ${ubuntu_codename}-proposed main restricted universe multiverse
${propoesd_prefix}deb-src ${http}://${domain}/ubuntu/ ${ubuntu_codename}-proposed main restricted universe multiverse
EOF" || {
		print_error "Failed to add Ubuntu mirror to ${config_file}"
		return 1
	}

	confirm_y "Do you want to apt update?" && {
		$sudo apt update || {
			print_error "apt update failed"
			return 1
		}
	}

	true
}

_linuxmint_uninstall() {
	config_file=$_linuxmint_config_file
    
	set_sudo
	$sudo mv ${config_file}.bak ${config_file} || {
		print_error "Failed to recover ${config_file}"
		return 1
	}
}

_linuxmint_is_deployed() {
	config_file=$_linuxmint_config_file
	result=0
	$sudo grep -q "${gen_tag}" ${config_file} || result=$?
	return $result
}

_linuxmint_can_recover() {
	bak_file=${_linuxmint_config_file}.bak
	result=0
	test -f $bak_file || result=$?
	return $result
}


_synonyms_arch="archlinux"
_archlinux_config_file="/etc/pacman.d/mirrorlist"

_archlinux_check() {
	source_os_release
	[ "$NAME" = "Arch Linux" ]
}

_archlinux_install() {
	config_file=$_archlinux_config_file
	set_sudo

	$sudo cp ${config_file} ${config_file}.bak || {
		print_error "Failed to backup ${config_file}"
		return 1
	}

	old_config=$(cat ${config_file})
	{
		cat << EOF | $sudo tee ${config_file} > /dev/null
# ${gen_tag}
Server = ${http}://${domain}/archlinux/\$repo/os/\$arch
${old_config}
EOF
	} || {
		print_error "Failed to add mirror to ${config_file}"
		return 1
	}
}

_archlinux_is_deployed() {
	config_file=$_archlinux_config_file
	pattern="^[^#]*Server\s*=\s*${http}://${domain}/archlinux/\\\$repo/os/\\\$arch"
	grep -qE "${pattern}" ${config_file} 
}

_archlinux_can_recover() {
	bak_file=${_archlinux_config_file}.bak
	result=0
	test -f $bak_file || result=$?
	return $result
}

_archlinux_uninstall() {
	config_file=$_archlinux_config_file
	set_sudo
	$sudo mv ${config_file}.bak ${config_file} || {
		print_error "Failed to recover ${config_file}"
		return 1
	}
}


_deepin_check() {
	source_os_release
	[ "$NAME" = "Deepin" ]
}

_deepin_install() {
	config_file="/etc/apt/sources.list"
	source_os_release

	if [ -z "$VERSION_CODENAME" ]; then
		print_error "Unsupported Deepin version"
		return 1
	fi

	codename=${VERSION_CODENAME}
	set_sudo

	$sudo cp ${config_file} ${config_file}.bak || {
		print_error "Failed to backup ${config_file}"
		return 1
	}

	new_file=$(sed -E -e "s|https?://([^/]+)/deepin|${http}://${domain}/deepin|" -e "s|https?://([^/]+)/${codename}|${http}://${domain}/deepin/${codename}|" $config_file)
	{
		cat << EOF | $sudo tee ${config_file} > /dev/null
# ${gen_tag}
${new_file}
EOF
	} || {
		print_error "Failed to add mirror to ${config_file}"
		return 1
	}

    confirm_y "Do you want to apt update?" && {
		$sudo apt update || {
			print_error "apt update failed"
			return 1
		}
	}

	true
}

_deepin_uninstall() {
	config_file="/etc/apt/sources.list"
	set_sudo
	$sudo mv ${config_file}.bak ${config_file} || {
		print_error "Failed to recover ${config_file}"
		return 1
	}
}

_deepin_is_deployed() {
	config_file="/etc/apt/sources.list"
	result=0
	$sudo grep -q "${gen_tag}" ${config_file} || result=$?
	return $result
}

_deepin_can_recover() {
	bak_file="/etc/apt/sources.list.bak"
	result=0
	test -f $bak_file || result=$?
	return $result
}


_synonyms_python="pypi"
_synonyms_pip="pypi"
_synonyms_pdm="pypi"

_pypi_python="python"
_pypi_backup_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hustmirror/backup"
_pypi_backup_pip="${_pypi_backup_dir}/pip.bak"
_pypi_backup_pdm="${_pypi_backup_dir}/pdm.bak"

_pypi_has_pip=""
_pypi_has_pdm=""

_pypi_isdeployed_pip=""
_pypi_isdeployed_pdm=""

_pypi_set_python() {
	if is_windows; then
		if ! has_command "python"; then
			_pypi_python=$(reg query HKCU\\Software\\Python //s //f ExecutablePath //e |
				grep ExecutablePath | head -n 1 | tr -s " " | cut -f 4 -d " ")
		fi
	fi
}

_pypi_check() {
	_pypi_set_python
	has_command "$_pypi_python" && "$_pypi_python" -m pip --version >/dev/null 2>&1 && _pypi_has_pip=1
	has_command "$_pypi_python" && pdm --version >/dev/null 2>&1 && _pypi_has_pdm=1

	[ -n "$_pypi_has_pip" ] || [ -n "$_pypi_has_pdm" ]
}

_pypi_is_deployed() {
	if [ -n "$_pypi_has_pip" ] &&
		$_pypi_python -m pip config get global.index-url 2>/dev/null | grep -qE "$domain"; then
		_pypi_isdeployed_pip=1
	fi
	if [ -n "$_pypi_has_pdm" ] &&
		pdm config pypi.url 2>/dev/null | grep -qE "$domain"; then
		_pypi_isdeployed_pdm=1
	fi
	[ -n "$_pypi_isdeployed_pip" ] && [ -n "$_pypi_isdeployed_pdm" ]
}

_pypi_can_recover() {
	[ -f "$_pypi_backup_pip" ] || [ -f "$_pypi_backup_pdm" ]
}

_pypi_install() {
	mkdir -p "$_pypi_backup_dir"
	if [ -n "$_pypi_has_pip" ]; then
		$_pypi_python -m pip config get global.index-url 2>/dev/null >"$_pypi_backup_pip"
		$_pypi_python -m pip config set global.index-url "${http}://${domain}/pypi/web/simple/"
		print_success "pip mirror has been set."
	fi
	if [ -n "$_pypi_has_pdm" ]; then
		pdm config pypi.url 2>/dev/null >"$_pypi_backup_pdm"
		pdm config pypi.url "${http}://${domain}/pypi/web/simple/"
		print_success "pdm mirror has been set."
	fi
}

_pypi_uninstall() {
	if [ -n "$_pypi_has_pip" ]; then
		last_url=$(cat "$_pypi_backup_pip")
		if [ -z "$last_url" ]; then
			$_pypi_python -m pip config unset global.index-url
		else
			$_pypi_python -m pip config set global.index-url "$last_url"
		fi
		print_success "pip mirror has been recovered."
	fi
	if [ -n "$_pypi_has_pdm" ]; then
		last_url=$(cat "$_pypi_backup_pdm")
		if [ -z "$last_url" ]; then
			pdm config -d pypi.url
		else
			pdm config pypi.url "$last_url"
		fi
		print_success "pdm mirror has been recovered."
	fi
}


_crates_cargo_home="${HOME}/.cargo"

_crates_support_sparse() {
	version=$(cargo --version 2>/dev/null | cut -f 2 -d " ")
	[ -n "$version" ] && \
		[ $(echo "$version" | cut -f 1 -d ".") -ge 1 ] && \
		[ $(echo "$version" | cut -f 2 -d ".") -ge 68 ]
}

_crates_check() {
	cargo --version >/dev/null 2>&1
}

_crates_is_deployed() {
	[ -f "${_crates_cargo_home}/config.toml" ] && \
		grep -qE "${gen_tag}" "${_crates_cargo_home}/config.toml"
}

_crates_install() {
	mkdir -p "${_crates_cargo_home}"

	if [ -f "${_crates_cargo_home}/config.toml" ] || \
		[ -f "${_crates_cargo_home}/config" ]; then
		confirm "You already have a config.toml file in your cargo home, do you want to continue?" || \
			return 1
		if [ -f "${_crates_cargo_home}/config" ]; then
			mv "${_crates_cargo_home}/config" "${_crates_cargo_home}/config.bak"
		else
			mv "${_crates_cargo_home}/config.toml" "${_crates_cargo_home}/config.bak"
		fi
	else
		touch "${_crates_cargo_home}/config.bak"
	fi

	if _crates_support_sparse; then
		_crates_mirror="sparse+${http}://${domain}/crates.io-index/"
	else
		_crates_mirror="${http}://${domain}/git/crates.io-index/"
	fi

	cat >"${_crates_cargo_home}/config.toml" <<EOF
# ${gen_tag}
[source.crates-io]
replace-with = 'hustmirror'

[source.hustmirror]
registry = "${_crates_mirror}"
EOF
	print_success "crates mirror has been set."
}

_crates_can_recover() {
	[ -f "${_crates_cargo_home}/config.bak" ]
}

_crates_uninstall() {
	mv "${_crates_cargo_home}/config.bak" "${_crates_cargo_home}/config"
	print_success "crates mirror has been unset."
}


_openeuler_config_file="/etc/yum.repos.d/openEuler.repo"

_openeuler_check() {
	source_os_release
	[ "$NAME" = "openEuler" ]
}

_openeuler_install() {
	config_file=$_openeuler_config_file
	set_sudo

	$sudo cp ${config_file} ${config_file}.bak || {
		print_error "Failed to backup ${config_file}"
		return 1
	}

	new_file=$(sed -E "s|https?://([^/]+)|${http}://${domain}/openeuler|" $config_file)
	{
		cat << EOF | $sudo tee ${config_file} > /dev/null
# ${gen_tag}
${new_file}
EOF
	} || {
		print_error "Failed to add mirror to ${config_file}"
		return 1
	}
}

_openeuler_is_deployed() {
	config_file=$_openeuler_config_file
	$sudo grep -q "${gen_tag}" ${config_file}
}

_openeuler_can_recover() {
	bak_file=${_openeuler_config_file}.bak
	test -f $bak_file
}

_openeuler_uninstall() {
	config_file=$_openeuler_config_file
	set_sudo
	$sudo mv ${config_file}.bak ${config_file} || {
		print_error "Failed to recover ${config_file}"
		return 1
	}
}


_alpine_config_file="/etc/apk/repositories"

_alpine_check() {
	source_os_release
	[ "$NAME" = "Alpine Linux" ]
}

_alpine_install() {
	config_file=$_alpine_config_file
	set_sudo

	$sudo cp ${config_file} ${config_file}.bak || {
		print_error "Failed to backup ${config_file}"
		return 1
	}

	{
		sed -i "1i # ${gen_tag}" ${config_file}
	} && {
		sed -i "s|https://dl-cdn.alpinelinux.org|${http}://${domain}|g" ${config_file} 
	} || {
			print_error "Failed to add mirror to ${config_file}"
			return 1
		}
	
}

_alpine_is_deployed() {
	config_file=$_alpine_config_file
	pattern="^[^#]*${http}://${domain}/*"
	grep -qE "${pattern}" ${config_file}
}

_alpine_can_recover() {
	bak_file=${_alpine_config_file}.bak
	result=0
	test -f $bak_file || result=$?
	return $result
}

_alpine_uninstall() {
	config_file=$_alpine_config_file
	set_sudo
	$sudo mv ${config_file}.bak ${config_file} || {
		print_error "Failed to recover ${config_file}"
		return 1
	}
}


_rustup_get_shell_rc() {
	sh=$(basename "$SHELL")
	case "$sh" in
		bash)
			_rustup_shell_rc="$HOME/.bashrc"
			;;
		zsh)
			_rustup_shell_rc="$HOME/.zshrc"
			;;
		fish)
			_rustup_shell_rc=""
			print_error "fish is not supported."
			;;
		*)
			_rustup_shell_rc=""
			;;
	esac
}

_rustup_gen_tag="${gen_tag}::rustup"

_rustup_check() {
	has_command rustup
}

_rustup_is_deployed() {
	_rustup_get_shell_rc
	if [ -z "$_rustup_shell_rc" ]; then
		print_error "Can not find shell rc file."
		return 1
	fi
	grep -qE "${_rustup_gen_tag}" "$_rustup_shell_rc"
}


_rustup_install() {
	_rustup_get_shell_rc
	if [ -z "$_rustup_shell_rc" ]; then
		print_error "Can not find shell rc file."
		return 1
	fi
	echo "export RUSTUP_DIST_SERVER=${http}://${domain}/rustup # ${_rustup_gen_tag}"  \
		>> "$_rustup_shell_rc"
	echo "export RUSTUP_UPDATE_ROOT=${http}://${domain}/rustup/rustup # ${_rustup_gen_tag}" \
		>> "$_rustup_shell_rc"
	print_success "rustup mirror will take effect next time shell start."
}

_rustup_can_recover() {
	_rustup_is_deployed
}

_rustup_uninstall() {
	_rustup_get_shell_rc
	if [ -z "$_rustup_shell_rc" ]; then
		print_error "Can not find shell rc file."
		return 1
	fi
	# do not use -i, because it is not supported by all sed, such as macOS
	sed "/${_rustup_gen_tag}/d" "$_rustup_shell_rc" > "$_rustup_shell_rc.tmp"
	mv "$_rustup_shell_rc.tmp" "$_rustup_shell_rc"
	print_success "rustup mirror has been unset."
}



_kali_check() {
	source_os_release
	[ "$NAME" = "Kali GNU/Linux" ]
}

_kali_install() {
	config_file="/etc/apt/sources.list"
	source_os_release

	codename=${VERSION_CODENAME}
	set_sudo

	$sudo cp ${config_file} ${config_file}.bak || {
		print_error "Failed to backup ${config_file}"
		return 1
	}

	new_file=$(sed -E -e "s|https?://([^/]+)/kali|${http}://${domain}/kali|" $config_file)
	{
		cat << EOF | $sudo tee ${config_file} > /dev/null
# ${gen_tag}
${new_file}
EOF
	} || {
		print_error "Failed to add mirror to ${config_file}"
		return 1
	}

    confirm_y "Do you want to apt update?" && {
		$sudo apt update || {
			print_error "apt update failed"
			return 1
		}
	}

	true
}

_kali_uninstall() {
	config_file="/etc/apt/sources.list"
	set_sudo
	$sudo mv ${config_file}.bak ${config_file} || {
		print_error "Failed to recover ${config_file}"
		return 1
	}
}

_kali_is_deployed() {
	config_file="/etc/apt/sources.list"
	result=0
	$sudo grep -q "${gen_tag}" ${config_file} || result=$?
	return $result
}

_kali_can_recover() {
	bak_file="/etc/apt/sources.list.bak"
	result=0
	test -f $bak_file || result=$?
	return $result
}


_openkylin_check() {
	source_os_release
	[ "$NAME" = "openKylin" ]
}

_openkylin_install() {
	config_file="/etc/apt/sources.list"
	source_os_release

	codename=${VERSION_CODENAME}
	set_sudo

	$sudo cp ${config_file} ${config_file}.bak || {
		print_error "Failed to backup ${config_file}"
		return 1
	}

	new_file=$(sed -E -e "s|https?://([^/]+)/openkylin|${http}://${domain}/openkylin|" $config_file)
	{
		cat << EOF | $sudo tee ${config_file} > /dev/null
# ${gen_tag}
${new_file}
EOF
	} || {
		print_error "Failed to add mirror to ${config_file}"
		return 1
	}

	confirm_y "Do you want to apt update?" && {
		$sudo apt update || {
			print_error "apt update failed"
			return 1
		}
	}

	true
}

_openkylin_uninstall() {
	config_file="/etc/apt/sources.list"
	set_sudo
	$sudo mv ${config_file}.bak ${config_file} || {
		print_error "Failed to recover ${config_file}"
		return 1
	}
}

_openkylin_is_deployed() {
	config_file="/etc/apt/sources.list"
	result=0
	$sudo grep -q "${gen_tag}" ${config_file} || result=$?
	return $result
}

_openkylin_can_recover() {
	bak_file="/etc/apt/sources.list.bak"
	result=0
	test -f $bak_file || result=$?
	return $result
}


_ubuntu_check() {
	source_os_release
	[ "$NAME" = "Ubuntu" ]
}

_ubuntu_install() {
	config_file="/etc/apt/sources.list"
	source_os_release
	codename=${VERSION_CODENAME}
	set_sudo

	$sudo cp ${config_file} ${config_file}.bak || {
		print_error "Backup ${config_file} failed"
		return 1
	}

	secure_url="${http}://${domain}/ubuntu/"
	confirm_y "Use official secure source? (Strongly recommended)" && \
		secure_url="http://security.ubuntu.com/ubuntu/"

	propoesd_prefix="# "
	confirm "Use proposed source?" && \
		propoesd_prefix=""

	src_prefix="# "
	confirm "Use source code?" && \
		src_prefix=""

	$sudo sh -e -c "cat <<EOF > ${config_file}
# ${gen_tag}
deb ${http}://${domain}/ubuntu/ ${codename} main restricted universe multiverse
${src_prefix}deb-src ${http}://${domain}/ubuntu/ ${codename} main restricted universe multiverse
deb ${http}://${domain}/ubuntu/ ${codename}-updates main restricted universe multiverse
${src_prefix}deb-src ${http}://${domain}/ubuntu/ ${codename}-updates main restricted universe multiverse
deb ${http}://${domain}/ubuntu/ ${codename}-backports main restricted universe multiverse
${src_prefix}deb-src ${http}://${domain}/ubuntu/ ${codename}-backports main restricted universe multiverse

deb ${secure_url} ${codename}-security main restricted universe multiverse
${src_prefix}deb-src ${secure_url} ${codename}-security main restricted universe multiverse

${propoesd_prefix}deb ${http}://${domain}/ubuntu/ ${codename}-proposed main restricted universe multiverse
${propoesd_prefix}deb-src ${http}://${domain}/ubuntu/ ${codename}-proposed main restricted universe multiverse
EOF" || {
		print_error "Write ${config_file} failed"
		return 1
	}

	confirm_y "Do you want to apt update?" && {
		$sudo apt update || {
			print_error "apt update failed"
			return 1
		}
	}

	true
}

_ubuntu_uninstall() {
	config_file="/etc/apt/sources.list"
	set_sudo
	$sudo mv ${config_file}.bak ${config_file} || {
		print_error "Failed to recover ${config_file}"
		return 1
	}
}

_ubuntu_is_deployed() {
	config_file="/etc/apt/sources.list"
	result=0
	$sudo grep -q "${gen_tag}" ${config_file} || result=$?
	return $result
}

_ubuntu_can_recover() {
	bak_file="/etc/apt/sources.list.bak"
	result=0
	test -f $bak_file || result=$?
	return $result
}


_ohmyzsh_check() {
	has_git || return 1
	git -C "${HOME}/.oh-my-zsh" remote -v >/dev/null 2>&1
}

_ohmyzsh_install() {
	git -C $ZSH remote set-url origin ${http}://${domain}/git/ohmyzsh.git
	print_success "oh-my-zsh repo mirror has been set."
}

_ohmyzsh_uninstall() {
	print_info "Recover oh-my-zsh mirror to official repo."
	git -C $ZSH remote set-url origin "https://github.com/ohmyzsh/ohmyzsh.git"
}


_anolis_config_dir="/etc/yum.repos.d"

_anolis_check() {
	source_os_release
	[ "$NAME" = "Anolis OS" ]
}

_anolis_install() {
	set_sudo

    for config_file in ${_anolis_config_dir}/*.repo; do
		[ -f "$config_file" ] || continue
		$sudo cp ${config_file} ${config_file}.bak || {
			print_error "Failed to backup ${config_file}"
			return 1
		}

		new_file=$(sed -E "s|https?://([^/]+)|${http}://${domain}|" $config_file)
		{
			cat << EOF | $sudo tee ${config_file} > /dev/null
# ${gen_tag}
${new_file}
EOF
		} || {
			print_error "Failed to add mirror to ${config_file}"
			return 1
		}
	done
}

_anolis_is_deployed() {

	for config_file in ${_anolis_config_dir}/*.repo; do
		[ -f "$config_file" ] || continue
		if $sudo grep -q "${gen_tag}" "${config_file}"; then
			return 0
		fi
	done

	return 1
}

_anolis_can_recover() {

	for config_file in ${_anolis_config_dir}/*.repo; do
		[ -f "$config_file" ] || continue
		if ! test -f "${config_file}.bak"; then
			return 1
		fi
	done

	return 0
}

_anolis_uninstall() {
	set_sudo

	for config_file in ${_anolis_config_dir}/*.repo; do
		[ -f "$config_file" ] || continue
		$sudo mv ${config_file}.bak ${config_file} || {
			print_error "Failed to recover ${config_file}"
			return 1
		}
	done

	return 0
}


# END MIRRORS

source_config() {
	hustmirror_config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hustmirror"
	hustmirror_config="${hustmirror_config_dir}/hustmirror.conf"
	if [ -f "${hustmirror_config}" ]; then
		. "${hustmirror_config}" || {
			print_error "Failed to read configuration file: ${hustmirror_config}"
			return 1
		}
	else
		return 1
	fi
	unset _flag
	for edomain in $domains; do
		if [ "$domain" = "$edomain" ]; then
			_flag=1
		fi
	done
	if [ -z "$_flag" ]; then
		print_error "Domains has been update, your configuration file need regenerate."
		set_default_domain $domains
		return 1
	fi
}

save_config() {
	hustmirror_config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hustmirror"
	hustmirror_config="${hustmirror_config_dir}/hustmirror.conf"
	mkdir -p "${hustmirror_config_dir}"
	cat <<EOF >"${hustmirror_config}"
# ${gen_tag}
domain="${domain}"
http="${http}"
EOF
}

regist_config() {
	print_warning "No configuration file found."
	if ! is_tty; then
		return 0
	fi
	if confirm_y "Do you want to autogenerate a default configuration file?"; then
		save_config
	else
		select_from_menu "Choose your prefer domain" $domains
		domain=$result
		http=https
		confirm_y "Do you want to use https rather than http?" ||
			http=http
		confirm_y "Do you want to save the configuration?" &&
			save_config
	fi
}

load_config() {
	print_status "Reading configuration file..."
	source_config || regist_config
	[ -n "$HM_HTTP" ] && http=$HM_HTTP
	[ -n "$HM_DOMAIN" ] && domain=$HM_DOMAIN
	true
}

# $1 disable output when is not zero string
set_mirror_list() {
	print_status "Checking the environment and mirrors to install..."
	for software in $supported_softwares; do
		# check if the software is ready to deploy
		if has_command _${software}_check && _${software}_check; then
			if has_command _${software}_is_deployed && _${software}_is_deployed; then
				continue
			fi
			ready_to_install="$ready_to_install ${software}"
		elif ! has_command _${software}_check; then
			unsure_to_install="$unsure_to_install ${software}"
		fi
	done

	# direct return to disable output
	if [ -n "$1" ]; then return 0; fi

	if [ -z "$ready_to_install" ] && [ -z "$unsure_to_install" ]; then
		print_warning "No software is ready to install."
		print_supported
		confirm "Do you want to continue to use other function?" || exit 0
	fi

	if [ -n "$ready_to_install" ]; then
		print_info "The following software(s) are available to install:"
		echo "   $ready_to_install"
	fi

	if [ -n "$unsure_to_install" ]; then
		print_info "The following software(s) are not suggested to install:"
		echo "   $unsure_to_install"
	fi
}

# $1 disable output when is not zero string
set_mirror_recover_list() {
	ready_to_uninstall=""
	for software in $supported_softwares; do
		# check if the software is ready to recover
		if has_command _${software}_check && _${software}_check &&\
		   has_command _${software}_can_recover && _${software}_can_recover
		then
			ready_to_uninstall="$ready_to_uninstall ${software}"
		fi
	done

	if [ -z "$ready_to_uninstall" ]; then
		print_warning "No software is ready to recover."
		confirm "Do you want to continue to use other function?" || exit 0
	fi

	if [ -n "$1" ]; then return 0; fi

	if [ -n "$ready_to_uninstall" ]; then
		print_info "The following software(s) are ready to recover:"
		echo "   $ready_to_uninstall"
	fi
}

# install hust-mirror
install() {
	install_path="/usr/local/bin"
	if ! is_root; then
		print_warning "Install hust-mirror to /usr/local/bin need root permission."
	fi
	install_target="$install_path/hustmirror-cli"
	set_sudo
	if [ ! -d "$install_path" ]; then
		print_status "Creating directory: $install_path"
		$sudo mkdir -p "$install_path"
	fi
	has_command curl || {
		print_error "curl is required."
		exit 1
	}
	print_status "Downloading latest hust-mirror..."
	$sudo curl -sSfL "${http}://${domain}/get" -o "$install_target" || {
		print_error "Failed to download hustmirror-cli."
		exit 1
	}
	$sudo chmod +x "$install_target"
	print_success "Successfully install hustmirror-cli."
	$sudo ln -sf $install_target "$install_path/hustmirror" # make link for legacy name
	has_command hustmirror-cli || print_warning "It seems /usr/local/bin is not in your path, try to add it to your PATH in ~/.bashrc or ~/.zshrc."
	print_success "Now, you can use \`hustmirror-cli\` in your command line"
}

# $1 software to recover
recover() {
	software=$1

	eval synonyms="\$_synonyms_${software}"

	if [ -n "${synonyms}" ]; then
		print_question "${software} is not supported, do you mean ${synonyms}?"
		# not return to avoid synonyms get occupied by mirrors
	fi

	if has_command _${software}_check && ! _${software}_check; then
		print_error "${software} is suitable here."
		return
	fi

	if has_command _${software}_can_recover && ! _${software}_can_recover; then
		print_error "${software} can not be recoverd."
		return 1
	fi

	if has_command _${software}_uninstall; then
		print_status "recover ${software}..."
		result=0
		_${software}_uninstall || result=$?
		if [ $result -eq 0 ]; then
			print_success "Successfully uninstalled ${software}."
		else
			print_error "Failed to uninstall ${software}."
			return 1
		fi
	else
		print_error "No uninstallation method for ${software}."
	fi
}

# $1 software to deploy
deploy() {
	software=$1

	eval synonyms="\$_synonyms_${software}"

	if [ -n "${synonyms}" ]; then
		print_question "${software} is not supported, do you mean ${synonyms}?"
		# not return to avoid synonyms get occupied by mirrors
	fi

	# check if the software is ready to deploy
	if has_command _${software}_check && ! _${software}_check; then
		print_error "${software} is suitable here."
		return
	fi

	if has_command _${software}_is_deployed && _${software}_is_deployed; then
		print_error "${software} has been deployed."
		return
	fi

	if has_command _${software}_install; then
		print_status "Deploying ${software}..."
		result=0
		_${software}_install || result=$?
		if [ $result -eq 0 ]; then
			print_success "Successfully deployed ${software}."
		else
			print_error "Failed to deploy ${software}."
			return 1
		fi
	else
		print_error "No installation method for ${software}."
	fi
}

cli_deploy() {
	if [ $# -eq 0 ]; then
		print_warning "Nothing to deploy."
	fi
	for item in $@
	do
		deploy $item || print_error "Failed to deploy $item. Ignoring..."
	done
}

cli_recover() {
	if [ $# -eq 0 ]; then
		print_warning "Nothing to recover."
	fi
	for item in $@
	do
		recover $item || print_error "Failed to recover $item. Ignoring..."
	done
}

cli_autorecover() {
	set_mirror_recover_list no
	for item in $ready_to_uninstall
	do
		recover $item || print_error "Failed to recover $item. Ignoring..."
	done
}

cli_main() {
	# parse arguments
	case "$1" in
		autodeploy | ad)
			[ "$2" = "-y" ] && silent_input="y"
			set_mirror_list no
			cli_deploy $ready_to_install
			unset silent_input
			;;
		deploy | d)
			shift 1
			cli_deploy $@
			;;
		list | l)
			set_mirror_list no
			;;
		recover | r)
			shift 1
			cli_recover $@
			;;
		install | i | update | up)
			install
			;;
		autorecover | ar)
			cli_autorecover
			;;
		*)
			print_error "Unknown argument $1, exit."
			;;
	esac
}

interact_recover() {
	set_mirror_recover_list

	get_input "What do you want to uninstall?
    <softwares> for specific softwares, use space to separate multiple softwares."

	uninstall_things="$input"

	# uninstall
	for software in $uninstall_things; do
		recover $software || confirm "Do you want to continue?" || exit 1
	done
}

interact_deploy() {
	for item in $@
	do
		deploy $item || confirm "Do you want to continue?" || exit 1
	done
}

interact_main() {
	if ! is_tty; then
		print_error "Interactive mode must be run in a tty. Arguments is required to enable cli mode, try '-h' to get more information about how to use cli mode."
		exit 1
	fi

	set_mirror_list

	install_things=""
	while true; do
		input=""
		get_input "What do you want to install? [all] 
    (a)ll for ready, all! (a!) for all software(s), including unsure ones.
    <softwares> for specific softwares, use space to separate multiple softwares.
    (l)ist for forcely list all softwares, even is not supported here.
    
    Other options:
    (r)ecover for uninstall software mirror.
    (i)nstall or update hust-mirror locally
    (q)uit for exit."

		# parse input
		if [ -z "$input" ]; then
			input="all"
		fi

		case "$input" in
			a | all)
				interact_deploy $ready_to_install
				break
				;;
			a! | all!)
				interact_deploy $ready_to_install $unsure_to_install
				break
				;;
			l | list)
				print_supported
				;;
			q | quit)
				exit 0
				;;
			r | recover)
				interact_recover
				break
				;;
			i | install)
				install
				break
				;;
			*)
				interact_deploy $input
				break
				;;
		esac
	done

}

# bootstrap
print_logo
# parse arguments
case "$1" in
	'') # no arguments
		display_help $@
		exit 0
		;;
	-V | --version)
		exit 0
		;;
	-h | --help | help) # print help
		shift 1
		display_help $@
		exit 0
		;;
	-i) # Enter interact mode
		load_config
		interact_main
		;;
	*)  # Pass arguments to cli
		load_config
		cli_main $@
esac

