#!/usr/bin/env bash
# vim: ft=bash tw=80 foldmethod=marker foldclose=all
#
# Ground-up rewrite of previous `conf`. Think I can make cleaner this time
# around.
#
# shellcheck disable=SC2178 #< gets confused with namerefs.
#-------------------------------------------------------------------------------
set -e

# Should always be, be just in case bash version <4.
if (( BASH_VERSINFO[0] < 4 )) ; then
   printf '(Bash version must be >=4)\n'  >&2
   exit 1
fi

declare -gr DATA_FILE="${XDG_DATA_HOME}/carlinigraphy/conf/data"
declare -gr DATA_DIR="${DATA_FILE%/*}"

mkdir -p "$DATA_DIR"
touch "$DATA_FILE"

# Database represented in bash as an array of 'entry' objects.
# DB := {
#     $nick1 := {path1, path2... pathN},
#     $nick2 := {path1, path2... pathN},
# }
declare -gA DB=()

# Maintain user's order.
declare -ga DB_ORDER=()

# Holds pointer to current entry array.
declare -g  ENTRY
declare -gi _ENTRY_NUM=0


function usage {
   local outfile
   case "$1" in
      0)  outfile=/dev/stdout ;;
      '') outfile=/dev/stdout ;;
      *)  outfile=/dev/stderr ;;
   esac

   cat <<EOF >"$outfile"
