#!/bin/dash

# License GNU GPLv3
# Copyright (C) 2019 step
# Version 1.1.0
# Requires: yad 0.42, GNU pgrep

export TEXTDOMAIN=fatdog OUTPUT_CHARSET=UTF-8

# Block diagram: see checksum.{box.txt,graph,svg}

# TL;DR
# checksum [filepath filepath...]                # with progress bars (parallel)
# FATDOG_CHECKSUM_FEATURES='1!' ./checksum ...   # without progress bars (sequential)
# FATDOG_PROGRESS_MAX_PAR_COMP=2 ./checksum ...  # set maximum number of progress bars (parallel jobs)
# The default maximum number is four times the number of processor cores, not to exceed 16.

# -----------------------------------------------------------------------------
# Configuration that the user or the environment may change {{{1

# Window appearance: default 600x400 font "Mono 9"
WIN_FONT="${FATDOG_CHECKSUM_FONT-Mono 9}"
WIN_WIDTH="${FATDOG_CHECKSUM_WIDTH-600}"
WIN_HEIGHT="${FATDOG_CHECKSUM_HEIGHT:-400}" # yes ":-"

# Command that copies stdin to primary selection (clipboard), leave blank if none available.
# If blank then all lines are printed to stdout, and selective output isn't possible.
COPY_CMD="${FATDOG_CHECKSUM_COPY_CMD-xsel -ip}"

# Comma-separated list of the column numbers to be copied out, 1 through 6.
# Default same as md5sum, which prints columns 2,3.
COPY_COLUMNS="${FATDOG_CHECKSUM_COPY_COLUMNS-2,3}"

# Separator between copied columns.
# Default same as md5sum which prints two spaces.
COLUMN_SEP="${FATDOG_CHECKSUM_COLUMN_SEP-  }"

# Newline-separated list of all available hash commands and whether to compute them.
# Set "off" to untick the checkbox in the properties dialog.
# backward-compatible with Xdialog format
# Name|Command Name State
HASH_SET="${FATDOG_CHECKSUM_HASHES:-
CRC|cksum CRC off
MD5|md5sum MD5 off
SHA1|sha1sum SHA1 off
SHA224|sha224sum SHA224 off
SHA256|sha256sum SHA256 off
SHA384|sha384sum SHA384 off
SHA512|sha512sum SHA512 off
SHA3|sha3sum SHA3 off
}"

# Comma-separated list of feature numbers and flags, such as showing the progress bars.
# Set each list element to a number N followed by single-character flag, as follows:
#   N! to hide feature number N in the property dialog
#   N- to untick the checkbox
#   N+ to tick ("+" can be omitted)
# If the same N occurs multiple times in the list, the last occurrence wins.
# N+ is the default for any assigned N not included in this list.
# N is assigned in range [1..nK] (no gaps) for the following feature names:
#   1    SHOW_PROGRESS_DIALOG_1
FEATURES="${FATDOG_CHECKSUM_FEATURES-,}"

# -----------------------------------------------------------------------------
# User don't change past this line {{{1

APP_ICON=/etc/xdg/rox.sourceforge.net/SendTo-resources/checksum.png
export YAD_OPTIONS="
	--title=\"$(gettext "File checksums")\"
	--window-icon=\"$APP_ICON\"
	--buttons-layout=center
	--no-markup
"

MAX_HASH=${MAX_HASH:-10} # >= size of hash set

