#!/bin/dash

# License GNU GPLv3
# Copyright (C) 2019 step
# Version 1.0.0

export TEXTDOMAIN=fatdog OUTPUT_CHARSET=UTF-8

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

# -----------------------------------------------------------------------------
# User or environment can 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 (selection not possible).
COPY_CMD="${FATDOG_CHECKSUM_COPY_CMD-xsel -ip}"

# List of column numbers to copy 1-6 (Linux *sums set 2,3)
COPY_COLUMNS="${FATDOG_CHECKSUM_COPY_COLUMNS-2,3}"

# Separator between copied columns (Linux *sum utilities set two spaces)
COLUMN_SEP="${FATDOG_CHECKSUM_COLUMN_SEP-  }"

# List all available hash commands and whether to select them.
# backward-compatible 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
}"

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

set_properties() # [$1-selected-hash-list $2-selected-copy-column-list] {{{1
# In hash list format: Name"|"Command [ "/"Name"|"Command... ]
# In copy-column list format: Digit[(","|"/")Digit ...]
# Output: "/"-separated COMBINED list of all selected elements, e.g.
#   [Name1"|"Command1"/"Name2"][|"Command2"/"2,3]
{
	local sel_hash="$1" sel_ccol="$2"

	printf "%s\n" hash "$HASH_SET" ${ccol:+ccol "$COPY_COLUMNS"} |
		#i18n U+2A2F
		awk -v CCOL_NAME="$(gettext "⨯|checksum|file name|hash|serial|file path")" \
			-v HASH="$sel_hash" -v CCOL="$sel_ccol" '#{{{awk
# Skip white lines
/^[[:space:]]*$/ { next }

# Change state
/^hash|ccol$/ { state = $0; next }

"hash" == state { # {{{2
# Initial hash set element on stdin: Name"|"Command" "Name" "State"\n"

	# Update default selection
	if(index("/"HASH"/", "/"$1"/")) { $3 = "on" }
	# Reverse columns for yad
	print $3"\n" $2"\n" $1
}

"ccol" == state { # {{{2
# Initial copy-column set elementS on stdin (all on one line) : Digit","Digit...

	# Append separator line - yad`s --sep-value=@@
	print "0\n@@\n"
	# Unpack copy-column names
	nK = split(CCOL_NAME, K, /\|/)
	# Normalize CCOL format
	gsub("/", ",", CCOL)
	delete V
	for(i = 1; i <= nK; i++) {
		# Initialize default selection
		V[i] = index(","$0",", ","i",") ? "on" : "off"
		# Update default selection
		if(CCOL) { V[i] = index(","CCOL",", ","i",") ? "on" : "off" }
		print V[i]"\n" K[i]"\n" i
	}
}
	#awk}}}' |

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

	# implode with '/' (backward-compatible Xdialog output format)
	sed -e :a -e '$!N; s:\n:/:; ta'
}

yad_ripper() # $1-key {{{1
{
	local key=${1:-$$}
	pkill -USR1 -f "yad.*--key=$key "
	sleep 0.1
	pkill -USR1 -f "yad.*--plug=$key "
	if [ "$YAD_PID" ]; then # release shared memory just in case
		ipcs -mp | while read shmid owner cpid lpid; do
			[ $YAD_PID = "$cpid" ] && ipcrm shm $shmid 2>/dev/null
		done
	fi
}

on_exit() # {{{1
{
	trap - HUP INT QUIT TERM ABRT 0
	yad_ripper
	pkill -f "tail .*$TMPS\\.feed" 2>/dev/null

	for p in "$TMPS".*; do rm "$p"; done
}

