#!/bin/bash
# Version: 0.2.0
# Copyright (c) 2017-2026:
#   Darren 'Tadgy' Austin <darren (at) afterdark.org.uk>
# Licensed under the terms of the GNU General Public License version 3.
#
# Automatically or interractively merge tagfiles from two directories into a new set.
#

# The (space separated) list of package sets.
PACKAGE_SETS="${PACKAGE_SETS:-a ap d e f k kde kdei l n t tcl x xap xfce y}"

# nullglob is required.
shopt -s nullglob

# Functions.
display_help() {
  #     |--------1---------2---------3---------4---------5---------6---------7---------8
  echo "Usage: ${0##*/} [options] <tagfiles dir1> <tagfiles dir2> <dest dir>"
  echo "Automatically or interactively merge tagfiles from <tagfiles dir1> and"
  echo "<tagfiles dir2>, writing the resultant tagfile set to <dest dir>."
  echo
  echo "Caveats:"
  echo "  * Priorities from <tagfiles dir2> sets take presidence over <tagfiles dir1>."
  echo "  * Only the packages listed in the <tagfiles dir1> sets are merged with the"
  echo "    priorities from the <tagfiles dir2> sets - packages in the <tagfiles dir2>"
  echo "    sets not in the <tagfiles dir1> set are removed in the <dest dir> sets."
  echo "  * Package descriptions are read from .txt files in either <tagfiles dir1> or"
  echo "    <tagfiles dir2>, whichever directory contains the file first."
#  echo "  * Only comments from <tagfiles dir2> sets are merged into <dest dir> sets."
  echo
  echo "Options:"
  echo "  -a, --default-add  Do not interactively prompt whether to ADD, REC, SKP or OPT"
  echo "                     any new packages - automatically use this default."
  echo "  -r, --default-rec  See above."
  echo "  -s, --default-skp  See above."
  echo "  -o, --default-opt  See above."
  echo "  -n, --no-recopt    Only allow the ADD or SKP priorities.  In interactive mode"
  echo "                     the replacement priority is prompted for, otherwise the"
  echo "                     options --default-add or --default-skp determine priority."
  echo "  -N, --no-desc      Do not show the package description in interactive mode."
  echo "  -V, --version      Display version and copyright information."
  echo "  -h, --help         Display this help, obviously."
  echo "      --             Force the end of option processing.  Any options following"
  echo "                     this are treated as arguments."
  echo "Option processing ceases with the first non-option or --."
  echo
  echo "Arguments (all of which are mandatory):"
  echo "  <tagfiles dir1>    The top-level directory of the first set of tagfiles."
  echo "  <tagfiles dir2>    The top-level directory of the second set of tagfiles."
  echo "  <dest dir>         Path to the directory in which to write the new tagfiles."
  return 0
}

display_version() {
  #     |--------1---------2---------3---------4---------5---------6---------7---------8
  echo "${0##*/} v0.2.0"
  echo "Copyright (c) 2017-2026 Darren 'Tadgy' Austin <darren (at) afterdark.org.uk>"
  echo "Licensed under the terms of the GNU GPL v3 <http://gnu.org/licenses/gpl.html>."
  echo "This program is free software: you can modify or redistribute it in accordence"
  echo "with the GNU GPL.  However, it comes with ABSOLUTELY NO WARRANTY; not even the"
  echo "implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE."
  return 0
}

check_directory() {
  # Arguments:		$1 = Directory to check.
  # Returns:		0 = OK.
  #			1 = Doesn't exist.
  #			2 = Not a directory.
  #			3 = No read permission.
  #			4 = No write permission.
  if [[ ! -e "$1" ]]; then
    echo "${0##*/}: no such directory: $1" >&2
    return 1
  elif [[ ! -d "$1" ]]; then
    echo "${0##*/}: not a directory: $1" >&2
    return 2
  elif [[ ! -r "$1" ]]; then
    echo "${0##*/}: read permission denied: $1" >&2
    return 3
  elif [[ ! -w "$1" ]]; then
    echo "${0##*/}: write permission denied: $1" >&2
    return 4
  fi
  return 0
}

write_line() {
  # Arguments:		$1 = The package name.
  #			$2 = The packages priority.
  #			$3 = The packages comment.
  #			$4 = File to write to.

  if [[ ! -z "$3" ]]; then
    printf "%s:%s\\t\\t%s\\n" "$1" "$2" "$3" >>"$4" || exit 1
  else
    printf "%s:%s\\n" "$1" "$2" >>"$4" || exit 1
  fi
}