select_properties() # [$1-selected-hash-list $2-selected-copy-column-list $3-selected-feature-list] {{{1
# In hash list format: Name"|"Command [ "/"Name"|"Command... ]
# In copy-column list format: Digit[(","|"/")Digit ...]
# In feature list format: Digit""Flag[(","|"/")Digit""Flag ...]
# Output: "\t"-separated list of slash-separated lists representing the selected elements, e.g.
#   "hash/"[Name1"|"Command1["/"Name2"|"Command2"]["\tccol/"2"/"3]["\tfeat/"1+]
{
	local sel_hash="$1" sel_ccol="$2" sel_feat="$3" sep_value=@@
	local bool label value context sep IFS="="

	printf "%s\n" hash "$HASH_SET" feat "${FEATURES:-,}" ${ccol:+ccol "$COPY_COLUMNS"} |

	#i18n U+2A2F
	awk -f "$0-select-properties.awk" -v SEP_VALUE="$sep_value" \
		-v CCOL_NAME="$(gettext "⨯|checksum|file name|hash|serial|file path")" \
		-v FEAT_NAME="$(gettext "show progress dialog")" \
		-v HASH="$sel_hash" -v CCOL="$sel_ccol" -v FEAT="$sel_feat" \
		|

	yad --center --list --checklist --bool-fmt=o --column=bool \
		--column=label --sep-column=2 --sep-value="$sep_value" \
		--column=value:HD --print-all \
		--borders=10 --height=$((300 +30${ccol:+ +150})) \
		--separator="$IFS" \
		--no-rules-hint --search-column=0 \
		--no-headers="keep:$TMPS" \
		--text="$(gettext "Select which hashes to calculate:")\r$(gettext "Select options:")${ccol:+\r$(gettext "Select which columns to copy:")}\r" \
		|

	# Implode hash, ccol and feat elements with '/' (backward-compatible Xdialog output format).
	while read bool label value; do
		case $value in
			context" "* ) # hash, ccol, feat
				context=${value#* }
				printf "${sep}%s" "$context"
				sep="\t"
				continue
				;;
		esac
		case $bool in
			on )
				printf "/%s" "$value"
				;;
			off )
				if [ feat = "$context" ]; then
					# special case "off" is made explicit as N-
					printf "/%s" "$value-"
				fi
				continue
				;;
		esac
	done
}

set_hash_ccol_feat() # $1-hash $2-ccol $3-feat {{{1
# show property dialog then set and save globals hash, ccol and feat
{
	hash="$1" ccol="$2" feat="$3"
	local sets="$(select_properties "$hash" "$ccol" "$feat")"
	local p IFS='	' # tab ^I
	set -- $sets
	for p; do
		p=${p#/}; p=${p%/}
		case $p in
			hash/* ) hash=${p#*/} ;;
			ccol/* ) ccol=${p#*/} ;;
			feat/* ) feat=${p#*/} ;;
		esac
	done

	# Save state
	[ "$hash" ] && echo "$hash" > "$TMPS".hash
	[ "$ccol" ] && echo "$ccol" > "$TMPS".ccol
	[ "$feat" ] && echo "$feat" > "$TMPS".feat
}

on_exit() # $1-status {{{1
{
	trap - HUP INT QUIT TERM ABRT 0
	local status=$1

	kill_all

	[ "$TMPS" ] && rm -f "$TMPS".*

	exit $((${STATUS:-0} | ${status:-0}))
}

kill_all() # {{{1
{
	local pid p

	# Note: don't use pgrep -g $$ because it'd include all pids in ROX-Filer's process group.
	set -- $(pgrep -f "(awk|tail).*$TMPS") #$(pgrep -P $$ "(xsel|xclip)")

	# the progress server
	[ -e "$TMPS".progress.pid ] && read pid < "$TMPS".progress.pid

	for p in $* $pid; do
		kill $p 2>/dev/null
		wait $p 2>/dev/null
	done

	if [ "$YAD_PID" ]; then # release shared memory just in case yad couldn't
		ipcs -mp > "$TMPS".ipcs
		while read shmid owner cpid lpid; do
			[ $YAD_PID = "$cpid" ] && ipcrm shm $shmid 2>/dev/null
		done < "$TMPS".ipcs
	fi
}

# Initialize {{{1

# Expose progress bars iff the progress server binary is installed
export PROGRESS_SRV_BIN=${FATDOG_PROGRESS_SRV_BIN:-yad-progress-server.sh}
if ! type "$PROGRESS_SRV_BIN" >/dev/null; then
	unset PROGRESS_SRV_BIN
	FEATURES="${FEATURES},1!" # hide feature SHOW_PROGRESS_DIALOG_1
