#!/bin/bash -efu

if [ -z "${__scandev_sh_functions-}" ]; then
__scandev_sh_functions=1

. localdev-sh-functions
. interactive-sh-functions

disk=
part=
fuid=
uuid=
label=
whole=
squash=
target=
method=
devices=

__scandev_last=


start_dev_scan()
{
	IM_ponder_start "[ Scanning devices... ]"
}

stop_dev_scan()
{
	IM_ponder_stop
}

devname2target()
{
	# Ex.: nvme0n1 (whole disk) or sdb2 (partition)
	local devname="${1#/dev/}"
	local sysfs="${2-}"
	local firstpart

	enter "devname2target"

	# By default, there is no parent disk
	whole=

	# Nothing special for the "oem" boot method
	if [ "$method" = oem ]; then
		target="/dev/$devname"

		leave "devname2target"
		return 0
	fi

	# The sysfs path of the specified device
	[ -n "$sysfs" ] || assign sysfs "$(devname2sysfs "$devname")"

	# If the device is not a partition
	if [ -z "$sysfs" ] || [ ! -r "$sysfs/partition" ]; then
		if [ "$(read_fstype "/dev/$devname")" = iso9660 ]; then
			whole="/dev/$devname"

			# If the device has an ISO-9660 filesystem,
			# try using the first partition of this disk
			#
			case "$devname" in
			*[0-9])	firstpart="${devname}p1";;
			*)	firstpart="${devname}1";;
			esac

			# Check for the first partition
			if [ -b "/dev/$firstpart" ] &&
			   [ -r "/sys/block/$devname/$firstpart/dev" ]
			then
				# This is an ISO Hybrid image
				devname="$firstpart"
			fi
		fi

		target="/dev/$devname"

		leave "devname2target"
		return 0
	fi

	# If the specified device is a partition,
	# try to find the appropriate parent disk
	#
	sysfs="$(parent_disk "$devname" "$sysfs")"

	# Check the whole disk
	if [ -n "$sysfs" ] && [ "$(read_fstype "$sysfs")" = iso9660 ]; then
		sysfs="${sysfs#/dev/}"

		# If the parent disk has an ISO-9660 filesystem,
		# try using the first partition of this disk
		#
		case "$sysfs" in
		*[0-9])	firstpart="${sysfs}p1";;
		*)	firstpart="${sysfs}1";;
		esac

		# Check for the first partition
		if [ -b "/dev/$firstpart" ] &&
		   [ -r "/sys/block/$sysfs/$firstpart/dev" ]
		then
			# This is an ISO Hybrid image
			devname="$firstpart"
			whole="/dev/$sysfs"
		fi
	fi

	target="/dev/$devname"

	leave "devname2target"
}

debug_found_target()
{
	if [ -z "$whole" ]; then
		debug "TARGET: '$target'"
	else
		debug "TARGET: '$target' (whole disk: '$whole')"
	fi
}