prompt_priority() {
  # Arguments:		$1 = Package name being processed.
  #			$2 = Previous priority of package being processed.
  #			$3 = Any comment from the tagfile for the package.
  # Globsl variables:	SET = The tagfile set being processed.
  #			TAGFILES_DIR1 = The first tagfiles directory given on the command line.
  #			TAGFILES_DIR2 = The second tagfiles directory given on the command line.
  # Returns:		The new priority for the package.

  local ANSWER DESC
  local -a DESCFILE

  FLAG_MANUAL_INPUT=1

  echo
  echo
  echo "Package: $SET/$1:"
  [[ ! -v FLAG_NO_DESC ]] && {
    DESCFILE=( "$TAGFILES_DIR1/$SET/$1"-*-*-*.txt "$TAGFILES_DIR2/$SET/$1"-*-*-*.txt )
    DESC="$(grep "^$1:" "${DESCFILE[0]}" 2>/dev/null | sed -re 's/^.*:[[:blank:]]*/    /g; s/^[[:blank:]]*$/    ./g')"
    if [[ -n "$DESC" ]]; then
      echo "  Description from ${DESCFILE[0]}:"
      echo "$DESC"
    else
      echo "  Description not found."
    fi
  }
  echo "  Current priority: ${2:-(none)}"
  echo "  Comment: ${3:-(none)}"

  while :; do
    echo
    if [[ -v FLAG_NO_RECOPT ]]; then
      read -r -n 1 -p "  Select new priority for package: (A)DD, or (S)KP: " ANSWER
    else
      read -r -n 1 -p "  Select new priority for package: (A)DD, (R)EC, (S)KP, or (O)PT: " ANSWER
    fi

    if [[ "${ANSWER^^}" = "A" ]]; then
      NEWPRI="ADD"
      break
    elif [[ ! -v FLAG_NO_RECOPT ]] && [[ "${ANSWER^^}" = "R" ]]; then
      NEWPRI="REC"
      break
    elif [[ "${ANSWER^^}" = "S" ]]; then
      NEWPRI="SKP"
      break
    elif [[ ! -v FLAG_NO_RECOPT ]] && [[ "${ANSWER^^}" = "O" ]]; then
      NEWPRI="OPT"
      break
    else
      continue
    fi
  done
}


process_tagfile() {
  # Arguments:		$1 = The tagfiles path to process.
  #
  # Globsl variables:	DEST_DIR = The destination directory to write the new tagfiles.
  #			SET = The tagfile set being processed.

  local COMMENT NEWPRI PACKAGE PRIORITY

  # Clear the destination tagfile.
  : >"$DEST_DIR/$SET/tagfile"

  # Open the input tagfile for reading.
  # Note: We do this with an fd so we can use another 'read' of stdin for prompting.
  exec {FD}<"$1/$SET/tagfile"

  # Process the tagfile line by line.
  while IFS=$'\t :' read -r -u "$FD" PACKAGE PRIORITY COMMENT; do
    if [[ -v FLAG_NO_RECOPT ]] && [[ ! "${PRIORITY^^}" =~ ^(ADD|SKP)$ ]]; then
      # Can't use REC or OPT - Get for new priority.
      if [[ -v DEFAULT_PRIORITY ]]; then
        NEWPRI="$DEFAULT_PRIORITY"
      else
        prompt_priority "$PACKAGE" "${PRIORITY^^}" "$COMMENT"				# Sets the NEWPRI variable
      fi
      [[ -z "$NEWPRI" ]] && {
        echo "Abort: failed to get new priority" >&2
        exit 1
      }
      write_line "$PACKAGE" "$NEWPRI" "$COMMENT" "$DEST_DIR/$SET/tagfile"
    elif [[ "${PRIORITY^^}" =~ ^(ADD|SKP)$ ]]; then
      # ADD or SKP in tagfile - write it out.
      write_line "$PACKAGE" "${PRIORITY^^}" "$COMMENT" "$DEST_DIR/$SET/tagfile"
    else
      echo "** Invalid priority ($PRIORITY) for $PACKAGE in tagfile:"
      echo "**   $1/$2/tagfile"
      # Invalid priority in tagfile - try to fix.
      if [[ -v DEFAULT_PRIORITY ]]; then
        echo "** Correcting using default priority: $DEFAULT_PRIORITY"
        NEWPRI="$DEFAULT_PRIORITY"
      else
        echo "** Prompting for new valid priority..."
        prompt_priority "$PACKAGE" "${PRIORITY^^}" "$COMMENT"				# Sets the NEWPRI variabl
      fi
      [[ -z "$NEWPRI" ]] && {
        echo "Abort: failed to get new priority" >&2
        exit 1
      }
      write_line "$PACKAGE" "$NEWPRI" "$COMMENT" "$DEST_DIR/$SET/tagfile"
    fi
  done

  # Close tagfile file descriptor.
  exec {FD}<&-
}

