#!/bin/bash

#
# File .......... swuk_repos_gen_fix_headers
# Author ........ Steve Haywood
# Website ....... http://www.spacewire.co.uk
# Project ....... Common (SpaceWire UK Tutorial)
# Date .......... 30 Jan 2025
# Version ....... 1.0
# Description ...
#   Iterate through a collection of project directories and create/correct any
# of the missing/erroneous headers. All functions are controlled by command line
# switches.
#
# Fields in the header :-
#
# File .......... Set to the filename of the source in question.
# Author ........ Set to a constant in this script.
# Website ....... Set to a constant in this script.
# Project ....... Set to the project name plus a constant in this script.
# Date .......... If not provided set to a default value, otherwise corrected to
#                 have a consistent format.
# Version ....... Major part set to be the number of changes provided from
#                 'git log' for the source in question. Minor part is optionally
#                 set to 0.
# Description ... If not provided set to a default value.
#

# Strict
set -euo pipefail

# General constants
declare -r c_newline=$'\n'

# Verbosity levels             # Summary - Err - Warn - Note - Files(Inc) - Files(Exc)
declare -ri c_verb_summary=0   # Yes     - No  - No   - No   - No         - No
declare -ri c_verb_errors=1    # Yes     - Yes - No   - No   - No         - No
declare -ri c_verb_warnings=2  # Yes     - Yes - Yes  - No   - No         - No
declare -ri c_verb_notes=3     # Yes     - Yes - Yes  - Yes  - No         - No
declare -ri c_verb_include=4   # Yes     - Yes - Yes  - Yes  - Yes        - No
declare -ri c_verb_exclude=5   # Yes     - Yes - Yes  - Yes  - Yes        - Yes

# Message types
declare -ri c_error=${c_verb_errors}
declare -ri c_warning=${c_verb_warnings}
declare -ri c_note=${c_verb_notes}
declare -ri c_action=${c_verb_include}

# User constants
declare -r c_author="Steve Haywood"
declare -r c_website="http://www.spacewire.co.uk"
declare -r c_project="SpaceWire UK Tutorial"
declare -r c_date="Sage date!"
declare -r c_description="Sage words!"

# Known filenames (very in-extensive!)
declare -ra c_name_hash=(Makefile)

# Known extensions (by no means extensive or 100% correct!)
declare -ra c_ext_hash=(bb bbappend cgi conf sh tcl xdc)
declare -ra c_ext_slash=(c h js sv v vh)
declare -ra c_ext_star=(css dtsi)
declare -ra c_ext_php=(php)
declare -ra c_ext_html=(htm html)

# Comment types (by no means extensive!)
declare -ri c_type_unknown=0
declare -ri c_type_hash=1
declare -ri c_type_slash=2
declare -ri c_type_star=3
declare -ri c_type_php=4
declare -ri c_type_html=5

# Opening comments
declare -rA c_hdr_open=(
  [${c_type_unknown}]="?"
  [${c_type_hash}]="#"
  [${c_type_slash}]="//"
  [${c_type_star}]="/*"
  [${c_type_php}]="<?php"
  [${c_type_html}]="<!--"
)

# Empty comments
declare -rA c_hdr_empty=(
  [${c_type_unknown}]="?"
  [${c_type_hash}]="#"
  [${c_type_slash}]="//"
  [${c_type_star}]=" *"
  [${c_type_php}]="//"
  [${c_type_html}]="//"
)

# Body comments
declare -rA c_hdr_body=(
  [${c_type_unknown}]="? "
  [${c_type_hash}]="# "
  [${c_type_slash}]="// "
  [${c_type_star}]=" * "
  [${c_type_php}]="// "
  [${c_type_html}]="// "
)

# Closing comments
declare -rA c_hdr_close=(
  [${c_type_unknown}]="?"
  [${c_type_hash}]="#"
  [${c_type_slash}]="//"
  [${c_type_star}]=" */"
  [${c_type_php}]="?>"
  [${c_type_html}]="-->"
)


