UPGRADE_FW_MAJOR="2020-03-29-0-6ec1a631"
UPGRADE_FW_VERSION="2022-04-29-0-5eb2c5fe-22.02.4-plus"
UPGRADE_FW_REQUIRE="2018-10-24-0-9e5687a2"

UPGRADE_FW_VERSION_SUFFIX=${UPGRADE_FW_VERSION#*-*-*-*-*-}
UPGRADE_FW_VERSION_PLUS=""

case $UPGRADE_FW_VERSION_SUFFIX in *plus*) UPGRADE_FW_VERSION_PLUS="+";; esac

CONF_DIR_PATH="/tmp/sysupgrade_cfg"
CONF_TGZ_PATH="/tmp/sysupgrade.tgz"

BOSMINER_CONF="/etc/bosminer.toml"

BOSMINER_CONF_DST="/tmp/bosminer.toml"
CONFIG_GENERATOR="sysupgrade_script"

get_bos_mode() {
	local bos_mode=$(cat "/etc/bos_mode" 2>/dev/null || cat "/tmp/bos_mode" 2>/dev/null)
	echo "${bos_mode:-nand}"
}

current_fw_version() {
	if ! cat /etc/bos_version 2>/dev/null; then
		awk '/Package: /{p=$2} /Version: /{v=$2} /Status: /{if (p == "firmware" && $NF == "installed") print v}' \
			'/usr/lib/opkg/status'
	fi
}

current_fw_major() {
	cat /etc/bos_major 2>/dev/null || echo unknown
}

check_fw_version() {
	echo "Gathering current firmware information..."

	local board_name=$1
	local fw_major=$(current_fw_major)
	local fw_version=$(current_fw_version)

	echo "board  : $board_name"
	echo "version: $fw_version"
	echo "major  : $fw_major"

	echo "Checking compatibility..."

	if [ "$UPGRADE_FW_VERSION" ">" "$fw_version" ]; then
		# firmware upgrade
		if [ "$UPGRADE_FW_REQUIRE" ">" "$fw_version" ]; then
			echo "Firmware upgrade to '$UPGRADE_FW_VERSION' is not possible!"
			echo "Firmware version '$UPGRADE_FW_REQUIRE' is required before upgrading to this version."
			return 1
		fi
	elif [ "$UPGRADE_FW_VERSION" "<" "$fw_version" ]; then
		# firmware downgrade
		if [ "$UPGRADE_FW_MAJOR" != "$fw_major" ]; then
			echo "Firmware downgrade to '$UPGRADE_FW_VERSION' is not possible!"
			echo "Downgrade is only possible among firmwares with major version '$UPGRADE_FW_MAJOR'."
			echo "Do the factory reset and try to upgrade to this version."
			return 1
		fi
	fi

	return 0
}

modify_configs() {
	# extract configuration files from tarball
	mkdir -p "$CONF_DIR_PATH"
	tar -xzf "$CONF_TGZ_PATH" -C "$CONF_DIR_PATH"
	echo "$CONF_DIR_PATH"

	return 0
}

finish_configs_modification() {
	if [ "$1" == "store" ]; then
		tar -czf "$CONF_TGZ_PATH" -C "$CONF_DIR_PATH" $(ls -A "$CONF_DIR_PATH")
	fi

	# cleanup system
	rm -rf "$CONF_DIR_PATH"

	return 0
}

get_bosminer_toml_version() {
	awk -f - "$BOSMINER_CONF" <<-EOF
		/^\[format\]\$/ {
			f = "y"; next
		}
		/^\[.*\]\$/ {
			f = "n"; next
		}
		\$1 == "version" {
			print substr(\$3, 2, length(\$3)-2)
		}
	EOF
	return 0
}

