#!/usr/bin/env bash
#
# NAME
#   redoflacs - BASH commandline FLAC compressor, verifier, organizer,
#               analyzer, and retagger
#
# FILE INFORMATION
#   Homepage:      https://github.com/sirjaren/redoflacs
#   Dependencies:  BASH >= 4.0
#
# DISTRIBUTION
#   The MIT License
#
#   Copyright 2025 Project Authors
#
#   Permission is hereby granted, free of charge, to any person
#   obtaining a copy of this software and associated documentation files
#   (the "Software"), to deal in the Software without restriction,
#   including without limitation the rights to use, copy, modify, merge,
#   publish, distribute, sublicense, and/or sell copies of the Software,
#   and to permit persons to whom the Software is furnished to do so,
#   subject to the following conditions:
#
#   The above copyright notice and this permission notice shall be
#   included in all copies or substantial portions of the Software.
#
#   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
#   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
#   BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
#   ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
#   CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#   SOFTWARE.
#

#############################  FUNCTIONS  ##############################

#
# Convenience functions to provide messages to the user with colored
# prefixes to indicate message severity, eg:
#   _warn 'This is obsolete'
#
# Creates a messsage like (with a yellow asterisk):
#    * This is obsolete
#
_info() { printf     ' %s %b\n' "$green*$reset" "$@";  }  # Green
_warn() { printf >&2 ' %s %b\n' "$yellow*$reset" "$@"; }  # Yellow
_error(){ printf >&2 ' %s %b\n' "$red*$reset" "$@";    }  # Red

#
# Display help
#
_help()
{
  cat << HELP_EOF
Usage: redoflacs [operations] [options] [target]

Operations:
  -c       Compress FLAC files
  -C       Compress FLAC files (even if COMPRESSION tag already exists)
  -t       Test integrity of FLAC files
  -m       Check for a valid MD5 signature in FLAC files' STREAMINFO block
  -a       Use auCDtect to authenticate FLAC files
  -A       Use auCDtect to authenticate FLAC files (create spectrogram)
  -l       Use LAC to authenticate FLAC files
  -L       Use LAC to authenticate FLAC files (create spectrogram)
  -e       Extract all embedded artwork from FLAC files
  -p       Remove metadata blocks from FLAC files (PADDING, PICTURE, etc)
  -g       Apply ReplayGain to FLAC files
  -G       Apply ReplayGain to FLAC files (even if ReplayGain tags exist)
  -r       Retag FLAC files preserving user-defined tags

Options:
  -j[N]    Allow N jobs at once (default: all cores, or 2 if unknown)
  -J[N]    Allow N jobs for compression >= FLAC 1.5.0 (default: cores/threads)
  -T[N]    Allow N threads for compression >= FLAC 1.5.0 (default: 2)
  -n       Disable color output
  -x       Do not apply COMPRESSION tag when compressing FLAC files
  -o       Generate a new configuration file and exit
  -v       Print the version number of redoflacs and exit
  -h       Print this message and exit

See redoflacs(1) for more information.
HELP_EOF
}

#
# Display usage
#
_usage()
{
  printf >&2 ' Usage: redoflacs [operations] [options] [target] ...\n'
}

#
# Display a message that there may have been some issues that is worth
# investigating for a given operation and where the log file to review
# said issues can be found
#
_display_issues_exist()
{
  # Operation-specific messaging
  case "$operation" in
    '_md5check')
      _error 'The MD5 Signature is unset for some FLAC files or there were'
      _error 'issues with some of the FLAC files, please check:'
      ;;
    '_test'|'_compress'|'_extract_images'|'_prune')
      _error 'There were issues with some of the FLAC files, please check:'
      ;;
    '_aucdtect')
      _error 'Some FLAC files may be lossy sourced, please check:'
      ;;
    '_replaygain')
      _error 'There were issues adding ReplayGain values, please check:'
      ;;
    '_retag')
      _error 'Some FLAC files have missing tags or there were issues with some'
      _error 'of the FLAC files, please check:'
      ;;
  esac

  _error "${cyan}${log_file}$reset"
  _error 'for details.'
}

