#!/bin/bash
# TODO use --keep-cwd option for pkexec since polkit version 121 instead of the following solution:
# workaround for restarting the qemoo with pkexec keeping the initial working directory

# internal parameter, need to save current directory when restart script with root permissions
if [ "$1" == "--workdir" ]; then
	cd "$2" || exit ${LINENO}
	shift 2
	SUDO="sudo "
fi

# internal parameter, need to save config name when restart script with root permissions
if [ "$1" == "--qemoocfg" ]; then
	QEMOOCFG=$2
	shift 2
fi

quote(){
	echo "$*"  |sed -e 's:^:":' -e 's:$:":'
}

QEMOO="$(realpath $0)"
ARGS="$(while [ "$1" ] ; do quote "$1" ; echo -n " "; shift ; done )"       # keep qemoo agruments to use with pkexec

PATH="/sbin:/usr/sbin:$PATH"
ACTION='run'    # what to do
IMG='none'      # name of image to boot
QCOW2='none'    # name for qcow2 image to install
SIZE='20'       # size for qcow2 image to install
RAM='auto'      # ram for machine
QEMUADD=''      # additional parameters for qemu
EFI_FIRMWARE="/usr/share/OVMF/OVMF_CODE.fd"
PREFIX='_qemoo' # prefix for generating qcow2 file name
SHARE='./'      # share dir
FIRSTPORT='6001'
QEMOOADD=""

### initialized empty, not for main config
DAEMON=''
EFI=''
INSTALL=''
EXT_DEVS=''
AMPER=''
SEPCFG=''
CONF=''
###

[ "$QEMOOCFG" ] || QEMOOCFG="/etc/qemoo.cfg"
QEMOOCFG="$(realpath "$QEMOOCFG")"

if [ -f "$QEMOOCFG" ] ; then
	echo "Using: $QEMOOCFG"
	source "$QEMOOCFG"
fi
[ "$2" ] || SEPCFG='yes'

while [ -n "$1" ]
do
	case "$1" in
	"-i" | "--install" ) ACTION='install' ;; # install from ISO to a qcow2 instead sipmle run
	"-r" | "--run"     ) ACTION='run' ;; # run (default)
	"-s" | "--size"    ) shift; SIZE=$1 ;; # <par> size for new qcow2 (only with --install)
	"-q" | "--qcow2"   ) shift; QCOW2="$1" ;; # <par> name for new or existing qcow2 (only with --install)
	"-m" | "--mem"     ) shift; RAM=$1 ;; # <par> size or RAM to guest machine MB
	"-a" | "--add"     ) shift; ADD="$1" ;; # <par> devices to be added. List separated by comma
	"-p" | "--port"	   ) shift; PORT="$1" ;; # <par> port for spice connection ( use with -d )
	"-c" | "--config"  ) shift; CONF="$1" ;; # <par> additional config file, if exists has the highest priority, if not exists will be created with current values
	"-L" | "--ll"   | "--lowlevel" ) REDIRUSB='yes' ;; # lowlevel redirect USB drive to guest machine, useful for tokens, modems, etc.
	"-e" | "--efi"  | "--uefi"     ) EFI="-bios $EFI_FIRMWARE" ;; # efi instead legacy bios
	"-t" | "--tpm2" | "--tpm"      ) TPM2='yes' ; EFI="-bios $EFI_FIRMWARE" ;; # enable tpm2 support (efi only)
	"-l" | "--loop" | "--losetup"  ) LOSETUP='yes' ;; # use ISO and IMG files attaching as loop device
	"-d" | "--daemon" | "--spice"  ) SPICE='yes' ;; # run as daemon, to connect via remote-viewer
	"-S" | "--show-cmdline"        ) SHOW='yes' ;; # do not run qemu, show qemu cmdline only
	"-h" | "--help"    ) # this help
	echo "Usage: $0 /path/to/image <parameters>"
	sed -n '/).*;;\s*#.*/s/).*#/ - /p' "$0" |grep -v cat
	echo -e "\n$0 uses the network bridge qemoobr0 or virbr0 if available."
	echo "virbr0 is available if libvirt is installed and the libvirtd service is running,"
	echo "to use qemoobr0, you need to create it manually and allow it in /etc/qemu/bridge.conf"
	exit;;
	"--" )  shift ; QEMUADD+=" $*" # <par> additional parameters for qemu in the end of cmdline
			break ;;
	*) IMG="$(realpath "$1")" ;;
	esac
	shift
done