fi

# When yad buttons call this script as a sub-routine
case $1 in
	--select_properties ) shift
		YAD_OPTIONS="$YAD_OPTIONS --on-top" # as sub-dialog of the main window

		# Initialize property sets for the selection sub-dialog
		read hash < "$TMPS".hash # selected digests
		read ccol < "$TMPS".ccol # selected copy columns
		read feat < "$TMPS".feat # selected features

		# Display sub-dialog
		set_hash_ccol_feat "$hash" "$ccol" "$feat"

		# Send the new hash set to the engine feed
		[ "$hash" ] && printf "\thash%s\n" "$hash" >> "$TMPS".feed

		exit ;;

	# RE-ENTER FROM exec
	--reload ) shift
		read HASH         < "$TMPS".hash
		read COPY_COLUMNS < "$TMPS".ccol
		read FEATURES < "$TMPS".feat
		;;
esac

# Keep after --select_properties's set_hash_ccol_feat
trap 'on_exit 128' HUP INT QUIT TERM ABRT
trap 'on_exit 0' 0

# Communication slots
export TMPS=/tmp/.checksum.$$
rm -f "$TMPS".list "$TMPS".feed
mkfifo "$TMPS".list "$TMPS".feed &&
# States
touch "$TMPS".hash "$TMPS".ccol "$TMPS".feat "$TMPS".rsel || exit 1
[ -s "$TMPS".rsel ] && mv "$TMPS".rsel "$TMPS".reload

# When ROX right-click calls this script
if ! [ "$HASH" ]; then
	set_hash_ccol_feat "" "" "$FEATURES"
	HASH=$hash FEATURES=$feat
fi
! [ "$HASH" ] && exit

# -----------------------------------------------------------------------------
# PRESENTATION PIPE - part 1 {{{1
echo "$COPY_COLUMNS" > "$TMPS".ccol
echo "$FEATURES" > "$TMPS".feat

{
# -----------------------------------------------------------------------------
# Section "List" - output Transform's output {{{2

# This will show the checksums
if [ "$COPY_CMD" ]; then
	options="--multiple"
else
	options="--no-selection"
fi

#i18n U+2A2F
x="$(gettext "⨯")"
#i18n U+00B7 · U+21F5 ⇵ / U+2195 ↕ /
h="$(gettext "·")"

# In:  checkbox, CHECKSUM, NAME, HASH, SERIAL, PATH, @font@
# Out: checkbox, CHECKSUM, NAME, HASH, SERIAL, PATH
yad --plug=$$ --tabnum=2 \
	--list --tail \
	--column="$x" --checklist --bool-fmt=1 \
	--column="$h" --search-column=2 \
	--column="$h" --column="$h" --column="$h":NUM --column="$h" --column=@font@ \
	--no-rules-hint --print-all $options \
	> "$TMPS".rsel # {{{2}}}

} < "$TMPS".list & # {{{2}}}

# -----------------------------------------------------------------------------
# ENGINE PIPE {{{1

yad_list_out_to_in() # $1-filepath {{{2
# Convert yad --list output format to yad --list input format (see section "List")
# In:  checkbox, CHECKSUM, NAME, HASH, SERIAL, PATH,         separator "|"
# Out: checkbox, CHECKSUM, NAME, HASH, SERIAL, PATH, @font@  separator "\n"
{
	sed -e "s/\$/$WIN_FONT/" -e 's/[|]/\n/g' "$1"
}

# -----------------------------------------------------------------------------
# Section "Feed" - feed Transform's pipe {{{2

