#!/bin/bash

# Author: Steven Baltakatei Sandoval
# Description: Template for bash scripts.
# Note: Use hide-show-block function to aid readability. (ex: https://www.emacswiki.org/emacs/HideShow ).

#==BEGIN Variable Initialization==

#===BEGIN Global constants===
SCRIPT_TTL=10                   # Limit script life to this in seconds.
PATH="/usr/local/bin/:$PATH"    # Add user binary executable directory to PATH.
PATH="/opt/bktei:$PATH"         # Add 'optional' executable directory to PATH.
SCRIPT_HOSTNAME=$(hostname)     # Save hostname of system running this script.
SCRIPT_VERSION="bktemplate.sh 0.0.0" # Define version of script. Used by function 'showVersion'.
SCRIPT_TIME_SHORT="$(date +%Y%m%dT%H%M%S%z)" # Save current date & time in ISO-8601 format (YYYYmmddTHHMMSS+zzzz).
SCRIPT_DATE_SHORT="$(date +%Y%m%d)"          # Save current date in ISO-8601 format.
#===END Global constants===

#===BEGIN Define script parameters
declare -Ag appRollCall # Associative array for storing app status (function checkapp())
declare -Ag fileRollCall # Associative array for storing file status (function checkfile())
#===END Define script parameters
#==END Variable Initialization==

#==BEGIN Function Definitions==
yell() { echo "$0: $*" >&2; } # Yell, Die, Try Three-Fingered Claw technique; # Ref/Attrib: https://stackoverflow.com/a/25515370
die() { yell "$*"; exit 111; }
try() { "$@" || die "cannot $*"; }

