#!/bin/bash
set -u

#==============================================================================
# Declarations and Environment

declare ThisScript=${0##*/}
declare CmdLine="$0 $*"
declare ThisHost=${HOSTNAME%%.*}
declare ThisUser=
declare ChangelistFile=
declare RemoteSpec=origin
declare Version=1.3.3
declare -i ErrorCount=0
declare -i NoOp=1
declare -i Debug=0
declare -i LineCount=0
declare -i ChangesFetchedCount=0
declare CL=
declare Cmd=
declare Log=
declare SleepDelayBetweenFetches=
declare AppHome=/p4/common/site/bin
declare AppCfg="${ThisScript%.sh}.cfg"

declare H1="=============================================================================="
declare H2="------------------------------------------------------------------------------"

#==============================================================================
# Local Functions

function msg () { echo -e "$*"; }
function dbg () { [[ "$Debug" -eq 0 ]] || msg "DEBUG: $*"; }
function errmsg () { msg "\nError: ${1:-Unknown Error}\n"; ErrorCount+=1; }
function bail () { errmsg "${1:-Unknown Error}"; exit "$ErrorCount"; }

#------------------------------------------------------------------------------
# Function: terminate
# shellcheck disable=SC2317
function terminate
{
   # Disable signal trapping.
   trap - EXIT SIGINT SIGTERM

   dbg "$ThisScript: EXITCODE: $ErrorCount"

   # Stop logging.
   [[ "$Log" == off ]] || msg "\nLog is: $Log\n${H1}"

   # With the trap removed, exit.
   exit "$ErrorCount"
}

#------------------------------------------------------------------------------
# Function: usage (required function)
#
# Input:
# $1 - style, either -h (for short form) or -man (for man-page like format).
# The default is -h.
#
# $2 - error message (optional).  Specify this if usage() is called due to
# user error, in which case the given message displayed first, followed by the
# standard usage message (short or long depending on $1).  If displaying an
# error, usually $1 should be -h so that the longer usage message doesn't
# obscure the error message.
#
# Sample Usage:
# usage
# usage -h
# usage -man
# usage -h "Incorrect command line usage."
#------------------------------------------------------------------------------
function usage
{
   declare style=${1:--h}
   declare errorMessage=${2:-Unset}

   if [[ "$errorMessage" != Unset ]]; then
      msg "\n\nUsage Error:\n\n$errorMessage\n\n"
   fi

   msg "USAGE for $ThisScript v$Version:

$ThisScript -c <changelist_file> [-r <remote_spec>] [-y] [-d|-D]

or

$ThisScript [-h|-man|-V]"

   if [[ $style == -man ]]; then
      msg "
DESCRIPTION:
	This script fetches changes one changelist at a time, using a
	specified changelist file and remote spec.  This enables an
	arbitrarily large fetch to be done.

OPTIONS:
 -c <changelist_file>
 	Specify the changelist file, a simple text file containing the
	changelist numbers in ascending order. This file is created with
	a command like:

	p4 -p <YourTargetServerPort> -u <YourTargetServerUser> login
	p4 -p <YourTargetServerPort> -u <YourTargetServerUser> -E P4CONFIG= -ztag -F \"%change%\" changes -r -s submitted //path1/... //path2/... //path3/... | sort -u > AllChangesToFetch.txt

 -r <remote_spec>
	Specify the name of the remote spec to use. The default is to use the p4d default remote spec name, 'origin'.

 -d	Debug mode. Displays extra output normally suppressed.

 -D	Extreme debug mode using bash 'set -x' mode. Implies '-d'.

 -L <log>
	Specify the path to a log file, or the special value 'off' to disable
	logging.  By default, all output (stdout and stderr) goes to:
	\${LOGS:-/tmp}/${ThisScript%.sh}.<DateStamp>.log

	NOTE: This script is self-logging.  That is, output displayed on the screen
	is simultaneously captured in the log file.  Do not run this script with
	redirection operators like '> log' or '2>&1', and do not use 'tee.'

 -y	Take real action. Without -y, this script displays commands instead of
 	running them.

THROTTLE CONTROL:
	${ThisScript%.sh}.cfg: This file uses an optional configuration file.

	Create this configuration file if you want to throttle the fetch. In the
	config file, set SleepDelayBetweenFetches to the number of seconds to sleep
	between fetches. This is a means of throttle control to reduce load on the
	target server. If server load is not a concern, set to 0 for no delay or
	simply don't create the configuration file.

	${ThisScript%.sh}.cfg.sample: A sample config file.

HELP OPTIONS:
 -h	Display short help message
 -man	Display man-style help message
 -V	Display version info for this script and its libraries.

EXAMPLES:
	Kick it off like so:

	First, operate in a designated directory, e.g. $AppHome:

	cd $AppHome

	Next, do a Dry Run:
	./$ThisScript -c AllChangesToFetch.txt -r YourRemoteSpec

	Review that output. If it looks good. give it a go:

	nohup ./$ThisScript -c AllChangesToFetch.txt -r YourRemoteSpec -y < /dev/null > ${LOGS:-/tmp}/${ThisScript%.sh}.extra.log 2>&1 &
"
   fi

   exit 2
}

#==============================================================================
# Command Line Processing

declare -i shiftArgs=0

set +u
while [[ $# -gt 0 ]]; do
   case $1 in
      (-h) usage -h;;
      (-man) usage -man;;
      (-V) msg "$ThisScript version $Version"; exit 2;;
      (-c) ChangelistFile="$2"; shiftArgs=1;;
      (-r) RemoteSpec="$2"; shiftArgs=1;;
      (-L) Log="$2"; shiftArgs=1;;
      (-y) NoOp=0;;
      (-d) Debug=1;;
      (-D) Debug=1; set -x;; # Debug; use 'set -x' mode.
      (-*) usage -h "Unknown option ($1).";;
      (*) usage -h "Unknown parameter ($1).";;
   esac

   # Shift (modify $#) the appropriate number of times.
   shift; while [[ $shiftArgs -gt 0 ]]; do
      [[ $# -eq 0 ]] && usage -h "Incorrect number of arguments."
      shiftArgs=$shiftArgs-1
      shift
   done
done
set -u

#==============================================================================
# Command Line Validation

[[ -n "$ChangelistFile" ]] ||\
   usage -h "The '-c <changelist_file>' argument is required."

#==============================================================================
# Main Program

trap terminate EXIT SIGINT SIGTERM

if [[ -r /p4/common/bin/p4_vars ]]; then
   dbg "Loading SDP Shell Environmet."
   # shellcheck disable=SC1091
   source /p4/common/bin/p4_vars "${SDP_INSTANCE:-1}"
else
   msg "Warning: Operating in Non-SDP environment. Ensure P4PORT, P4USER, etc. are set correctly."
fi

[[ -n "$Log" ]] ||\
   Log="${LOGS:-/tmp}/${ThisScript%.sh}.$(date +'%Y-%m-%d-%H%M%S').log"

if [[ "$Log" != off ]]; then
   exec > >(tee "$Log")
   exec 2>&1

   msg "${H1}\nLog is: $Log\n"
fi

ThisUser=$(id -n -u)
msg "Starting $ThisScript version $Version as $ThisUser@$ThisHost on $(date) as:\n$CmdLine"

[[ -r "$ChangelistFile" ]] || bail "Missing expected changelist file: $ChangelistFile"

if p4 remote --exists -o "$RemoteSpec" > /dev/null; then
   msg "Verified: Remote spec exists: $RemoteSpec"
else
   bail "The remote spec does not exist: $RemoteSpec"
fi

Cmd="p4 login -s -r $RemoteSpec"
if $Cmd > /dev/null; then
   msg "Verified: RemoteUser specified in remote spec '$RemoteSpec' is logged in."
else
   bail "Could not verify RemoteUser for remote spec '$RemoteSpec' is logged in. Tried:\n\t$Cmd\n\nFix this by doing:\n\tp4 login -r $RemoteSpec\n"
fi

msg "${H2}\nStarting fetch of changes listed in $ChangelistFile with remote spec $RemoteSpec."

while read -r CL; do
   LineCount+=1

   if [[ ! "$CL" =~ ^[0-9]+$ ]]; then
      errmsg "Ignoring bogus non-numeric data line $LineCount in $ChangelistFile: $CL"
      continue
   fi

   # Read in Sleep value from config file. Check each loop iteration since the file
   # can be created or removed after the script is running.
   if [[ -r "$AppCfg" ]]; then
      SleepDelayBetweenFetches=$(grep -E '^\s*SleepDelayBetweenFetches=' "$AppCfg" | head -1 | cut -d= -f2) 
      if [[ ! "$SleepDelayBetweenFetches" =~ ^[0-9]+$ ]]; then
         # If we hit this error, we'll get an every iteration. Force an extra sleep to avoid log spamming.
         errmsg "Could not read value for SleepDelayBetweenFetches from file '$AppCfg'; value loaded was '$SleepDelayBetweenFetches'. Sleeping 10s to avoid aggressive log spamming and then assuming SleepDelayBetweenFetches=1."
         sleep 10
         SleepDelayBetweenFetches=1
      fi
   else
      SleepDelayBetweenFetches=
   fi

   Cmd="p4 -s fetch -r $RemoteSpec //...@$CL,@$CL"
   [[ "$NoOp" -eq 1 ]] && Cmd="echo $Cmd"

   msg "Running: $Cmd"
   if $Cmd; then
      ChangesFetchedCount+=1
   else 
      Cmd="p4 -s fetch -I -r $RemoteSpec //...@$CL,@$CL"
      msg "Running: $Cmd"
      if $Cmd; then
         ChangesFetchedCount+=1
      else
         errmsg "Failed to fetch change $CL.  Consider using P4Transfer as a workaround? Aborting fetch processing."
         break
      fi 
   fi

   # Give the remote server a moment to breathe (unless SleepDelayBetweenFetches is 0).
   if [[ -n "$SleepDelayBetweenFetches" ]]; then
      msg "Sleeping for $SleepDelayBetweenFetches seconds."
      sleep "$SleepDelayBetweenFetches."
   fi
done < "$ChangelistFile"

msg "\nSummary:

   Changes Fetched:  $ChangesFetchedCount
   Errors:           $ErrorCount"

msg "Time: $ThisScript took $((SECONDS/3600)) hours $((SECONDS%3600/60)) minutes $((SECONDS%60)) seconds.\n"

if [[ "$ErrorCount" -eq 0 ]]; then
   msg "\nSuccess: All processing completed without errors."
else
   errmsg "Processing completed, but with $ErrorCount errors. Review the log carefully."
fi
