#!/bin/sh

PROGRAMV="1.1"
DICT="/usr/share/dict/words"
PDICT="$HOME/.shpdict"

check_words() {
  WORDS=$1
  DICTFILE=$2
  # Find words matching dict entries:
  MATCHES=$(echo "$WORDS" | grep -xif - "$DICTFILE")
  # Get mismatches by filtering out matches from words:
  echo "$WORDS" | grep -vix "$MATCHES"
}

strip_html() {
  # Rule number one: Never parse HTML with regular expressions!
  # Tags (<xxx>):
  sed "s/</\n</g" |\
  sed "s/>/>\n/g" |\
  grep -v "^<.*>$" |\
  # Entities (&xxx;):
  sed "s/&/\n&/g" |\
  sed "s/;/;\n/g" |\
  grep -v "^&[a-zA-Z]*;$"
}

split_words() {
  # Punctuation marks to remove. Note: includes Unicode chars.
  PUNCTS="\!\"\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~\—"

  # Remove carriage returns:
  tr -d "\r" |\
  # Convert tabs:
  tr "\t" " " |\
  # Simplify apostrophes (Unicode):
  sed "s/’/'/g" |\
  # All words on separate lines:
  sed "s/ /\n/g" |\
  # Remove numbers:
  tr -d "[:digit:]" |\
  # Split hyphenated words:
  tr "-" "\n" |\
  # Remove punctuation marks except apostrophe and hyphen:
  tr "$PUNCTS" "\n" |\
  # Remove leading puncts including apostrophe and hyphen:
  sed "s/^[[:punct:]]//g" |\
  # Remove trailing puncts including apostrophe and hyphen:
  sed "s/[[:punct:]]$//g"|\
  # Convert to lowercase:
  tr "[:upper:]" "[:lower:]" |\
  # Remove empty lines (fix this!):
  grep -xv "" |\
  sort |\
  uniq
}

spellcheck() {
  WORDS=$(split_words)

  # Check against global dictionary:
  MISMATCH=$(check_words "$WORDS" "$DICT")

  # Check against personal dict if applicable:
  if [ -f "$PDICT" ]; then
    MISMATCH=$(check_words "$MISMATCH" "$PDICT")
  fi

  # Print mismatched words:
  echo "$MISMATCH" | grep -vix ""
}

spellcheck_html() {
  strip_html | spellcheck
}

ensure_personal() {
  if [ ! -f "$PDICT" ]; then
    echo "Personal dictionary not found. Use --add to create it." 1>&2
    exit 1
  fi
}

read_config() {
  if [ -f "$HOME/.shpellrc" ]; then
    UCFG_DICT=$(grep -vx "" "$HOME/.shpellrc" | sed "s/^ //" | sed "s/ *$//")
  fi
  if [ -f "$UCFG_DICT" ]; then
    DICT="$UCFG_DICT"
  fi
}

ensure_arg() {
  if [ -z "$1" ]; then
    echo "No argument supplied" 1>&2
    exit 1
  fi
}

list_personal() {
  ensure_personal
  cat "$PDICT"
}

add_personal() {
  ensure_arg "$1"
  echo "$1" >> "$PDICT"
  SORTED=$(sort "$PDICT" | uniq)
  echo "$SORTED" > "$PDICT"
}

del_personal() {
  ensure_personal
  ensure_arg "$1"
  PWORDS=$(cat "$PDICT")
  echo "$PWORDS" | grep -v "$1" | sort | uniq > "$PDICT"
}

purge_personal() {
  ensure_personal
  rm "$PDICT"
}

show_license() {
  if [ ! -z "$1" ]; then
    echo "shpell $PROGRAMV (c) 2020 Carl Svensson."
  fi
  cat << END_LIC_TEXT
Usage of the works is permitted provided that this instrument is
retained with the works, so that any entity that uses the works is
notified of this instrument.
DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY.
END_LIC_TEXT
}

show_long_help() {
  cat << END_DOC_TEXT
shpell $PROGRAMV (c) 2020 Carl Svensson.
A spellchecker written in POSIX sh.

 Usage
=======
Start shpell from the command line:
  shpell [OPTION] [ARGUMENT]

When called without options, shpell will spellcheck STDIN:
  shpell < FILE
  cat FILE | shpell

Misspelled words will be printed, one per line, to STDOUT.

Options:
  -h
    Show short help.
  --help
    Show this extensive help and program information.
  -c, --check [FILE]
    Spellcheck FILE if given, otherwise STDIN. Example:
      shpell -c FILE
      cat FILE | shpell --check
  -m, --html [FILE]
    Like --check, but try to strip away HTML markup from input.
      shpell -m FILE
      cat FILE | shpell --html
  -l, --list
    List contents of personal dictionary.
  -a, --add WORD
    Add WORD to personal dictionary.
  -d, --del WORD
    Delete WORD from personal dictionary.
  --purge
    Purge entire personal dictionary.
  --license
    Print software license.

 Personal Dictionary
=====================
The personal dictionary is for adding words on a per-user basis.
Words added to the personal dictionary will be written to a file
called ".shpdict" in the user's home directory.

 Global Dictionary
===================
Since most Unix-like systems comes with at least one dictionary file,
shpell relies on this file for the bulk of its operations. The global
dictionary can also be configured on a per-user basis.

The currently configured path to the global dictionary file is:
  $DICT

A file called ".shpellrc" can be created in a user's home directory.
It should contain just a path to a custom global dictionary, which
will be used in place of the default. Example:
  echo "/usr/share/dict/british-english" > "\$HOME/.shpellrc"

 Known issues
==============
 * Tested with English dictionaries only.
 * HTML tag stripping is rudimentary at best.
 * Files with a lot of UTF-8 punctuation marks will likely cause
   problems.

 License
=========
END_DOC_TEXT
show_license
}

show_short_help() {
  cat << END_HLP_TEXT
shpell $PROGRAMV - A spellchecker written in POSIX sh.
Usage: shpell [OPTION] [ARGUMENT]
When called without options, shpell will spellcheck STDIN.
Misspelled words will be printed, one per line, to STDOUT.
Options:
  -h                   Show this help.
  --help               Show extensive help and program information.
  -c, --check [FILE]   Spellcheck FILE if given, otherwise STDIN
  -m, --html [FILE]    Like --check, but try to strip away HTML
                       markup from input.
  -l, --list           List contents of personal dictionary.
  -a, --add WORD       Add WORD to personal dictionary.
  -d, --del WORD       Delete WORD from personal dictionary.
  --purge              Purge entire personal dictionary.
  --license            Print software license.
END_HLP_TEXT
}

handle_args() {
  case $1 in
    "-c"|"--check")
      if [ -z "$2" ]; then
        spellcheck
      else
        spellcheck < "$2"
      fi
    ;;
    "-m"|"--html")
      if [ -z "$2" ]; then
        spellcheck_html
      else
        spellcheck_html < "$2"
      fi
    ;;
    "-l"|"--list")
      list_personal
    ;;
    "-a"|"--add")
      add_personal "$2"
    ;;
    "-d"|"--del")
      del_personal "$2"
    ;;
    "--purge")
      purge_personal
    ;;
    "-h")
      show_short_help
    ;;
    "--help")
      show_long_help
    ;;
    "--license")
      show_license 1
    ;;
    *)
      echo "Unknown option, use -h for help" 1>&2
      exit 1
    ;;
  esac
}

# Initialize and handle input:

read_config

if [ -z "$1" ]; then
  spellcheck
else
  handle_args "$1" "$2"
fi