#
# Display a banner before script execution which shows all options
# configured as well as runtime information
#
# The structure of the banner:
#
#   --------------------------------------------------------------------
#                           Runtime Information
#   --------------------------------------------------------------------
#                           redoflacs 1.0
#                                FLAC 1.5.0
#                         Global Jobs 16
#                    Compression Jobs 8
#                 Compression Threads 2
#   --------------------------------------------------------------------
#                 Configuration Options (<config_path>)
#   --------------------------------------------------------------------
#                    preserve_modtime true
#                        prepend_zero true
#                   compression_level 8
#                      remove_artwork true
#                          skip_lossy true
#                           error_log <default>
#              temporary_wav_location <default>
#                spectrogram_location <default>
#                    artwork_location <default>
#   --------------------------------------------------------------------
#
# Where the width of the banner is dynamically sized to the width of the
# terminal or to the maximum length supported on script execution
#
_runtime_banner()
{
  local i option field value line align_cpu job_type

  # Generate a blue line the which matches the maximum length supported
  printf -v line "%$(( MAX_LENGTH - 1 ))s" ''
  line="${blue}${line// /-}$reset"

  # Replace $HOME with tilde
  local config_file_title="$config_file"
  config_file_title="~${config_file_title#"$HOME"}"

  # Dynamically center banner config title
  local config_title="Configuration Options ($config_file_title)"
  local config_title_center=$(( (MAX_LENGTH / 2) - (${#config_title} / 2) ))

  # Dynamically center banner runtime title
  local runtime_title='Runtime Information'
  local runtime_title_center=$(( (MAX_LENGTH / 2) - (${#runtime_title} / 2) ))

  # Dynamically center configuration and runtime options and values
  #
  # NOTE
  #   The '- 1' accounts for space between field and value
  local fields_center=$(( (MAX_LENGTH / 2) - 1 ))

  # Runtime information
  printf '%s\n' "$line"
  printf "\033[$(_row);${runtime_title_center}H%s\n" "$runtime_title"
  printf '%s\n' "$line"

  # Display centered script runtime information
  printf "%${fields_center}s ${cyan}%s${reset}\n" 'redoflacs' "$script_version"
  printf "%${fields_center}s ${cyan}%s${reset}\n" 'FLAC' "$flac_version"

  # Multithreaded FLAC distinguishes global jobs from jobs
  [[ "$multithreaded_flac" ]] && job_type='Global Jobs' || job_type='Jobs'

  # Choose the longest character value between all jobs and threads
  align_cpu="${#compress_jobs}"
  (( align_cpu < ${#compress_threads} )) && align_cpu="${#compress_threads}"
  (( align_cpu < ${#global_jobs} )) && align_cpu="${#global_jobs}"

  # Let the user know of possible CPU starvation if the number of global
  # jobs is higher than total cores available
  if [[ "$cpu_job_starvation" ]]; then
    printf \
      "%${fields_center}s ${red}%-${align_cpu}s$reset ${yellow}%s$reset\n" \
      "$job_type" "$global_jobs" '(CPU Starvation Risk!)'

  # Display jobs which appear to be reasonable
  else
    printf "%${fields_center}s ${cyan}%s${reset}\n" "$job_type" "$global_jobs"
  fi

  # Let the user know of possible CPU starvation if the number of
  # compression jobs and compression threads specified are higher than
  # total cores available
  if [[ "$multithreaded_flac" && "$cpu_thread_starvation" ]]; then
    printf \
      "%${fields_center}s ${red}%-${align_cpu}s$reset ${yellow}%s${reset}\n" \
      'Compression Jobs' "$compress_jobs" '(CPU Starvation Risk!)'

    printf \
      "%${fields_center}s ${red}%-${align_cpu}s$reset ${yellow}%s${reset}\n" \
      'Compression Threads' "$compress_threads" '(CPU Starvation Risk!)'

  # Display compression jobs and compression threads which appear to be
  # reasonable
  elif [[ "$multithreaded_flac" ]]; then
    printf "%${fields_center}s ${cyan}%s${reset}\n" \
      'Compression Jobs' "$compress_jobs"
    printf "%${fields_center}s ${cyan}%s${reset}\n" \
      'Compression Threads' "$compress_threads"
  fi

  # Configuration options
  printf '%s\n' "$line"
  printf "\033[$(_row);${config_title_center}H%s\n" "$config_title"
  printf '%s\n' "$line"

  # Display all configured options, centered
  for option in "${config_options[@]}"; do
    # Option field (shell variable)
    field="${option%%=*}"

    # Option value (shell variable value), with a default if empty
    value="${!field:-${magenta}<default>${reset}}"

    # Eg: $HOME -> ~/
    # shellcheck disable=SC2088
    [[ "$value" == "$HOME" ]] && value='~/'

    # Eg: $HOME/user/dir -> ~/user/dir
    # shellcheck disable=SC2088
    [[ "$value" == "$HOME"/* ]] && value="~/${value#"$HOME"/}/"

    # Display centered configuration option and value
    printf "%${fields_center}s ${cyan}%s${reset}\n" "$field" "$value"
  done

  printf '%s\n' "$line"
}

#
# Print out current operation title
#
# The structure of an operation title:
#
#   '<prefix> <title>                   (<status>) (<number_of_issues>)'
#
# Which may look like:
#
#   '* Verifying MD5...                                       COMPLETED'
#   '* Testing...                                   COMPLETED 10 issues'
#   '* Pruning METADATA...                                  INTERRUPTED'
#   '* Compressing To Level 8...                   INTERRUPTED 1 issues'
#   '* Extracting Images...                                 DID NOT RUN'
#
# Where the status may or may not be displayed and/or the number of
# issues may or may not be displayed
#
_operation_title()
{
  local fmt status_banner issues_banner right_align

  # Total number of issues found
  local -i issues
  issues="$(_num_issues)"

  # Ensure we do not run if running from a countdown or before an
  # operation begins (only if SIGINT was sent during these times)
  [[ "${FUNCNAME[1]}" == '_cleanup' && -z "$operation" ]] && return 0

  # Generate a banner indicating total issues found so far
  if (( issues > 0 )); then
    issues_banner="$issues issue"

    # Add plural for more than one issue found
    (( issues != 1 )) && issues_banner+='s'
  fi

  # Generate an interrupted banner if the user sent SIGINT otherwise
  # default to a completed status
  #
  # NOTE:
  #   The parent function will be the signal handler for SIGINT
  if [[ "${FUNCNAME[1]}" == '_cleanup' ]]; then
    status_banner='INTERRUPTED'

    # Add an extra space to separate issues banner if it exists
    (( issues > 0 )) && status_banner+=' '

  # Generate a completed banner if the current operation finished
  # processing all items
  elif (( num == ${#total_items[@]} )); then
    status_banner='COMPLETED'

    # Add an extra space to separate issues banner if it exists
    (( issues > 0 )) && status_banner+=' '
  fi

  # Determine where to place cursor in terminal column to right align
  # possible SIGINT interruption and the number of issues to display
  right_align=$(( MAX_LENGTH - ${#status_banner} - ${#issues_banner} ))

  fmt+=$"\033[${title_row}H"  # Cursor row; beginning of line
  fmt+=" ${green}*$reset "    # Colored title prefix
  fmt+='%s'                   # Modifier; title of operation

  # Cursor at operation title; right-aligned for issues banner
  fmt+=$"\033[${title_row};${right_align}H"

  # These may not be displayed if no issues, no SIGINT, or not completed
  if [[ "$status_banner" == 'INTERRUPTED'* ]]; then
    fmt+="${cyan}${status_banner}$reset"    # Interrupted banner
  else
    fmt+="${green}${status_banner}$reset"   # Completed banner
  fi
  fmt+="${red}${issues_banner}$reset\n"     # Issues banner

  # Display generated operation title message
  # shellcheck disable=SC2059
  printf "$fmt" "${operation_title[$operation]}"
}

#
# Displays the currently processed item on a dedicated row within the
# terminal
#
# The filename is truncated if the length of the filename is greater
# than 80 characters or if greater than the width of the terminal if
# that terminal width is less than 80. This width is set by 'MAX_LENGTH'
#
# Filename truncation also occurs to allow the operation label (via $1)
# and the total number of completed items to be displayed on the same
# row as the displayed item
#
# The structure of a displayed item:
#   '<percent> <basename>          <operation_label> <number_completed>'
#
# Which may look like:
#   '     06 - Some Amazing Song.flac                     Testing  6/20'
#
# This function does NOT create the display the percentage completed.
# That is handled later
#
# The file basename is left-aligned (leaving room for the percentage),
# while the operation label and total number of completed items are
# right-aligned
#
# An example of filename truncation:
#   '01 - This is a long name showing truncation.flac'
#
# This filename would be displayed during an operation as:
#   '     01 - This is a long name showing truncation.fla Testing  6/20'
#
# An example of how this would look with a percentage displayed:
#   ' 56% 01 - This is a long name showing truncation.fla Testing  6/20'
#
_print_item()
{
  local fmt truncated
  local basename="${item##*/}"
  local label="${1:+ $1}"  # Operation label (with leading space)

  # The total number of items out of the total number of items, used to
  # determine the maximum width these values can use, eg:
  #   ' 237/237'
  #
  # The leading space is intentional, to ensure a space is between the
  # item basename and this value
  local total=" ${#total_items[@]}/${#total_items[@]}"

  # This is the column position after the item basename is displayed.
  # This allows the operation label and total number of completed items
  # to be displayed after this column, effectively truncating the
  # current item if longer than this column position
  #
  # NOTE:
  #   The item basename may be longer than the terminal width, which is
  #   why line wrapping is disabled and restored, to prevent the display
  #   of the item basename from traversing multiple lines
  truncated="$(( MAX_LENGTH - ${#total} - ${#label} ))"

  # Disable line wrapping if enabled
  [[ "$LINE_WRAPPING" ]] && fmt+=$'\033[?7l'

  # Belows builds the 'printf' format string
  fmt+=$"\033[${row};6H"              # Cursor row; 6 columns to right
  fmt+=$"\033[0K"                     # Erase line from cursor to right
  fmt+='%s'                           # Modifier; Current item basename
  fmt+=$"\033[${row};${truncated}H"   # Cursor row; Move to allow items
  fmt+=$"\033[0K"                     # Erase line from cursor to right
  fmt+="${magenta}%s"                 # Modifier; Operation label
  fmt+="${cyan}%${#total}s${reset}"   # Modifier; Items, right-aligned

  # Restore line wrapping if enabled
  [[ "$LINE_WRAPPING" ]] && fmt+=$'\033[?7h'

  # Displays formatted, currently processed item
  # shellcheck disable=SC2059
  printf "$fmt" "$basename" "$label" "${num}/${#total_items[@]}"
}

#
# These functions are used after '_print_item()' to update/indicate the
# status of a given item in an operation
#
_print_item_ok()   { printf '%b' "\033[${row}H${green}100%$reset";  }
_print_item_fail() { printf '%b' "\033[${row}H${red}fail$reset";    }
_print_item_check(){ printf '%b' "\033[${row}H${yellow}chck$reset"; }
_print_item_half() { printf '%b' "\033[${row}H$yellow 50%$reset";   }
_print_item_skip() { printf '%b' "\033[${row}H${cyan}skip$reset";   }

#
# Manage termination and cleanup of this script
#
# The signal processed by this function is via $exit_code, which, if
# non-zero, is from a trap capturing a termination signal
#
# Abnormal signals caught will perform cleanup by removing files and
# killing any jobs which may be running
#
# On script end, any operation that was not run is displayed, eg:
#
#   '* Testing...                                   COMPLETED 10 issues'
#   '* Compressing To Level 8...                            DID NOT RUN'
#   '* Retagging...                                         DID NOT RUN'
#   '* Pruning METADATA...                                  DID NOT RUN'
#
_cleanup()
{
  local i fmt op match status_banner right_align

  # Total number of issues found
  local -i issues
  issues="$(_num_issues)"

  # If this global variable is non-zero, a trap caught an abnormal
  # signal and redefined it to an exit code value matching the signal
  # received
  if (( exit_code != 0 )); then
    _operation_title          # Display operation was interrupted
    _kill_jobs "$(jobs -rp)"  # Kill any running jobs

    # Clear all the operation lines by moving up each line and clearing
    # it until we are just below the operation's title message
    for (( i=1; i<=items_processed; i++ )); do
      printf '%b' "\033[$(( post_row - i ))H\033[0K"
    done

    # Remove temporary WAV and FLAC files
    rm -rf "${temporary_wav_location_sub_dir:-/dev/null/foo}"
    rm -f "${directory:-/dev/null/foo}"/**/*_redoflacs_$$.wav
    rm -f "${directory:-/dev/null/foo}"/**/*.tmp,fl-ac+en\'c

    # Remove temporary picture blocks (from artwork extraction)
    rm -f /tmp/redoflacs_picture_blocks.[[:digit:]]*
  fi

  # Due to catching an abnormal signal or if there were issues found,
  # loop through all operations and display any operation which did not
  # run
  for op in "${operations[@]}"; do
    # Matched operation is either interrupted or completed with issues
    [[ "$op" == "$operation" ]] && match='true' && continue

    # Skip completed operations (without issues)
    [[ ! "$match" ]] && continue

    status_banner='DID NOT RUN'

    # Determine where to place cursor in terminal column to right
    # align the operation status
    right_align=$(( MAX_LENGTH - ${#status_banner} ))

    fmt=" ${green}*$reset "  # Colored title prefix
    fmt+='%s'                # Modifier; title of operation

    # Cursor at operation title; right-aligned for issues banner
    fmt+=$"\033[$(_row);${right_align}H"

    # Operation status banner
    fmt+="${magenta}${status_banner}$reset\n"

    # Display generated operation title message
    # shellcheck disable=SC2059
    printf "$fmt" "${operation_title[$op]}"
  done

  # Ensure user is notified of a log file on script interruption/exit
  [[ -f "$log_file" ]] && _display_issues_exist

  # Remove temporary job FIFO and temporary errors/issues file
  rm -f "${job_fifo:-/dev/null/foo}"
  rm -f "${issue_ticks:-/dev/null/foo}"

  # Restore terminal cursor
  printf '\033[?25h'

  # Issues found without an abnormal signal
  (( exit_code == 0 && issues > 0 )) && exit_code=1
}

#
# Kill any children process (obtained via $@), hiding errors and
# suppressing the shell's notification of terminated jobs
#
_kill_jobs()
{
  local pid

  for pid in "$@"; do
    kill "$pid" 2>/dev/null
    wait "$pid" 2>/dev/null
  done
}

#
# Return the current cursor's row position by sending the following
# control sequence to the controlling terminal:
#   CSI 6 n            # Query cursor position
#
# Which reports/returns the cursor position/terminal size as:
#   CSI height ; width R  # eg. '\033[80;23R'
#
_row()
{
  local -i row

  # Send CSI escape to (and read from) STDERR to not pollute STDIN
  IFS='[;' read -rsu2 -d'R' -t1 -p $'\033[6n' _ row _

  printf '%d' "${row:-0}"  # Default to 0, if failure
}

#
# Return the current cursor's column position by sending the following
# control sequence to the controlling terminal:
#   CSI 6 n            # Query cursor position
#
# Which reports/returns the cursor position/terminal size as:
#   CSI height ; width R  # eg. '\033[80;23R'
#
_col()
{
  local -i col

  # Send CSI escape to (and read from) STDERR to not pollute STDIN
  IFS='[;' read -rsu2 -d'R' -t1 -p $'\033[6n' _ _ col

  printf '%d' "${col:-0}"
}

#
# If necessary, scroll the terminal
#
_scroll_terminal()
{
  local -i row to_scroll fmt

  # Obtain current row position
  row="$(_row)"

  # Default to scrolling the terminal based on the number of jobs
  to_scroll="$operation_jobs"

  # Scrolling by number of items to process
  (( operation_jobs > ${#total_items[@]} )) && to_scroll="${#total_items[@]}"

  # Only scroll if there are more items to display than lines available
  if (( --to_scroll > (lines - row) )); then
    # Scroll terminal the required number of lines
    printf '%b' "\033[$(( to_scroll - (lines - row) ))S"

    # Place cursor back to where it was in relation to current output
    printf '%b' "\033[$(( lines - to_scroll ))H"
  fi
}

#
# Create a configuration file
#
_create_config()
{
  local -i item=0

  # Ensure the parent directory exists
  mkdir -p "${config_file%/*}"

  # If there already is a configuration file, do not overwrite it
  until [[ ! -f "$config_file" ]]; do
    config_file="${HOME}/.config/redoflacs/config.$(( ++item ))"
  done

  # Don't expand variables when using heredoc
  cat > "$config_file" << "END_OF_CONFIG"
#
# redoflacs configuration
#
# Any line that is _NOT_ prepended with a '#' will be interpreted as an option
# (except for blank lines -- these are not interpreted)
#
#------------------------------------------------------------------------------
# TAGGING SECTION
#------------------------------------------------------------------------------
#
# List the tags to be kept in each FLAC file. The default is listed below.
#
# Another common tag not added by default is ALBUMARTIST. Uncomment ALBUMARTIST
# below to allow script to keep this tag.
#
# NOTE: Whitespace _IS_ allowed for the these tag fields, ie:
#   ALBUM ARTIST
#   CATALOG NUMBER ISBN
#
TITLE
ARTIST
#ALBUMARTIST
ALBUM
DISCNUMBER
DATE
TRACKNUMBER
TRACKTOTAL
GENRE

# The COMPRESSION tag is a custom tag to allow the script to determine which
# level of compression the FLAC file(s) has/have been compressed at.
COMPRESSION

# The RELEASETYPE tag is a custom tag the author of this script uses to
# catalog what kind of release the album is (ie, Full Length, EP, Demo, etc.).
RELEASETYPE

# The SOURCE tag is a custom tag the author of this script uses to catalog
# which source the album has derived from (ie, CD, Vinyl, Digital, etc.).
SOURCE

# The MASTERING tag is a custom tag the author of this script uses to catalog
# how the album has been mastered (ie, Lossless, or Lossy).
MASTERING

# The REPLAYGAIN tags below, are added by the '-g' or '-G' argument.
# If you want to keep the ReplayGain tags, make sure you leave these here.
REPLAYGAIN_REFERENCE_LOUDNESS
REPLAYGAIN_TRACK_GAIN
REPLAYGAIN_TRACK_PEAK
REPLAYGAIN_ALBUM_GAIN
REPLAYGAIN_ALBUM_PEAK

#------------------------------------------------------------------------------
# OPTIONS
#------------------------------------------------------------------------------
#
# The options listed below are shell syntax where option values are
# evaluated/interpolated as expected with the caveat that command substitutions
# are not allowed, eg:
#
#   error_log='$HOME/logs'            # $HOME/logs
#   error_log='${HOME}/logs'          # ${HOME}/logs
#   error_log="$HOME/logs"            # /home/user/logs
#   error_log="${HOME}/logs"          # /home/user/logs
#   error_log="$(pwd)/logs"           # $(pwd)/logs
#   error_log="`pwd`/logs"            # `pwd`/logs
#   error_log="${HOME}/$(date)/logs"  # ${HOME}/$(date)/logs
#   error_log='${HOME}/$(date)/logs'  # ${HOME}/$(date)/logs
#
# Any single quoted value and values with command substitutions are treated as
# literal strings.
#
#------------------------------------------------------------------------------
#
# PRESERVE FILE MODIFICATION TIME:
#
# Changes whether both 'flac' and 'metaflac' programs ensure the updated files
# have the same timestamps/permissions as the input files.
#
# For 'flac' usage, this is the default, but 'metaflac' does update timestamps
# and permissions.
#
# This is enabled by setting 'preserve_modtime' option as 'true'. All other
# values are interpreted as 'false'.
#
preserve_modtime='true'

#
# PREPEND TRACK NUMBER:
#
# Change whether the retag (-r) operation will re-tag singular track numbers
# and track totals from:
#    1, 2, 3, 4, 5, 6, 7, 8, 9
# to
#    01, 02, 03, 04, 05, 06, 07, 08, 09
#
# For example, if you had:
#    TRACKNUMBER=4
#     TRACKTOTAL=9
#
# You would end up with:
#    TRACKNUMBER=04
#     TRACKTOTAL=09
#
# This is enabled by setting 'prepend_zero' option as 'true'. All other values
# are interpreted as 'false'.
#
prepend_zero='false'

#
# SET COMPRESSION:
#
# Set the type of COMPRESSION strength when compressing the FLAC files. Numbers
# range from '1-8', with '1' being the lowest compression and '8' being the
# highest compression. The default is '8'.
#
compression_level='8'

#
# REMOVE ARTWORK:
#
# Set whether to remove embedded artwork within FLAC files. By default, this
# script will remove any artwork it can find in the PICTURE block of a FLAC
# file. Set 'remove_artwork' as 'true' to remove embedded artwork. All other
# values are intepreted as 'false'.
#
remove_artwork='true'

#
# AUCDTECT/LAC SKIP LOSSY:
#
# Set whether FLAC files should be skipped if the MASTERING tag is already set
# as 'Lossy' when analyzed with auCDtect or LAC. Set 'skip_lossy' as 'true' to
# skip FLAC files that have the tag: 'MASTERING=Lossy'. All other values are
# intepreted as 'false'.
#
skip_lossy='true'

#
# ERROR LOG DIRECTORY:
#
# Set where you want error logs to be placed. By default, they are stored in
# the user's HOME directory.
#
# All values for 'error_log' are interpreted as a directory. If left blank, the
# default location will be used.
#
error_log=''

#
# WAV FILE DIRECTORY
#
# Set where to place temporary created WAV files when decoding FLAC files for
# use with auCDtect or LAC (Lossless Audio Checker).
#
# By default the WAV file is created in the same location as the FLAC file and
# removed when the operation is completed for that file.
#
# Users may prefer to place all WAV files on TMPFS backed storage or some other
# location depending on their use case.
#
# All values for 'temporary_wav_location' are interpreted as a directory. If
# left blank, the default location will be used.
#
# NOTE:
#   Setting this value to a directory on a filesystem with minimal space may
#   increase the chance of filesystem exhaustion if running with a large number
#   of parallel jobs
#
temporary_wav_location=''

#
# SPECTROGRAM DIRECTORY:
#
# Set where created spectrogram images should be stored. By default, they are
# stored in the same directory as the analyzed FLAC files. Each image will have
# the same name as the tested FLAC file but with an integer-based suffix to
# allow for uniqueness. The type of image created is PNG.
#
# All values for 'spectrogram_location' are interpreted as a directory. If left
# blank, the default location will be used.
#
# An example of a user-defined location:
#
#    spectrogram_location="${HOME}/specs"
#
# Example spectrogram:
#
#    ${HOME}/specs/music/artist/album/file.flac.spec.png.0
#
spectrogram_location=''

#
# EXTRACTED ARTWORK DIRECTORY:
#
# Set where the extracted artwork images should be stored.
#
# By  default, each extracted image will be placed in a subdirectory where the
# FLAC file is located. The subdirectory housing the extracted artwork will
# have a similar name as the currently processed FLAC. If a directory already
# exists, an integer is appended to the directory (to prevent overwriting and
# mixing files). For example:
#
#    /music/artist/album/file.flac          # FLAC file with embedded images
#    /music/artist/album/file.flac.art.0/   # Directory housing artwork
#    /music/artist/album/file.flac.art.1/   # '1' if above directory exists
#    /music/artist/album/file.flac.art.2/   # '2' if above directory exists
#
# All values for 'artwork_location' are interpreted as a directory. If left
# blank, the default location will be used.
#
# If there is a user-defined location, the extracted images will be placed in a
# subdirectory in that location with a naming scheme similar to above:
#
#    artwork_location="${HOME}/artwork"    # User-defined configuration option
#    /music/artist/album/file.flac         # FLAC file with embedded artwork
#
#    # Directory housing artwork (incremented if already existing)
#    ${HOME}/artwork/music/artist/album/file.flac.art.0/
#    ${HOME}/artwork/music/artist/album/file.flac.art.1/
#    ${HOME}/artwork/music/artist/album/file.flac.art.2/
#
artwork_location=''
END_OF_CONFIG

  # Indicate if there was an existing configuration file
  if [[ "$config_file" =~ ^.+[[:digit:]]$ ]]; then
    _warn 'An existing configuration file is here:'
    _warn "${cyan}${config_file%.*}$reset\n"

    _info 'A new configuration file was created here:'
    _info "${cyan}${config_file}$reset\n"

    _info 'Ensure the new configuration file is copied here:'
    _info "${cyan}${config_file%.*}$reset\n"

    _info 'In order for this program to make use of it'

  # No existing configuration file was found, indicate review of file
  else
    _info 'A configuration file has been created here:'
    _info "${cyan}${config_file}$reset\n"

    _info 'It is recommended to review the new configuration file before'
    _info 'running this program.'
  fi
}

#
# Parse the configuration file
#
_parse_config()
{
  local line option value valid_option

  # Used to check for missing, but required, configuration options
  local -a missing_options

  # Used in 'flac' and 'metaflac' invocations
  local -ga flac_extra_options metaflac_extra_options

  # Used in _retag() function
  local -ga tags_to_keep

  # Used in script banner
  local -ga config_options

  # Skip irrelevant configuration file lines
  local regex_skip='^([[:space:]]+|#.*|)$'

  # Matches on configuration options (shell variables)
  local regex_options='^([^[:space:]]+=.*)$'

  # Matches on possible command substitutions in option values
  local regex_cmds='\$\(|`'

  # Unnecessary, as these variables are dynamically exported from the
  # config but are defined to silence shellcheck of unused variables
  local -g \
    preserve_modtime \
    prepend_zero \
    compression_level \
    remove_artwork \
    skip_lossy \
    error_log \
    temporary_wav_location \
    spectrogram_location \
    artwork_location

  # Key/value associative array used to evaluate valid/missing options
  local -A valid_config_options
  valid_config_options=(
    [preserve_modtime]='true'
    [prepend_zero]='true'
    [compression_level]='true'
    [remove_artwork]='true'
    [skip_lossy]='true'
    [error_log]='true'
    [temporary_wav_location]='true'
    [spectrogram_location]='true'
    [artwork_location]='true'
  )

  # Process the configuration file, storing all user-defined tags and
  # evaluate each configuration option
  while IFS=$'\n' read -r line; do
    # Skip empty lines, comment lines and lines with whitespace
    [[ "$line" =~ $regex_skip ]] && continue

    # Non-option (shell variables) are treated as user-defined FLAC tags
    if [[ ! "$line" =~ $regex_options ]]; then
      # FLAC tags stored into a global array
      tags_to_keep+=( "${line^^}" )
      continue
    fi

    # Skip if option is not part of the valid options
    [[ ! "${valid_config_options["${line%%=*}"]}" ]] && continue

    # Store configuration option key/value
    config_options+=( "$line" )

    # Break up line into shell variable and value
    IFS='=' read -r option value <<< "$line"

    # Remove leading/trailing whitespace from value (if any)
    IFS=$' \t\n' read -r value <<< "$value"

    # Do not interpolate value if single quotes are around value
    if [[ "$value" =~ ^\'.*\'$ ]]; then
      # Remove single quotes from value
      value="${value#\'}"
      value="${value%\'}"

      # Set configuration option and value into environment
      export "$option"="$value"
      continue
    fi

    # Remove double quotes from value (if any)
    value="${value#\"}"
    value="${value%\"}"

    # Do not interpolate possible command subsitutions and only
    # interpolate values which contain a variable prefix
    #
    # NOTE:
    #   BASH >= 4.4.x allows for parameter transformations which are
    #   safer than using 'eval'
    if [[ ! "$value" =~ $regex_cmds && "$value" == *\$* ]]; then
      if (( ${BASH_VERSINFO[0]}${BASH_VERSINFO[1]} >= 44 )); then
        value="${value@P}"  # Expand like a prompt string
      else
        value="$(eval "printf '%s' \"$value\"" 2>/dev/null)"
      fi
    fi

    # Interpolate configuration option and value into environment
    export "$option"="$value"
  done < "$config_file"

  # Look for any missing options in the user's configuration file and
  # notify the user
  for valid_option in "${!valid_config_options[@]}"; do
    if [[ " ${config_options[*]%%=*} " != *" $valid_option "* ]]; then
      missing_options+=( "$valid_option" )
    fi
  done

  # Exit if there were any missing options from the configuration file
  if [[ "${missing_options[*]}" ]]; then
    _error 'The following required options are missing:'
    for option in "${missing_options[@]}"; do
      _error "  ${cyan}${option}$reset"
    done

    _error

    _error 'Recreate a configuration file (non-destructive) via:'
    _error "  ${cyan}redoflacs -o $reset"
    exit 1
  fi

  # Handle timestamp preseveration for both 'flac' and 'metaflac'
  if [[ "${preserve_modtime,,}" == 'true' ]]; then
    flac_extra_options+=( '--preserve-modtime' )
    metaflac_extra_options+=( '--preserve-modtime' )

  # Metaflac defaults to not preserving modification times and does not
  # have a flag to turn preservation off
  else
    flac_extra_options+=( '--no-preserve-modtime' )
  fi
}

#
# Display countdown before retagging to allow user to quit script safely
#
_countdown_metadata()
{
  # Warning message
  _error "${yellow}CAUTION!${reset} These are the tag fields that will be kept"
  _error 'when re-tagging the selected files:\n'

  # Creates the listing of tags to be kept
  printf '     %s\n' "${tags_to_keep[@]}"
  printf '\n'

  # Warning message about embedded coverart
  _error "By default, this script will ${red}REMOVE${reset} the legacy"
  _error "${cyan}COVERART${reset} tag.\n"

  _error "Add the ${cyan}COVERART${reset} tag to the list of tags to be kept"
  _error "in the ${cyan}TAGGING SECTION${reset} of the configuration file.\n"

  _error "Keep in mind, if the ${cyan}remove_artwork${reset} option is set to"
  _error "${cyan}false${reset}, embedded artwork in the ${cyan}PICTURE${reset}"
  _error "block will be kept when using the ${cyan}-p, --prune${reset} option"
  _error "as well.\n"

  _warn "Waiting ${red}20${reset} seconds before starting program..."
  _warn 'Ctrl+C (Control-C) to abort...\n'

  printf '%b' " ${green}*${reset} Starting in: "

  # 10 second countdown
  for count in {20..1}; do
    printf "${red}%d ${reset}" "$count"
    read -rt1  # Sleep 1
  done
  printf '\n'
}

#
# Return an integer detailing the number of issues an operation may have
# had, returning '0', if no issues were found
#
_num_issues()
{
  local ticks

  # Read in number of issue ticks, hiding missing file output
  { read -r ticks < "$issue_ticks"; } 2>/dev/null
  printf '%d' "${#ticks}"  # Return number of ticks (0, if empty)
}

#
# Obtain and process the positional parameters invoked with the script
#
# The following global variables are defined, should options which
# define them are specified:
#
#   $create_spectrogram
#   $directory
#   $force_compression
#   $force_replaygain
#   $no_extra_tags
#   $operations[@]
#
_process_arguments()
{
  # Global variables used in other functions/operations
  local -g create_spectrogram force_compression force_replaygain \
    no_extra_tags directory operations global_jobs compress_jobs \
    compress_threads

  local arg invalid op
  local -a conflicting operations_to_run

  # If no arguments are made to the script show usage and help
  if (( $# == 0 )); then
    _help
    exit 1
  fi

  # Processes all the possible positional parameters, short options
  # only, handling conflicting parameters
  while getopts ':j:J:T:mtcCaAgLlGrepnhvox' arg "$@"; do
    case "$arg" in
      # Given argument is not valid. 'OPTARG' is 'arg' before 'arg' is
      # set to '?'
      '?')
        [[ "$invalid" == *"-$OPTARG"* ]] && continue
        invalid+="${invalid:+ }-$OPTARG"
        ;;

      # Required parameter for a given argument was not specified
      ':')
        _error "Option -$OPTARG, requires a non-zero integer, eg: -${OPTARG}4"
        _help
        exit 1
        ;;

      # Help message and exit
      'h') _help; exit 0 ;;

      # Dipslay script version and exit
      'v') printf 'redoflacs %s\n' "$script_version"; exit 0 ;;

      # Create a new configuration file and exit
      'o') _create_config; exit 0 ;;

      # Disable color output
      'n') unset -v 'reset' 'red' 'green' 'yellow' 'blue' 'magenta' 'cyan' ;;

      # Prevent COMPRESSION tag from being applied during compression
      'x') no_extra_tags='true' ;;

      # Validate MD5 checksum in FLAC files
      'm') operations_to_run+=( '_md5check' ) ;;

      # Test FLAC files
      't') operations_to_run+=( '_test' ) ;;

      # Compression and force compression arguments
      'c'|'C')
        # Skip as both compression arguments were already specified
        [[ "${conflicting[*]}" == *'-c, -C'* ]] && continue

        operations_to_run+=( '_compress' )
        [[ "$arg" == 'C' ]] && force_compression='true'

        # '-C' was already specified and '-c' is the current argument
        if [[ "$arg" == 'c' && "$force_compression" ]]; then
          conflicting+=( '-c, -C' )

        # '-c' was already specified and '-C' is the current argument
        elif [[ "$arg" == 'C' && ! "$force_compression" ]]; then
          conflicting+=( '-c, -C' )
        fi
        ;;

      # auCDtect and auCDtect with spectrogram arguments
      'a'|'A')
        # Skip as both auCDtect arguments were already specified
        [[ "${conflicting[*]}" == *'-a, -A'* ]] && continue

        operations_to_run+=( '_aucdtect' )
        [[ "$arg" == 'A' ]] && create_spectrogram='true'

        # '-A' was already specified and '-a' is the current argument
        if [[ "$arg" == 'a' && "$create_spectrogram" ]]; then
          conflicting+=( '-a, -A' )

        # '-a' was already specified and '-A' is the current argument
        elif [[ "$arg" == 'A' && ! "$create_spectrogram" ]]; then
          conflicting+=( '-a, -A' )
        fi
        ;;

      # LAC and LAC with spectrogram arguments
      'l'|'L')
        # Skip as both LAC arguments were already specified
        [[ "${conflicting[*]}" == *'-l, -L'* ]] && continue

        operations_to_run+=( '_lac' )
        [[ "$arg" == 'L' ]] && create_spectrogram='true'

        # '-L' was already specified and '-l' is the current argument
        if [[ "$arg" == 'l' && "$create_spectrogram" ]]; then
          conflicting+=( '-l, -L' )

        # '-l' was already specified and '-L' is the current argument
        elif [[ "$arg" == 'L' && ! "$create_spectrogram" ]]; then
          conflicting+=( '-l, -L' )
        fi
        ;;

      # ReplayGain and force ReplayGain arguments
      'g'|'G')
        # Skip as both ReplayGain arguments were already specified
        [[ "${conflicting[*]}" == *'-g, -G'* ]] && continue

        operations_to_run+=( '_replaygain' )
        [[ "$arg" == 'G' ]] && force_replaygain='true'

        # '-G' was already specified and '-g' is the current argument
        if [[ "$arg" == 'g' && "$force_replaygain" ]]; then
          conflicting+=( '-g, -G' )

        # '-g' was already specified and '-G' is the current argument
        elif [[ "$arg" == 'G' && ! "$force_replaygain" ]]; then
          conflicting+=( '-g, -G' )
        fi
        ;;

      # Retag FLAC files
      'r') operations_to_run+=( '_retag' ) ;;

      # Extract embedded artwork from FLAC files
      'e') operations_to_run+=( '_extract_images' ) ;;

      # Prune unnecessary metadata blocks from FLAC files
      'p') operations_to_run+=( '_prune' ) ;;

      # Specify number of jobs/threads
      'j'|'J'|'T')
        # OPTARG is the (required) number after $arg
        if [[ ! "$OPTARG" =~ ^[[:digit:]]+$ ]] || (( OPTARG == 0 )); then
          _error "Option -$arg, requires a non-zero integer, eg: -${arg}4"
          _error "Current value applied: $OPTARG"
          _help
          exit 1
        fi

        # Set the relevant jobs/threads based on type of argument
        [[ "$arg" == 'j' ]] && global_jobs="$OPTARG"
        [[ "$arg" == 'J' ]] && compress_jobs="$OPTARG"
        [[ "$arg" == 'T' ]] && compress_threads="$OPTARG"
        ;;
    esac
  done

  # Invalid parameters were specified
  if [[ "$invalid" ]]; then
    _error 'Invalid arguments specified:'
    _error "  ${cyan}${invalid}$reset"
  fi

  # Conflicting parameters were specified
  if (( ${#conflicting[@]} > 0 )); then
    _error 'Conflicting operations specified:'
    for option in "${conflicting[@]}"; do
      _error "  ${cyan}${option}$reset"
    done
  fi

  # Display help and exit if invalid or conflicting parameters exist
  if [[ "$invalid" ]] || (( ${#conflicting[@]} )); then
    _help
    exit 1
  fi

  # Clear current arguments processed by getopts, including the last
  # non-option encountered
  shift $(( OPTIND - 1 ))

  # Make sure a directory is specified (non-option argument)
  if (( $# == 0 )); then
    _error 'Please specify a valid directory!'
    _help
    exit 1

  # Ensure only directory is specified, which also shows any invalid
  # non-option arguments applied
  elif (( $# > 1 )); then
    _error 'Unexpected arguments:'
    _error "  ${cyan}$*${reset}"
    _error 'All options must come before the target directory!'
    _help
    exit 1
  fi

  # The last element passed in is the directory to process FLAC files
  directory="${1%/}"  # Remove possible ending slash

  # Ensure the directory exists
  if [[ ! -d "$directory" ]]; then
    _error 'Please specify a valid directory!'
    _help
    exit 1
  fi

  # Configure the order to run operations as this script may be called
  # to run one or all operations on a target directory of FLAC files
  if [[ " ${operations_to_run[*]} " == *' _md5check '* ]]; then
    operations+=( '_md5check' )
  fi
  if [[ " ${operations_to_run[*]} " == *' _test '* ]]; then
    operations+=( '_test' )
  fi
  if [[ " ${operations_to_run[*]} " == *' _compress '* ]]; then
    operations+=( '_compress' )
  fi
  if [[ " ${operations_to_run[*]} " == *' _aucdtect '* ]]; then
    operations+=( '_aucdtect' )
  fi
  if [[ " ${operations_to_run[*]} " == *' _lac '* ]]; then
    operations+=( '_lac' )
  fi
  if [[ " ${operations_to_run[*]} " == *' _replaygain '* ]]; then
    operations+=( '_replaygain' )
  fi
  if [[ " ${operations_to_run[*]} " == *' _retag '* ]]; then
    operations+=( '_retag' )
  fi
  if [[ " ${operations_to_run[*]} " == *' _extract_images '* ]]; then
    operations+=( '_extract_images' )
  fi
  if [[ " ${operations_to_run[*]} " == *' _prune '* ]]; then
    operations+=( '_prune' )
  fi
}

#
# Check for missing programs required by this script
#
_check_missing_programs()
{
  local program
  local -a programs=( 'rm' 'mkdir' 'mkfifo' 'metaflac' 'flac' )
  local -a missing_programs

  for program in "${programs[@]}"; do
    type -P "$program" >/dev/null || missing_programs+=( "$program" )
  done

  # auCDtect binary
  if [[ " ${operations[*]} " == *' _aucdtect '* ]]; then
    type -P auCDtect >/dev/null || missing_programs+=( 'auCDtect' )
  fi

  # Lossless Audio Checker (LAC) binary
  if [[ " ${operations[*]} " == *' _lac '* ]]; then
    type -P LAC >/dev/null || missing_programs+=( 'LAC' )
  fi

  if [[ "$create_spectrogram" ]]; then
    type -P sox >/dev/null || missing_programs+=( 'sox' )
  fi

  if [[ "${missing_programs[*]}" ]]; then
    _error 'The following required programs were not found:'
    for program in "${missing_programs[@]}"; do
      _error "  ${cyan}${program}$reset"
    done

    _error

    # shellcheck disable=SC2016
    _error 'Ensure these programs are installed and available in $PATH'
    exit 1
  fi
}

#
# Run a given operation with a specified number of jobs
#
_run_parallel()
{
  local item            # FLAC file (or directory) to process
  local -i row

  # Starting cursor row to display first item and operation progress
  row=$(( $(_row) - 1 ))

  # Start as many operations as specified by the number of jobs
  for item in "${total_items[@]:0:$operation_jobs}"; do
    (( row++, num++ ))
    "$operation" &
  done

  # Wait on operations if number of items is less than total jobs
  (( ${#total_items[@]} <= operation_jobs )) && wait

  # Completion of an operation sends an integer (terminal row number)
  # and newline to a FIFO (job manager).
  #
  # After each newline is read from the FIFO, another item is processed
  while read -r row; do
    (( num >= ${#total_items[@]} )) && break  # Stop if no more FLACs
    item="${total_items[num++]}"              # Current item to process
    "$operation" &
  done <&3  # Read from FIFO

  # Wait on operations
  wait
}

#
# Sends the value of $row (integer) to the job manager FIFO which is
# from the parent function, _run_parallel()
#
# This value is where the cursor is located for the current operation
# on a given item, allowing the job manager to know where to put output
# for the next job, if any
#
# The newline is needed in order for the 'read' to finish reading the
# output
#
_send_operation_finished()
{
  printf >&3 '%d\n' "$row"
}

#
# Decode FLAC to WAV file (used in operation functions)
#
# NOTE:
#   This function will return 1 if failure was detected and no other
#   operations need to be performed (to exit the parent function)
#
_decode_to_wav()
{
  local output previous error_msg

  # Basename of FLAC, splitting on whitespace since the 'read' below
  # splits on whitespace (to get percentage token, error messages, etc)
  local base="${item##*[ /]}"

  # The parent function's name, eg: _lac -> lac
  local function_label="${FUNCNAME[1]#_}"
  function_label="${function_label^^}"  # lac -> LAC

  # Used in other functions/operations
  local -g item_wav

  # Generate the WAV file path to be created from the current FLAC file
  if [[ "$temporary_wav_location_sub_dir" ]]; then
    # Eg: /tmp/tmp_redoflacs_dir.1234.1 ->
    #     /tmp/tmp_redoflacs_dir.1234.1/music/artist/album/01.flac
    item_wav="${temporary_wav_location_sub_dir%/}/${item#/}"

    # Ensure the relative path is created in the temporary directory
    mkdir -p "${item_wav%/*}"

    # Eg: /tmp/tmp_redoflacs_dir.1234.1/music/artist/album/01.flac ->
    #     /tmp/tmp_redoflacs_dir.1234.1/music/artist/album/01_redoflacs_1234.wav
    item_wav="${item_wav%.*}_redoflacs_$$.wav"
  else
    # Eg: /music/artist/album/01.flac ->
    #     /music/artist/album/01_redoflacs_1234.wav
    item_wav="${item%.*}_redoflacs_$$.wav"
  fi

  _print_item 'Decoding'

  # As indicated by '[FIRST]', '[SECOND]', and '[LAST]', this is the
  # order each of the statements will evaluate to 'true'. '[FIRST]' may
  # never evaluate to 'true' (if the FLAC file fails to decode)
  while IFS=$'\n' read -rd ' ' output; do
    # [LAST] Store error message
    if [[ "$error_msg" && -n "$output" ]]; then
      [[ "$output" == 'state' ]] && break  # End of error message
      error_msg+=" $output"
      continue
    fi

    # [SECOND] An error occurred, storing first word
    if [[ "$output" == 'ERROR' && "$previous" == *"${base}:" ]]; then
      error_msg="$output"
      continue
    fi

    # [FIRST] FLAC decoding percentage complete
    if [[ "$output" == *[[:digit:]]% ]]; then
      output="${output//$'\b'}"  # Remove all backspace characters

      # Remove beginning 'complete' string (from backspace characters)
      printf "\033[${row}H${yellow}%4s$reset" "${output#complete}"
    fi

    # Used to validate previous output is an error and not a filename
    previous="$output"
  done < <(flac 2>&1 "${flac_extra_options[@]}" -d "$item" -o "$item_wav")

  # Log failures if there are any errors
  if [[ "$error_msg" ]]; then
    _print_item_fail

    printf >> "$log_file" '%s [%s] %s\n' "$item" "$function_label" "$error_msg"
    printf >> "$issue_ticks" '.'  # Add one tick to total issues

    # Update operation title message with current number of issues
    _operation_title

    # Return 1 to break out of parent fuction
    _send_operation_finished
    return 1
  else
    _print_item_ok
  fi
}

#
# Generate a spectrogram of current FLAC (used in operation functions)
#
_generate_spectrogram()
{
  local output rest_of_output spectrogram

  # The parent function's name, eg: _aucdtect -> aucdtect
  local function_label="${FUNCNAME[1]#_}"
  function_label="${function_label^^}"  # aucdtect -> AUCDTECT

  # Determine where to put spectrograms by checking user config
  if [[ "$spectrogram_location" ]]; then
    # Eg: /media/specs -> /media/specs/artist/album/file.flac
    spectrogram="${spectrogram_location%/}/${item#/}"

    # Ensure the relative path is created in the spectrogram location
    mkdir -p "${spectrogram%/*}"

    # Eg: /media/specs/artist/album/file.flac ->
    #     /media/specs/artist/album/file.flac.spec.png.0
    spectrogram="${spectrogram}.spec.png.0"
  else
    # Eg: /music/artist/album/file.flac.spec.png.0
    spectrogram="${item}.spec.png.0"
  fi

  # User requested a spectrogram, SoX is run to generate a spectrogram
  # in the location as the current item or a user-defined location
  _print_item 'Spectrogram'

  # Ensure we don't clobber the spectrogram, eg:
  #   /music/artist/album/file.flac.spec.png.0 ->
  #   /music/artist/album/file.flac.spec.png.1
  until [[ ! -f "$spectrogram" ]]; do
    spectrogram="${spectrogram%.*}.$(( ${spectrogram##*.} + 1 ))"
  done

  # SoX display progress percentage on STDERR as well as any error
  # messages. Example output:
  #
  # Error messages:
  #   sox FAIL formats: can't open input file `file.wav': WAVE: RIF<...>
  #   sox FAIL formats: can't open input file `file.flacd': No such<...>
  while IFS=$'\n' read -rd ' ' output; do
    if [[ "$output" != *'In:'[[:digit:]]*'%' ]]; then
      rest_of_output+=" $output"  # Re-add spacing
      continue
    fi

    # Strip from leading string
    output="${output##*In:}"  # Strip leading string
    output="${output%\%}"     # Strip trailing percentage
    output="${output%%.*}"    # Strip floating point from output

    # Re-add percentage when printing
    printf "\033[${row}H${yellow}%4s$reset" "$output%"
  done < <(
    sox 2>&1 "$item" -S -n spectrogram -o "$spectrogram" -t "$item" \
      -c '' -p 1 -z 90 -Z 0 -q 249 -w Hann -x 1800 -y 513
  )

  # Strip leading space from generated output
  IFS=' ' read -r rest_of_output <<< "$rest_of_output"

  # Log failures if there are any errors from SoX
  if [[ "$rest_of_output" == 'sox FAIL'* ]]; then
    _print_item_fail
    printf >> "$log_file" \
      '%s [%s] %s\n' "$item" "${function_label}:Spectrogram" "$rest_of_output"

  # Log operation status as well as generated spectrogram location
  else
    _print_item_check
    printf >> "$log_file" \
      '%s [%s] %s (%s)\n' \
      "$item" "${function_label}:Spectrogram" \
      "$lossless_check_results" "$spectrogram"
  fi

  printf >> "$issue_ticks" '.'  # Add one tick to total issues

  # Update operation title message with current number of issues
  _operation_title
  _send_operation_finished
}

#
# Apply ReplayGain tags to each FLAC file in a given directory.
# Conditionally skip FLAC files if ReplayGain tags are already set. If
# there are any errors, they are logged
#
_replaygain()
{
  local output error_msg
  local -a error_log files

  # Used to look for error messages and store wanted values
  local regex="^(${item}/.*): (ERROR:.*)"

  _print_item 'Applying ReplayGain'

  # If a user chooses not to force apply ReplayGain tags, then each file
  # is checked to ensure all ReplayGain tags are available, skipping the
  # directory of files if all ReplayGain tags are already set
  #
  # NOTE:
  #   All errors are ignored here as they will be caught during
  #   application ReplayGain tags
  if [[ ! "$force_replaygain" ]]; then
    files=( "$item"/*.flac )  # Used for number of files

    # Obtain ReplayGain tags from all files in the current directory
    mapfile -n0 -t replaygain_tags < <(
      metaflac 2>/dev/null "${metaflac_extra_options[@]}" \
        --show-tag='REPLAYGAIN_REFERENCE_LOUDNESS' \
        --show-tag='REPLAYGAIN_TRACK_GAIN' \
        --show-tag='REPLAYGAIN_TRACK_PEAK' \
        --show-tag='REPLAYGAIN_ALBUM_GAIN' \
        --show-tag='REPLAYGAIN_ALBUM_PEAK' \
        "${files[@]}"
    )

    # All ReplayGain tags are set, so this directory can be skipped
    #
    # The total number of ReplayGain tags found on all files in the
    # current directory must equal the total number of files found
    # multiplied by 5 (the number of ReplayGain tags added to each file)
    if (( ${#replaygain_tags[@]} == ${#files[@]} * 5 )); then
      _print_item_skip
      _send_operation_finished
      return
    fi
  fi

  _print_item_half  # Metaflac doesn't display a percentage complete

  # Since this operation applies to multiple files in a given directory,
  # all error messages (per file) is stored into an array, then logged
  # to a file
  #
  # Metaflac only displays error messages to STDERR, nothing to STDOUT
  while IFS=$'\n' read -r output; do
    # Any output is an error message
    if [[ "$output" =~ $regex ]]; then
      error_msg="${BASH_REMATCH[1]} [ReplayGain] "  # Absolute filename

      # Error reading FLAC file (possibly fake), eg:
      #   ERROR: reading metadata, status = "FLAC__METADA ..."
      if [[ "${BASH_REMATCH[2]}" == *", status = "* ]]; then
        error_msg+="${BASH_REMATCH[2]%, status = *} "      # Error

      # Files with differing sample rates, or problems decoding, eg:
      #   ERROR: sample rate of 44100 Hz does not match ...
      #   ERROR: during analysis (decoding file)
      else
        error_msg+="${BASH_REMATCH[2]}"
      fi

      # All FLAC files are stored here to be logged
      error_log+=( "$error_msg" )
    fi
  done < <(
    metaflac 2>&1 "${metaflac_extra_options[@]}" \
      --add-replay-gain "$item"/*.flac
  )

  # Log failures if there are any errors
  if [[ "${error_log[0]}" ]]; then
    _print_item_fail
    printf >> "$log_file" '%s\n' "${error_log[@]}"
    printf >> "$issue_ticks" '.'  # Add one tick to total issues

    # Update operation title message with current number of issues
    _operation_title
  else
    _print_item_ok
  fi

  _send_operation_finished
}

#
# Compress FLAC files with user-defined compression, verifying
# integrity. Conditionally skip FLAC files if the 'COMPRESSION' tag
# exists for a given file and matches the compression level set in the
# user configuration file
#
_compress()
{
  local file_compression output previous error_msg warning_msg

  # Basename of FLAC, splitting on whitespace since the 'read' below
  # splits on whitespace (to get percentage token, error messages, etc)
  local base="${item##*[ /]}"

  _print_item 'Compressing'

  # If a user chooses not to force compression, then each file is
  # checked for a 'COMPRESSION' tag that matches the compression level
  # set in the user configuration. If the 'COMPRESSION' tag exists and
  # matches what is set in the configuration, then this file is skipped
  #
  # NOTE:
  #   All errors are ignored here as they will be caught during
  #   compression
  if [[ ! "$force_compression" ]]; then
    file_compression="$(
      metaflac 2>/dev/null "${metaflac_extra_options[@]}" \
        --show-tag='COMPRESSION' "$item"
    )"

    # 'compression_level' is obtained from the configuration file, in
    # form of 'COMPRESSION=<value>'
    if
      [[ -n "$file_compression" ]] \
      && (( ${file_compression##*=} == compression_level ))
    then
      _print_item_skip
      _send_operation_finished
      return
    fi
  fi

  # As indicated by '[FIRST]', '[SECOND]', '[THIRD]', '[FOURTH]', and
  # '[LAST]', this is the order each of the statements will evaluate to
  # 'true'. '[FIRST]' may never evaluate to 'true' (if the FLAC file
  # fails to decode
  while IFS=$'\n' read -rd ' ' output; do
    # [LAST] Store error message
    if [[ "$error_msg" && -n "$output" ]]; then
      # End of error message
      [[ "$output" == 'state' || "$output" == '"flac"' ]] && break
      error_msg+=" $output"
      continue
    fi

    # [FOURTH] An error occurred, storing first word
    if [[ "$output" == 'ERROR:' && "$previous" == *"${base}:" ]]; then
      error_msg="$output"
      continue
    fi

    # [THIRD] Store rest of warning message
    if [[ "$warning_msg" && -n "$output" ]]; then
      # Stop if there is an error message and build that separately
      [[ "$output" == *'ERROR:' ]] && error_msg='ERROR:' && continue
      warning_msg+=" $output"
      continue
    fi

    # [SECOND] A warning occurred, storing first word
    #
    # NOTE:
    #   Due to 'read' using a space a delimiter, the WARNING message may
    #   have preceding text (word + newline)
    if [[ "$output" == *'WARNING:' ]]; then
      warning_msg='WARNING:'
      continue
    fi

    # [FIRST] FLAC compression percentage complete
    if [[ "$output" == *[[:digit:]]% ]]; then
      output="${output//$'\b'}"  # Remove all backspace characters

      # Remove beginning ratio string (from backspace characters)
      printf "\033[${row}H${yellow}%4s$reset" "${output#ratio=?.???}"
    fi

    # Used to validate previous output is an error and not a filename
    previous="$output"
  done < <(
    flac 2>&1 "${flac_extra_options[@]}" \
      -f -"$compression_level" -V "$flac_compress_threads" "$item"
  )

  # Add warning message to error message for logging
  [[ "$warning_msg" ]] && error_msg="$warning_msg|$error_msg"

  # Log failures if there are any warnings and/or errors
  if [[ "$error_msg" ]]; then
    _print_item_fail
    error_msg="${error_msg%,}"          # Strip possible trailing comma
    error_msg="${error_msg/$'\n'Type}"  # Strip possible multiline error
    printf >> "$log_file" '%s [Compress] %s\n' "$item" "$error_msg"
    printf >> "$issue_ticks" '.'        # Add one tick to total issues

    # Update operation title message with current number of issues
    _operation_title
    _send_operation_finished
    return
  fi

  # Add a 'COMPRESSION' tag to the current FLAC file, to indicate which
  # compression level was used when compressing (only if user did not
  # specify no extra tags to be applied)
  #
  # NOTE:
  #   Errors are ignored as the FLAC file was verified during encoding
  if [[ ! "$no_extra_tags" ]]; then
    metaflac 2>/dev/null "${metaflac_extra_options[@]}" \
      --remove-tag='COMPRESSION' \
      --set-tag="COMPRESSION=$compression_level" "$item"
  fi

  _print_item_ok
  _send_operation_finished
}

#
# Tests current FLAC file integrity, reading all output, displaying the
# current percentage complete. If there is an error that error is logged
#
_test()
{
  local output previous error_msg

  # Basename of FLAC, splitting on whitespace since the 'read' below
  # splits on whitespace (to get percentage token, error messages, etc)
  local base="${item##*[ /]}"

  _print_item 'Testing'

  # As indicated by '[FIRST]', '[SECOND]', and '[LAST]', this is the
  # order each of the statements will evaluate to 'true'. '[FIRST]' may
  # never evaluate to 'true' (if the FLAC file fails to decode)
  while IFS=$'\n' read -rd ' ' output; do
    # [LAST] Store error message
    if [[ "$error_msg" && -n "$output" ]]; then
      [[ "$output" == 'state' ]] && break  # End of error message
      error_msg+=" $output"
      continue
    fi

    # [SECOND] An error occurred, storing first word
    if [[ "$output" == 'ERROR' && "$previous" == *"${base}:" ]]; then
      error_msg="$output"
      continue
    fi

    # [FIRST] FLAC testing percentage complete
    if [[ "$output" == *[[:digit:]]% ]]; then
      printf "\033[${row}H${yellow}%4s$reset" "$output"
    fi

    # Used to validate previous output is an error and not a filename
    previous="$output"
  done < <(flac 2>&1 "${flac_extra_options[@]}" -t "$item")

  # Log failures if there are any errors
  if [[ "$error_msg" ]]; then
    _print_item_fail
    printf >> "$log_file" '%s [Test] %s\n' "$item" "$error_msg"
    printf >> "$issue_ticks" '.'  # Add one tick to total issues

    # Update operation title message with current number of issues
    _operation_title
  else
    _print_item_ok
  fi

  _send_operation_finished
}

#
# Test FLAC validity with Lossless Audio Checker (LAC)
#
_lac()
{
  local mastering output rest_of_output spectrogram
  local -i count

  # Used in other functions
  local -g lossless_check_results

  # Unit separator (replace newlines from LAC's output with US)
  local us=$'\037'

  # Used to look for error messages and file status in LAC which is
  # captured via STDOUT
  #
  # Example STDOUT from LAC (error message):
  #
  #   Lossless Audio Checker 2.0.5
  #   Copyright (c) 2012-2016 Julien Lacroix & Yann Prime
  #   http://losslessaudiochecker.com
  #   ==================================================
  #   File:   invalid_file.wav
  #   Result: WAVE header RIFF chunk: bad Size
  #
  # Example STDOUT from LAC (file status):
  #
  #   Lossless Audio Checker 2.0.5
  #   Copyright (c) 2012-2016 Julien Lacroix & Yann Prime
  #   http://losslessaudiochecker.com
  #   ==================================================
  #   File:   bad_file.wav
  #   Result: Upsampled
  #
  # Example regex capture groups obtained:
  #
  # Error messages:
  #
  #   WAVE header RIFF chunk: bad Size
  #   WAVE header FMT chunk: not found
  #   WAVE header EXTENSIBLE chunk: bad Channel Mask
  #   WAVE header DATA chunk: bad ID
  #
  # Successful operations:
  #
  #   Clean
  #   Transcoded
  #   Upscaled
  #   Upsampled
  local regex="${us}Result: (.*)${us}$"

  # Check for a result from LAC that is NOT clean
  local regex_non_clean='^(transcoded|upscaled|upsampled)'

  _print_item 'LAC'

  # Get the MASTERING tag of a FLAC file. Errors will be caught during
  # decoding
  mastering="$(
    metaflac 2>/dev/null "${metaflac_extra_options[@]}" \
      --show-tag='MASTERING' "$item"
  )"

  # Skip FLAC file if MASTERING=Lossy is set and the 'skip_lossy'
  # configuration option is 'true'
  if [[ "$mastering" == 'Lossy' && "$skip_lossy" == 'true' ]]; then
    _print_item_skip
    _send_operation_finished
    return
  fi

  # Decode FLAC file to WAV file as LAC does not support FLAC or decoded
  # streams over a pipe
  #
  # NOTE:
  #   Break out of current function if there were failures decoding
  _decode_to_wav || return

  _print_item 'LAC'

  # Runs LAC and parses out the progress bar (made up of '=') and
  # generates a percentage of completion along with storing the result
  # of the operation
  #
  # NOTE:
  #   Processing LAC's output is done by character where 'read' is
  #   reading in a single character at a time
  while IFS=$'\n' read -rn1 output; do
    # The first '=' found is the progress bar (max 50), which is counted
    # and integer scaling division is performed to get the percentage
    if [[ "$output" == '=' ]]; then
      printf "\033[${row}H${yellow}%4s$reset" "$(( (++count * 100) / 50 ))%"
      continue
    fi

    # The rest of the output (after progress bar) is the file and result
    if (( count == 50 )); then
      # All newlines are null, which is replaced by a unit separator
      [[ -z "$output" ]] && rest_of_output+="$us" || rest_of_output+="$output"
    fi
  done < <(LAC "$item_wav")

  # Remove temporary WAV file
  rm -f "${item_wav:-/dev/null/foo}"

  # Looks for the result from LAC
  [[ "$rest_of_output" =~ $regex ]]

  # Either 'Clean', 'Transcoded', 'Upscaled', or 'Upsampled'
  lossless_check_results="${BASH_REMATCH[1]}"

  # LAC reports a clean file, continue with next item
  if [[ "${lossless_check_results,,}" == 'clean' ]]; then
    _print_item_ok
    _send_operation_finished
    return
  fi

  # Log failure if not clean and no spectrogram is requested
  if
    [[ "${lossless_check_results,,}" =~ $regex_non_clean ]] \
    && [[ ! "$create_spectrogram" ]]
  then
    _print_item_check
    printf >> "$log_file" \
      '%s [%s] %s\n' "$item" 'LAC' "$lossless_check_results"
    printf >> "$issue_ticks" '.'  # Add one tick to total issues

    # Update operation title message with current number of issues
    _operation_title
    _send_operation_finished
    return
  fi

  # Generate a spectrogram of the current FLAC file and log the
  # results of the operation along with the spectrogram location
  _generate_spectrogram
}

#
# Test FLAC validity with auCDtect
#
# NOTE:
#   While not officially supported, should a user have a dynamically
#   linked auCDtect binary, 'MALLOC_CHECK_' will to be set to '0', eg:
#
#     MALLOC_CHECK_='0' auCDtect <rest_of_options>
#
_aucdtect()
{
  local output aucdtect_stdout aucdtect_stderr spectrogram
  local -a bits_mastering

  # Used in other functions
  local -g lossless_check_results

  # Used to look for error messages and file status in auCDtect which is
  # captured via STDOUT
  #
  # Example STDOUT from auCDtect (error message):
  #
  #   auCDtect: CD records authenticity detector, version 0.8
  #   Copyright (c) 2004 Oleg Berngardt. All rights reserved.
  #   Copyright (c) 2004 Alexander Djourik. All rights reserved.
  #   error:  file format     is not supported
  #
  # Example STDOUT from auCDtect (file status):
  #
  #   auCDtect: CD records authenticity detector, version 0.8
  #   Copyright (c) 2004 Oleg Berngardt. All rights reserved.
  #   Copyright (c) 2004 Alexander Djourik. All rights reserved.
  #   ------------------------------------------------------------
  #   Processing file:        [01 - The Manifesto.wav]
  #
  #   ------------------------------------------------------------
  #   This track looks like CDDA with probability 85%
  #
  # Example regex capture groups obtained:
  #
  # Error messages:
  #
  #   error:  can     not     open file <WAV_FILE>
  #   error:  file format     is not supported
  #
  # Successful operations:
  #
  #   This track looks like CDDA with probability 100%
  #   This track looks like CDDA with probability 49%
  #   This track looks like MPEG with probability 100%
  #   This track looks like MPEG with probability 92%
  #   Could not qualify the source of this track.
  local regex=$'(error:.*)|Processing file.*--------.*\n(.*)$'

  _print_item 'auCDtect'

  # Get the bit depth and MASTERING tag of a FLAC file. Errors will be
  # caught during decoding. Indices are:
  #   bits_mastering[0] = bit depth (eg: 16)
  #   bits_mastering[1] = MASTERING tag and value (eg: MASTERING=Lossy)
  mapfile -n2 -t bits_mastering < <(
    metaflac 2>/dev/null "${metaflac_extra_options[@]}" \
      --show-bps --show-tag='MASTERING' "$item"
  )

  # auCDtect does not support a bit depth greater than 16 (CD quality
  # only), skipping file
  #
  # NOTE:
  #   A default value of 0 is specified in the event the bit depth could
  #   not be obtained. Errors will be caught during decoding
  if (( ${bits_mastering[0]:-0} > 16 )); then
    _print_item_skip
    _send_operation_finished
    return
  fi

  # Skip FLAC file if MASTERING=Lossy is set and the 'skip_lossy'
  # configuration option is 'true'
  if [[
    "${bits_mastering[1]#*=}" == 'Lossy'
    && "$skip_lossy" == 'true'
  ]]; then
    _print_item_skip
    _send_operation_finished
    return
  fi

  # Decode FLAC file to WAV file as LAC does not support FLAC or decoded
  # streams over a pipe
  #
  # NOTE:
  #   Break out of current function if there were failures decoding
  _decode_to_wav || return

  # Runs auCDtect with medium accuracy setting (for speed):
  #   STDOUT: auCDtect STDOUT captured to be parsed
  #   STDERR: auCDtect progress percentage completed sent to FD4
  _print_item 'auCDtect:Fast'
  exec 4>&1
  aucdtect_stdout="$(
    MALLOC_CHECK_='0' auCDtect -m20 "$item_wav" 2> >(
      while IFS=$'\n' read -rd $'\r' aucdtect_stderr; do
        aucdtect_stderr="${aucdtect_stderr##*[}"
        printf >&4 "\033[${row}H${yellow}%4s$reset" "${aucdtect_stderr%]}"
      done
    )
  )"
  exec 4>&-

  # Looks for error messages and file status from auCDtect
  #   BASH_REMATCH[1]: auCDtect error messages
  #   BASH_REMATCH[2]: auCDtect file status
  [[ "$aucdtect_stdout" =~ $regex ]]

  # Store auCDtect status of file
  lossless_check_results="${BASH_REMATCH[2]}"

  # Log failures if there are any errors
  if [[ "${BASH_REMATCH[1]}" ]]; then
    _print_item_fail
    printf >> "$log_file" \
      '%s [%s] %s\n' "$item" 'auCDtect' "${BASH_REMATCH[1]}"
    printf >> "$issue_ticks" '.'  # Add one tick to total issues

    # Remove temporary WAV file
    rm -f "${item_wav:-/dev/null/foo}"

    # Update operation title message with current number of issues
    _operation_title
    _send_operation_finished
    return

  # auCDtect reports CDDA of 100%, continue with next item
  elif [[ "$lossless_check_results" == *'CDDA'*'100%'* ]]; then
    _print_item_ok

    # Remove temporary WAV file
    rm -f "${item_wav:-/dev/null/foo}"

    _send_operation_finished
    return
  fi

  # Current item is not CDDA of 100%, auCDtect is run with highest
  # accuracy setting:
  #   STDOUT: auCDtect STDOUT captured to be parsed
  #   STDERR: auCDtect progress percentage completed sent to FD4
  _print_item 'auCDtect:Slow'
  exec 4>&1
  aucdtect_stdout="$(
    MALLOC_CHECK_='0' auCDtect -m0 "$item_wav" 2> >(
      while IFS=$'\n' read -rd $'\r' aucdtect_stderr; do
        aucdtect_stderr="${aucdtect_stderr##*[}"
        printf >&4 "\033[${row}H${yellow}%4s$reset" "${aucdtect_stderr%]}"
      done
    )
  )"
  exec 4>&-

  # Looks for error messages and file status from auCDtect
  #   BASH_REMATCH[1]: auCDtect error messages
  #   BASH_REMATCH[2]: auCDtect file status
  [[ "$aucdtect_stdout" =~ $regex ]]

  # Store auCDtect status of file (also used in other functions)
  lossless_check_results="${BASH_REMATCH[2]}"

  # Log failures if there are any errors
  if [[ "${BASH_REMATCH[1]}" ]]; then
    _print_item_fail
    printf >> "$log_file" \
      '%s [%s] %s\n' "$item" 'auCDtect' "${BASH_REMATCH[1]}"
    printf >> "$issue_ticks" '.'  # Add one tick to total issues

    # Remove temporary WAV file
    rm -f "${item_wav:-/dev/null/foo}"

    # Update operation title message with current number of issues
    _operation_title
    _send_operation_finished
    return
  fi

  # auCDtect reports CDDA of 100%, continue with next item
  if [[ "$lossless_check_results" == *'CDDA'*'100%'* ]]; then
    _print_item_ok

    # Remove temporary WAV file
    rm -f "${item_wav:-/dev/null/foo}"

    _send_operation_finished
    return
  fi

  # Log failure if not CDDA with 100% and no spectrogram is requested
  if
    [[ "$lossless_check_results" != *'CDDA'*'100%'* ]] \
    && [[ ! "$create_spectrogram" ]]
  then
    _print_item_check
    printf >> "$log_file" \
      '%s [%s] %s\n' "$item" 'auCDtect' "$lossless_check_results"
    printf >> "$issue_ticks" '.'  # Add one tick to total issues

    # Remove temporary WAV file
    rm -f "${item_wav:-/dev/null/foo}"

    # Update operation title message with current number of issues
    _operation_title
    _send_operation_finished
    return
  fi

  # Remove temporary WAV file
  rm -f "${item_wav:-/dev/null/foo}"

  # Generate a spectrogram of the current FLAC file and log the
  # results of the operation along with the spectrogram location
  _generate_spectrogram
}

#
# Check for a valid MD5 checksum in FLAC file
#
_md5check()
{
  local output error_msg md5sum

  _print_item 'Verifying MD5'

  # Metaflac will display the MD5 checksum or an error (on one line).
  # These values are captured and logged, appropriately
  while IFS=$'\n' read -r output; do
    # Error reading FLAC file (possibly fake), eg:
    #   fake/fake.flac: ERROR: reading metadata, status = "FLAC ..."
    if [[ "$output" == *'ERROR'* ]]; then
      error_msg="${output%, status = *}"      # Strip status
      error_msg="ERROR${error_msg#*: ERROR}"  # Strip filename
      break                                   # Stop reading lines
    fi

    # This is the MD5 checksum
    md5sum="$output"
  done < <(metaflac 2>&1 "${metaflac_extra_options[@]}" --show-md5sum "$item")

  # Log failures if there are any errors
  if [[ "$error_msg" ]]; then
    _print_item_fail
    printf >> "$log_file" '%s [MD5check] %s\n' "$item" "$error_msg"
    printf >> "$issue_ticks" '.'  # Add one tick to total issues

    # Update operation title message with current number of issues
    _operation_title

  # Possible valid FLAC, but MD5 signature is unset in STREAMINFO block
  elif [[ "$md5sum" == '00000000000000000000000000000000' ]]; then
    _print_item_fail
    printf >> "$log_file" '%s [MD5check] %s\n' "$item" \
      'ERROR: Unset MD5 signature (00000000000000000000000000000000)'
    printf >> "$issue_ticks" '.'  # Add one tick to total issues

    # Update operation title message with current number of issues
    _operation_title
  else
    _print_item_ok
  fi

  _send_operation_finished
}

#
# Looks for the tags specified in the configuration file, stripping all
# other tags from a given FLAC file, and logging any tags which are
# missing
#
# Process substitution when extracting tags is used to ensure trailing
# newlines are not stripped
#
_retag()
{
  local output error_msg line extracted_tag key empty_tag tag missing_tags_str
  local -i index='-1'
  local -a missing_tags found_tags

  _print_item 'Retagging'

  # Extracts the first tag from the list of tags to keep and does error
  # handling on this operation. Other extracted tags are processeed in
  # the following loop after the error handling
  #
  # Metaflac will display errors on one line. These are handled first
  # with the tag value obtained last (if no failures)
  while IFS=$'\n' read -r output; do
    # Error reading FLAC file (possibly fake), eg:
    #   fake/fake.flac: ERROR: reading metadata, status = "FLAC ..."
    if [[ "$output" == *'ERROR'* ]]; then
      error_msg="${output%, status = *}"      # Strip status
      error_msg="ERROR${error_msg#*: ERROR}"  # Strip filename
      break                                   # Stop reading lines
    fi

    # This is the extracted tag (could be empty or multiple lines)
    if [[ "$extracted_tag" ]]; then
      extracted_tag+=$'\n'"$output"
    else
      extracted_tag+="$output"
    fi
  done < <(
    metaflac 2>&1 "${metaflac_extra_options[@]}" \
      --show-tag="${tags_to_keep[0]}" "$item"
  )

  # Log failures if there are any errors
  if [[ "$error_msg" ]]; then
    _print_item_fail
    printf >> "$log_file" '%s [Retag] %s\n' "$item" "$error_msg"
    printf >> "$issue_ticks" '.'  # Add one tick to total issues

    # Update operation title message with current number of issues
    _operation_title
    _send_operation_finished
    return
  fi

  # Look for each of the tags to keep in the current FLAC flac,
  # separating each of the tags as separate indices in an array. If
  # there are any missing tags, these are stored as well
  for key in "${!tags_to_keep[@]}"; do
    # The first tag (index=0) was already obtained during error checking
    if (( key != 0 )); then
      IFS='' read -rd '' extracted_tag < <(
        metaflac 2>&1 "${metaflac_extra_options[@]}" \
          --show-tag="${tags_to_keep[key]}" "$item"
      )

      # Strips trailing newline due to the process substitution
      # preserving trailing newlines
      extracted_tag="${extracted_tag%$'\n'}"
    fi

    # The tag doesn't exist in the current item
    if [[ -z "$extracted_tag" ]]; then
      missing_tags+=( "${tags_to_keep[key]}" )
      continue
    fi

    # Processes the extracted tag, looking for multiple tags of the same
    # type as well and handling multiple line values for a given tag
    # value. For any valid tags, each are stored as a separate index in
    # an array
    #
    # For any stored tag, the tag field is converted to uppercase and
    # the tag values are normalized (whitespace, zero-prefixed, etc)
    while IFS=$'\n' read -r line; do
      # Tag exists but the value is empty. This may be a missing tag
      if [[ "${line^^}" == "${tags_to_keep[key]^^}=" ]]; then
        # If a previous line was an empty tag field, this means the
        # previous tag was, in fact, empty
        [[ "$empty_tag" ]] && missing_tags+=( "$empty_tag" )

        # Sets the tag field as empty and is used to check any
        # additional lines which may be part of this tag
        empty_tag="${tags_to_keep[key]^^}"
        continue

      # Tag exists and has a value (first line value only)
      elif [[ "${line^^}" == "${tags_to_keep[key]^^}="* ]]; then
        # Strip leading/trailing whitespace
        IFS=$' \t\n' read -r line <<< "${line#*=}"

        # Convert TRACKNUMBER and TRACKTOTAL to zero-prefixed numbers if
        # the 'prepend_zero' configuration option is 'true'
        if [[
          (
            "${tags_to_keep[key]}" == 'TRACKNUMBER'
            || "${tags_to_keep[key]}" == 'TRACKTOTAL'
          )
          && "$prepend_zero" == 'true'
        ]]; then
          # Only modify value if the value is an integer
          if [[ "$line" =~ ^[[:digit:]]+$ ]]; then
            printf -v line '%02d' $(( 10#$line ))
          fi
        fi

        # Ensure tag field is uppercase and array index, incremented.
        # The index starts at -1, so is pre-incremented to 0 before
        # setting a value
        found_tags[++index]="${tags_to_keep[key]^^}=$line"
        continue
      fi

      # Strip leading/trailing whitespace
      IFS=$' \t\n' read -r line <<< "$line"

      # If this variable is set, it means the previous line was an empty
      # tag field but since this point is reached, the current line is a
      # part of the tag so the previous line is a valid tag. The tag is
      # considered NOT empty and is saved with this line as part of the
      # tag's value. An example tag where this could happen:
      #
      #    LYRICS=
      #
      #    This is a line for the above 'LYRICS' tag field.
      #    This is another line which is a part of the same tag.
      #
      # The first line (LYRICS=) is originally considered an empty tag,
      # but the following lines are processed and added to the tag
      # making this tag valid and non-empty
      if [[ "$empty_tag" ]]; then
        found_tags[++index]="${empty_tag}="$'\n'"$line"
        empty_tag='' # Reset as the tag is considered valid
      else
        # Another value (separate line) for the current tag field
        found_tags[index]+=$'\n'"${line}"
      fi
    done <<< "$extracted_tag"
  done

  # Runs through the missing tags and only stores the tag as missing if
  # there are no other tags found which match the tag field. This could
  # happen, if there are multiple tags of the same tag field and some
  # are empty, but at least one is valid, eg:
  #
  #   DATE=
  #   DATE=
  #   DATE=2001
  #
  # The DATE tag is considered missing, but when processed below, would
  # be considered found and valid (the other empty DATE tags will be
  # removed)
  for tag in "${!missing_tags[@]}"; do
    # Looks through all the valid and normalized tags found
    for valid_tag in "${found_tags[@]}"; do
      # Matched a tag and value for a missing tag, no longer missing
      if [[ "$valid_tag" == "${missing_tags[tag]}="* ]]; then
        unset -v 'missing_tags[tag]'
        break
      fi
    done
  done

  # Removes all tags from the current FLAC file and sets each found tag
  # all in one operation. A single operation prevents SIGINT from
  # leaving the current FLAC file without any tags
  #
  # Metaflac >= 1.4.3 added '--remove-all-tags-except=NAME1=NAME2...'
  # which is a safer operation
  if
    ((
      flac_major > 1 || flac_minor > 4 || (flac_minor == 4 && flac_patch >= 3)
    ))
  then
    # Store only the tag fields and join into a '=' separated string
    printf -v found_tags '%s=' "${found_tags[@]/%=*}"

    # Ensure trailing '=' is removed
    metaflac >/dev/null 2>&1 "${metaflac_extra_options[@]}" \
      --remove-all-tags-except="${found_tags%=}"  "$item"
  else
    metaflac >/dev/null 2>&1 "${metaflac_extra_options[@]}" \
      --remove-all-tags "${found_tags[@]/#/--set-tag=}" "$item"
  fi

  # Logs missing tags, if any were found
  if [[ "${missing_tags[*]}" ]]; then
    _print_item_check

    # Generates a comma-separated list of missing tags to be logged.
    # Ensures spaces in tag fields are properly handled
    for tag in "${missing_tags[@]}"; do
      if [[ "$missing_tags_str" ]]; then
        missing_tags_str+=", $tag"
      else
        missing_tags_str+="$tag"
      fi
    done

    printf >> "$log_file" \
      '%s [Retag] Missing tags: %s\n' "$item" "$missing_tags_str"
    printf >> "$issue_ticks" '.'  # Add one tick to total issues

    # Update operation title message with current number of issues
    _operation_title
  else
    _print_item_ok
  fi

  _send_operation_finished
}

#
# Extact embedded artwork from a given FLAC
#
_extract_images()
{
  local -a artwork
  local error_msg block_id art_id art_description mime img
  local blocks="/tmp/redoflacs_picture_blocks.$BASHPID"

  # Ensure we don't clobber the temporary PICTURE blocks file, eg:
  #   /tmp/redoflacs_picture_blocks.0 -> /tmp/redoflacs_picture_blocks.1
  until [[ ! -f "$blocks" ]]; do
    blocks="${blocks%.*}.$(( ${blocks##*.} + 1 ))"
  done

  _print_item 'Extracting Images'

  # Retrieve PICTURE blocks from current item, to a temporary file
  error_msg="$(
    metaflac 2>&1 >"$blocks" "${metaflac_extra_options[@]}" \
      --list --block-type=PICTURE "$item"
  )"

  # Log FLAC failure
  if [[ -n "$error_msg" ]]; then
    error_msg="${error_msg%, status = *}"       # Strip end of error msg
    error_msg="ERROR ${error_msg##*: ERROR: }"  # Strip filename

    _print_item_fail
    printf >> "$log_file" '%s [ExtractArtwork] %s\n' "$item" "$error_msg"
    printf >> "$issue_ticks" '.'  # Add one tick to total issues
    rm -f "${blocks:-/dev/null/foo}"

    # Update operation title message with current number of issues
    _operation_title
    _send_operation_finished
    return
  fi

  # Skip if there were no PICTURE blocks found in current FLAC
  if [[ ! -s "$blocks" ]]; then
    _print_item_skip
    rm -f "${blocks:-/dev/null/foo}"
    _send_operation_finished
    return
  fi

  _print_item_half  # Metaflac doesn't display a percentage complete

  # Store the values which can identify each image (in each PICTURE
  # block) to be used during image extraction. The values needed are:
  #   'METADATA block #<num>'           -> $block_id
  #   '  type: <num> (<description>)'   -> $art_id AND $art_description
  #   '  MIME type: image/jpeg'         -> $mime
  #
  # NOTE:
  #   Reading the PICTURE block from a temporary file is much faster
  #   than reading from process substitution (there can be many PICTURE
  #   blocks in a single FLAC file and each image could be very large)
  while IFS=$'\n' read -r; do
    # Skip all the unnecessary information
    [[ "$REPLY" != 'METADATA'* && "$REPLY" != '  '[tM]* ]] && continue

    if [[ "$REPLY" == 'METADATA'* ]]; then
      block_id="${REPLY##*#}"
      continue
    fi

    # Ensure we don't use: 'type: 6 (PICTURE)'
    if [[ "$REPLY" == '  type: '[^6]* ]]; then
      IFS=' ' read -r _ art_id art_description <<< "${REPLY//\//-}"
      art_description="${art_description//[()]}"  # Strip parenthesis
      art_description="${art_description// /_}"   # Spaces underscores
      continue

    # Done with a PICTURE block; the values can be stored and the
    # variables reset
    elif [[ "$REPLY" == '  MIME'* ]]; then
      mime="${REPLY##*/}"     # Eg: 'MIME type: image/jpeg' -> 'jpeg'
      mime="${mime/jpeg/jpg}" # Eg: 'jpeg'                  -> 'jpg'

      # Eg: '4:3-Cover_front.jpg'
      artwork+=( "${block_id}:${art_id}-${art_description}.${mime}" )
      unset -v 'block_id' 'art_id' 'art_description' 'mime'
    fi
  done < "$blocks"
  rm -f "${blocks:-/dev/null/foo}"

  # Continue only if there was artwork to be extracted
  if [[ -z "${artwork[*]}" ]]; then
    _send_operation_finished
    return
  fi

  # Determine where to put extracted artwork by checking user config
  if [[ "$artwork_location" ]]; then
    # Eg: /media/images -> /media/images/artist/album/file.flac
    artwork_location="${artwork_location%/}/${item#/}"

    # Ensure the relative path is created in the artwork location
    mkdir -p "${artwork_location%/*}"

    # Eg: /media/images/artist/album/file.flac ->
    #     /media/images/artist/album/file.flac.art.0
    artwork_location="${artwork_location}.art.0"
  else
    # Eg: /music/artist/album/file.flac.art.0
    artwork_location="${item}.art.0"
  fi

  # Ensure we don't clobber the artwork directory, eg:
  #   /music/artist/album/file.flac.art.0 ->
  #   /music/artist/album/file.flac.art.1
  until [[ ! -d "$artwork_location" ]]; do
    artwork_location="${artwork_location%.*}.$((
      ${artwork_location##*.} + 1
    ))"
  done
  mkdir -p "$artwork_location"

  # Performs artwork extraction on the current item. Each array value:
  #   '<block_id>:<art_id>-<art_description>.<mime>
  #
  # Which looks like:
  #   '4:3-Cover_front.jpg'
  for value in "${artwork[@]}"; do
    # Eg: '/music/artist/album/file.flac.art.0/3-Cover_front.jpg.0'
    img="${artwork_location}/${value#*:}.0"
    block_id="${value%%:*}"  # Eg: '4:3-Cover_front.jpg' -> '4'

    # Ensure we don't clobber the extracted image, eg:
    #   /music/artist/album/file.flac.art.0/3-Cover_front.jpg.0 ->
    #   /music/artist/album/file.flac.art.0/3-Cover_front.jpg.1
    until [[ ! -f "$img" ]]; do
      img="${img%.*}.$(( ${img##*.} + 1 ))"
    done

    # Extracts each image by METADATA block number
    metaflac "${metaflac_extra_options[@]}" \
      --block-number="$block_id" --export-picture-to="$img" "$item"
  done

  _print_item_ok
  _send_operation_finished
}

#
# Prune user configurable FLAC metadata from FLAC files
#
_prune()
{
  local output error_msg

  _print_item 'Pruning'
  _print_item_half  # Metaflac doesn't display a percentage complete

  # Remove all information but STREAMINFO,VORBIS_COMMENTs, and
  # possibly METADATA_BLOCK_PICTURE
  while IFS=$'\n' read -r output; do
    # Error reading FLAC file (possibly fake), eg:
    #   fake/fake.flac: ERROR: reading metadata, status = "FLAC ..."
    if [[ "$output" == *'ERROR'* ]]; then
      error_msg="${output%, status = *}"      # Strip status
      error_msg="ERROR${error_msg#*: ERROR}"  # Strip filename
      break                                   # Stop reading lines
    fi
  done < <(
    metaflac 2>&1 "${metaflac_extra_options[@]}" \
      --remove --dont-use-padding \
      --except-block-type="$dont_prune_flac_metadata" "$item"
  )

  # Log failures if there are any errors
  if [[ "$error_msg" ]]; then
    _print_item_fail
    printf >> "$log_file" '%s [Prune] %s\n' "$item" "$error_msg"
    printf >> "$issue_ticks" '.'  # Add one tick to total issues

    # Update operation title message with current number of issues
    _operation_title
  else
    _print_item_ok
  fi

  _send_operation_finished
}

#########################  PRE-SCRIPT CHECKS  ##########################

# For colored output
reset=$'\033[0m'
red=$'\033[31m'
green=$'\033[32m'
yellow=$'\033[33m'
blue=$'\033[34m'
magenta=$'\033[35m'
cyan=$'\033[36m'

script_version='1.1.0'              # Redoflacs version

#
# Make sure we are running BASH 4 or greater
#
if (( BASH_VERSINFO[0] < 4 )); then
  _error "You must be running ${cyan}BASH 4$reset or greater to use this"
  _error 'program! Current version:'
  _error "  ${cyan}${BASH_VERSION}$reset"
  exit 1
fi

# BASH 4: Used to recursively find (glob) FLAC files (eg, /**/*.flac)
shopt -s globstar 2>/dev/null

# BASH 2: Make all globbing case-insensitive
shopt -s nocaseglob 2>/dev/null

#
# Redoflacs configuration file
#
config_file="${HOME}/.config/redoflacs/config"

#
# Create a new configuration file if one does not exist
#
if [[ ! -f "$config_file" ]]; then
  _create_config
  exit 0
else
  _parse_config
fi

#
# Process/validate script positional parameters. This function provides
# variables which are used by later functions/operations (based on
# whether certain operations are specified)
#
_process_arguments "$@"

#
# Check for missing programs required by this script
#
_check_missing_programs

#
# Manage any user-defined directories from the configuration checking
# for directory existence and if writable
#
# NOTE:
#   Configuration keys are passed in by name, allowing reference to
#   derive the key name and indirection for the key value
#
for dir in \
  error_log \
  temporary_wav_location \
  artwork_location \
  spectrogram_location
do
  # Skip any default (empty) configuration values
  [[ "${!dir}" ]] || continue

  # Check for existence
  if [[ ! -d "${!dir}" ]]; then
    _error "${cyan}${!dir}$reset does NOT exist!"
    _error

  # Check for writable
  elif [[ ! -w "${!dir}" ]]; then
    _error "${cyan}${!dir}$reset is NOT writable!"
    _error
  fi

  # Rest of error messaging regardless of type of failure
  if [[ ! -d "${!dir}" || ! -w "${!dir}" ]]; then
    _error "Ensure a valid value for ${cyan}${dir}$reset is used"
    _error 'in the following configuration file:'
    _error "  ${cyan}${config_file}$reset"
    exit 1
  fi
done

#
# Generate log file location
#
# Ensure we don't overwrite an existing log file
item=0
log_file="${error_log:-${HOME}}/redoflacs_$$.log"
until [[ ! -f "$log_file" ]]; do
  log_file="${error_log:-${HOME}}/redoflacs_$$.$(( ++item )).log"
done

#
# Generate a unique temporary directory to house temporary WAV files if
# a user specified a temporary directory
#
# Ensure we don't clobber the temporary directory, eg:
#   /tmp/tmp_redoflacs_dir.1234.0 -> /tmp/tmp_redoflacs_dir.1234.1
#
# NOTE:
#   This directory is only created if an operation will decode FLAC
#   files to WAV files
#
if [[ "$temporary_wav_location" ]]; then
  user_tmp_dir="tmp_redoflacs_dir.$$.0"
  until [[ ! -d "${temporary_wav_location%/}/$user_tmp_dir" ]]; do
    user_tmp_dir="${user_tmp_dir%.*}.$(( ${user_tmp_dir##*.} + 1 ))"
  done

  # Set the user temporary directory to a unique subdirectory to allow
  # easy removal after script completion/termination
  temporary_wav_location_sub_dir="${temporary_wav_location%/}/$user_tmp_dir"
fi

###############################  MAIN  #################################

export LANG=C
export LC_ALL=C

# Used in _cleanup() (via trap) to determine if an abnormal signal
# caught or there generalized (logged) failures caught by the script
exit_code=0

# Manage cleanup on interruption and script exit
trap 'exit_code=130; exit' SIGINT
trap 'exit_code=143; exit' SIGTERM
trap '_cleanup; exit $exit_code' EXIT

# Hide cursor
printf '\033[?25l'

job_fifo="/tmp/redoflacs_fifo_$$"           # Job manager FIFO
issue_ticks="/tmp/redoflacs_issue_file_$$"  # Issues/Errors file

# Set FLAC metadata BLOCKS to NOT remove, used when pruning FLAC files
if [[ "$remove_artwork" == 'true' ]]; then
  # Remove PICTURE block
  dont_prune_flac_metadata='STREAMINFO,VORBIS_COMMENT'
else
  # Do not remove PICTURE block
  dont_prune_flac_metadata='STREAMINFO,PICTURE,VORBIS_COMMENT'
fi

#
# Obtain FLAC version (used in banner)
#
flac_version="$(flac --version)"    # eg: flac 1.4.3
flac_version="${flac_version##* }"  # eg: 1.4.3

#
# Break out FLAC version string to major, minor and patch
#
IFS='.' read -r flac_major flac_minor flac_patch <<< "$flac_version"

#
# Determine the number of cores available, defaulting to 2
#
cores=''
cores="${cores:-"$(command -p nproc 2>/dev/null)"}"
cores="${cores:-"$(command -p getconf _NPROCESSORS_ONLN 2>/dev/null)"}"
cores="${cores:-"$(command -p sysctl -n hw.ncpu 2>/dev/null)"}"
cores="${cores:-2}"

#
# FLAC 1.5.0 introduced support for multithreaded encoding of FLAC files
# which is checked to support multithreaded usage in compression
# operations
#
if (( flac_major > 1 || flac_minor >= 5 )); then
  # FLAC version does support multithreaded encoding
  multithreaded_flac='true'

  # Compression jobs default to global jobs if global jobs is specified
  # but no compression jobs were specified
  [[ "$global_jobs" && ! "$compress_jobs" ]] && compress_jobs="$global_jobs"

  # Default to number of cores for all other jobs (2, if cores is
  # undetermined)
  global_jobs="${global_jobs:-$cores}"

  # Default to 2 compression threads
  compress_threads="${compress_threads:-2}"

  # Default to half as many jobs as threads when compresssing
  compress_jobs="${compress_jobs:-$(( cores / compress_threads ))}"

  # Sent to 'flac' during compression (encoding)
  flac_compress_threads="--threads=$compress_threads"

  # Check for CPU starvation with compression jobs * threads
  cpu_thread_starvation=''
  if (( (compress_jobs * compress_threads) > cores )); then
    cpu_thread_starvation='true'
  fi

  # Check for CPU starvation in user-defined global jobs
  cpu_job_starvation=''
  (( global_jobs > cores )) && cpu_job_starvation='true'
else
  # FLAC version does not support multithreaded encoding
  multithreaded_flac=''

  # Default to number of cores for all jobs (2, if cores is
  # undetermined)
  global_jobs="${global_jobs:-$cores}"

  # FLAC < 1.5.0 does not support more than one thread
  compress_threads=1
  flac_compress_threads=''
  cpu_thread_starvation=''

  # Check for CPU starvation in user-defined global jobs
  cpu_job_starvation=''
  (( global_jobs > cores )) && cpu_job_starvation='true'
fi

#
# These are operation title messages which are used by
# _operation_title() to dynamically display a title via key lookup
#
declare -A operation_title
operation_title['_md5check']='Verifying MD5...'
operation_title['_test']='Testing...'
operation_title['_compress']="Compressing To Level ${compression_level}..."
operation_title['_aucdtect']='auCDtect Validation...'
operation_title['_lac']='Lossless Audio Checker (LAC) Validation...'
operation_title['_replaygain']='Applying ReplayGain...'
operation_title['_retag']='Retagging...'
operation_title['_extract_images']='Extracting Images...'
operation_title['_prune']='Pruning METADATA...'

#
# Obtain the total number of lines and columns available in the terminal
# by sending the following control sequences to the terminal driver:
#
#   CSI s                # Save cursor position
#   CSI 999999 999999 H  # Move cursor to the end of the last row
#   CSI 6 n              # Query cursor position
#   CSI u                # Restore cursor position
#
# 'CSI 6 n' reports/returns the cursor position/terminal size as:
#
#   CSI height ; width R
#
# An example:
#
#   '\033[80;23R'
#
# Send CSI escape to (and read from) STDERR to not pollute STDIN
IFS='[;' \
  read -rsu2 -d'R' -t1 -p $'\033[s\033[999999;999999H\033[6n\033[u' \
  _ lines columns

#
# The terminal uses 1-based column indexing (columns start at 1), but
# shell arithmetic is 0-based
#
# To right-align text without manual '+ 1' adjustments in every
# calculation, MAX_LENGTH is set to '$columns + 1'
#
if (( columns < 80 )); then
  readonly MAX_LENGTH="$(( columns + 1 ))"
else
  readonly MAX_LENGTH='81'
fi

#
# Print a long blank line that is one column higher than the total
# columns in the termninal to test for line wrapping
#
printf "%$(( columns + 1 ))s" ''

#
# If the current cursor column is equal to the total columns in the
# terminal (clamping) then line wrapping is disabled
#
# If the current cursor column is greater than the total columns in the
# terminal (non-clamping) then line wrapping is also disabled
#
if (( $(_col) >= columns )); then
  readonly LINE_WRAPPING=''

  # Move back to column 0
  printf '\r'
else
  readonly LINE_WRAPPING='true'

  # Move back up to the previous row at column 0
  printf '\r\033[1A'
fi

# Display a banner for runtime information and configuration settings
_runtime_banner

# _retag(): Display a countdown
[[ " ${operations[*]} " == *' _retag '* ]] && _countdown_metadata

#
# Obtain the total FLAC files to process (files and/or directories)
#
# NOTE:
#   The array of FLAC files/directories contain absolute pathnames which
#   allows logging to determine where each item resides
#
_info 'Looking for FLAC files to process...'
pushd "$directory" >/dev/null || exit
total_items=( "${PWD}"/**/*.flac )
popd >/dev/null || exit

#
# If there were no FLAC files found, the glob will exist as index 0, eg:
#
#   /music/artist/album/files/**/*.[Ff][Ll][Aa][Cc]
#
if [[ ! -f "${total_items[0]}" ]]; then
  found_flacs_msg='NOT FOUND'

  # Right align message
  found_flacs_align=$(( MAX_LENGTH - ${#found_flacs_msg} ))

  # Update title message with right aligned message indicating there
  # were no FLAC files found
  printf "\033[$(( $(_row) - 1 ));${found_flacs_align}H${red}%s${reset}\n" \
    "$found_flacs_msg"

  _error 'There were no FLAC files found!'
  exit 1
else
  found_flacs_msg='OK'

  # Right align message
  found_flacs_align=$(( MAX_LENGTH - ${#found_flacs_msg} ))

  # Update title message with right aligned message indicating success
  # in finding FLAC files to process
  printf "\033[$(( $(_row) - 1 ));${found_flacs_align}H${green}%s${reset}\n" \
    "$found_flacs_msg"
fi

#
# Run through all the operations specified by the user on the provided
# FLAC files directory
#
for operation in "${operations[@]}"; do
  # Used to determine current item number being processed and used by
  # the operation title function
  num=0

  # Use compression jobs (tied with threads) for compression operation
  if [[ "$multithreaded_flac" && "$operation" == '_compress' ]]; then
    operation_jobs="$compress_jobs"
  else
    operation_jobs="$global_jobs"
  fi

  # ReplayGain application requires operating on a directory of files
  # instead of each file, individually
  if [[ "$operation" == '_replaygain' ]]; then
    # Backup all known FLAC files to process
    total_items_backup=( "${total_items[@]}" )

    # Obtain all the parent directories for each FLAC file (album) and
    # store each album directory once into an array to apply ReplayGain
    # on a directory of files instead of individual files
    current=''
    total_items=()
    for dir in "${total_items_backup[@]%/*}"; do
      previous="$current"
      current="$dir"

      # Non-matching indicates a "new" directory to store
      [[ "$previous" != "$current" ]] && total_items+=( "$current" )
    done
  fi

  # Display title message of operation
  title_row="$(_row)"
  _operation_title

  # Scroll terminal up (if needed)
  _scroll_terminal
  title_row=$(( $(_row) - 1 ))

  # Clear job manager file descriptor (tied to FIFO)
  exec 3<&- 3>&-       # Close file descriptor

  # Remove FIFO if it exists
  rm -f "${job_fifo:-/dev/null/foo}"

  mkfifo "$job_fifo"   # Create FIFO
  exec 3<>"$job_fifo"  # Open file descriptor read/write

  # If the items to process are less than the number of jobs, add the
  # number of items to the current row position, else add the number
  # of jobs to run
  if (( ${#total_items[@]} < operation_jobs )); then
    items_processed="${#total_items[@]}"
  else
    items_processed="$operation_jobs"
  fi

  post_row=$(( $(_row) + items_processed ))  # Operation row position

  # Run current operation with optional parallel jobs
  _run_parallel

  # Clear all the operation lines by moving up each line and clearing it
  # until we are just below the operation's title message
  for (( i=1; i<=items_processed; i++ )); do
    printf '%b' "\033[$(( post_row - i ))H\033[0K"
  done

  # Display completed operation
  _operation_title

  # Break out if there were any issues found in the current operation
  #
  # NOTE:
  #   _cleanup() is run after script exit
  (( $(_num_issues) > 0 )) && break

  # Restore the items to process from directories to FLAC files after
  # applying ReplayGain
  if [[ "$operation" == '_replaygain' ]]; then
    total_items=( "${total_items_backup[@]}" )
  fi
done
