#!/bin/bash
# Per course speelcking wrapper
# Written by: Behan Webster
#################################
set -e

VERSION=1.7
CMD=$(basename $0)

#################################
version() {
	echo "$CMD v$VERSION"
	exit 0
}

ACHECK=
ACHECK_OPTS=
ASPELL=
ASPELL_OPTS=
BADFILE=
DEFAULT_FILEEXTS="tex inc"
FILEEXTS=
WORDFILE=

#################################
default_config() {
	export LANGUAGE="en"
	LOCALE=en_US.UTF-8
	ADICT="american"
	export LANG=$LOCALE
	export LC_ALL=$LOCALE
	export LC_COLLATE=$LOCALE

	ASPELL_OPTS="--home-dir=. --mode=tex --tex-check-comments "
	ACHECK_OPTS="--ignore-repl --sug-mode=bad-spellers"

	BADFILE="badwords.txt"
	WORDFILE="wordlist.txt"
	FILEEXTS="$DEFAULT_FILEEXTS"
}

#################################
# Read config file
read_config() {
	local DIR=${1:-.}
	local CONFIG="$DIR/.spellcheckrc"

	default_config

	if [[ -e $CONFIG ]] ; then
		info "Reading local config file: $CONFIG"
		if [[ -n $DEBUG ]] ; then
			debug "Contents of config file"
			cat $CONFIG
		fi
		source "$CONFIG"
	fi

	ASPELL="aspell ${LANGUAGE:+--lang=$LANGUAGE} $ASPELL_OPTS"
	ACHECK="$ASPELL ${ADICT:+--master=$ADICT} $ACHECK_OPTS check"
}
read_config "$HOME"

#################################
colors() {
	BACK="\e[0m"
	BLUE="\e[0;34m"
	CYAN="\e[0;36m"
	#GREEN="\e[0;32m"
	RED="\e[0;31m"
	YELLOW="\e[0;33m"
}
BACK=
BLUE=
CYAN=
RED=
YELLOW=

#################################
debug() {
	[[ -z $DEBUG ]] || echo -e "${BLUE}D: $*${BACK}" >&2
}
error() {
	echo -e "${RED}E: $*${BACK}" >&2
	exit 1
}
info() {
	[[ -n $QUIET ]] || echo -e "${CYAN}I: $*${BACK}" >&2
}
warn() {
	[[ -n $QUIET ]] || echo -e "${YELLOW}W: $*${BACK}" >&2
}
verbose() {
	[[ -z $VERBOSE ]] || echo "+ $*" >&2
	# shellcheck disable=SC2048
	[[ -n $TEST ]] || $*
}

#################################
# Overwrite file only if it has changed preserving mtime
mv_if_changed() {
	if cmp -s "$1" "$2" ; then
		rm -f "$2"
	else
		cp "$2" "$1"
	fi
}

#################################
sort_list() {
	local WL=$1 T
	T=$(mktemp /dev/shm/spellcheck-dict.XXXXXX)
	sort -u "$WL" \
		| sort > "$T" \
		&& mv_if_changed "$WL" "$T"
	rm -f "$T" # Should have been mv or rm in previous step
}

#################################
# Find the directories which have wordlists
build_word_list_dirs() {
	debug "build_word_list_dirs: $*"
	local DIR D P
	for DIR in "$@" ; do
		# Use only absolute paths, because aspell is picky
		D=$(realpath "$DIR")
		P=$(realpath "${D%/*}")
		debug "  Examining $DIR for wordlist: $D $P"
		echo "$D"
		echo "$P"
	done | sort --unique
}

#################################
# (re)build all the known wordlists from the .aspell files
build_word_lists() {
	debug "build_word_lists: $*"
	local UNIQ
	build_word_list_dirs "$@" | while read -r UNIQ ; do
		local AS BW WL
		AS="$UNIQ/.aspell.en.pws"
		BW="$UNIQ/$BADFILE"
		WL="$UNIQ/$WORDFILE"
		if [[ ! -e "$WL" || -e "$AS" && "$AS" -nt "$WL" ]] ; then
			if [[ -e "$AS" ]] ; then
				debug "Building $WL from $AS"
				sed -e 1d "$AS" >> "$WL"
				if [[ $(dirname "$AS") == $(pwd) ]] ; then
					debug "Truncating $AS"
					sed -i -e '2,$d' "$AS"
				fi
			fi
			if [[ -e "$BW" && "$BW" -nt "$WL" ]] ; then
				local T WORD
				debug "Applying $BW to $WL"
				T=$(mktemp /dev/shm/spellcheck-sedbl.XXXXXX)
				# shellcheck disable=SC2013
				for WORD in $(cat "$BW") ; do
					echo "/^$WORD$/d"
				done > "$T"
				verbose sed -i -f "$T" "$WL"
				rm -f "$T"
			fi
		fi
		if [[ -s "$WL" ]] ; then
			sort_list "$WL"
			echo "$UNIQ"
		fi
	done | sed -e 's/'$COURSE'/zzz/' | sort --unique | sed -e 's|$|/wordlist.txt|; s/zzz/'$COURSE'/'
}