migrate_bosminer_toml_to_open() {
	local version="$1"

	echo "Migrating BOSminer+ configuration file to open version..."

	local timestamp=$(date +%s)
	awk -f - "$BOSMINER_CONF" > "$BOSMINER_CONF_DST" <<-EOF
		function reset() {
			f = "n"; c = "n"
			g = "n"; t = "n"
		}

		function print_generator(nl) {
			if (f == "y") {
				if (g == "n") print "generator = '${CONFIG_GENERATOR}'"
				if (t == "n") print "timestamp = ${timestamp}"
				if (nl && (g == "n" || t == "n")) print ""
			}
		}

		BEGIN {
			reset()
		}
		/^\[format\]\$/ {
			reset(); f = "y"
			print; next
		}
		/^\[model_detection\]\$/ ||
		/^\[autotuning\]\$/ ||
		/^\[power_scaling\]\$/ ||
		/^\[management\]\$/ {
			reset(); c = "y"
			print "# " \$0; next
		}
		/^\[.*\]\$/ {
			print_generator(1)
			reset()
		}
		f == "y" && \$1 == "version" {
			print "# " \$0; print "version = '${version}'"; next
		}
		f == "y" && \$1 == "generator" {
			g = "y"; print "generator = '${CONFIG_GENERATOR}'"; next
		}
		f == "y" && \$1 == "timestamp" {
			t = "y"; print "timestamp = ${timestamp}"; next
		}
		c == "y" {
			print "# " \$0; next
		}
		{print}
		END {
			print_generator(0)
		}
	EOF
	return 0
}

migrate_bosminer_toml_to_plus() {
	local version="$1"

	echo "Migrating BOSminer configuration file to plus version..."

	local miner_psu_power_limit=$(fw_printenv -n miner_psu_power_limit 2>/dev/null)
	local autotuning_enabled="yes"

	case $miner_psu_power_limit in
		default)
			miner_psu_power_limit=
		;;
		"")
			autotuning_enabled="no"
			miner_psu_power_limit=
		;;
	esac

	local timestamp=$(date +%s)
	awk -f - "$BOSMINER_CONF" > "$BOSMINER_CONF_DST" <<-EOF
		function reset() {
			f = "n"
			g = "n"; t = "n"
		}

		function print_generator(nl) {
			if (f == "y") {
				if (g == "n") print "generator = '${CONFIG_GENERATOR}'"
				if (t == "n") print "timestamp = ${timestamp}"
				if (nl && (g == "n" || t == "n")) print ""
			}
		}

		BEGIN {
			reset()
		}
		/^\[format\]\$/ {
			reset(); f = "y"
			print; next
		}
		/.*^\[.*\]\$/ {
			print_generator(1)
			reset()
		}
		f == "y" && \$1 == "version" {
			print "# " \$0; print "version = '${version}'"; next
		}
		f == "y" && \$1 == "generator" {
			g = "y"; print "generator = '${CONFIG_GENERATOR}'"; next
		}
		f == "y" && \$1 == "timestamp" {
			t = "y"; print "timestamp = ${timestamp}"; next
		}
		{print}
		END {
			print_generator(0)
			if ("${autotuning_enabled}" == "yes") {
				print ""
				print "[autotuning]"
				print "enabled = true"
				if ("${miner_psu_power_limit}") {
					print "psu_power_limit = ${miner_psu_power_limit}"
				}
			}
		}
	EOF
	return 0
}

fix_bosminer_toml_version() {
	local curr_ver="$1"
	local next_ver="$2"

	local fw_version=$(current_fw_version)

	# Do the fix only during firmware upgrade
	[ "$UPGRADE_FW_VERSION" ">" "$fw_version" ] || return 0
	# Skip the fix when the format version is the same
	[ "$curr_ver" != "$next_ver" ] || return 0

	echo "Fixing BOSminer configuration file version..."

	local timestamp=$(date +%s)
	awk -f - "$BOSMINER_CONF" > "$BOSMINER_CONF_DST" <<-EOF
		function reset() {
			f = "n"
			g = "n"; t = "n"
		}

		function print_generator(nl) {
			if (f == "y") {
				if (g == "n") print "generator = '${CONFIG_GENERATOR}'"
				if (t == "n") print "timestamp = ${timestamp}"
				if (nl && (g == "n" || t == "n")) print ""
			}
		}

		BEGIN {
			reset()
		}
		/^\[format\]\$/ {
			reset(); f = "y"
			print; next
		}
		/.*^\[.*\]\$/ {
			print_generator(1)
			reset()
		}
		f == "y" && \$1 == "version" {
			print "# " \$0; print "version = '${next_ver}'"; next
		}
		f == "y" && \$1 == "generator" {
			g = "y"; print "generator = '${CONFIG_GENERATOR}'"; next
		}
		f == "y" && \$1 == "timestamp" {
			t = "y"; print "timestamp = ${timestamp}"; next
		}
		{print}
		END {
			print_generator(0)
		}
	EOF
	return 0
}