scan_devices()
{
	enter "scan_devices"

	local major minor n_blocks devname
	local x f excludes="0 1 2 7 35 37"
	local n_targets=0 saved=
	local all_parts=

	_check_field()
	{
		local key="$1" value="$2"

		x="$(read_udev_field "ID_FS_$key" "/dev/$devname")"
		[ -n "$x" ] ||
			x="$(read_blkid_field "$key" "/dev/$devname")"

		if [ "$key" != LABEL ]; then
			[ "$x" = "$value" ] ||
				return 1
		else
			# shellcheck disable=SC2254
			case "$x" in
			$value)	;;
			*)	return 1;;
			esac
		fi
	}

	_check_volume()
	{
		x="$(read_udev_field ID_FS_USAGE "/dev/$devname")"
		[ -z "$x" ] || [ "$x" = filesystem ] ||
			return 1
		x="$(read_fstype "/dev/$devname")"

		case "$x" in
		''|swap|*_member)
			return 1
			;;
		esac
	}

	_add_device()
	{
		target="$1"
		[ -n "$target" ] ||
			whole=
		devices="${devices:+$devices }$devname"
	}

	_in_array()
	{
		local needle="$1"; shift

		while [ "$#" -gt 0 ]; do
			[ "$needle" != "$1" ] ||
				return 0
			shift
		done

		return 1
	}

	devices=
	target=
	whole=

	# Check for procfs
	if [ ! -r /proc/partitions ]; then
		message "can't read the information about the partitions"
		message "may be the /proc filesystem is not mounted?"
		leave "scan_devices"
		return 1
	fi

	debug "Idle before scan devices"
	sleep 1

	# Read the /proc/partitions file
	while read -r major minor n_blocks devname; do
		[ -n "$major" ] && [ "$major" != major ] ||
			continue
		[ "$n_blocks" -ge 32 ] ||
			continue
		[ -b "/dev/$devname" ] ||
			continue
		f=0

		# Skip these block devices:
		#
		#   0: Unnamed devices (e.g. non-device mounts)
		#   1: RAM disk
		#   2: Floppy disks
		#   7: Loopback devices
		#  35: Slow memory ramdisk
		#  37: Zorro II ramdisk
		#
		# See also:
		# https://docs.kernel.org/admin-guide/devices.html
		#
		for x in $excludes; do
			if [ "$x" = "$major" ]; then
				f=1
				break
			fi
		done

		[ "$f" = 0 ] ||
			continue
		all_parts="${all_parts:+$all_parts }$devname"
	done < /proc/partitions

	# Have the situations changed?
	if [ "$all_parts" = "${__scandev_last}" ]; then
		debug "the list of devices has not changed since the last scan"
		all_parts=
	fi

	# Check only the new situation
	for devname in $all_parts _; do
		[ "$devname" != _ ] ||
			continue
		x="$(mountpoint -x -- "/dev/$devname")"
		saved="$target"

		# Use the specification to find the target device
		debug "probing the device: /dev/$devname ($x)"

		if [ "$method" != cdrom ]; then
			_check_volume ||
				continue
			target=
		else
			# This is required for the "cdrom" boot method
			_check_field TYPE iso9660 ||
				continue
			f="/proc/sys/dev/cdrom/info"
			target=

			# For the "cdrom" boot method, if we boot
			# the system from a single CD/DVD drive, this
			# is enough to consider the bootable media found.
			# This means that "automatic=method:cdrom" may
			# work without additional specification if
			# there is no the second optical drive.
			#
			if [ -r "$f" ] && [ -r "/sys/block/$devname/dev" ]; then
				x="$(sed -n -E 's/^drive name:\s+//p' "$f")"
				[ "$x" != "$devname" ] || target=1
			fi
		fi

		# Save the current values
		minor="$target"
		f="$whole"

		# Determine the target devices and save them
		devname2target "$devname"
		n_blocks="$target"
		major="$whole"

		# Skip devices that have already been added
		# and all partitions of the ISO Hybrid disk
		#
		[ -n "$whole" ] && devname="${whole#/dev/}" ||
			devname="${target#/dev/}"
		if _in_array "$devname" $devices; then
			target="$saved"
			whole="$f"
			continue
		fi

		# Restore the saved values
		target="$minor"
		whole="$f"

		if [ -n "$uuid" ]; then
			if _check_field UUID "$uuid"; then
				target=1
			else
				_add_device "$saved"
				continue
			fi
		elif [ -n "$fuid" ] && _check_field UUID "$fuid"; then
			target=1
		fi

		if [ -n "$label" ]; then
			if _check_field LABEL "$label"; then
				target=1
			else
				_add_device "$saved"
				continue
			fi
		fi

		if [ -n "$part" ]; then
			if [ "$part" = "$devname" ]; then
				target=1
			else
				_add_device "$saved"
				continue
			fi
		fi

		if [ "$disk" = "$devname" ]; then
			target=1
		elif [ -n "$disk" ] && [ -z "$part" ]; then
			_add_device "$saved"
			continue
		fi

		if [ -z "$target" ]; then
			_add_device "$saved"
			continue
		fi

		# Add a device
		whole="$major"
		_add_device "$n_blocks"
		n_targets=$((1 + $n_targets))
		x="$(mountpoint -x -- "$target")"
		message "a suitable device was found: $target ($x)"
	done

	unset _in_array
	unset _add_device
	unset _check_volume
	unset _check_field

	f=1

	if [ -n "$all_parts" ]; then
		debug "DEVICES: $devices"

		if [ "$n_targets" = 1 ]; then
			debug_found_target
			f=0
		else
			target=
			whole=
		fi

		__scandev_last="$all_parts"
	fi

	leave "scan_devices"
	return $f
}