mkSepCfg(){
cat  << EOF > "${1}.conf"
ACTION='run'
RAM="$RAM"
ADD="$ADD"
EFI="$EFI"
TPM2="$TPM2"
PORT="$PORT"
REDIRUSB="$REDIRUSB"
LOSETUP="$LOSETUP"
SPICE="$SPICE"
SHARE="$SHARE"
QEMOOADD="$QEMOOADD"
EOF
}

getport(){
	start=$1
	while ss -tulpn |grep  -q ':'"$start" ; do
		start=$(( $start + 1 ))
	done
	echo $start
}

run() {
	cmdline="$*"
	if [ "$SHOW" ] ; then
		echo "Qemu cmdline:"
		echo "${SUDO}$cmdline"
	else
		eval "$cmdline"
	fi
}

checkPerms(){
	[ $UID -eq 0 ] && return 0
	granted=$(echo -e "$1\n${2//,/\\n}" | while read a ; do
		[ -z "$a" ] && continue
		if ! [ -w "$a" ] ; then
			echo "no"
			break
		fi
	done)
	[ "$granted" != 'no' ] && return 0
	return 111
}

checkImg(){
	local IMG
	IMG=$1
	if [ "$IMG" == 'none' ] ; then
		echo "No image name" 1>&2
		echo 'error'
		return
	elif ! [ -e "$IMG" ] ; then
		echo "$IMG - not exists" 1>&2
		echo 'error'
		return
	elif echo "$IMG" |grep -q '^/dev/\(cdrom\|sr[0-9]\+\)'; then
		echo "iso"
		return 0
	elif echo "$IMG" |grep -q '^/dev/.\+'; then
		if grep -q "$IMG" /proc/mounts; then
			echo "$IMG is mounted" 1>&2
			echo 'error'
			return
		elif [ -b "$IMG" ]; then
			echo "blockdev"
			return 0
		else
			echo "$IMG - is not block device" 1>&2
			echo 'error'
			return
		fi
	elif  file "$IMG" | grep -q "ISO 9660"; then
		echo "iso"
		return 0
	else
		echo "virt"
		return 0
	fi
}

checkFormat(){
	set -o pipefail
	qemu-img info "$1" |grep 'format:' |cut -d' ' -f3
	set +o pipefail
}

drive_cmdline(){
	local CDROM=''
	IMG="$(realpath "$1")"
	type=$(checkImg "$IMG")
	[ "$type" == "iso" ] && CDROM=",media=cdrom"
	[ "$type" == 'error' ] && return  1
	IMGFORMAT=$(checkFormat "$IMG")
	echo -n "-drive file=$(quote "$IMG"),format=$IMGFORMAT,cache=none"
	echo "$CDROM"
}

nameGen(){
	local NAME LAST
	LAST="$(ls -1 ./ |grep ${PREFIX}'.*qcow2$' |sort -V |head -n1)"
	NAME="$(basename "$IMG")"
	if [ -n "$LAST" ] ; then
		n="$(echo "$LAST" |sed 's/'$PREFIX'//' |sed 's/_...\.qcow2//')"
		expr $n + 1 >/dev/null 2>&1 && suffix=$(($n + 1))
	else
		suffix=1
	fi
	echo "${PREFIX}${suffix}_${NAME:0:3}.qcow2"
}

tpm2(){
	local IMAGE TPM2DIR DIR
	IMAGE=$1
	DIR=$2
	if [ -d "${IMAGE}.tpm2" ] ; then
		TPM2DIR="$(realpath ${IMAGE}.tpm2)"
	else
		TPM2DIR="$(realpath ${DIR}/$(basename "$IMAGE").tpm2)"
	fi
	if ! [ $SHOW ] ; then
	mkdir -p $TPM2DIR
	swtpm socket --tpmstate dir="${TPM2DIR}" --ctrl type=unixio,path="${TPM2DIR}/swtpm-sock" \
		--log level=20 --tpm2 > "${TPM2DIR}/tpm2.log" 2>&1 &
		echo "$!" > "${TPM2DIR}/kill.pid"
	if pgrep swtpm > /dev/null 2>&1 ; then
		echo "TPM2 dir: ${TPM2DIR}" 1>&2
		echo "${TPM2DIR}"
		return 0
	fi
	return 1
	else
		echo "TPM2 dir: ${TPM2DIR}" 1>&2
		echo ${TPM2DIR}
	fi
}

mkQcow2(){
    if [ -f "$QCOW2" ] ; then
		echo "$QCOW2 already exists" 1>&2
		echo "$QCOW2"
		return 0
	fi
	if run qemu-img create -f qcow2 "${QCOW2}" "${SIZE}G" 1>&2 ; then
		echo "$QCOW2"
	else
		echo 'error'
	fi
}

