#!/bin/dash

# License GNU GPLv3
# Copyright (C) 2019-2025 step
# Version 1.2.0
# Requires: yad 0.42.x, 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

YAD=${YAD_BIN:-yad}

# Main window defaults: 600x400 font "Monospace 9"
WIN_FONT="${FATDOG_CHECKSUM_FONT-Monospace 9}"
WIN_WIDTH="${FATDOG_CHECKSUM_WIDTH-600}"    # note  "-"
WIN_HEIGHT="${FATDOG_CHECKSUM_HEIGHT:-400}" # note ":-"
# Progress dialog defaults
PROGRESS_WIDTH=$(($WIN_WIDTH - 100))
# Hash/property selection dialog defaults: 300x(300 + 150)
SEL_WIDTH=300 SEL_HEIGHT=300 SEL_EXTRA=150

# 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 each.
# 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

YAD_VERSION="$($YAD_BIN --version)"

# Adapt dimensions to hi-dpi screens
adapt_dims_to_hidpi_screen() {
# IF GTK2 || GTK3 && GDK_SCALE < 2 or unset
if [ "${YAD_VERSION#*GTK+ 2}" != "$YAD_VERSION" -o "${GDK_SCALE:-1}" = 1 ]; then
	type fatdog-hidpi-lib > /dev/null || return 0
	. fatdog-hidpi-lib || return 1
								##### Keep WIN_HEIGHT last #####
	set -- $(hidpi_apply_scale $SEL_WIDTH $SEL_HEIGHT $SEL_EXTRA $PROGRESS_WIDTH $WIN_WIDTH $WIN_HEIGHT)
	SEL_WIDTH=$1 SEL_HEIGHT=$2 SEL_EXTRA=$3 PROGRESS_WIDTH=$4 WIN_WIDTH=$5 WIN_HEIGHT=$6
fi
}

adapt_dims_to_hidpi_screen

APP_ICON=/etc/xdg/rox.sourceforge.net/SendTo-resources/checksum.png
export YAD_OPTIONS="
	--title=\"$(gettext "File checksums")\"
	--window-icon=\"$APP_ICON\"
	--buttons-layout=spread
	--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} ${ccol:+"$COPY_COLUMNS"} |

	awk -f "$0-select-properties.awk" -v SEP_VALUE="$sep_value" \
		-v CCOL_NAME="$(gettext "x|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" \
		|

	env YAD_OPTIONS="$YAD_OPTIONS
--center --list --bool-fmt=o --column=bool:CHK
--column=label --sep-column=2 --sep-value=\"$sep_value\"
--column=value:HD --print-all
--borders=10 --height=$(($SEL_HEIGHT +30${ccol:+ +$SEL_EXTRA}))
--width=$SEL_WIDTH
--separator=\"$IFS\"
--no-rules-hint --search-column=0
	" $YAD \
		--text="\
$(gettext "In this dialog:")
$(gettext "* Select hashes to calculate")
$(gettext "* Select options")\
${ccol:+"\r$(gettext "* Select columns to copy")"}
" \
		--no-headers="$TMPS" `# KEEP` \
		|

	# 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

x="$(gettext "checksum x")"
h="$(gettext "checksum ...")"
x="${x#* }" h="${h#* }"

# In:  checkbox, CHECKSUM, NAME, HASH, SERIAL, PATH, @font@
# Out: checkbox, CHECKSUM, NAME, HASH, SERIAL, PATH
env YAD_OPTIONS="$YAD_OPTIONS
--list --tail
--column=\"$x\":CHK --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
--tabnum=2
" $YAD --plug=$$ --list > "$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
	env YAD_OPTIONS="$YAD_OPTIONS
--tooltip --text \"$(gettext "\
* Drop files onto this gutter to calculate new checksums.
* Click column headers to group results.
")\"
--tabnum=1
" $YAD --plug=$$ --dnd &

	# -------------
	# 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" "$PROGRESS_WIDTH" 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

sep="        "

if [ "$COPY_CMD" ]; then
	ttip="$(gettext "[Copy] puts selected rows on the clipboard.  Leave all rows unselected to [Copy] all.")"
	b1="--button=\"gtk-copy!!$ttip:0\"" b2="--button=gtk-quit:1"
else
	b1="--button=gtk-ok:0" b2="--button=gtk-cancel:1"
fi

env YAD_OPTIONS="$YAD_OPTIONS
--orient=hor --splitter=20 --center
${WIN_WIDTH:+--width=$WIN_WIDTH} ${WIN_HEIGHT:+--height=$WIN_HEIGHT}
--text=\"$(gettext "\
You may drop more files onto the gutter.     Resize the window to see all columns.
")\"
$b1 $b2
--print-xid=\"$TMPS\".progress.xid
--button=gtk-preferences:\"$0 --select_properties\"
" $YAD --key=$$ --paned &

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