migrate_bosminer_toml() {
	# Do not migrate BOSminer config when BOSminer config is not present
	[ ! -f "$BOSMINER_CONF" ] && return 0

	local current_toml_version=$(get_bosminer_toml_version)
	# Exit when format version cannot be determined
	[ -z "$current_toml_version" ] && return 0

	local current_toml_version_plus=""
	case $current_toml_version in *+) current_toml_version_plus="+";; esac

	if [ -n "$UPGRADE_FW_VERSION_PLUS" ]; then
		# Do the migration when upgrading from open to plus version
		[ -z "$current_toml_version_plus" ] \
			&& migrate_bosminer_toml_to_plus "$BOSMINER_CONF_VERSION_PLUS" \
			|| fix_bosminer_toml_version "$current_toml_version" "$BOSMINER_CONF_VERSION_PLUS"
	else
		# Do the migration when downgrading from plus to open version
		[ -n "$current_toml_version_plus" ] \
			&& migrate_bosminer_toml_to_open "$BOSMINER_CONF_VERSION_OPEN" \
			|| fix_bosminer_toml_version "$current_toml_version" "$BOSMINER_CONF_VERSION_OPEN"
	fi

	return 0
}

replace_bosminer_conf() {
	local conf_dir_path=$(modify_configs)
	local conf_action="discard"

	cp "${BOSMINER_CONF_DST}" "${conf_dir_path}/etc/" 2>/dev/null && \
		conf_action="store"

	finish_configs_modification $conf_action
	return 0
}

ignore_uhttpd_conf() {
	local conf_dir_path=$(modify_configs)
	local conf_action="discard"

	rm "${conf_dir_path}/etc/config/uhttpd" 2>/dev/null && \
		conf_action="store"

	finish_configs_modification $conf_action
	return 0
}
BOSMINER_CONF_VERSION_OPEN="1.0"
BOSMINER_CONF_VERSION_PLUS="1.2+"

CGMINER_CONF="/etc/cgminer.conf"

sort_list() {
	echo "$1" | \
	sed -e 's/,\+$//' -e 's/\(,\+\)/\n/g' | sed 's/^0\+\(\d\+\)/\1/' | \
	sort -n${2} 2> /dev/null
}

get_min() {
	sort_list "$1" | head -n1
}

get_max() {
	sort_list "$1" "r" | head -n1
}

begins_with() {
	case "$2" in
		"$1"*) true;;
		*) false;;
	esac
}

has_substr() {
	case "$2" in
		*"$1"*) true;;
		*) false;;
	esac
}