#################################
# Build per directory wordlists from the local "global" list
distribute_word_lists() {
	debug "distribute_word_lists: $*"
	local WLS DIRS DIR UNIQ
	WLS=$(build_word_lists "$@"); shift
	DIRS=$(for DIR in "$@" ; do
		debug "Updating wordlist for $DIR"
		realpath "$DIR"
	done | sort --unique)

	#for DIR in "$@" ; do
	#	debug "Updating wordlist for $DIR"
	#	realpath "$DIR"
	#done | sort --unique | while read -r UNIQ ; do

	#for UNIQ in $DIRS ; do
	#	echo $UNIQ
	#done
	#exit

	for UNIQ in $DIRS ; do
		local WL="$UNIQ/$WORDFILE" WORD FILES
		FILES=$(find "$UNIQ" -maxdepth 1 -type f -regex '.*\.\(tex\|inc\)')
		info "Building $WL"
		debug "  Checking $FILES"
		sort -u $WLS | while read -r WORD ; do
			if grep -q -w "$WORD" $FILES ; then
				echo "$WORD"
			fi
		done | sort -u >"$WL"
	done
}

#################################
# Build a aspell word dictionart from a simple wordlist
build_word_dict() {
	debug "build_word_dict: $*"
	local WL WD
	for WL in "$@" ; do
		if [[ "$WL" ]] ; then
			debug "Found $WL"
			WD=${WL/txt/dict}
			if [[ ! -e "$WD" || "$WL" -nt "$WD" ]] ; then
				debug "Building $WD"
				TEST=1 verbose "$ASPELL" create master "$WD"
				$ASPELL create master "$WD" < "$WL" 2>/dev/null
				[[ -e $WD ]] || error "$WD not created"
			fi
			echo "$WD"
		fi
	done
}

#################################
# Find other directories to check from the tex file
get_tex_dirs() {
	local FILE=$1
	( grep -vE '^%' "$FILE" | grep 'subimport' || true ) \
		| sed -re 's|^.*\{(.*)\/}\{index\}.*$|\1|;'
	( grep -vE '^%' "$FILE" | grep 'input' || true ) \
		| sed -re 's|\\input\{(.*)\}$|\1.tex|;'
}
 
######################################################################
# Read singular config from course tex file
read_tex() {
        local FILE=$1
        local KEY=$2

        # shellcheck disable=SC1003
        grep -E '\\newcommand{\\'"$KEY"'}' "$FILE" | head -1 \
                | sed -re 's/.*\{(.*)\}$/\1/; s/\\_/_/g'
}

#################################
# Find text files to spellcheck from file extensions
find_tex_files() {
	local DIR EXT
	for DIR in "$@" ; do
		DIR="$(sed -re 's/\\arch/'$ARCH'/g' <<<"$DIR")"
		DIR="$(sed -re 's/\\board/'$BOARD'/g' <<<"$DIR")"
		for EXT in $FILEEXTS ; do
			debug "Look for *.$EXT in $DIR"
			# Dereference all symlinks with realpath,
			# or we'll destroy symlinks
			find "$DIR" -maxdepth 4 -name "*.$EXT" -print0 \
				| xargs -0 --no-run-if-empty realpath \
				| sort
		done
	done
}

#################################
# List all the text files which are to be spellchecked
list_texfiles() {
	local FILE=$1
	debug "list_tex_files: $FILE"
	local DIRS

	DIRS="$(pwd) "$(get_tex_dirs "$FILE")
	# shellcheck disable=SC2086
        debug "list_textfiles:" $DIRS
	# shellcheck disable=SC2086
	find_tex_files $DIRS
}

#################################
count_tex_files() {
	local COUNT
	COUNT=$(wc -l <<< "$@")
	info "Spellchecked $COUNT files"
}