merge_tagfiles() {
  # Globsl variables:	DEST_DIR = The destination directory to write the new tagfiles.
  #			SET = The tagfile set being processed.
  #			TAGFILES_DIR1 = The first tagfiles directory given on the command line.
  #			TAGFILES_DIR2 = The second tagfiles directory given on the command line.

  local COMMENT1 COMMENT2 PACKAGE NEWPRI PRIORITY1 PRIORITY2

  # Open the first tagfile for reading.
  # Note: We do this with an fd so we can use another 'read' of stdin for prompting.
  exec {FD}<"$TAGFILES_DIR1/$SET/tagfile"

  while IFS=$'\t :' read -r -u "$FD" PACKAGE PRIORITY1 COMMENT1; do
    PRIORITY2="$(grep -E "^${PACKAGE//+/\\+}:" "$TAGFILES_DIR2/$SET/tagfile" | sed -re "/^${PACKAGE//+/\\\+}:/ s/[^[:blank:]:]+:[[:blank:]]*([^[:blank:]]+)(\$|[[:blank:]].*\$)/\\1/")"
    COMMENT2="$(grep -E "^${PACKAGE//+/\\+}:" "$TAGFILES_DIR2/$SET/tagfile" | sed -re "/^${PACKAGE//+/\\\+}:/ s/[^[:blank:]:]+:[[:blank:]]*([^[:blank:]]+)(\$|[[:blank:]](.*)\$)/\\3/")"
    if [[ -z "$PRIORITY2" ]]; then
      # Package wasn't in tagfile2.
      if [[ ! -z "$DEFAULT_PRIORITY" ]]; then
        # Use default priority.
        write_line "$PACKAGE" "$DEFAULT_PRIORITY" "$COMMENT1" "$DEST_DIR/$SET/tagfile"
      else
        # Prompt for new priority.
        prompt_priority "$PACKAGE" "$PRIORITY1" "$COMMENT1"				# Sets the NEWPRI variabl
        [[ -z "$NEWPRI" ]] && {
          echo "Abort: failed to get new priority" >&2
          exit 1
        }
        write_line "$PACKAGE" "$NEWPRI" "$COMMENT1" "$DEST_DIR/$SET/tagfile"
      fi
    elif [[ ! "${PRIORITY2^^}" =~ ^(ADD|REC|SKP|OPT) ]]; then
      # Package was listed in tagfile2, but priority is invalid.
      if [[ ! -z "$DEFAULT_PRIORITY" ]]; then
        # Use default priority.
        write_line "$PACKAGE" "$DEFAULT_PRIORITY" "${COMMENT2:-$COMMENT1}" "$DEST_DIR/$SET/tagfile"
      else
        # Prompt for new priority.
        prompt_priority "$PACKAGE" "$PRIORITY2" "${COMMENT2:-$COMMENT1}"		# Sets the NEWPRI variabl
        [[ -z "$NEWPRI" ]] && {
          echo "Abort: failed to get new priority" >&2
          exit 1
        }
        write_line "$PACKAGE" "$NEWPRI" "${COMMENT2:-$COMMENT1}" "$DEST_DIR/$SET/tagfile"
      fi
    else
      # Package was listed in tagfile2 and priority was valid.
      write_line "$PACKAGE" "${PRIORITY2^^}" "${COMMENT2:-$COMMENT1}" "$DEST_DIR/$SET/tagfile"
    fi
  done
}

# Process command line options.
while :; do
  case "$1" in
    '-a'|'-default-add'|'--default-add')
      if [[ "$DEFAULT_PRIORITY" =~ ^(REC|SKP|OPT)$ ]]; then
        echo "${0##*/}: multiple --default-* options specified" >&2
        exit 1
      else
        DEFAULT_PRIORITY="ADD"
      fi
      shift
      ;;
    '-r'|'-default-rec'|'--default-rec')
      [[ -v FLAG_NO_RECOPT ]] && {
        echo "${0##*/}: cannot use --no-recopt with --default-rec" >&2
        exit 1
      }
      if [[ "$DEFAULT_PRIORITY" =~ ^(ADD|SKP|OPT)$ ]]; then
        echo "${0##*/}: multiple --default-* options specified" >&2
        exit 1
      else
        DEFAULT_PRIORITY="REC"
      fi
      shift
      ;;
    '-s'|'-default-skp'|'--default-skp')
      if [[ "$DEFAULT_PRIORITY" =~ ^(ADD|REC|OPT)$ ]]; then
        echo "${0##*/}: multiple --default-* options specified" >&2
        exit 1
      else
        DEFAULT_PRIORITY="SKP"
      fi
      shift
      ;;
    '-o'|'-default-opt'|'--default-opt')
      [[ -v FLAG_NO_RECOPT ]] && {
        echo "${0##*/}: cannot use --no-recopt with --default-opt" >&2
        exit 1
      }
      if [[ "$DEFAULT_PRIORITY" =~ ^(ADD|REC|SKP)$ ]]; then
        echo "${0##*/}: multiple --default-* options specified" >&2
        exit 1
      else
        DEFAULT_PRIORITY="OPT"
      fi
      shift
      ;;
    '-n'|'-no-recopt'|'--no-recopt')
      if [[ "$DEFAULT_PRIORITY" = "REC" ]]; then
        echo "${0##*/}: cannot use --no-recopt with --default-rec" >&2
        exit 1
      elif [[ "$DEFAULT_PRIORITY" = "OPT" ]]; then
        echo "${0##*/}: cannot use --no-recopt with --default-opt" >&2
        exit 1
      else
        FLAG_NO_RECOPT=1
      fi
      shift
      ;;
    '-N'|'-no-desc'|'--no-desc')
      FLAG_NO_DESC=1
      shift
      ;;
    '-V'|'-version'|'--version')
      display_version
      exit 0
      ;;
    '-h'|'-help'|'--help')
      display_help
      exit 0
      ;;
    '--')
      shift
      break
      ;;
    -*)
      # This prevents the first non-option argument being an entry in the
      # current directory which begins with a '-'.  Use the '--' option
      # or ./-foo where this becomes a problem.
      echo "${0##*/}: invalid option: $1" >&2
      echo "Try: ${0##*/} --help" >&2
      exit 1
      ;;
    *)
      break
      ;;
  esac