migrate_cgminer_conf() {
	# Do not migrate CGMiner config when BOSminer config is already generated
	[ -f "$BOSMINER_CONF_DST" -o ! -f "$CGMINER_CONF" ] && return 0

	echo "Migrating obsolete CGMiner configuration..."

	. /usr/share/libubox/jshn.sh

	cat > "$BOSMINER_CONF_DST" <<-END
		[format]
		version = '1.0${UPGRADE_FW_VERSION_PLUS}'
		model = 'Antminer S9'
		generator = '${CONFIG_GENERATOR}'
		timestamp = $(date +%s)
	END

	json_init
	json_load "$(cat $CGMINER_CONF)"

	local multi_version
	local frequency
	local voltage

	if json_get_type Type "multi-version" && [ "$Type" == string ]; then
		json_get_var multi_version "multi-version"
	fi
	if json_get_type Type "bitmain-freq" && [ "$Type" == string ]; then
		json_get_var frequency "bitmain-freq"
	fi
	if json_get_type Type "bitmain-voltage" && [ "$Type" == string ]; then
		json_get_var voltage "bitmain-voltage"
	fi

	# Chain ID in original CGMiner config does not correspond to real ID and
	# it is also dynamic so per chain conversion is not possible
	frequency=$(get_min $frequency)
	voltage=$(get_max $voltage)

	if [ "$multi_version" != "4" -o -n "$frequency" -o -n "$voltage" ]; then
		cat >> "$BOSMINER_CONF_DST" <<-END

			[hash_chain_global]
		END
		[ "$multi_version" != "4" ] && echo "asic_boost = false" >> "$BOSMINER_CONF_DST"
		[ -n "$frequency" ] && echo "frequency = ${frequency}" >> "$BOSMINER_CONF_DST"
		[ -n "$voltage" ] && echo "voltage = ${voltage}" >> "$BOSMINER_CONF_DST"
	fi

	cat >> "$BOSMINER_CONF_DST" <<-END

		[[group]]
		name = 'Default'
	END

	if json_get_type Type "pools" && [ "$Type" == array ]; then
		local pool_url pool_user pool_pass

		json_select "pools"
		local idx="1"
		while json_get_type Type "$idx" && [ "$Type" == object ]; do
			json_select "$idx"
			json_get_var pool_url "url"
			json_get_var pool_user "user"
			json_get_var pool_pass "pass"
			json_select ..

			# Detect stratum server
			begins_with "stratum+tcp://" "$pool_url"
			local stratum=$?

			if [ $stratum -eq 0 ] || ! has_substr "://" "$pool_url"; then
				if [ $stratum -eq 0 ]; then
					cat >> "$BOSMINER_CONF_DST" <<-END

						[[group.pool]]
						url = '${pool_url}'
					END
				else
					cat >> "$BOSMINER_CONF_DST" <<-END

						[[group.pool]]
						# url = '${pool_url}'
						url = 'stratum+tcp://${pool_url}'
					END
				fi
				cat >> "$BOSMINER_CONF_DST" <<-END
					user = '${pool_user:-!non-existent-user!}'
				END
				[ -n "$pool_pass" ] && \
				cat >> "$BOSMINER_CONF_DST" <<-END
					password = '${pool_pass}'
				END
			else
				cat >> "$BOSMINER_CONF_DST" <<-END

					# Incompatible pool
					# [[group.pool]]
					# url = '${pool_url}'
					# user = '${pool_user}'
				END
				[ -n "$pool_pass" ] && \
				cat >> "$BOSMINER_CONF_DST" <<-END
					# password = '${pool_pass}'
				END
			fi

			$((idx++)) 2> /dev/null
		done
		json_select ..
	fi
	return 0
}

# Fix old firmwares where original U-Boot wasn't stored in recovery partition
fix_recovery_partition() {
	# affected firmwares do not have linked gzip in sysupgrade rootfs
	[ -f "/bin/gzip" ] && return 0

	/bin/busybox ln -s /bin/busybox /bin/gzip
	/bin/busybox ln -s /bin/busybox /usr/bin/head

	local UBOOT_MTD=/dev/mtd1
	local RECOVERY_MTD=/dev/mtd6
	local RECOVERY_MTD_NAME=recovery

	local UBOOT_RECOVERY_OFFSET=0x1520000
	# All previous U-Boots have the same size
	local UBOOT_SIZE=573700

	# Dump first bytes of original U-Boot from recovery partition
	recovery_magic=$(nanddump -s ${UBOOT_RECOVERY_OFFSET} -ql 4 ${RECOVERY_MTD} | hexdump -v -n 4 -e '1/1 "%02x"')

	# If U-Boot in recovery partition is not erased then no fix is needed
	[ "$recovery_magic" == "ffffffff" ] || return 0

	echo "Fixing missing U-Boot in recovery partition..."

	# Use current U-Boot for creating backup in recovery partition
	nanddump -l ${UBOOT_SIZE} ${UBOOT_MTD} | head -c ${UBOOT_SIZE} | gzip | \
	mtd -np ${UBOOT_RECOVERY_OFFSET} write - ${RECOVERY_MTD_NAME}

	# cleanup system
	rm /usr/bin/head /bin/gzip

	return 0
}