#################################
do_spell() {
	local FILE=$1

	info "Spellchecking $FILE"

	local DIRS
	DIRS="$(pwd) "$(get_tex_dirs "$FILE")

	#########################
	# Build aspell options and dictionaries
	local OPTS LISTS DICTS DICT
	OPTS=$BACKUP
	# shellcheck disable=SC2086
	LISTS=$(build_word_lists $DIRS)
	# shellcheck disable=SC2086
	DICTS=$(build_word_dict $LISTS)
	for DICT in $DICTS ; do
		debug "Add extra dict $DICT"
		OPTS+=" --add-extra-dicts=$DICT"
	done

	#########################
	# spellcheck files
	local FILES F
	# shellcheck disable=SC2086
	FILES=$(find_tex_files $DIRS)
	for F in $FILES ; do
		info "Processing $F"
		verbose "$ACHECK" "$OPTS" "$F"
	done

	#########################
	# Rebuild wordlist from .aspell files
	# shellcheck disable=SC2086
	build_word_lists $DIRS >/dev/null

	#########################
	# Delete temporary dict files
	# shellcheck disable=SC2086
	if [[ -z $DEBUG && -n $DELETE ]] ; then
		verbose rm -f $DICTS
	fi

	count_tex_files "$FILES"
}

#################################
# Generate file list from commandline
get_files() {
	local FILE
	for FILE in "$@" ; do
		if [[ -d $FILE ]] ; then
			if [[ -f "$FILE/index.tex" ]] ; then
				echo "$FILE/index.tex"
			else
				echo "$FILE"/*.tex
			fi
		else
			echo "$FILE"
		fi
	done
}

#################################
usage() {
	echo "Usage: $CMD <options> [course.tex]"
	echo "    -c --color          Pretty colors!"
	echo "    -e --extention EXT  Look for *.EXT in each directory"
	echo "    -h --help           Print this help"
	echo "    -l --list           List tex files which will be spell checked"
	echo "    -q --quiet          No output (except for running aspell)"
	echo "    -v --verbose        Show all the commands executed"
	echo "    -V --version        Print out the version of this script"
	echo "    -w --wordlist       List word lists being consulted"
	echo "    -W --distribute-wordlist  Build word lists in each directory"
	echo "    -x --dont-backup    Don't make backup files when spellchecking"
	echo
	echo "    .../$WORDFILE    List of words for dictionary (checkin)"
	echo "    .../$BADFILE    Words which shouldn't be added to wordlist (checkin)"
	echo "    .../wordlist.dict   Temporary dictionary file (can delete)"
	exit 0
}
 
#################################
# Initialize variables
BACKUP="--backup"
CHECK=
DEBUG=
DELETE=
DISTRIBUTE=
LIST=
QUIET=
TEST=
VERBOSE=
WORDLIST=

#################################
while [[ $# -gt 0 ]] ; do
	case $1 in
		-c|--color) colors ;;
		--debug) DEBUG=y ;;
		--Debug) colors; DEBUG=y; VERBOSE=y ;;
		--strict|--strict-debug) set -euo pipefail ;;
		-e|--extension) DEFAULT_FILEEXTS+=" $2"; shift ;;
		-l|--list) LIST=y ;;
		-q|--quiet) QUIET=y ;;
		--test) TEST=y; VERBOSE=y ;;
		--trace) set -x ;;
		-v|--verbose) VERBOSE=y ;;
		-V|--version) version ;;
		-w|--wordlist) WORDLIST=y ;;
		-W|--distribute-wordlist) DISTRIBUTE=y ;;
		-x|--dont-backup) BACKUP="--dont-backup"; DELETE=y ;;
		-h|--help) usage ;;
		--|*) break ;;
	esac
	shift
done

#################################
# Build starting list of top-level tex files
COURSE=${COURSE:-$(basename "$PWD")}
FILES=$(get_files "$@")
if [[ -z $FILES ]] ; then
	if [[ -f "index.tex" ]] ; then
		FILES="index.tex"
	else
		FILES="${1:-$COURSE.tex}"
		ARCH="$(read_tex "$FILES" arch)"
		BOARD="$(read_tex "$FILES" board)"
	fi
fi

if [[ -n $WORDLIST ]] ; then
	[[ -z $VERBOSE ]] || echo -e "Directories:\n$DIRS\nWordlists:"
fi

#################################
for FILE in $FILES ; do
	if [[ ! -e $FILE ]] ; then
		[[ $FILE =~ '*' ]] || warn "File(s) not found: $FILE"
		continue
	fi

	DIR=$(dirname "$FILE")
	FILE=$(basename "$FILE")
	read_config "$DIR"
	pushd "$DIR" >/dev/null

	if [[ -n $LIST ]] ; then
		list_texfiles "$FILE"
	elif [[ -n $WORDLIST ]] ; then
		build_word_lists "$(pwd)" $(get_tex_dirs "$FILE") 
	else
		CHECK=y
		do_spell "$FILE"
	fi

	popd >/dev/null
done

#################################
if [[ -n $CHECK && -n $DISTRIBUTE ]] ; then
	DIRS=$(get_tex_dirs "$FILE") 
	[[ -z $VERBOSE ]] || echo -e "Directories:\n$DIRS"
	# shellcheck disable=SC2086
	distribute_word_lists $(dirname $(realpath "$FILE"))"/$WORDFILE" $DIRS
fi