echoerr() {
    # Usage: echo [ arguments ]
    # Description: Prints provided arguments to stderr.
    # Input: unspecified
    # Output: stderr
    # Script function dependencies: none
    # External function dependencies: echo
    # Last modified: 2020-04-11T21:16Z
    # Last modified by: Steven Baltakatei Sandoval
    # License: GPLv3+
    # Ref./Attrib:
    #  [1]: # Roth, James (2010-06-07). ["How to print text to stderr instead of stdout"](https://stackoverflow.com/a/2990533). Licensed CC BY-SA 4.0.
    
    echo "$@" 1>&2; # Define stderr echo function. See [1].
    return 0; # Function finished.
} # Define stderr message function.
vbm() {
    # Usage: vbm "DEBUG:verbose message here"
    # Description: Prints verbose message ("vbm") to stderr if OPTION_VERBOSE is set to "true".
    # Input:
    #   - OPTION_VERBOSE  variable set by processArguments function. (ex: "true", "false")
    #   - "$@"            positional arguments fed to this function.
    # Output: stderr
    # Script function dependencies: echoerr
    # External function dependencies: echo
    # Last modified: 2020-04-11T23:57Z
    # Last modified by: Steven Baltakatei Sandoval
    # License: GPLv3+
    # Ref./Attrib:

    if [ "$OPTION_VERBOSE" = "true" ]; then
	FUNCTION_TIME=$(date --iso-8601=ns); # Save current time in nano seconds.
	echoerr "[$FUNCTION_TIME] ""$*"; # Display argument text.
    fi

    # End function
    return 0; # Function finished.
} # Verbose message display function.
run() {
    # Usage: run [cmd]
    # Description: Runs command; reports success if command exit code 0; exits otherwise.
    # Input: none
    # Output: stdout
    # Script function dependencies: none
    # Last modified: 2020-06-19T22:09Z
    # Last modified by: Steven Baltakatei Sandoval
    # License: GPLv3+
    # Ref.Attrib: https://stackoverflow.com/a/18880585
    cmd_output=$(eval $1)
    return_value=$?
    if [ $return_value != 0 ]; then
	echo "Command $1 failed";
	exit -1;
    else
	echo "output: $cmd_output";
	echo "Command succeeded.";
    fi
    return $return_value
} # 
showUsage() {
    # Usage: showUsage
    # Description: Displays script usage information.
    # Input: none
    # Output: stderr
    # Script function dependencies: echoerr
    # External dependencies: bash (5.0.3), echo
    # Last modified: 2020-05-04T16:11Z
    # Last modified by: Steven Baltakatei Sandoval
    # License: GPLv3+
    # Ref./Attrib.:
    
    echoerr "USAGE:"
    echoerr "    bktemplate.sh [ options ]"
    echoerr
    echoerr "OPTIONS:"
    echoerr "    -h, --help"
    echoerr "            Display help information."
    echoerr
    echoerr "    --version"
    echoerr "            Display script version."
    echoerr
    echoerr "    -v, --verbose"
    echoerr "            Display debugging info."
    echoerr
    echoerr "    -o, --output-file [ file ]"
    echoerr "            Specify output file."
    echoerr
    echoerr "    -i, --input-file [ file ]"
    echoerr "            Specify input file."
    echoerr
    echoerr "    -o, --output-dir [ directory ]"
    echoerr "            Specify output directory."
    echoerr
    echoerr "    -i, --input-dir [ directory ]"
    echoerr "            Specify input directory."
    echoerr

    # End function
    return 0; # Function finished.
} # Display information on how to use this script.
showVersion() {
    # Usage: showVersion
    # Descriptoin: Displays script version and license information.
    # Input: unspecified
    # Output: stderr
    # Script function dependencies: echoerr
    # External function dependencies: echo
    # Last modified: 2020-04-11T23:57Z
    # Last modified by: Steven Baltakatei Sandoval
    # License: GPLv3+
    # Ref./Attrib:

    # Initialize function
    vbm "DEBUG:showVersion function called."

    # Perform work
    OUTPUT="$SCRIPT_VERSION"

    # Display results
    echoerr "$OUTPUT";

    # End function
    vbm "DEBUG:showVersion function ended."
    return 0; # Function finished.
} # Display script version.
processArguments() {
    # Usage: processArguments "$@"
    # Description: Processes provided arguments in order to set script option variables useful for
    #   changing how other functions behave. For example, it may:
    #   1. Activate verbose mode
    # Input: "$@"          (list of arguments provided to the function)
    # Output: Sets following variables used by other functions:
    #   OPTION_VERBOSE     Indicates verbose mode enable status.  (ex: "true", "false")
    #   DIROUT1            Path to output directory.
    #   FILEOUT1           Path to output file.
    #   DIRIN1             Path to input directory.
    #   FILEIN1            Path to input file.
    #   OPTION_FILEOUT1_OVERWRITE Indicates whether file FILEOUT1 should be overwritten (ex: "true", "false')
    # Script function dependencies:
    #   - echoerr          Displays messages to stderr.
    #   - vbm              Displays messsages to stderr if OPTION_VERBOSE set to "true".
    # External dependencies: bash (5.0.3), echo
    # Last modified: 2020-05-04T14:41Z
    # Last modified by: Steven Baltakatei Sandoval
    # License: GPLv3+
    # Ref./Attrib.:
    #  [1]: Marco Aurelio (2014-05-08). "echo that outputs to stderr". https://stackoverflow.com/a/23550347

    # Initialize function
    #vbm "DEBUG:processArguments function called."

    # Perform work
    while [ ! $# -eq 0 ]; do   # While number of arguments ($#) is not (!) equal to (-eq) zero (0).
	#1>&2 echo "DEBUG:Starting processArguments while loop." # Debug stderr message. See [1].
        #1>&2 echo "DEBUG:Provided arguments are:""$*" # Debug stderr message. See [1].
	case "$1" in
	    -h | --help) showUsage; exit 1;; # Display usage.
	    --version) showVersion; exit 1;; # Show version
	    -v | --verbose) OPTION_VERBOSE="true"; vbm "DEBUG:Verbose mode enabled.";; # Enable verbose mode. See [1].
	    -i | --input-file) # Define input file path
		if [ -f "$2" ]; then # If $2 is file that exists, set FILEIN1 to $2, pop $2.
		    FILEIN1="$2";
		    shift;
		    vbm "DEBUG:Input file FILEIN1 set to:""$2";
		else
		    echoerr "ERROR: Specified input file does not exist:""$2";
		    echoerr "Exiting.";
		    exit 1;
		fi ;; 
	    -I | --input-dir) # Define input directory path
		if [ -d "$2" ]; then # If $2 is dir that exists, set DIRIN1 to $2, pop $2.
		    DIRIN1="$2";
		    shift;
		    vbm "DEBUG:Input directory DIRIN1 set to:""$2";
		else # Display error if $2 is not a valid dir.
		    echoerr "ERROR:Specified input directory does not exist:""$2";
		    echoerr "Exiting.";
		    exit 1;
		fi ;; 
	    -o | --output-file) # Define output file path
		if [ -f "$2" ]; then # If $2 is file that exists, prompt user to continue to overwrite, set FILEOUT1 to $2, pop $2.
		    echoerr "Specified output file $2 already exists. Overwrite? (y/n):"
		    read m; case $m in
				y | Y | yes) OPTION_FILEOUT1_OVERWRITE="true";;
				n | N | no) OPTION_FILEOUT1_OVERWRITE="false";;
				*) echoerr "Invalid selection. Exiting."; exit 1;;
			    esac
		    if [ "$OPTION_FILEOUT1_OVERWRITE" == "true" ]; then
			FILEOUT1="$2";
			shift;
			vbm "DEBUG:Output file FILEOUT1 set to:""$2";
		    else
			echoerr "ERORR:Exiting in order to not overwrite output file:""$FILEOUT1";
			exit 1;
		    fi
		else
		    FILEOUT1="$2";
		    shift;
		    vbm "DEBUG:Output file FILEOUT1 set to:""$2";
		fi ;;
	    -O | --output-dir) # Define output directory path
		if [ -d "$2" ]; then # If $2 is dir that exists, set DIROUT1 to $2, pop $2
		    DIROUT1="$2";
		    shift;
		    vbm "DEBUG:Output directory DIROUT1 set to:""$2";
		else
		    echoerr "ERROR:Specified output directory is not valid:""$2";
		    echoerr "Exiting.";
		    exit 1;
		fi ;; 
	    *) echoerr "ERROR: Unrecognized argument."; exit 1;; # Handle unrecognized options. See [1].
	esac
	shift
    done

    # End function
    vbm "DEBUG:processArguments function ended."
    return  0; # Function finished.
} # Evaluate script options from positional arguments (ex: $1, $2, $3, etc.).
checkExecutables() {
    # Usage: checkExecutables [ command1 ] [ command2 ] [...] [ commandN ]
    # Description: Checks that provided commands exist and displays those that do not exist.
    # Input:
    #   - command names (arguments)
    # Output: commands that don't exist (stderr)
    # Script function dependencies:
    #   - echoerr          for displaying errors via stderr
    #   - processArguments for setting OPTION_VERBOSE
    #   - vbm              for displaying verbose messages if OPTION_VERBOSE is "true"
    # External dependencies: bash (5.0.3), command
    # Last modified: 2020-04-11T23:59Z
    # Last modified by: Steven Baltakatei Sandoval
    # License: GPLv3+
    # Ref./Attrib.:
    #  [1]: SiegeX (2010-12-12). ["Difference between return and exit in Bash functions."](https://stackoverflow.com/a/4419971). Licensed CC BY-SA 4.0.
    #  [2]: Darryl Hein (2009-12-23). ["Add a new element to an array without specifying the index in Bash"](https://stackoverflow.com/a/1951523). Licensed CC BY-SA 4.0.
    #  [3]: kojiro (2012-10-03). ["Convert command line arguments into an array in Bash"](https://stackoverflow.com/a/12711853)
    #  [4]: niieani (2016-01-12). ["How to copy an array in Bash?"](https://stackoverflow.com/a/34733375/10850071). Licensed CC BY-SA 4.0.

    # Initialize function
    vbm "DEBUG:checkExecutables function called."
    declare -a candidateCommandsNames # Initialize array for storing positional arguments provided to this function.
    candidateCommandsNames=("$@") # Save positional arguments to variable as string. See [3].
    vbm "DEBUG:candidateCommandsNames:""$*"
    vbm "DEBUG:candidateCommandsNames[0]:""${candidateCommandsNames[0]}"
    vbm "DEBUG:candidateCommandsNames[1]:""${candidateCommandsNames[1]}"
    vbm "DEBUG:candidateCommandsNames[2]:""${candidateCommandsNames[2]}"
    declare -a outputInvalidCommandsArray # Initialize arary for storing names of invalid commands.
    declare -a outputValidCommandsArray # Initialize array for storing names of valid commands.

    # Perform work
    for candidateCommandName in "${candidateCommandsNames[@]}"; do  # Search through all space-delimited text for valid commands.
	if command -v "$candidateCommandName" 1>/dev/null 2>/dev/null; then # Check if a command is valid or not.
	    outputValidCommandsArray+=("$candidateCommandName") ; # See [2].
	    vbm "DEBUG:Adding $candidateCommandName to outputValidCommandsArray."
	else
	    outputInvalidCommandsArray+=("$candidateCommandName") ; # See [2].
	    vbm "DEBUG:Adding $candidateCommandName to outputInvalidCommandsArray."
	fi
    done

    # Output results
    if [ ${#outputInvalidCommandsArray[@]} -gt 0 ]; then # if number of elements in outputInvalidCommandsArray greater than 0, then display offending commands and exit 1.
	echoerr "ERROR: Invalid commands found:""${outputInvalidCommandsArray[@]}"; # display invalid commands as error
	exit 1; # See [1].
    elif [ ${#outputInvalidCommandsArray[@]} -eq 0 ]; then # if number of elements in outputInvalidCommandsArray equals zero, then return 0.
	vbm "DEBUG: Valid commands are:""${outputValidCommandsArray[@]}"; # display valid commands if verbose mode enabled
	return 0; # See [1].
    else
	echoerr "ERROR: Check outputInvalidCommandsArray.";
    fi

    # End function
    vbm "DEBUG:checkExecutables function ended."
    return 0; # Function finished.
} # Check that certain executables exist.
updateTimeConstants() {
    # Usage: updateTimeConstants
    # Description: Updates time-related variables for use by other scripts.
    # Input: (none)
    # Output: Sets following variables:
    #   TIME_CURRENT       Current time in long ISO-8601 format.  (ex: YYYY-mm-ddTHH:MM:SS+ZZZZ)
    #   TIME_CURRENT_SHORT Current time in short ISO-8601 format. (ex: YYYYmmddTHHMMSS+ZZZZ)
    #   DATE_CURRENT       Current date in ISO-8601 format.       (ex: YYYY-mm-dd)
    #   DATE_CURRENT_SHORT Current date in short ISO-8601 format. (ex: YYYYmmdd)
    #   DATE_TOMORROW      Tomorrow's date in ISO-8601 format.    (ex: YYYY-mm-dd)
    #   TIME_NEXT_MIDNIGHT Time of tomorrow's midnight in long    (ex: YYYY-mm-ddTHH:MM:SS+ZZZZ)
    #                       ISO-861 format.
    #   SEC_TIL_MIDNIGHT   Seconds until next midnight.
    # Script function dependencies:
    #   - echoerr          for displaying errors via stderr
    #   - processArguments for setting OPTION_VERBOSE
    #   - vbm              for displaying verbose messages if OPTION_VERBOSE is "true"
    # External dependencies: bash (5.0.3), date, echo
    # Last modified: 2020-04-11T23:59Z
    # Last modified by: Steven Baltakatei Sandoval
    # License: GPLv3+
    # Ref./Attrib.:

    # Initialize function
    vbm "DEBUG:updateTimeConstants function called."

    # Perform work
    TIME_CURRENT="$(date --iso-8601=seconds)" ;
    TIME_CURRENT_SHORT="$(date -d "$TIME_CURRENT" +%Y%m%dT%H%M%S%z)"
    DATE_CURRENT="$(date -d "$TIME_CURRENT" --iso-8601=date)" ;
    DATE_CURRENT_SHORT="$(date -d "$TIME_CURRENT" +%Y%m%d)" ;
    DATE_TOMORROW="$(date -d "$TIME_CURRENT next day" --iso-8601=date)" ;
    TIME_NEXT_MIDNIGHT="$(date -d "$DATE_TOMORROW" --iso-8601=seconds)" ;
    SECONDS_UNTIL_NEXT_MIDNIGHT="$((  $(date +%s -d "$TIME_NEXT_MIDNIGHT") - $(date +%s -d "$TIME_CURRENT")  ))" ;

    # End function
    vbm "DEBUG:updateTimeConstants function ended."
    return 0; # Function finished.
} # Update time constants
checkapp() {
    # Desc: If arg is a command, save result in assoc array 'appRollCall'
    # Usage: checkapp arg1 arg2 arg3 ...
    # Input: global assoc. array 'appRollCall'
    # Output: adds/updates key(value) to global assoc array 'appRollCall'
    local returnState    
    #echo "DEBUG:$(date +%S.%N)..Starting checkapp function."
    #echo "DEBUG:args: $*"
    #echo "DEBUG:returnState:$returnState"

    #===Process Args===
    for arg in "$@"; do
	#echo "DEBUG:processing arg:$arg"
	if command -v "$arg" 1>/dev/null 2>&1; then # Check if arg is a valid command
	    appRollCall[$arg]="true";
	    #echo "DEBUG:appRollCall[$arg]:"${appRollCall[$arg]}
	    if ! [ "$returnState" = "false" ]; then returnState="true"; fi
	else
	    appRollCall[$arg]="false"; returnState="false";
	fi
    done
    
    #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done
    #echo "DEBUG:evaluating returnstate. returnState:"$returnState

    #===Determine function return code===
    if [ "$returnState" = "true" ]; then
	#echo "DEBUG:checkapp returns true for $arg";
	return 0;
    else
	#echo "DEBUG:checkapp returns false for $arg";
	return 1;
    fi
} # Check that app exists
checkfile() {
    # Desc: If arg is a file path, save result in assoc array 'fileRollCall'
    # Usage: checkfile arg1 arg2 arg3 ...
    # Input: global assoc. array 'fileRollCall'
    # Output: adds/updates key(value) to global assoc array 'fileRollCall';
    # Output: returns 0 if app found, 1 otherwise
    local returnState

    #===Process Args===
    for arg in "$@"; do
	#echo "DEBUG:processing arg:$arg"
	if [ -f "$arg" ]; then
	    fileRollCall["$arg"]="true";
	    #echo "DEBUG:fileRollCall[\"$arg\"]:"${fileRollCall["$arg"]}
	    if ! [ "$returnState" = "false" ]; then returnState="true"; fi
	else
	    fileRollCall["$arg"]="false"; returnState="false";
	fi
    done

    #for key in "${!fileRollCall[@]}"; do echo "DEBUG:fileRollCall key [$key] is:${fileRollCall[$key]}"; done
    #echo "DEBUG:evaluating returnstate. returnState:"$returnState
    
    #===Determine function return code===
    if [ "$returnState" = "true" ]; then
	#echo "DEBUG:checkapp returns true for $arg";
	return 0;
    else
	#echo "DEBUG:checkapp returns false for $arg";
	return 1;
    fi
} # Check that file exists
main() {
    # Usage: main "$@"    # See [1].
    # Input: unspecified (note: all Global Constants declared at beginning of script assumed to be available)
    #   OPTION_VERBOSE   (used by vbm, set by processArguments)  (ex: "true", "false")
    # Output: unspecified
    # Script function dependencies:
    #   - echoerr          for displaying errors via stderr
    #   - vbm              for displaying verbose messages if OPTION_VERBOSE is "true"
    #   - processArguments for setting OPTION_VERBOSE
    #   - checkExecutables for checking that specified commands are available
    # External dependencies: bash (5.0.3), echo
    # Last modified: 2020-05-04T16:50Z
    # Last modified by: Steven Baltakatei Sandoval
    # License: GPLv3+
    # Ref./Attrib.:
    #  [1]: ErichBSchulz (2011-11-20). [How to correctly pass bash script arguments to functions](https://stackoverflow.com/a/8198970). Licensed CC BY-SA 4.0.

    # Initialize function
    processArguments "$@" # Process arguments.
    vbm "DEBUG:main function called."
    vbm "DEBUG:main function arguments are:""$@"
    checkExecutables "echo" "date" "cut" "awk" # Confirm that executables necessary to run are available. Exit if any fail.

    # Process inputs
    if [ -v FILEIN1 ]; then # VERBOSE: Display contents of FILEIN1 if FILEIN1 has been set.
	vbm "DEBUG:main function detects input file:""$FILEIN1" ;
	vbm "DEBUG:contents of following file is displayed below:""$FILEIN1" ;
	if [ "$OPTION_VERBOSE" == "true" ]; then # display FILEIN1 contents via cat if verbose mode specified
	    echoerr "========""$FILEIN1"" START""========" ;
	    cat "$FILEIN1" 1>&2 ;
	    echoerr "========""$FILEIN1"" END""========" ;
	fi
    fi
    
    # Perform work
    OUTPUT="$OUTPUT""Hello world.\n"
    if [ -v FILEIN1 ]; then
	OUTPUT="$OUTPUT""The b2sum hash of $FILEIN1 is: $(b2sum "$FILEIN1" | awk '{print $1}').\n";
    fi
    OUTPUT="$OUTPUT""The current time is:""$SCRIPT_TIME_SHORT\n"

    # Output results
    echo -e "$OUTPUT"

    if [ -v FILEOUT1 ]; then  # if FILEOUT1 set, write to this file.
	vbm "DEBUG:main function detects output file:""$FILEOUT1" ;
	echo -e "$OUTPUT" > "$FILEOUT1" ;
	vbm "DEBUG:main funtion wrote OUTPUT to:""$FILEOUT1" ;
    elif [ -v DIROUT1 ]; then  # else if DIROUT1 set, set FILEOUT1 to timestamped filename, combine into PATHOUT1, write output to PATHOUT1.
	vbm "DEBUG:main function detects output directory:""$DIROUT1" ;
	FILEOUT1="$SCRIPT_TIME_SHORT"..output
	PATHOUT1="$DIROUT1"/"$FILEOUT1"
	echo -e "$OUTPUT" > $PATHOUT1 ;
	vbm "DEBUG:main function wrote output file to:""$PATHOUT1" ;
    fi

    # End function
    vbm "DEBUG:main function ended."
    return 0; # Function finished.
} # Main function
#==END Function Definitions==

#==BEGIN Perform work and exit==
main "$@" # Run main function.
exit 0;
#==END Perform work and exit==