checkRam() {
	HOSTRAM="$(( $(grep MemTotal /proc/meminfo |awk '{print $2}') / 1000 ))"
	if [ "$RAM" == 'auto' ] ; then
		RAM="$(( "$HOSTRAM" / 2 ))"
		[ "$RAM" -gt 4272 ] && RAM='4272' # (free -g: total 4G)
		echo "$RAM"
	else
		if expr "$RAM" + 1 >/dev/null 2>&1 ; then
			if [ "$RAM" -lt "$HOSTRAM" ] ; then
				echo "$RAM"
			else
				echo "RAM size: $RAM greater then host RAM" 1>&2
				echo 'error'
			fi
		else
			echo "RAM size: $RAM - is not digit" 1>&2
			echo 'error'
		fi
	fi
}

checkUSB(){
	[ -b "$IMG" ] || return
	# shellcheck source=/dev/null
	source <(udevadm info -q property "$IMG" |grep 'ID_' |sed 's/=\(.*\)/="\1"/')
	if [ "$ID_BUS" = "usb" ]; then
		echo "-usb -device usb-host,vendorid=0x${ID_VENDOR_ID},productid=0x${ID_MODEL_ID}"
	fi
}

QEMU="qemu-system-$(arch)"

if ! command -V "$QEMU" >/dev/null; then
	echo "$QEMU not found"
	exit ${LINENO}
fi
if ! checkPerms "$IMG" "$ADD"; then
	if [ "$QEMOO" == "/usr/bin/qemoo" ]; then
		# TODO use `pkexec --keep-cwd` since polkit version 121 to keep initial working directory
		eval exec pkexec "$QEMOO" --workdir "$PWD" --qemoocfg "$QEMOOCFG" $ARGS
	else
		echo "Could not open '$IMG': Permission denied" 1>&2
		exit ${LINENO}
	fi
fi

# apply separate config
if [ "$SEPCFG" == 'yes' ] && [ -f "${IMG}.conf" ] ; then
	echo "Using: ${IMG}.conf"
	source "${IMG}.conf"
fi

# apply specified config
if [ "$CONF" ] ; then
	if [ -f "$CONF" ] ; then
		echo "Using: $CONF"
		source "$CONF"
	else
		mkSepCfg "$IMG"
	fi
fi
type="$(checkImg "$IMG")"
[ "$type" == 'error' ] && exit ${LINENO}
IMGFORMAT="$(checkFormat "$IMG")"

if [ "$ACTION" == 'install' ] ; then
	[ "$QCOW2" == 'none' ] && QCOW2=$(nameGen)
	if ! [ -w "$(dirname $(realpath "$QCOW2" ))" ] ; then
		eval exec pkexec "$QEMOO" --workdir "$PWD" --qemoocfg "$QEMOOCFG" $ARGS
	fi
	qImg="$( realpath "$(mkQcow2)" )"
	[ "$qImg" == 'error' ] && exit ${LINENO}
	if [ "$EFI" ] ; then
		cp -f /usr/share/OVMF/OVMF_VARS.fd "${qImg}.nvram"
		EFI="-drive if=pflash,format=raw,readonly=on,file='$EFI_FIRMWARE' -drive if=pflash,format=raw,file='${qImg}.nvram'"
	fi
	mkSepCfg "$qImg"
fi

if [ "$LOSETUP" ] && [ "$IMGFORMAT" = "raw" ] && echo "$IMG" |grep -qv '^/dev/'; then
	LOOPDEV="$(losetup -f)"
	losetup "$LOOPDEV" "$IMG" && { IMG="$LOOPDEV"; trap "losetup -d $LOOPDEV" EXIT; } || exit ${LINENO}
fi

if ! echo "$QEMUADD" |grep -q -e '-audiodev'; then
        # TODO: pipeware
        if pgrep --uid "$UID" 'pulseaudio' ; then
            SOUND='-device ich9-intel-hda -device hda-duplex,audiodev=audio0 -audiodev alsa,id=audio0'
        elif pgrep 'pulseaudio' && [ $HOME != '/root' ] ; then
            # for root
            SOUND="-device ich9-intel-hda -device hda-duplex,audiodev=audio0 -audiodev pa,id=audio0,\
server=$(find  /run/user/ -type d -name 'pulse' 2>/dev/null |head -n1)/native"
        else
            SOUND='-device ich9-intel-hda -device hda-duplex,audiodev=audio0 -audiodev alsa,id=audio0'
        fi
fi

SHARE=$(realpath $SHARE)

vRam=$(checkRam)
[ "$vRam" == 'error' ] && exit ${LINENO}