################################################################################
# Display formatted message for comments, warnings, errors & notes.
# Arguments ...... $1 = Level (2 = Error 3 = Warning, 4 = Note)
#                  $2 = Line number (1..) (0 = Do not display line number)
#                  $3 = Message text
# Return ......... None
# Shared (In) .... errors*, file, filenamedisp*, verbosity, warnings*
# Shared (Out) ... errors*, filenamedisp*, warnings*
msg() {
  local -ri level=$1    # Int: Error level
  local -ri lineno=$2   # Int: Line number
  local -r  message=$3  # Str: Error message
  local -rA lookup=(
    [${c_error}]="\e[31mError\e[0m ....."
    [${c_warning}]="\e[36mWarning\e[0m ..."
    [${c_note}]="\e[32mNote\e[0m ......"
    [${c_action}]="\e[93mAction\e[0m ...."
  )
  if [[ ${verbosity} -ge ${level} ]]; then
    if [[ ${filenamedisp} -eq 0 ]]; then
      >&2 echo ${file}
      filenamedisp=1
    fi
    >&2 echo -ne "  ${lookup[${level}]}"
    [[ ${lineno} -gt 0 ]] && >&2 printf " %02d :" ${lineno} || >&2 echo -n "... :"
    >&2 echo " ${message}"
  fi

  if [[ ${level} -eq ${c_warning} ]]; then
    warnings=$((warnings+1))
  fi

  if [[ ${level} -eq ${c_error} ]]; then
    errors=$((errors+1))
  fi
}


################################################################################
# Get the file type based on name and/or extension.
# Arguments ...... None
# Return ......... File type
# Shared (In) .... file
# Shared (Out) ... None
getcomtype()
{
  # Default to unknown type
  local -i type=${c_type_unknown}    # Int: File type

  # Standard path/filename split     #      $file ..... path/to/dir/filename.extension
  local fname=$(basename "${file}")  # Str: $fname .... filename.extension
  local fbase="${fname%.*}"          # Str: $fbase .... filename
  local fext="${fname##*.}"          # Str: $fext ..... extension

  # Get comment type (name is primary, extension is secondary)
  if [[ " ${c_name_hash[*]} " =~ " ${fname} " ]]; then
    type=${c_type_hash}
  elif [[ " ${c_ext_hash[*]} " =~ " ${fext} " ]]; then
    type=${c_type_hash}
  elif [[ " ${c_ext_slash[*]} " =~ " ${fext} " ]]; then
    type=${c_type_slash}
  elif [[ " ${c_ext_star[*]} " =~ " ${fext} " ]]; then
    type=${c_type_star}
  elif [[ " ${c_ext_php[*]} " =~ " ${fext} " ]]; then
    type=${c_type_php}
  elif [[ " ${c_ext_html[*]} " =~ " ${fext} " ]]; then
    type=${c_type_html}
  fi

  echo ${type}
}


################################################################################
# Read the comment header block from a source file.
# Arguments ...... None
# Return ......... None
# Shared (In) .... com*, end*, file, header*, lines*, start*, type*, valid*
# Shared (Out) ... com*, end*, header*, lines*, start*, type*, valid*
#
# Note: The single line read used below is the stock way of doing this, where :-
# 1. IFS= (or IFS='') prevents leading/trailing whitespace from being trimmed.
# 2. -r prevents backslash escapes from being interpreted.
# 3. [[ -n ${line} ]] prevents the last line from being ignored if it doesn't end with a \n.
#
read_header()
{
  # Declare local variables
  local line=""     # Str: Line string
  local -i pos=1    # Int: Line position
  local -i stage=0  # Int: Operation stage (0=Pre-header, 1=Header)

  # Initialise shared variables
  type=0            # Int: File type (0=Unknown, 1=Script)
  com=""            # Str: Comment style
  valid=0           # Int: Found a valid header (0:No, 1:Yes)
  start=0           # Int: Start position of header
  end=0             # Int: End position of header
  header=""         # Str: Captured header block (string)
  lines=()          # Arr: Captured header block (array)

  # Iterate through lines of file
  while IFS= read -r  line || [[ -n "${line}" ]] && [[ ${pos} -lt 100 ]]; do
    if [[ (${pos} -eq 1) && (${line:0:2} == "#!") ]]; then
      type=1
    elif [[ "${line}" =~ ^[[:space:]]*$ ]]; then
      [[ ${stage} -eq 1 ]] && break  # End of header
    else
      [[ ${stage} -eq 0 ]] &&
        start=${pos}
      end=${pos}
      stage=1
      if [[ "${line}" =~ ^.*[Ff][Ii][Ll][Ee]\ \.{3,}\ .+$ ]]; then
        valid=1
        com=$(echo "${line}" | sed -n 's/^\(.*\)[Ff][Ii][Ll][Ee] \.\{3,\} .\+/\1/p')
      fi
      header+="${line}${c_newline}"
      lines+=("${line}")
    fi
    pos=$((pos+1))
  done < "${file}"

  # Clean up positions
  if [[ ${valid} -eq 0 ]]; then
    end=0
    if [[ ${type} -eq 0 ]]; then
      start=0
    else
      start=2
    fi
  fi
}