PROGRESS_FIFOIN="$TMPS".progress
{
	# -------------
	# This will accept dropped files after the initial checksums are shown
	#i18n U+26AB U+26AA
	yad --plug=$$ --dnd --tabnum=1 --tooltip \
		--text "$(gettext "\
⚫Drop files onto the left border to calculate new checksums.
⚫Click column headers to group results.
⚫[Copy] puts selected rows on the clipboard.
⚪ Leave all unselected to [Copy] all.
")" &

	# -------------
	# This is the progress bar dialog server that actually outputs the digests
	# The coming yad --key process (main window) will create file *.progress.xid.
	if [ "$PROGRESS_SRV_BIN" ]; then
		FATDOG_PROGRESS_XID_PATH="$TMPS".progress.xid \
			"$PROGRESS_SRV_BIN" "$PROGRESS_FIFOIN" $((WIN_WIDTH - 100)) checksum &
		echo $! > "$TMPS".progress.pid

		# Wait up to 2 seconds for the server to accept data
		i=0; while [ $i -lt 20 ]; do
			[ -e "$PROGRESS_FIFOIN" ] && break
			i=$(($i +1)); sleep 0.1
		done
	fi

	# -------------
	# Relay external commands, such as changing the hash set
	tail -f "$TMPS".feed &

	# Start the feed with values set by Initialize:

	## the starting hash set
	printf "\thash%s\n" "$HASH"

	## and the file path(s) from ROX SendTo
	printf "%s\n" "$@"

	# -------------
	# Reload list rows, if any

	if [ -s "$TMPS".reload ]; then
		printf '\treload\n'
		yad_list_out_to_in "$TMPS".reload
		printf '\treload-end\n'
		rm "$TMPS".reload
	fi
} \
	|

# -----------------------------------------------------------------------------
# Section "Transform" - request progress for incoming file paths; process responses and in-band commands {{{2

	# This script computes and pretty-prints the checksums or hands-off
	# the computation to the progress server. It also deals with in-band
	# commands.
	# FS         "\b"
	# $1         "\thash"<hash set> | <filepath \
	# $2..$NF    filepath-continued>

	# For mawk add option -W interactive
	>> "$TMPS".list awk -f "$0-transform.awk" \
		-v TMPS="$TMPS" -v MAX_HASH=$MAX_HASH -v FONT="$WIN_FONT" \
		-v FIFO_OUT="$PROGRESS_FIFOIN" \
		&

# Display the main window and block {{{1

#i18n separator
sep="$(gettext "        ")"

if [ "$COPY_CMD" ]; then
	b1="--button=gtk-copy:0" b2="--button=gtk-quit:1"
else
	b1="--button=gtk-ok:0" b2="--button=gtk-cancel:1"
fi

#i18n U+2304
yad --key=$$ --paned --orient=hor --center \
	${WIN_WIDTH:+--width=$WIN_WIDTH} ${WIN_HEIGHT:+--height=$WIN_HEIGHT} \
	--splitter=20 \
	--text="$(gettext " ⌄ drop (hover)")" "$b1" $b2 \
	--print-xid="$TMPS".progress.xid \
	--button=gtk-properties:"$0 --select_properties" &

YAD_PID=$!
wait $YAD_PID
STATUS=$?

# due to button Quit/Cancel, keypress [Esc], window [x] or error
if [ 0 != $STATUS ] || ! [ -s "$TMPS".rsel ]; then
	exit $STATUS
fi

# -----------------------------------------------------------------------------
# PRESENTATION PIPE - part 2 {{{1

# -----------------------------------------------------------------------------
# Section "Copy" - copy selected rows to the clipboard {{{2
# Fall back to printing all of yad's output rows when COPY_CMD is null.

awk -F"|" -f "$0-copy.awk" \
	-v TMPS="$TMPS" -v COPY_CMD="$COPY_CMD" -v SEP="$COLUMN_SEP" \
	-v CCOL="$COPY_COLUMNS" \
	"$TMPS".rsel \
	;

# -----------------------------------------------------------------------------
# Section "Reload" - restart the main window, which reloads the current hash/ccol/feat state {{{1
# Preserve PID and do not go through on_exit().

kill_all
exec "$0" --reload