done

# Process command line arguments.
(( $# != 3 )) && {
  echo "${0##*/}: incorrect number of non-option arguments" >&2
  echo "Try: ${0##*/} --help" >&2
  exit 1
}
for ARG in "$@"; do
  ERROR_TEXT="$(check_directory "$ARG" 2>&1)"
  ERROR_CODE=$?
  if [[ -z "$TAGFILES_DIR1" || -z "$TAGFILES_DIR2" ]]; then
    # <tagfiles dir1> and <tagfiles dir2>
    if (( ERROR_CODE >= 1 && ERROR_CODE <= 3 )); then		# Ignore write permission failure.
      echo "$ERROR_TEXT" >&2
      exit 1
    else
      TAGFILES_DIR2="${TAGFILES_DIR1:+$ARG}"
      TAGFILES_DIR1="${TAGFILES_DIR1:-$ARG}"			# Yes, these are correct and in the right order!
    fi
  else
    # <dest dir>
    if (( ERROR_CODE >= 2 )); then				# Non-existant (ERROR_CODE=1) is OK here.
      echo "$ERROR_TEXT" >&2
      exit 1
    else
      DEST_DIR="$ARG"
    fi
  fi
done
[[ -z "$TAGFILES_DIR1" || -z "$TAGFILES_DIR2" || -z "$DEST_DIR" ]] && {
  echo "Abort: argument processing failure" >&2
  exit 1
}

# Create destination directory.
if [[ -e "$DEST_DIR" ]]; then
  echo "${0##*/}: directory exists: $DEST_DIR" >&2
  exit 1
else
  # shellcheck disable=SC2174
  mkdir -p -m 755 "$DEST_DIR" || exit 1
fi

# Main loop.
FLAG_MANUAL_INPUT=0
for SET in $PACKAGE_SETS; do
  mkdir "$DEST_DIR/$SET" || exit 1
  if [[ -e "$TAGFILES_DIR1/$SET/tagfile" ]]; then
    # Found tagfile in dir1.
    if [[ -e "$TAGFILES_DIR2/$SET/tagfile" ]]; then
      # Found tagfiles in both dirs - merge the tagfiles into dest dir.
      merge_tagfiles
    else
      # Process tagfile in dir1 and write to dest dir.
      process_tagfile "$TAGFILES_DIR1" || exit 1
    fi
  else
    # No tagfile in dir1 - check dir2.
    if [[ -e "$TAGFILES_DIR2/$SET/tagfile" ]]; then
      # Found tagfile in dir2 - process it and write to dest dir.
      process_tagfile "$TAGFILES_DIR2" || exit 1
    else
      # No tagfile in either dir - create a stub file.
      touch "$DEST_DIR/$SET/tagfile" || exit 1
    fi
  fi
done

(( FLAG_MANUAL_INPUT == 1 )) && echo