fix_system_hostname() {
	# affected firmwares do not have linked sed in sysupgrade rootfs
	[ -f "/bin/sed" ] && return 0

	/bin/busybox ln -s /bin/busybox /bin/sed

	local conf_dir_path=$(modify_configs)
	local conf_action="discard"

	sys_hostname=$(sed -n "/config system/,/config/s/.*option hostname '\(.*\)'/\1/p" \
		"$conf_dir_path/etc/config/system" 2>/dev/null)
	eth_hostname=$(sed -n "/config.*'lan'/,/config/s/.*option hostname '\(.*\)'/\1/p" \
		"$conf_dir_path/etc/config/network" 2>/dev/null)

	# detect obsolete miner system hostname
	if [ "$sys_hostname" = "MINER" -a -n "$eth_hostname" ]; then
		echo "Fixing obsolete miner system hostname..."
		# move ethernet specific hostname to global system hostname
		sed -i "/config system/,/config/s/\(.*option hostname\) .*/\1 '$eth_hostname'/" \
			"$conf_dir_path/etc/config/system" 2>/dev/null && \
		sed -i "/config.*'lan'/,/config/{/.*option hostname '/d}" \
			"$conf_dir_path/etc/config/network" 2>/dev/null && \
		conf_action="store"
	fi

	finish_configs_modification $conf_action

	# cleanup system
	rm /bin/sed

	return 0
}

replace_cgminer_conf() {
	local conf_dir_path=$(modify_configs)
	local conf_action="discard"

	rm "${conf_dir_path}${CGMINER_CONF}" 2>/dev/null && \
		conf_action="store"
	cp "${BOSMINER_CONF_DST}" "${conf_dir_path}/etc/" 2>/dev/null && \
		conf_action="store"

	finish_configs_modification $conf_action
	return 0
}

# Support of SPL upgrade for old firmwares
compatible_write_spl() {
	# affected firmwares do not implement 'zynq_write_spl' function
	type 'zynq_write_spl' >/dev/null 2>/dev/null && return 0

	local board_name=$1
	local tar_file=$2

	local spl_length=`(tar xf ${tar_file} sysupgrade-$board_name/spl -O | wc -c) 2> /dev/null`

	[ "$spl_length" != 0 ] && {
		echo "Upgrading SPL..."
		mtd erase boot
		tar xf ${tar_file} sysupgrade-$board_name/spl -O | mtd write - boot
	}
}

# Support of U-Boot default environment upgrade for old firmwares
compatible_write_uenv() {
	# affected firmwares do not implement 'zynq_write_uenv' function
	type 'zynq_write_uenv' >/dev/null 2>/dev/null && return 0

	local board_name=$1
	local tar_file=$2

	local file_path=sysupgrade-$board_name/uenv
	local uenv_length=`(tar xf ${tar_file} $file_path -O | wc -c) 2>/dev/null`

	[ "$uenv_length" != 0 ] && {
		echo "Upgrading U-Boot default environment..."
		tar xf ${tar_file} $file_path -O | fw_setenv --script -
		fw_printenv
	}
}

fix_keep_file() {
	local keep_path="/lib/upgrade/keep.d/$1"
	local conf_file="$2"

	if [ -f "$keep_path" ]; then
		grep -q "$conf_file" "$keep_path" || \
		echo "$conf_file" >> "$keep_path"
	fi
}