################################################################################
# Parse header block into an associative array.
# Arguments ...... None
# Return ......... None
# Shared (In) .... default*, keys*, lines, type
# Shared (Out) ... default*, keys*
parse_header()
{
  # Declare local variables
  local     textblock=""          # Str: Block of text (description, history)
  local -ir linecnt=${#lines[@]}  # Int: Header line count
  local -i  linenum               # Int: Header line number
  local     line                  # Str: Header line string
  local -i  sling                 # Int: Sling trailing description line (0=keep, 1=sling)
  local     parts                 # Str: Field parts string (2 or 3 parts, | separated)

  IFS=''
  for (( linenum=0; linenum<${linecnt}; linenum++ )); do
    line=${lines[linenum]}
    line="${line%"${line##*[![:space:]]}"}"  # Trim trailing whitespace
    local -a fields=()  # Header fields array

    if [[ ${textblock} != "" ]]; then
      sling=0
      case ${linenum} in
        $((linecnt-3)))
          if [[ ${type} == ${c_type_php} && ${line} == " *" ]]; then
            sling=1  # Sling pre-pre-closing ' *' for PHP headers
          fi
        ;;
        $((linecnt-2)))
          if [[ (${type} == ${c_type_php}) && (${line} == "//" || ${line} == "*/" || ${line} == " */") ]]; then
            sling=1  # Sling pre-closing '//', '*/' & ' */' for PHP headers
          fi
        ;;
        $((linecnt-1)))
          if [[ ${type} == ${c_type_star} || ${type} == ${c_type_php} || ${type} == ${c_type_html} ]]; then
            sling=1  # Sling closing ' */', '?>' & '-->' for CSS, PHP & HTML headers
          fi
        ;;
      esac

      if [[ ${sling} -eq 0 ]]; then
        default[${fid}]+="${line}|"
      fi
    fi

    # Obtain the 3 part variant of the field (name dots value) in the header block
    parts=$(echo "${line}" | sed -n 's/^.*\([A-Z][a-z]\{3,11\}\) \(\.\{3,11\}\) \(.*\)/\1|\2|\3/p')
    # Split the field parts into an array
    IFS='|' read -ra fields <<< "${parts}"
    if [[ ${#fields[@]} -eq 3 ]]; then
      # Extra check
      local tmp="${fields[0]}${fields[1]}"
      if [[ ${#tmp} -eq 14 ]]; then
        local fid=${fields[0],,}  # Lower case fid
        #local fdots=${fields[1]}
        local fvalue=${fields[2]}
        default[${fid}]=${fvalue}
        keys+=("${fid}")  # Add discovered field (name) to the keys list
        [[ ${textblock} != "" ]] && textblock=""
      fi
    fi

    # Obtain the 2 part variant of the field (name dots) in the header block
    parts=$(echo "${line}" | sed -n 's/^.*\([A-Z][a-z]\{3,11\}\) \(\.\{3,11\}\)$/\1|\2/p')
    # Split the field parts into an array
    IFS='|' read -ra fields <<< "${parts}"
    # Determine if field is known
    if [[ ${#fields[@]} -eq 2 ]]; then
      # Extra check
      local tmp="${fields[0]}${fields[1]}"
      if [[ ${#tmp} -eq 14 ]]; then
        local fid=${fields[0],,}  # Lower case fid
        #local fdots=${fields[1]}
        default[${fid}]=""
        keys+=("${fid}")  # Add discovered field (name) to the keys list
        [[ ${textblock} != "" ]] && textblock="" || textblock=${fid}
      fi
    fi

  done
  unset IFS
}


################################################################################
# Write the 'ed' commands required to strip out the source file headers and
# replace them with new ones.
# Arguments ...... None
# Return ......... None
# Shared (In) .... addheader, addminor, addproject, addwebsite, com, end, file,
#                  fixfilename, formatdate, gitversion, hdr_body, hdr_close,
#                  hdr_empty, hdr_open, lines, projdir, remstandard,
#                  renconception, reorganise, rewrite, start, type, valid
# Shared (Out) ... default*, keys*, lines, type
rewrite_write()
{
  # Declare local variables
  [[ ${type} == ${c_type_html} || ${type} == ${c_type_php} ]] &&
    local post_open="${c_newline}${hdr_empty}" || local post_open=""

  [[ ${type} == ${c_type_html} || ${type} == ${c_type_php} ]] &&
    local pre_close="${hdr_empty}${c_newline}" || local pre_close=""

  # Declare shared variables
  local -a keys=()    # Arr : List of fields discovered in header
  local -A default=(  # ARR: Default header
    [file]=""
    [author]=""
    [website]=""
    [project]=""
    [conception]=""
    [date]=""
    [version]=""
    [history]=""
    [description]=""
  )

  # Obtain filename from full path
  local -r filename=$(basename "${file}")

  # Obtain major version based on log entry count
  local -ir major=$(git log --oneline ${file} | wc -l)

  # Add minor version (fixed at .0)
  local minor=""
  [[ ${addminor} -eq 1 ]] && local minor=".0"

  local -Ar projects=(  # ARR: Project names
    ["common"]="Common"
    ["zedboard_hello_world"]="Zedboard Hello World"
    ["zedboard_leds_buttons"]="Zedboard LEDs & Buttons"
    ["zedboard_leds_switches"]="Zedboard LEDs & Switches"
    ["zedboard_linux"]="Zedboard Linux"
  )

  # Obtain project
  local proj=$(echo ${file} | cut -d'/' -f1)

  if [[ ${valid} -eq 1 ]]; then  # Header detected, process it

    # Parse header block into associated array & keys list
    parse_header

    # Iterate though all the fields in the default header & apply the options requested
    for key in ${!default[@]}
    do
      local fieldvalue=${default[${key}]}
      case ${key} in

        "file")  ## 'mandatory'
          # Check for difference between field value vs. expected field value
          if [[ (${fixfilename} -eq 1) && (${filename} != ${fieldvalue}) ]]; then
            # Throw a warning to signal file modification
            msg c_warning 0 "Changing value of '${key^}' field from '${default[${key}]}' to '${filename}'."
            # Override 'File' field value
            default[${key}]=${filename}
          fi
        ;;

        "date")
          # Check for a field value
          if [[ ${default[${key}]} != "" ]]; then
            # Evaluate 'Date' value using the date command
            local date=$(date -d "${fieldvalue}" +'%d %b %Y' 2>/dev/null)
            # Check for field viability
            if [[ ${date} != "" ]]; then
              # Check for difference between field value vs. expected field value
              if [[ (${formatdate} -eq 1) && (${date} != ${fieldvalue}) ]]; then
                # Throw a warning to signal file modification
                msg c_warning 0 "'${key^}' field is incorrectly formatted, found '${default[${key}]}', changing to '${date}'."
                # Override 'Date' field value
                default[${key}]=${date}
              else
                # No big deal, throw note
                msg c_note 0 "'${key^}' field is correctly formatted, found '${default[${key}]}'."
              fi
            else
              # Unrecognizable date, throw error
              msg c_error 0 "'${key^}' field is invalid, expecting a valid date, found '${default[${key}]}'."
            fi
          fi
        ;;

        "conception")
          # Check for a field value
          if [[ (${renconception} -eq 1) && (${default[${key}]} != "") ]]; then
            # Evaluate 'Date' value using the date command
            local date=$(date -d "${fieldvalue}" +'%d %b %Y' 2>/dev/null)
            # Check for field viability
            if [[ ${date} != "" ]]; then
              # Check for difference between field value vs. expected field value
              if [[ (${formatdate} -eq 1) && (${date} != ${fieldvalue}) ]]; then
                # Throw a warning to signal file modification
                msg c_warning 0 "'${key^}' field detected, renaming it to 'Date', field is incorrectly formatted, found '${default[${key}]}', changing to '${date}'."
                # Override 'Date' value with 'Conception' value
                default[date]=${date}
              else
                # Throw a warning to signal file modification
                msg c_warning 0 "'${key^}' field detected, renaming to 'Date', field is correctly formatted, found '${default[${key}]}'."
                default[date]=${date}
              fi
            else
              # Unrecognizable date, throw error
              msg c_error 0 "'${key^}' field detected, renaming to 'Date', field is invalid, expecting a valid date, found '${default[${key}]}'."
              default[date]=${default[${key}]}
            fi
            # Override field value with default
            default[${key}]=""
          fi
        ;;

        "version")
          # Obtain major version based on log entry count
          local versexp="${major}"
          # Check for difference between field value vs. expected field value
          if [[ (${gitversion} -eq 1) && (${versexp} != ${fieldvalue}) ]]; then
            # Throw a warning to signal file modification
            msg c_warning 0 "'${key^}' field incorrect, found '${default[${key}]}', changing to '${versexp}'."
            # Override field value with expected
            default[${key}]="${versexp}${minor}"
          else
            # No big deal, throw note
            msg c_note 0 "'${key^}' field is correct, found '${default[${key}]}'."
          fi
        ;;

        "standard")
          # Check for a field value
          if [[ (${remstandard} -eq 1) && (${default[${key}]} != "") ]]; then
            # Throw a warning to signal modification
            msg c_warning 0 "'${key^}' field detected, removing '${c_website}'."
            # Override field value with default
            default[${key}]=""
          fi
        ;;

        "website")
          # Check for a field value
          if [[ (${addwebsite} -eq 1) && (${default[${key}]} == "") ]]; then
            # Throw a warning to signal modification
            msg c_warning 0 "'${key^}' field missing, inserting '${c_website}'."
            # Override field value with default
            default[${key}]=${c_website}
          fi
        ;;

        "project")
          # Check for a field value
          if [[ (${addproject} -eq 1) && (${default[${key}]} == "") ]]; then
            # Throw a warning to signal modification
            msg c_warning 0 "'${key^}' field missing, inserting '${c_project}'."
            # Override field value with default
            default[${key}]="${projects[${proj}]} (${c_project})"
          elif [[ (${addproject} -eq 1) && (${default[${key}]} == "${c_project}") ]]; then
            # Throw a warning to signal modification
            msg c_warning 0 "'${key^}' field incomplete, adding '${projects[${proj}]}'."
            # Override field value with default
            default[${key}]="${projects[${proj}]} (${c_project})"
          fi
        ;;

        "author"|"history"|"version"|"description")
          :  # Ignore known!
        ;;

        *)
          msg c_error 0 "'${key^}' field is unknown, its value is '${default[${key}]}', ignoring."
        ;;

      esac
    done

    #################################
    # Output the final header block #
    #################################

    echo -e "\ned ${projdir}/${file} <<END" >> ../${rewrite}
    echo "${start},${end}d" >> ../${rewrite}
    echo "." >> ../${rewrite}
    echo "$((start-1))a" >> ../${rewrite}

    local text=""  # Str: Text from texts array
    local key      # Str: Key from keys array

    # Put the 'mandatory' fields in the 'correct' display order
    [[ ${reorganise} -eq 1 ]] &&
      keys=(file author website project date version history description)
    # Opening comment
    echo "${hdr_open}${post_open}" >> ../${rewrite}
    # Iterate though all the 'mandatory' fields
    for key in ${keys[@]}; do
      local dots=$(printf '.%.0s' {1..28} | head -c $((14-${#key})))

      case ${key} in

        "description")
          echo "${hdr_body}${key^} ${dots}" >> ../${rewrite}
          # Split the 'whole string' text block back into separate 'lines'
          IFS='|' read -ra texts <<< "${default[${key}]}"
          # Get the comment marker of the very first line
          local -l lcom=${#com}
          # Display separate lines with their comment markers exchanged
          for text in "${texts[@]}"; do
            local ttmp="${text:lcom}"
            if [[ ${#ttmp} -eq 0 ]]; then
              echo "${hdr_empty}" >> ../${rewrite}
            else
              echo "${hdr_empty} ${text:lcom}" >> ../${rewrite}
            fi
          done
        ;;

        *)
          if [[ -n ${default[${key}]} ]]; then
            echo "${hdr_body}${key^} ${dots} ${default[${key}]}" >> ../${rewrite}
          fi
        ;;
      esac
    done

    # Closing comment (adjust according to last comment marker of description)
    if [[ "${text}" != "${pre_close}${hdr_close}" ]]; then
      echo "${pre_close}${hdr_close}" >> ../${rewrite}
    fi

    echo "." >> ../${rewrite}
    echo "wq" >> ../${rewrite}
    echo "END" >> ../${rewrite}

  else  # No header detected, add a new one

    if [[ ${addheader} -eq 1 ]]; then  # '--add' option, output 'ed' commands to add a new header

      # Throw as a warning due to file modification
      msg c_warning 0 "No identifiable header comment block found, inserting a new header @ line $((start+1))."

cat << EOF >> ../${rewrite}

ed ${projdir}/${file} <<END
${start}a
${hdr_open}${post_open}
${hdr_body}File .......... ${filename}
${hdr_body}Author ........ ${c_author}
${hdr_body}Website ....... ${c_website}
${hdr_body}Project ....... ${projects[${proj}]} (${c_project})
${hdr_body}Date .......... ${c_date}
${hdr_body}Version ....... ${major}${minor}
${hdr_body}Description ...
${hdr_body}  ${c_description}
${pre_close}${hdr_close}

.
wq
END
EOF

    else  # No '--add' option, output commented out 'ed' commands to add a new header

cat << EOF >> ../${rewrite}

#ed ${projdir}/${file} <<END
#${start}a
#Sage header!
#.
#wq
#END
EOF
    fi
  fi
}


################################################################################
# Write the 'ed' commands required to strip out the source file headers and
# replace them with new ones (in this case the existing ones).
# Arguments ...... None
# Return ......... None
# Shared (In) .... end, file, lines, projdir, start, template, valid
# Shared (Out) ... None
template_write()
{
  # Take appropriate action depending on header presence
  if [[ ${valid} -eq 1 ]]; then
    # Write commands to replace original header with itself
    echo -e "\ned ${projdir}/${file} <<END" >> ../${template}
    echo "${start},${end}d"                 >> ../${template}
    echo "."                                >> ../${template}
    echo "$((start-1))a"                    >> ../${template}
    printf '%s\n' "${lines[@]}"             >> ../${template}
    echo -e "."                             >> ../${template}
    echo "wq"                               >> ../${template}
    echo "END"                              >> ../${template}
  else
    # Write commented out insert header stub
cat << EOF >> ../${template}

#ed ${projdir}/${file} <<END
#${start}a
#Sage header!
#.
#wq
#END
EOF
  fi
}


################################################################################
# Process single project directory & produce 'ed' scripts.
# Arguments ...... None
# Return ......... None
# Shared (In) .... addheader, addminor, addproject, addwebsite, argv,
#                  considered*, examined*, exclude, fixfilename, formatdate,
#                  gitversion, optwhitespace, projdir, remstandard,
#                  renconception, reorganise, rewrite, template, verbosity
# Shared (Out) ... addheader, addminor, addproject, addwebsite, argv, com*,
#                  considered*, end*, examined*, file, fixfilename, formatdate,
#                  gitversion, hdr_body, hdr_close, hdr_empty, hdr_open,
#                  header*, lines*, projdir, remstandard, renconception,
#                  reorganise, rewrite, start*, template, type*, valid*
process_project()
{
  # Declare local variables
  local -a files=()        # Arr: List of repository files (array)
  local file               # Str: File name
  local -i filenamedisp=0  # Int: Filename displayed flag (0=no, 1=yes)

  # Declare local (shared) variables
  local -i type=0          # Int: File type (0=Unknown, 1=Script)
  local com=""             # Str: Comment style
  local -i valid=0         # Int: Found a valid header (0:No, 1:Yes)
  local -i start=0         # Int: Start position of header
  local -i end=0           # Int: End position of header
  local header=""          # Str: Captured header block (string)
  local lines=()           # Arr: Captured header block (array)

    # Write template
    echo -e "\n#### ${projdir} ######################################################################" >> ../${template}
    echo -e "\n#### ${projdir} ######################################################################" >> ../${rewrite}

    # Get repository file list
    readarray -t files < <(git ls-files)

    # Iterate though all the repository files
    for file in "${files[@]}"; do

      # Set header for changes (filename) to not yet displayed
      filenamedisp=0

      # Exclude files that are not created or edited source
      if [[ ! " ${exclude[*]} " =~ " ${file} " ]]; then

        # Remove trailing whitespace (in place)
        if [[ ! -L ${file} ]]; then
          if (($(option_set ${optwhitespace}))); then
            dos2unix ${file}
            sed -i 's/[[:blank:]]*$//' ${file}
          fi
        fi

        # Display action for inclusive file
        if [[ ${verbosity} -ge ${c_verb_include} ]]; then
          msg c_action 0 "Processing"
        fi

        # Read comment header block into an array
        read_header

        # Determine file type from filename
        [[ ${type} -eq ${c_type_unknown} ]] &&
          type=$(getcomtype)

        # Check for known file type
        if [[ ${type} -ne ${c_type_unknown} ]]; then

          # Check the source file exists
          if [[ -L ${file} ]]; then
            msg c_note 0 "Source file is a link, skipping."
          elif [[ -f ${file} ]]; then
            # Obtain comment construction parts
            local hdr_open=${c_hdr_open[${type}]}
            local hdr_empty=${c_hdr_empty[${type}]}
            local hdr_body=${c_hdr_body[${type}]}
            local hdr_close=${c_hdr_close[${type}]}

            template_write
            rewrite_write
          else
            msg c_error 0 "Source file not found, skipping."
          fi
        else
          msg c_error 0 "Source file type is unknown, skipping."
        fi
        # Increase examined files count
        examined=$((examined+1))
      else
        # Display action for exclusive file
        [[ ${verbosity} -ge ${c_verb_exclude} ]] &&
          msg c_action 0 "Skipping"
      fi
      # Increase considered files count
      considered=$((considered+1))
    done
}


################################################################################
# Process project directories & produce 'ed' scripts.
# Arguments ...... None
# Return ......... None
# Shared (In) .... addheader, addminor, addproject, addwebsite, argv,
#                  considered*, examined*, exclude, fixfilename, formatdate,
#                  gitversion, optwhitespace, projdirs, remstandard,
#                  renconception, reorganise, rewrite, template, verbosity
# Shared (Out) ... addheader, addminor, addproject, addwebsite, argv,
#                  considered*, examined*, exclude, fixfilename, formatdate,
#                  gitversion, optwhitespace, projdir, remstandard,
#                  renconception, reorganise, rewrite, template, verbosity
process_projects()
{
  local projdir  # Str: Current subdirectory name

  # Iterate through subdirectories
  for projdir in ${projdirs[@]}
  do
    # Move into subdirectory
    cd ${projdir}

    echo -e "\nProcessing ... ${projdir}\n"

    # Process subdirectory
    process_project

    # Move out of subdirectory
    cd ..
  done
}


################################################################################
# Check if an argument option is set.
# Arguments ...... $1 ... Str: Option
# Return ......... Set (0=unset, 1=set)
# Shared (In) .... argv
# Shared (Out) ... None
option_set()
{
  [[ " ${argv[*]} " =~ " ${1} " ]] && echo 1 || echo 0
}


################################################################################
# Main function
# Arguments ...... None
# Return ......... None
# Exit code ...... Status (0=success, 1=failure)
# Shared (In) .... argv
# Shared (Out) ... addheader, addminor, addproject, addwebsite, argv,
#                  considered*, examined*, exclude, fixfilename, formatdate,
#                  gitversion, optwhitespace, projdirs, remstandard,
#                  renconception, reorganise, rewrite, template, verbosity
main()
{
  # Declare local constants
  local -r  optadd="--add"                # Str: Add option name
  local -r  optfilename="--filename"      # Str: Filename option name
  local -r  optwebsite="--website"        # Str: Website option name
  local -r  optproject="--project"        # Str: Project option name
  local -r  optfmtdate="--fmtdate"        # Str: Format Date option name
  local -r  optconception="--conception"  # Str: Conception option name
  local -r  optversion="--version"        # Str: Version option name
  local -r  optminor="--minor"            # Str: Minor option name
  local -r  optstandard="--standard"      # Str: Standard option name
  local -r  optreorg="--reorg"            # Str: Reorganise option name
  local -r  optall="--all"                # Str: All option name
  local -r  optwhitespace="--whitespace"  # Str: Whitespace option name
  local -r  opthelp="--help"              # Str: Help option name
  local -Ar options=(                     # ARR: Options & associated help information
    [${optadd}]="Add missing header block and populated it with default fields."
    [${optfilename}]="Correct 'File' field if incorrect."
    [${optwebsite}]="Add 'Website' field if missing."
    [${optproject}]="Add 'Project' field if missing."
    [${optfmtdate}]="Reformat 'Date' field to DD Mmm YYYY."
    [${optconception}]="Rename 'Conception' field to 'Date' & reformat it to DD Mmm YYYY."
    [${optversion}]="Update the 'Version' field to pair with the GIT commit count."
    [${optminor}]="Add minor part (x.0) to the version string."
    [${optstandard}]="Remove 'Standard' field if present."
    [${optreorg}]="Reorganise header into desired field order."
    [${optall}]="Action all above options, except ${optminor}."
    [${optwhitespace}]="Remove all trailing whitespace in file (done directly not via the 'ed' scripts)."
    [${opthelp}]="Display this help and exit."
  )
  local -ar optorder=(                    # Arr: Help options display order
    ${optadd}
    ${optfilename}
    ${optwebsite}
    ${optproject}
    ${optfmtdate}
    ${optconception}
    ${optversion}
    ${optminor}
    ${optstandard}
    ${optreorg}
    ${optall}
    ${optwhitespace}
    ${opthelp}
  )

  # Declare local variables
  local    arg                            # Str: Current argument from argv array
  local    project=""                     # Str: Project directory [arg] (mandatory)
  local    exfile=""                      # Str: Exclude file [arg] (mandatory)
  local -i verbosity=10                   # Int: Verbosity level [arg] (optional)

  local -i addheader=0
  local -i fixfilename=0
  local -i addwebsite=0
  local -i addproject=0
  local -i formatdate=0
  local -i renconception=0
  local -i gitversion=0
  local -i addminor=0
  local -i remstandard=0
  local -i reorganise=0

  local    option                         # Str: Current option from optorder array

  # Display help information
  if (($(option_set ${opthelp}))); then
    echo "Usage: $(basename $0) PROJECT-DIRECTORY... EXCLUDE-FILE... [VERBOSITY]... [OPTION]..."
    echo "Generate 'ed' scripts for adding & correcting source file headers in the PROJECT-DIRECTORY."
    echo
    echo "Mandatory arguments to long options are mandatory for short options too."
    for option in ${optorder[@]}
    do
      echo "      ${option} $(printf ' %.0s' {1..12} | head -c $((12-${#option}))) ${options[${option}]}"
    done
    echo
    echo "The VERBOSITY argument:"
    echo " 0  Summary"
    echo " 1  Summary + Errors"
    echo " 2  Summary + Errors + Warnings"
    echo " 3  Summary + Errors + Warnings + Notes"
    echo " 4  Summary + Errors + Warnings + Notes + Include"
    echo " 5  Summary + Errors + Warnings + Notes + Include + Exclude"
    exit 0
  fi

  # Get & check the arguments
  for arg in ${argv[@]}; do
    if [[ ${arg:0:2} == "--" ]]; then  # Option
      [[ ! -v options[${arg}] ]] && echo "Option (${arg}) is not recognised!" && exit 1

      case ${arg} in
        "--add")        addheader=1 ;;
        "--filename")   fixfilename=1 ;;
        "--website")    addwebsite=1 ;;
        "--project")    addproject=1 ;;
        "--fmtdate")    formatdate=1 ;;
        "--conception") renconception=1 ;;
        "--version")    gitversion=1 ;;
        "--minor")      addminor=1 ;;
        "--standard")   remstandard=1 ;;
        "--reorg")      reorganise=1 ;;
        "--all")
          addheader=1
          fixfilename=1
          addwebsite=1
          addproject=1
          formatdate=1
          renconception=1
          gitversion=1
         #addminor=1
          remstandard=1
          reorganise=1
        ;;
      esac

    elif [[ -z ${project} ]]; then  # Project directory
      project=${arg}
      [[ ! -d ${project} ]] && echo "Project directory (${arg}) does not exist!" && exit 1
    elif [[ -z ${exfile} ]]; then  # Exclude file
      exfile=${arg}
      [[ ! -f ${exfile} ]] && echo "Exclude file (${arg}) does not exist!" && exit 1
    elif [[ ${verbosity} -eq 10 ]]; then  # Verbosity level
      verbosity=${arg}
    else
      echo "Unexpected argument (${arg}) found!" && exit 1
    fi
  done

  # Further check the arguments
  [[ -z ${project} ]] && echo "No project directory specified!" && exit 1
  [[ -z ${exfile} ]] && echo "No exclude file specified!" && exit 1

  local template="do_proof"
  local rewrite="do_fixes"

  # Get all subdirectories into an ordered array
  local -a projdirs=()  # List of project subdirectories (array, alphanumeric order)
  readarray -t projdirs < <(find ${project} -mindepth 1 -maxdepth 1 -type d | sort | cut -d'/' -f2-)
  [[ ${#projdirs[@]} -eq 0 ]] && echo "Project Directory specified (${argv[0]}) does not contain any subdirectories!" && exit 1

  # Get excluded file list
  local -a exclude=()  # List of excluded files (array)
  readarray -t exclude < ${argv[1]}

  # Variables
  local -i considered=0  # Number of files considered
  local -i examined=0    # Number of files examined
  local -i errors=0      # Number of errors encountered
  local -i warnings=0    # Number of warnings encountered

  # Move into project directory
  cd ${project}

  # Kick off template & rewrite scripts
  echo "#!/bin/bash" > ${template}
  echo "#!/bin/bash" > ${rewrite}
  chmod +x ${template} ${rewrite}

  # Perform the operations
  process_projects

  # Move out of project directory
  cd ..

  [[ ${verbosity} -ge ${c_verb_summary} ]] &&
    echo -e "\nConsidered ${considered} files, skipped $((considered-examined)), examined ${examined}, detected ${errors} errors and ${warnings} warnings."
}


################################################################################
# Opening gambit.
# Arguments ...... None
# Return ......... None
# Exit code ...... Status (0=success, 1=failure)
# Shared (In) .... $#, $@
# Shared (Out) ... argc, argv
declare -ri argc=${#}    # Int: Get argument count
declare -ra argv=(${@})  # Arr: Get argument values (space-separated) into array
main
exit 0