usage: ${BASH_SOURCE[0]##*/} command NICKNAME [param1..paramN]

commands:
   cd          \`cd\` to NICKNAME's in a subshell
   clean       \`rm\` all but last 5 database backups
   db          edit conf's db
   edit ARGS   edit NICKNAME w/ optional args
   help CMD    prints help text for CMD
   list        lists all nicknames in db
   set  PATH+  associates PATH(s) with a given NICKNAME
   sort        \`sort\` conf's db

EOF
exit "$1"
}

#                              database nonsense
#-------------------------------------------------------------------------------
function new_entry {
   local lineno="$1"  nick="$2"

   local entry="_ENTRY_$(( ++_ENTRY_NUM ))"
   declare -gA "$entry"

   local items="_ENTRY_ITEMS_${_ENTRY_NUM}"
   declare -ga "$items"

   local -n entry_r="$entry"
   entry_r['items']="$items"

   # *New* entries must be added to the DB order. Existing entries to be updated
   # must not be added twice.
   if [[ $lineno && $nick ]] ; then
      DB["$nick"]="$entry"
      DB_ORDER+=( "$nick" )
      entry_r['lineno']="$lineno"
   fi

   declare -g ENTRY="$entry"
}


function db_store {
   local -i n
   local target
   local base="${DATA_DIR}"/backup

   ## Hmm, maybe turn this into a slightly more "clever" one-liner? Can't use
   ## the --backup flag, as it's not included w/ the BSD `mv` or `install`
   ## commands.
   #while ! mv -n "$DATA_FILE" "${base}.$(( ++n ))" ; do :; done

   while
      target="${base}.~$(( ++n ))~" &&
      [[ -f "$target" ]]
   do :; done

   mv "$DATA_FILE" "$target"

   { for nick in "${DB_ORDER[@]}" ; do
      local -n entry_r="${DB[$nick]}"
      local -n items_r="${entry_r['items']}"
      printf '%s\n'     "$nick"
      printf '   %s\n'  "${items_r[@]}"
      printf '\n'
   done } > "$DATA_FILE"
}


function db_load {
   local -i lineno=0

   while IFS=$'\n' read -r line ; do
      (( ++lineno ))

      is_whitespace "$line" && continue
      is_comment    "$line" && continue

      if is_nick "$line" ; then
         new_entry "$lineno" "${BASH_REMATCH[1]}"
         continue
      fi

      if ! local -n entry_r="$ENTRY" 2>/dev/null ; then
         printf '(Unexpected indent on line %d.)\n'  "$lineno" >&2
         exit 1
      fi

      local -n items_r="${entry_r['items']}"
      items_r+=( "${line##* }" )
   done < "$DATA_FILE"
}

function is_whitespace { [[ $1 =~ ^[[:space:]]*$   ]] ;}
function is_comment    { [[ $1 =~ ^[[:space:]]*#   ]] ;}
function is_nick       { [[ $1 =~ ^([^[:space:]]+) ]] ;}


function db_validate {
   local -i errors=0
   local -a unfound=()

   local nick
   for nick in "${!DB[@]}" ; do
      local -n entry_r="${DB[$nick]}"
      local -n items_r="${entry_r['items']}"

      local -a files
      mapfile -t files < <(
         xargs -I{} bash -c "printf '%s\n' {}" <<< "${items_r[@]}"
      )

      if [[ ! $files ]]  ; then      
         printf  'Nickname [%s] has no associated paths.'  "${nick}"
         (( ++errors ))
      fi

      local f
      for f in "${files[@]}" ; do
         if [[ ! -f ${f} ]] &&
            [[ ! -r ${f} ]] &&
            [[ ! -L ${f} ]]
         then
            unfound+=( "$f" )
            (( ++errors ))
         fi
      done
   done

   if (( ${#unfound[@]} )) ; then
      { printf '(Does not exist, or is not file:\n'
        printf "  '%s'\n"  "${unfound[@]}"
        printf ')\n'
      } >&2
   fi

   if (( errors )) ; then
      exit 1
   fi
}

#                              command :: list
+-- 29 lines: ----------------------------------------------------------------------------

#                              command :: cd
+-- 57 lines: ----------------------------------------------------------------------------

#                              command :: set
+-- 44 lines: ----------------------------------------------------------------------------

#                              command :: edit
+-- 48 lines: ----------------------------------------------------------------------------

#                              command :: sort
+-- 24 lines: ----------------------------------------------------------------------------

#                              command :: clean
+-- 37 lines: ----------------------------------------------------------------------------

#                              command :: db
+-- 24 lines: ----------------------------------------------------------------------------

#                              command :: help
+-- 21 lines: ----------------------------------------------------------------------------

#                                    engage
#-------------------------------------------------------------------------------
if (( ! $# )) ; then
   usage 1
fi

declare -ga  possible=()
declare -grA opts=(
   ['--help']=1
   ['help']=1
   ['edit']=1
   ['set']=1
   ['cd']=1
   ['sort']=1
   ['clean']=1
   ['list']=1
   ['fmt']=1
   ['db']=1
   ['.']=1
   [',']=1
)

declare cmd="$1"

# Only necessary if $1 is not an exact match for an option. Uses `compgen` to
# expand to a command. E.g., `conf c` -> `conf clean`.
if [[ ! ${opts[$cmd]} ]] ; then
   mapfile -t possible < <(
      compgen -W "${!opts[*]}" -- "$1"
   )

   if (( ${#possible[@]} > 1 )) ; then
      { printf "(Ambiguous command \`%s' can be...\n"  "$1"
        printf ' %s\n'  "${possible[@]}"
        printf ')\n'
      } >&2
      exit 1
   fi

   # shellcheck disable=SC2128
   cmd="${possible}"
fi

case "$cmd" in
   -h | --help)
            usage 0              ;;
   'help')  cmd_help   "${@:2}"  ;;
   'set')   cmd_set    "${@:2}"  ;;
   'cd')    cmd_cd     "${@:2}"  ;;
   'sort')  cmd_sort   "${@:2}"  ;;
   'clean') cmd_clean  "${@:2}"  ;;
   'db')    cmd_db     "${@:2}"  ;;
   'list')  cmd_list   "${@:2}"  ;;
   'edit')  cmd_edit   "${@:2}"  ;;

   # 'Shortcuts'. Faster to type.
   '.')     cmd_edit   "${@:2}"  ;;
   ',')     cmd_cd     "${@:2}"  ;;

   # Try defaulting to `edit' mode, maybe it's a valid $nick.
   *)       db_load
            if [[ ! ${DB[$1]} ]] ; then
               usage 1
            else
               cmd_edit "$@"
            fi
            ;;
esac