MAC="0a:$({ LC_MESSAGES=en fdisk -l "$IMG" 2>/dev/null |grep ident |grep -v 00000000 || stat -c%W "$IMG"; } |
	md5sum |sed 's/\(..\)/\1:/g' |cut -c 1-14)"
pgrep -f mac="$MAC" >/dev/null && MAC="0a:$(head -c64 /dev/urandom | md5sum |sed 's/\(..\)/\1:/g' |cut -c 1-14)"

NETDEVTYPE="user"
if ip link show qemoobr0 &>/dev/null; then
	NETDEVTYPE="bridge,br=qemoobr0"
elif ip addr show virbr0 2>/dev/null | grep -q 'inet '; then
	NETDEVTYPE="bridge,br=virbr0"
fi

DRIVEPARS="file=$(quote $IMG),format=$IMGFORMAT,cache=none"

COMMON="$QEMU $EFI $SOUND \
	-cpu max \
	-vga qxl \
	-smp 2 \
	-machine q35,accel=kvm:tcg \
	-name \"$(basename "$IMG")\" \
	-netdev $NETDEVTYPE,id=net0 -device virtio-net-pci,netdev=net0,mac='$MAC' \
	-m ${vRam}M \
	-rtc base=localtime \
	-virtfs local,path='$SHARE',mount_tag=hostdir,security_model=mapped,id=hostdir
"

[ "$REDIRUSB" == "yes" ] && usbDev="$(checkUSB)"

[ "$ACTION" == 'install' ] && INSTALL="-drive file='$qImg',cache=none"

case $type in
	'virt' )
		echo "Virtual machine image: $IMG"
		cmdline="
		-boot c \
		-drive $DRIVEPARS"
		;;
	'blockdev')
		echo "Block device: $IMG"
		cmdline="
		-boot c \
		${usbDev:--drive $DRIVEPARS}"
		;;
	'iso')
		echo "ISO: $IMG"
		cmdline="
		-boot d \
		-drive $DRIVEPARS,media=cdrom"
		;;
	*)
		echo "unknown type: $type"
		exit ${LINENO}
esac

if [ "$TPM2" ] ; then
	if [ $ACTION == 'install' ] ; then
		TPM2DIR=$(tpm2 "$qImg" ./)
	else
		TPM2DIR=$(tpm2 "$IMG" /tmp)
	fi
	if [ "$TPM2DIR" ] ; then
		TPM2ADD="-chardev socket,id=chrtpm,path='${TPM2DIR}/swtpm-sock' \
		-tpmdev emulator,id=tpm0,chardev=chrtpm -device tpm-tis,tpmdev=tpm0"
	fi
fi

if [ "$ADD" ] ; then
	EXT_DEVS=$(echo -e "${ADD//,/\\n}" | while read IMG  ; do
	drive_cmdline "$IMG"
	done)
fi

if [ "$SPICE" ] ; then
	AMPER='&'
	FREEPORT="$PORT"
	[ -z "$PORT" ] && FREEPORT="$(getport $FIRSTPORT)"
	DAEMON="-device virtio-serial -chardev spicevmc,id=vdagent,debug=0,name=vdagent \
			-device virtserialport,chardev=vdagent,name=com.redhat.spice.0 \
			-spice port=$FREEPORT,disable-ticketing=on -vga virtio"
fi

if [ "$SHOW" != 'yes' ] ; then
echo "Host share:
	$(realpath "$SHARE")
Linux guest mount command example:
	mkdir /mnt/hostdir
	mount -t 9p  hostdir /mnt/hostdir
	(add '-o trans=virtio,msize=100000000' if mount command ends with errors)
"
[ "$SPICE" ] && echo "PORT = $FREEPORT"
fi

run $COMMON $cmdline $INSTALL $EXT_DEVS $DAEMON $QEMUADD $TPM2ADD $AMPER
ppid=$!

# if SPICE
[ "$SPICE" ] && [ "$SHOW" != 'yes' ] && echo "PID = $ppid"
[ "$SPICE" ] && [ "$SHOW" != 'yes' ] && [ -f ${TPM2DIR}/kill.pid ] && \
echo "tpm2 PID: $(cat ${TPM2DIR}/kill.pid)" && exit

# if not
[ ! "$SPICE" ] && [ -f ${TPM2DIR}/kill.pid ] && kill $(cat ${TPM2DIR}/kill.pid) 2>/dev/null && sleep 1
[ -f ${TPM2DIR}/kill.pid ]  && ps $(cat ${TPM2DIR}/kill.pid) >/dev/null 2>&1 && \
echo "Can not kill tpm2 emulator. PID: $(cat ${TPM2DIR}/kill.pid)"