device_choice()
{
	[ -n "$devices" ] ||
		return 0
	enter "device_choice"

	local sysfs devname text dlgcmd rc=0

	_get_field()
	{
		local __x __varname="$1" __key="$2"

		__x="$(read_udev_field "ID_FS_${__key}" "/dev/$devname")"
		[ -n "${__x}" ] ||
			__x="$(read_blkid_field "${__key}" "/dev/$devname")"
		assign "${__varname}" "${__x}"
	}

	_get_model()
	{
		local __x __r="" __varname="$1"

		[ -n "$sysfs" ] ||
			return 0
		[ ! -r "$sysfs/device/vendor" ] ||
			read -r __r <"$sysfs/device/vendor" ||:

		if [ -r "$sysfs/device/model" ]; then
			read -r __x <"$sysfs/device/model" || __x=
			[ -z "${__x}" ] || __r="${__r:+$__r }${__x}"
		fi

		if [ -r "$sysfs/device/serial" ]; then
			read -r __x <"$sysfs/device/serial" || __x=
			[ -z "${__x}" ] || __r="${__r:+$__r }${__x}"
		fi

		assign "${__varname}" "${__r//\"/\'}"
	}

	_fill_item_description()
	{
		local bs fssize fstype title=
		local mul=1024 ms="KiB MiB GiB TiB"

		# Device sysfs path
		sysfs="$(devname2sysfs "$devname")"

		# Fill in the optional title and use a
		# different multiplexer for whole disks
		#
		if [ -d "/sys/block/$devname" ]; then
			_get_model title
			ms="KB MB GB TB"
			mul=1000
		fi

		# Determine the canonical device name
		[ -n "$sysfs" ] && [ -r "$sysfs/dm/name" ] &&
		read -r text <"$sysfs/dm/name" &&
		[ -n "$text" ] && [ -L "/dev/mapper/$text" ] ||
			text="$devname"

		# Determine the device capacity in bytes
		[ -n "$sysfs" ] && [ -r "$sysfs/size" ] &&
		read -r fssize <"$sysfs/size" ||
			fssize=0
		fssize="$(( $fssize * 512 ))"

		# Human-readable size
		for bs in bytes $ms; do
			if [ "$bs" = "${ms##* }" ] || [ "$fssize" -lt "$mul" ]; then
				fssize="$fssize $bs"
				break
			fi
			fssize=$(( $fssize / $mul ))
		done

		# File system type
		_get_field fstype TYPE
		[ -n "$fstype" ] ||
			fstype=unknown

		# Volume label
		_get_field bs LABEL
		[ -z "$bs" ] ||
			title="${bs//\"/\'}"

		# Header formatting
		if [ -n "$title" ]; then
			title=": '$title'"
		else
			_get_field bs UUID
			[ -z "$bs" ] ||
				title=", UUID=$bs"
		fi

		bs=

		# Partition number
		if [ -n "$sysfs" ] && [ -r "$sysfs/partition" ]; then
			read -r bs <"$sysfs/partition" ||
				bs=
			[ -z "$bs" ] ||
				bs="Part #$bs"
		fi

		# Fill the item description
		text="$text (${bs:-Volume}: $fssize, ${fstype}${title})"
	}

	text="Please choose"

	if [ "$method" = oem ]; then
		text="$text the source containing the OEM Drivers Update"
	else
		text="$text the boot source"

		if [ -n "$squash" ]; then
			text="$text containing the squash $squash"
		elif [ -n "${OEM_DISTRIBUTION-}" ]; then
			text="$text containing $OEM_DISTRIBUTION"
		fi
	fi

	text="$text or cancel and try again."
	dlgcmd="IM_choice devname \"$text\""

	for devname in $devices; do
		_fill_item_description
		dlgcmd="$dlgcmd \"$devname\" \"$text\""
	done

	devname=
	debug "RUN: $dlgcmd"

	if eval "$dlgcmd" && [ -n "$devname" ]; then
		devname2target "$devname"
		debug_found_target
	else
		rc="$?"
		target=
		whole=
	fi

	unset _fill_item_description
	unset _get_model
	unset _get_field

	leave "device_choice"
	return $rc
}

fi # __scandev_sh_functions