# Initialize {{{1

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

		# The current hash set will be used to initialize the sub-dialog
		read hash < "$TMPS".hash

		# Likewise for the current copy-column set
		read ccol < "$TMPS".ccol

		# Select the new hash and copy-column sets
		sets="/$(set_properties "$hash" "$ccol")"
		hash=${sets%%/[1-9]*}; ccol=${sets#$hash}
		hash=${hash#/} ccol=${ccol#/}

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

		# Save copy-column state
		[ "$ccol" ] && echo "$ccol" > "$TMPS".ccol

		exit ;;

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

trap on_exit HUP INT QUIT TERM ABRT 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".rsel || exit 1

# When ROX right-click calls this script
! [ "$HASH" ] && HASH="$(set_properties)"
! [ "$HASH" ] && exit

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

{
# -----------------------------------------------------------------------------
# Section "List" outputs 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}}}

# -----------------------------------------------------------------------------
# TRASFORM PIPE {{{1

yad_list_out_to_in() # $1-filepath {{{1
# 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" feeds Transform's pipe {{{2

{
	# 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 will accept external commands, such as changing the hash set
	tail -f "$TMPS".feed &

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

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

	# and reload rows, if any
	if [ -s "$TMPS".rsel ]; then
		printf '\treload\n'
		yad_list_out_to_in "$TMPS".rsel
		printf '\treload-end\n'
	fi
} \
	|

# -----------------------------------------------------------------------------
# Section "Transform" transforms paths and processes in-band commands {{{2

	# This computes and pretty-prints the checksums
	# FS         "\b"
	# $1         "\thash"<hash set> | <filepath \
	# $2..$NF    filepath-continued>

	# For mawk add option -W interactive
	>> "$TMPS".list awk -v TMPS="$TMPS" -v MAX_HASH=$MAX_HASH -v FONT="$WIN_FONT" '#{{{awk
BEGIN {
	FS = "\b"
}
/^[[:space:]]*$/ { next }

# Change the hash set
$1 ~ "\thash" {
	hash = substr($1, length("hash")+2)
	# export current hash set to file
	print hash > TMPS".hash"; close(TMPS".hash")

	delete H
	nH = split(hash, H, /\/|\|/)
	next
}

# Reload state: pass through output lines between "\treload" and "\treload-end"
$1 == "\treload" {
	while(0 < getline) {
		if($1 == "\treload-end") { fflush(); next }
		print
	}
}

# Apply hash set to incoming files
{
	path = $0; sub("^file://", "", path) # yad dnd adds this
	if("" == path) { next }              # when script started without arguments
	name = path; sub(".*/", "", name)

	for(i = 2; i <= nH; i += 2) {
		do_file(path, name, H[i], H[i -1])
	}
}
function do_file(path, name, cmd, hash,   checksum) { #{{{3
	cmd = cmd" \""path"\" 2>&1"
	cmd | getline checksum # digits and spaces <path>
	# If non-zero then path was a directory or the checksum command failed.
	if(0 == close(cmd)) {
		checksum = substr(checksum, 1, length(checksum) - length(path))
		sub(/ +$/, "", checksum)
	}
	# checkbox, CHECKSUM, NAME, HASH, SERIAL, PATH, @font@
	print "0\n" checksum "\n" name "\n" tolower(hash) "\n" (MAX_HASH * (NR -1) + i) "\n" path  "\n" FONT
	fflush()
}
#awk}}}' &

# -----------------------------------------------------------------------------
# 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 \
	--button=gtk-properties:"$0 --set_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" copies selected rows to the clipboard {{{2
# Fall back to printing all of yad's output rows when COPY_CMD is null.

awk -F"|" -v TMPS="$TMPS" -v COPY_CMD="$COPY_CMD" -v SEP="$COLUMN_SEP" \
	-v CCOL="$COPY_COLUMNS" '#{{{awk

# Get copy-column set update, if any
1 == NR {
	if(0 < (getline ccol < (TMPS".ccol"))) {
		close(TMPS".ccol")
		gsub("/", ",", ccol)
		CCOL = ccol
	}
}

# Save all input rows in array R separating checked from unchecked.
# Input fields: # checkbox(0|1), CHECKSUM, NAME, HASH, SERIAL, PATH

{ any = any || $1 }

COPY_CMD {
	sep = rec = ""
	for(i = 1; i <= NF; i++) {
		if(index(","CCOL",", ","i",")) {
			rec = rec sep $i
			sep = SEP
		}
	}
	R[++nR,$1] = rec
	next
}

# Fallback
{ R[++nR,$1] = $0; next }

# If no row (!any) was selected then output all rows (I know, it`s weird
# but it`s also convenient) otherwise output just the selected rows.
# The following substitutions hold: if !any i,0 = i,any, if any i,1 = i,any
END {
	if(COPY_CMD) {
		for(i = 1; i <= nR; i++) {
			if((i,any) in R) {
				print R[i,any] | COPY_CMD
			}
		}
	} else {
		for(i = 1; i <= nR; i++) {
			print R[i,any]
		}
	}
close(COPY_CMD)
}
#awk}}}' "$TMPS".rsel # {{{2}}}

# -----------------------------------------------------------------------------
# Section "Restart" reloads the main window with all associated state {{{1

# Preserve PID and do not go through on_exit().
exec "$0" --reload-state