fix_system_log() {
	local conf_dir_path=$(modify_configs)
	local conf_action="discard"

	local system_file="$conf_dir_path/etc/config/system"

	# affected firmwares do not have 'log_file' option in 'system' configuration
	if ! grep -q "^.*option.*log_file " "$system_file" 2>/dev/null; then
		echo "Fixing logging configuration..."
		# get current settings of 'log_size' attribute
		local log_size=$(sed -n "/config system/,/config/s/.*option log_size .*'\(.*\)'.*/\1/p" \
			"$system_file" 2>/dev/null)
		# set log site to at least 1 MiB
		log_size=$(( log_size > 1024 ? log_size : 1024 ))
		# delete attributes 'log_size' and 'log_file' from 'system' section
		sed -i "/config system/,/config/{/^.*option \(log_size\|log_file\) .*/d}" "$system_file"
		# insert after hostname all new log attributes
		local log_insert
		log_insert="$log_insert\n\toption log_size '$log_size'"
		log_insert="$log_insert\n\toption log_file '\/var\/log\/syslog'"
		sed -i '/config system/,/config/{/.*hostname.*/s//&'"$log_insert"'/}' "$system_file"
		conf_action="store"
	fi

	finish_configs_modification $conf_action

	return 0
}

fix_bos_config() {
	local conf_dir_path=$(modify_configs)
	local conf_action="discard"

	local bos_file="$conf_dir_path/etc/config/bos"

	# affected firmwares have socat option in 'bos' configuration
	if grep -q "^.*option.*socat " "$bos_file" 2>/dev/null; then
		# old bos config can be deleted because new one will be generated after system upgrade
		rm -f "$bos_file" && {
			echo "Deleted obsolete '/etc/config/bos'!"
			conf_action="store"
		}

		# socat utility was deleted from rootfs and configuration file is obsolete
		# delete the file only if it is not modified
		( cd "$conf_dir_path" && echo "6afb8381f8895dbcdb0415f8d22f90fd  etc/config/socat" \
		| md5sum -cs ) 2>/dev/null && \
		rm -f "$conf_dir_path/etc/config/socat" && {
			echo "Deleted obsolete '/etc/config/socat'!"
			conf_action="store"
		}
	fi

	finish_configs_modification $conf_action

	return 0
}

fix_crontabs() {
	local conf_dir_path=$(modify_configs)
	local conf_action="store"

	local user_file="/tmp/crontabs_user"
	local crontabs_file="$conf_dir_path/etc/crontabs/root"

	# affected firmwares have old script 'bos_update.sh' in crontabs
	if grep -q "bos_update.sh" "$crontabs_file" 2>/dev/null; then
		echo "Fixing Braiins OS cron jobs..."

		# create file only with user specified crontabs entries
		grep -v -e "/usr/sbin/bos_up" -e "/etc/logrotate.conf" \
			"$crontabs_file" > "$user_file" 2>/dev/null || true
		if [ -s "$user_file" ]; then
			# create updated crontabs with user specified entries
			cat > "$crontabs_file" - "$user_file" <<-END
				*/1 * * * * /usr/sbin/logrotate /etc/logrotate.conf 2>&1 | logger -t logrotate
				0 0 * * * /usr/sbin/bos_upgrade_at 2>&1 | logger -t upgrade
			END
			rm "$user_file"
		else
			# delete crontabs and use rootfs one when there is no modifications
			rm "$crontabs_file" 2>/dev/null || \
				conf_action="discard"
		fi
	fi

	finish_configs_modification $conf_action

	return 0
}

check_image() {
	check_fw_version $1 && {
		local bos_mode=$(get_bos_mode)

		[ "$bos_mode" == nand ] && fix_keep_file "bosminer" "/etc/bosminer-autotune.json"
		migrate_bosminer_toml
		[ "$bos_mode" == nand ] && migrate_cgminer_conf
		return 0
	}
}

pre_upgrade() {
	local board_name=$1
	local tar_file=$2
	local bos_mode=$(get_bos_mode)

	[ "$bos_mode" == nand ] && {
		fix_recovery_partition
		fix_system_hostname
		fix_system_log
		fix_bos_config
		fix_crontabs
	}

	ignore_uhttpd_conf
	replace_cgminer_conf

	[ "$bos_mode" == nand ] && {
		compatible_write_spl $board_name $tar_file
		compatible_write_uenv $board_name $tar_file
	}
	return
}
