#!/usr/bin/perl --
# @(#) File: tkping - Machine Status Tool
# @(#) $Source: ~/tkping-0.1.1/tkping

use strict qw(vars subs);	# prevent stupid mistakes
use vars qw($NAME $THISDIR $VERSION $SYNOPSIS $ARGUMENTS $DESCRIPTION);

## Get basename and dirname of program path
BEGIN {
	($NAME, $THISDIR) = ($0 =~ m|^(.*)/([^/]+)$|o) ? ($2, $1) : ($0, '.');
}

$VERSION = "1.1.1";

$SYNOPSIS = "$NAME [options]";

$ARGUMENTS = "
 -r/ows {nbr}       Set the {nbr} of rows to be displayed
 -c/olumns {nbr}    Set the {nbr} of columns to be displayed
 -w/intitle {titleStr}  Set window title to {titleStr}
 -i/gnore           Do not ping grid automatically, wait for manual rqst

 -p/ackets {nbr}    {nbr} of packets to send to each node (default 5)
 -s/leep {secs}     {secs} between traversals of the host list (default 120)
 -t/imeout {mSecs}  How long to wait for packet before assuming node no longer
                     responding (default 500 milliseconds)

 -x/defs {fSpec}    Load default values from {fSpec} instead of default
                     (default /etc/tkping/tkping.conf)
 -n/odes {fSpec}    Load host (node) list from {fSpec} instead of default
                     (default ~/.tkpingrc)
 -l/ogfile {fSpec}  Log error/fail events to {fSpec}

 -h/elp        Display (h)elp (this screen)
 -d/ebug       (d)ebug: display DEBUG messages   (for use by developer)
 -v/erbose     (v)erbose: show more detailed info regarding progress

NOTE: options can be in any order.

RETURN CODES:
 0 - successful (or help was requested)
 2 - invalid parms, internal error, etc.\n
";


$DESCRIPTION = "
$NAME - Ver$VERSION Machine Status Tool

by Stephen Moraco, stephen\@debian.org
Copyright (c) 2001-2004,\nStephen M Moraco.\n
";

### =====================================================================
#Z#                   * Hook to external pieces parts
### ---------------------------------------------------------------------
###

### !!Uncomment the following line to help in debugging use statements!!
# BEGIN { $Exporter::Verbose = 1; }   # good Package debugging technique...

require 5.004;

use English;
use lib "/usr/local/share/tkping";	# if installed in /usr/local
use lib "/usr/share/tkping";		# if install on Debian
use lib "$THISDIR";						# if running from current durectory

require "newgetopt.pl";

use Tk;
use Tk qw(:eventtypes);
use Tk::widgets qw(Dialog Button Label Entry Menubar);
require Tk::ErrorDialog;
use Tk::After;

use tkpingLib 1.10;	#  import helper routines... (we need ver 1.10 or greater)
use tkpingLib qw(
   $NOTSET_STR
);

## Force a flush after every write or print on the currently selected output
## filehandle.
use FileHandle;
STDOUT->autoflush;
STDERR->autoflush;


###
### ---------------------------------------------------------------------
#Z#                * End of external pieces parts hooks
### =====================================================================

my $glbNbrRows = 5;			# default
my $glbNbrColumns = 1;		# default
my $glbTimeoutMillSecs = 500;	# default
my $glbSleepSecs = 120;		# default
my $glbNbrPackets = 5;		# default
my $glbTitleStr = $NOTSET_STR;
my $glbLogFspec = $NOTSET_STR;
my $glbHostsFspec = $NOTSET_STR;
my $glbConfFspec = $NOTSET_STR;

$newgetopt::ignorecase = 0;   # force case-sensitivity!

my @origArgsAr = @ARGV; # Capture before polluted by interpretation
my $origArgsStr = join " ", @origArgsAr;

Usage( 0 ) if( $ARGV[0] eq "\-\?" );

my $rc = NGetOpt("debug", "help", "verbose", "ignore",
              "rows:i", \$glbNbrRows, "columns:i", \$glbNbrColumns,
              "timeout:i", \$glbTimeoutMillSecs, "sleep:i", \$glbSleepSecs,
              "wintitle:s", \$glbTitleStr, "logfile:s", \$glbLogFspec,
              "nodes:s", \$glbHostsFspec, "packets:i", \$glbNbrPackets,
              "xdefs:s", \$glbConfFspec);

use vars qw($opt_debug $opt_help $opt_verbose $opt_ignore
            $opt_rows $opt_columns $opt_timeout $opt_sleep $opt_wintitle
			$opt_logfile $opt_nodes $opt_packets $opt_xdefs);

$^W = 1;	# I want to see warnings from here to end-of-file!

### User asked for help...  This should always be _fast_ so is here before
### further validation!
Usage(0) if ($opt_help);	# exit with success (0) return code!

Usage(2) if (!$rc);			# exit with error (2) if option parse failure

###
#Z#  * Validate environment
###
###   [Here we validate that key directories, key central files exist, etc.]
###
DebugMsg("Checking environment");

my $LOGFILE = "$NAME.log";
SetLog($LOGFILE);

my ($realId,$effId) = GetUserNames();

my $FPING_CMD = "/usr/bin/fping";

###
###    Validate/internalize parms
###
###   [Here we validate parm combinations, presence and translate for later use]
###
###
DebugMsg("Evaluating parms");

###
#Z#  * Get and deep-validate/xlate parms
###

if(@ARGV > 0) {
	Msg(-err,-nl,"Too many parms! [$origArgsStr]\007");
	$rc = 0;
}

$rc =0 if(!RequireParm("wintitle", $glbTitleStr, "{titleStr}"));
$rc =0 if(!RequireParm("nodes", $glbHostsFspec, "{fspec}"));
$rc =0 if(!RequireParm("xdefs", $glbConfFspec, "{fspec}"));
$rc =0 if(!RequireParm("logfile", $glbLogFspec, "{fspec}"));


if(! $rc) {
	Usage(2);
}

###  determine if any options require command line only MODE!

###
#Z#  * Initialize Global Variables
###
my $NOTSET_FLAG = "** NOT set **";

###  This is the list of supported parameters.
my %glbValidParametersHs = ();
$glbValidParametersHs{'messageTextColor'} = 1;
$glbValidParametersHs{'allPacketsBackColor'} = 1;
$glbValidParametersHs{'somePacketsBackColor'} = 1;
$glbValidParametersHs{'noPacketsBackColor'} = 1;
$glbValidParametersHs{'ignoredColor'} = 1;
$glbValidParametersHs{'changedColor'} = 1;
$glbValidParametersHs{'errorColor'} = 1;
$glbValidParametersHs{'Rows'} = 1;
$glbValidParametersHs{'Columns'} = 1;
$glbValidParametersHs{'Packets'} = 1;
$glbValidParametersHs{'Sleep'} = 1;
$glbValidParametersHs{'Timeout'} = 1;
$glbValidParametersHs{'WinTitle'} = 1;
$glbValidParametersHs{'Ignore'} = 1;
$glbValidParametersHs{'Logfile'} = 1;

###  Now declare text-color and active-text-color settings to be valid
foreach my $colorKey (grep /Color$/, (keys %glbValidParametersHs)) {
	my $reasonPrefix = $colorKey;
	$reasonPrefix =~ s/Color$//;
	$glbValidParametersHs{"${reasonPrefix}TextColor"} = 1;
	$glbValidParametersHs{"${reasonPrefix}ActiveTextColor"} = 1;
}

###  Configuration Settings (Colors, etc.)
my %glbConfigDataHs = ();

###
#Z#  * !LIVE! variables!!! (write these and the main window updates!
###
my $glbGeneralStatusMsg = "";
my $glbEvaluationStatusMsg = "";


###
#Z#  * Locate/identify the files we are about to load
###
if(!defined $opt_nodes) {
	my $HOMEDIR = $ENV{'HOME'};
	$glbHostsFspec = "$HOMEDIR/.tkpingrc";
	my $TEST_HOSTS = "./test.hosts";
	if(-f $TEST_HOSTS) {
		$glbHostsFspec = $TEST_HOSTS;  ###  Overide with test file if present
	}
}
DebugMsg("glbHostsFspec=[$glbHostsFspec]");

if(!defined $opt_xdefs) {
	# is in Debian location?
	$glbConfFspec = "/etc/tkping/tkping.conf";
	if(! -f $glbConfFspec) {
		# no, assume is in /usr/local location
		$glbConfFspec = "/usr/local/etc/tkping.conf";
	}
	my $TEST_CONF = "./test.conf";
	if(-f $TEST_CONF) {
		$glbConfFspec = $TEST_CONF;
	}
}
DebugMsg("glbConfFspec=[$glbConfFspec]");

my $idStr = ($realId eq $effId) ? "$realId" : "$effId (as $realId)";
LogMsg("  by $idStr as: $NAME $origArgsStr");


my $MW;		# our main window object

my $glbModeCommandLineOnly = 0;
my $glbEnableDiagnosticLogging = 0;		# if things so south, log more so we can figure out issue

### ---------------------------------------------------------------------
#Z#  * Status tracking variables, used in INFO dialog
### ---------------------------------------------------------------------

###
###  Starting date/time of Application
###
my $glbApplicationStartTimeStr = "{not set}";


###  total number of pings sent since app launched
my $glbTotalPingsSentCount = 0;
###  total number of traversals since app launched
my $glbTotalTraversalCount = 0;

###
###  track average ping response time per host
###    using simple moving averages
###    NOTE: we track 4 moving averages per host!
###       (1, 50, 100 and 200 traversals)
###
my @glbTraversalCountIntervalsAr = (1, 50, 100, 200);
#my @glbTraversalCountIntervalsAr = (1, 3, 5, 7, 10); #for testing

###  NOTE: hostKey is short for "{hostNm}{intervalNbr}"
my %glbTraversalRunningSumByHostKeyHs = ();
my %glbTraversalPingCountByHostKeyHs = ();
###  and track quality by host by interval (NEW)
my %glbTraversalCountAllByHostKeyHs = ();
my %glbTraversalCountSomeByHostKeyHs = ();
my %glbTraversalCountNoneByHostKeyHs = ();
my %glbTraversalCountUnknownHostByHostKeyHs = ();
my %glbTraversalCountDownByHostKeyHs = ();

###  for traversal history, by host, we save:
###     - sum of return times for this traversal
###     - count of returns for this traversal
###     - quality of returns for this travesal [all|some|none|hostNotFound]
###
###  This info is kept in four identical depth [$MAX_TRAVERSAL_ENTRIES] FIFOs
#  NOTE: these four are symbolically named arrays (with $hostNm in array name)
#         which are setup by initHostTracking()
#my @glbTraversalPingsReturnedSum${hostNm}Ar = ();
#my @glbTraversalPingsReturnedsCount${hostNm}Ar = ();
#my @glbTraversalQuality${hostNm}Ar = ();
my %glbMasterTraversalArrayHostListHs = ();	# keep track of hosts for which arrays are allocated
my %glbTraversalEntryCountByHostHs = ();	# current FIFO depth (fills to max over time)

### NOTE: our last entry in interval array (+1) is our max number kept
my $CONST_MAX_TRAVERSAL_ENTRIES = $glbTraversalCountIntervalsAr[$#glbTraversalCountIntervalsAr] + 1;

###  Quality Strings
my $CONST_QUAL_UNKNOWN = "HostUnknown";		###  BLACK
my $CONST_QUAL_ALL = "AllPacketsRcvd";		###  GREEN
my $CONST_QUAL_SOME = "SomePacketsRcvd";	###  YELLOW
my $CONST_QUAL_NONE = "NoPacketsRcvd";   	###  RED
my $CONST_QUAL_STARTUP = "AppStartup";		###  <default>
my $CONST_QUAL_RESTART = "Restarting";      ###  <default>
my $CONST_QUAL_DOWN = "MarkedDown";         ###  ORANGE

###
###  track CURR/PRIOR state per host
###
my %glbPriorStatusByHostHs = ();	# Immediately-previous Status
my %glbTimeEnteredPriorStatusByHostHs = ();	# time at which we entered previous state

my %glbCurrentStatusByHostHs = ();	# Current Status
my %glbTimeEnteredCurrentStatusByHostHs = ();	# time at which we entered curr state

###
###  Track pings by host
###
my %glbPingsReceivedCountByHostHs = ();
###  if host was ever marked down the following
###    differs from overall total sent by app
my %glbPingsSentCountByHostHs = ();


### ---------------------------------------------------------------------
#Z# - Globals for Graphical User Interface
### ---------------------------------------------------------------------
my $glbButtonGrid;
my $glbMenuBar;
my $glbOnHostMenu;

my $CONST_HOST_SEP = "-- NO MORE HOSTS AFTER HERE --";

### some color def's
my $CONST_COLOR_CYAN   = "#00FFFF";
my $CONST_COLOR_BLACK  = "#000000";
my $CONST_COLOR_WHITE  = "#FFFFFF";
my $CONST_COLOR_TK_DEF = "#D9D9D9";
### chose our 'selected' color
my $CONST_SELECTED_COLOR = $CONST_COLOR_CYAN;

###  Actions from the File Menu
my $CONST_FILE_ACTION_RECHECK = "Recheck";
my $CONST_FILE_ACTION_DOWN    = "MarkDown";

###  Actions against a posted dialog during ping
my $CONST_DLG_BUSY   = "markDialogBusy";
my $CONST_DLG_UNBUSY = "markDialogNotBusy";

my %glbBtnListByHostHs = ();  		# button-OBJ by hostname (full set)
my %glbBtnActiveByHostHs = ();	    # button-OBJ by hostname (active subset!)
my %glbBtnDownedByHostHs = ();		# button-OBJ by hostNm (deactivated subset!)
my %glbBtnSelectedByHostHs = ();	# button-OBJ by hostname (selected subset!)
my $glbDefaultBtnColor = $CONST_COLOR_TK_DEF;
my %glbPriorBtnColorByHostHs = ();

my %glbFQDNByHostHs = ();           # FullyQualidifedNames by hostname(full set)
my %glbHostByFQDNHs = ();           # hostnames by FullyQualidifedName(full set)

my $glbCurrentRightMouseHost = "";

my $glbDlgIsCurrentlyPosted = 0;	# preset to false
my $glbPostedDialog;				# dialog object to make busy when pinging


### =====================================================================
#Z#                      * Begin Subroutines
### ---------------------------------------------------------------------
###  onQuit()  - give user  chance to make final saves before exiting
###       (This procedure is called when the user clicks on 'file->quit')
###
sub onQuit()
{
	DebugMsg("onQuit() - entry");
	doExit(0,"exit from file->Exit");
}



### ---------------------------------------------------------------------
###  doExit()  - do final wrapup...
###
sub doExit($$)
{
	my $retCode = shift;
	my $exitMsg = shift;

	###  shut down our ping-ing loop....
	killPingLoop();

	###
	###  We're Done!!!
	###
	DebugMsg("Done");
	if($retCode) {
		LogMsg("Exit RC=$retCode: $exitMsg");
	}
	EndLog();
	exit($retCode);	# exit with or without saving
}


### ---------------------------------------------------------------------
###  beep - make noise when called!
###
sub beep()
{
	$MW->bell;
}


### ---------------------------------------------------------------------
###  getFIFOArrayNamesForHost - we've four symbolic names for our arrays
###                          return them with embedded host name
###
sub getFIFOArrayNamesForHost($)
{
	my $hostNm = shift;
	my @nameListAr = ("${hostNm}TraversalPingsReturnedSumAr",
					  "${hostNm}TraversalPingsReturnedsCountAr",
					  "${hostNm}TraversalQualityAr");
	return @nameListAr;
}


### ---------------------------------------------------------------------
###  initHostTracking - if this host not yet seen, initialize all ping
###                     and traversal tracking data stores
my %isInittedByHostHs = ();
###
sub initHostTracking($)
{
	my $hostNm = shift;

	if(!exists $isInittedByHostHs{$hostNm}) {
		### not setup, init tracking system for this host

		foreach my $intervalId (@glbTraversalCountIntervalsAr) {

			my $hostKey = "${hostNm}${intervalId}";

			$glbTraversalRunningSumByHostKeyHs{$hostKey} = 0.0;
			$glbTraversalPingCountByHostKeyHs{$hostKey} = 0;
			###  and track quality by host by interval (NEW)
			$glbTraversalCountAllByHostKeyHs{$hostKey} = 0;
			$glbTraversalCountSomeByHostKeyHs{$hostKey} = 0;
			$glbTraversalCountNoneByHostKeyHs{$hostKey} = 0;
			$glbTraversalCountUnknownHostByHostKeyHs{$hostKey} = 0;
			$glbTraversalCountDownByHostKeyHs{$hostKey} = 0;
		}

		###  for traversal history, by host, we save:
		###     - sum of return times for this traversal
		###     - count of returns for this traversal
		###     - quality of returns for this travesal [all|some|none|hostNotFound]
		###
		###  This info is kept in four identical depth [$MAX_TRAVERSAL_ENTRIES] FIFOs
		#
		#  NOTE: these three are symbolically named arrays (with $hostNm in array name)
		#         which are setup here:
		#
		no strict qw(refs);		# This OFF while creating arrays

		foreach my $arrayNm (getFIFOArrayNamesForHost($hostNm)) {
			@$arrayNm = ();		# create empty arrays
		}

		use strict qw(refs);	# now turn checking back on

		# keep track of hosts for which arrays are allocated
		$glbMasterTraversalArrayHostListHs{$hostNm} = 1;
		# current FIFO depth (fills to max over time)
		$glbTraversalEntryCountByHostHs{$hostNm} = 0;

		###
		###  track state per host
		###
		# Immediately-previous Status
		$glbPriorStatusByHostHs{$hostNm} = $CONST_QUAL_STARTUP;
		DebugMsg("PRIOR: INIT $hostNm=[$glbPriorStatusByHostHs{$hostNm}]");
		# time at which we entered prior state
		$glbTimeEnteredPriorStatusByHostHs{$hostNm} = $glbApplicationStartTimeStr;
		# Current Status
		$glbCurrentStatusByHostHs{$hostNm} = $CONST_QUAL_STARTUP;
		DebugMsg("CURR: INIT $hostNm=[$glbCurrentStatusByHostHs{$hostNm}]");
		# time at which we entered curr state
		$glbTimeEnteredCurrentStatusByHostHs{$hostNm} = $glbApplicationStartTimeStr;

		###
		###  Track pings by host
		###
		$glbPingsReceivedCountByHostHs{$hostNm} = 0;
		###  if host was ever marked down, or is unknown, the following
		###    differs from overall total sent by app
		$glbPingsSentCountByHostHs{$hostNm} = 0;

		### now ensure we don't do this again -for this host-
		$isInittedByHostHs{$hostNm} = 1;
	}
}


### ---------------------------------------------------------------------
###  removeDepartingSumsForHost - moving average, remove departing value
###                               from each of the intervals we are tracking
###
sub removeDepartingSumsForHost($)
{
	my $hostNm = shift;

	#  for each of our intervals...
	foreach my $intervalId (@glbTraversalCountIntervalsAr) {
		# if FIFO is not full for this interval there is nothing to subtract from moving average
		if($glbTraversalEntryCountByHostHs{$hostNm} > $intervalId) {

			#  we have enough ENTRIES in FIFO, subtract values of the entry that has
			#   just left the end of the FIFO...
			my $hostKey = "${hostNm}${intervalId}";

			#  NOTE: FIFO[0] thru FIFO[$intervalId-1] is DATA, FIFO[$intervalId] is item just leaving FIFO
			my $departingEntryIdx = $intervalId;

			#  access sums for departing traversal of this host
			my ($pingsReturnedSumArrayNm,
			    $pingsReturnedCountArrayNm,
			    $qualityArrayNm) = getFIFOArrayNamesForHost($hostNm);

			no strict qw(refs);		# This OFF while accessing arrays

			my $pingReturnTimesSum = $$pingsReturnedSumArrayNm[$departingEntryIdx];
			my $pingReceivedCt = $$pingsReturnedCountArrayNm[$departingEntryIdx];
			my $qualityStr = $$qualityArrayNm[$departingEntryIdx];

			use strict qw(refs);	# now turn checking back on

			DebugMsg("SUMS(RMV) hostNm=[$hostNm], itrvl=[$intervalId], pings=[$pingReceivedCt], sum=[$pingReturnTimesSum], qual=[$qualityStr]");

			#  now remove these departing sums from the moving average
			$glbTraversalRunningSumByHostKeyHs{$hostKey} -= $pingReturnTimesSum;
			$glbTraversalPingCountByHostKeyHs{$hostKey} -= $pingReceivedCt;

			###  and track quality by host by interval (NEW)
			foreach ($qualityStr) {
				/$CONST_QUAL_ALL/ and do {
					$glbTraversalCountAllByHostKeyHs{$hostKey}--;
					last;
				};
				/$CONST_QUAL_SOME/ and do {
					$glbTraversalCountSomeByHostKeyHs{$hostKey}--;
					last;
				};
				/$CONST_QUAL_NONE/ and do {
					$glbTraversalCountNoneByHostKeyHs{$hostKey}--;
					last;
				};
				/$CONST_QUAL_UNKNOWN/ and do {
					$glbTraversalCountUnknownHostByHostKeyHs{$hostKey}--;
					last;
				};
				/$CONST_QUAL_DOWN/ and do {
					$glbTraversalCountDownByHostKeyHs{$hostKey}--;
					last;
				};
				Msg(-err,"removeDepartingSumsForHost($hostNm) Unknown quality string [$qualityStr]");
			}
		}
	}
}


### ---------------------------------------------------------------------
###  addArrivingSumForHost - Add new entry to each of the intervals we
###                          are tracking
###
sub addArrivingSumForHost($$$$)
{
	my $hostNm = shift;
	my $pingReturnTimesSum = shift;
	my $pingReceivedCt = shift;
	my $qualityStr = shift;

	foreach my $intervalId (@glbTraversalCountIntervalsAr) {
		my $hostKey = "${hostNm}${intervalId}";

		DebugMsg("SUMS(ADD) hostNm=[$hostNm], itrvl=[$intervalId], pings=[$pingReceivedCt], sum=[$pingReturnTimesSum], qual=[$qualityStr]");

		$glbTraversalRunningSumByHostKeyHs{$hostKey} += $pingReturnTimesSum;
		$glbTraversalPingCountByHostKeyHs{$hostKey} += $pingReceivedCt;

		###  and track quality by host by interval (NEW)
		foreach ($qualityStr) {
			/$CONST_QUAL_ALL/ and do {
				$glbTraversalCountAllByHostKeyHs{$hostKey}++;
				last;
			};
			/$CONST_QUAL_SOME/ and do {
				$glbTraversalCountSomeByHostKeyHs{$hostKey}++;
				last;
			};
			/$CONST_QUAL_NONE/ and do {
				$glbTraversalCountNoneByHostKeyHs{$hostKey}++;
				last;
			};
			/$CONST_QUAL_UNKNOWN/ and do {
				$glbTraversalCountUnknownHostByHostKeyHs{$hostKey}++;
				last;
			};
			/$CONST_QUAL_DOWN/ and do {
				$glbTraversalCountDownByHostKeyHs{$hostKey}++;
				last;
			};
			Msg(-err,"addArrivingSumForHost($hostNm) Unknown quality string [$qualityStr]");
		}
	}
}


### ---------------------------------------------------------------------
###  postNewTraversalToFIFO - record new entry in FIFO, trim to fixed
###                           length if got one too many, Adjust moving
###                           averages for each of the intervals we are
###                           tracking
###
sub postNewTraversalToFIFO($$$$)
{
	my $hostNm = shift;
	my $pingReturnTimesSum = shift;
	my $pingReceivedCt = shift;
	my $qualityStr = shift;

	#  get names of arrays we use for this host
	my ($pingsReturnedSumArrayNm, $pingsReturnedCountArrayNm, $qualityArrayNm) =
				 getFIFOArrayNamesForHost($hostNm);

	no strict qw(refs);		# This OFF while creating arrays

	#  post new value to 0'th location in array
	unshift @$pingsReturnedSumArrayNm, $pingReturnTimesSum;
	unshift @$pingsReturnedCountArrayNm, $pingReceivedCt;
	unshift @$qualityArrayNm, $qualityStr;

	#  count this new entry for this host
	$glbTraversalEntryCountByHostHs{$hostNm}++;

	#  show what's just been added to FIFO
	DebugMsg("FIFO(ADD) hostNm=[$hostNm], pings=[$pingReceivedCt], sum=[$pingReturnTimesSum], qual=[$qualityStr]");

	#  IFF FIFO has more than should then remove oldest entry (FIFO is fixed length)
	if($glbTraversalEntryCountByHostHs{$hostNm} > $CONST_MAX_TRAVERSAL_ENTRIES) {
		###  remove tail entry from FIFO (all four parts) IFF have more than needed
		my $pingReturnTimesSum = pop @$pingsReturnedSumArrayNm;
		my $pingReceivedCt = pop @$pingsReturnedCountArrayNm;
		my $qualityStr = pop @$qualityArrayNm;

		#  show what's just been removed from FIFO
		DebugMsg("FIFO(RMV) hostNm=[$hostNm], pings=[$pingReceivedCt], sum=[$pingReturnTimesSum], qual=[$qualityStr]");
	}

	use strict qw(refs);	# now turn checking back on

	#  Remove departing sum from running total for each interval
	removeDepartingSumsForHost($hostNm);

	#  Add arriving sum to running total for each interval
	addArrivingSumForHost($hostNm, $pingReturnTimesSum, $pingReceivedCt, $qualityStr);
}


### --------------------------------------------------------------------------
###  setHostColor - given host name get object and inform it of new color
###                 change, return the color before the change to the caller
###
sub setHostColor($$;$)
{
	my $btnObj = shift;
	my $colorValue = shift;

#	DebugMsg("btnObj=[$btnObj], colorValue=[$colorValue]");

	my $colorReason = shift;	# pickup optional parameter

#	if(!defined $colorReason) {
#	}

	# preserve prior button color
	my $priorBGcolor = $btnObj->cget('-background');

	# set button colors
	$btnObj->configure(-background => $colorValue);
	$btnObj->configure(-activebackground => $colorValue);

	# set text colors (based on button colors)
	my ($nml, $actv) = ($colorValue =~ /black|#000000/i) ? ("white","red") : ("black", "white");
	$btnObj->configure(-foreground => $nml);
	$btnObj->configure(-activeforeground => $actv);

	$MW->idletasks();   ###  force display update (so menu shadows go away!)
	# return prior button color to caller
	return $priorBGcolor;
}


### ---------------------------------------------------------------------
###  accumulateTraversalWithPings -
###
sub accumulateTraversalWithPings($$$;$)
{
	my $hostNm = shift;				# host to which this string applies
	my $pingsSentCt = shift;		# number of entries in returnTimesStr
	my $pingTripTimesStr = shift;	# the round-trip times by attempt

	my $isHostFound = shift;		# OPTIONAL parameter
	if(!defined $isHostFound) {
		$isHostFound = 1;			# IFF not passed, assume host is found on net
	}

	DebugMsg("accumulateTraversalWithPings() hostNm=[$hostNm], pingsSentCt=[$pingsSentCt], returnTimesStr=[$pingTripTimesStr], isHostFound=[$isHostFound]");

	initHostTracking($hostNm);		# setup counters for this host if not already done

	#  Separate record into list of results
	my @fldAr = split /\s+/,$pingTripTimesStr;
	my $fldCt = @fldAr;

	if($fldCt != $pingsSentCt) {
		### if was parsing regenerated record, we've a [CODE] problem. Otherwise
		###   it could be bad format data received from fping(1m) [CODE/DATA].
		$glbEnableDiagnosticLogging = 1;
		LogMsg("DIAGLOG: Some pings missing! ($pingsSentCt\?\)");
		my $errTypeStr = (!$isHostFound) ? "[CODE]" : "[CODE/DATA]";
		Msg(-err,"$errTypeStr failed to parse result string: pings($pingsSentCt)\n\t[$hostNm : $pingTripTimesStr]");
		return;
	}

	#  Count the number of real returns
	my $noReturnCt = 0;
	my $pingReceivedCt = $pingsSentCt;
	if($isHostFound) {
		my @noReturnAr = grep /-/, @fldAr;
		$noReturnCt = @noReturnAr;
		$pingReceivedCt = $pingsSentCt - $noReturnCt;
	} else {
		#  shorter runtime, we know this answer
		$noReturnCt = $pingsSentCt;
		$pingReceivedCt = 0;
	}

	#  Calculate sum for this group of pings received
	my $pingReturnTimesSum = 0.0;
	if($isHostFound and $pingReceivedCt > 0) {
		foreach my $pingReturnTime (@fldAr) {
			if($pingReturnTime eq '-') {
				next;   # skip non-returns
			}
			$pingReturnTimesSum += $pingReturnTime;
		}
	}

	#  Calulate current quality text
	my $qualityStr = "";
	if(exists $glbBtnDownedByHostHs{$hostNm}) {
		$qualityStr = $CONST_QUAL_DOWN;			###  ORANGE
		Msg(-war,"got host down WITH PINGS!");
	} elsif(!$isHostFound) {
		$qualityStr = $CONST_QUAL_UNKNOWN;		###  BLACK
	} elsif($pingReceivedCt == $pingsSentCt) {
		$qualityStr = $CONST_QUAL_ALL;			###  GREEN
	} elsif($pingReceivedCt > 0) {
		$qualityStr = $CONST_QUAL_SOME;			###  YELLOW
	} else {
		$qualityStr = $CONST_QUAL_NONE;   		###  RED
	}

	DebugMsg("NEW hostNm=[$hostNm], noReturnCt=[$noReturnCt], pingReceivedCt=[$pingReceivedCt], pingReturnTimesSum=[$pingReturnTimesSum], qualityStr=[$qualityStr]");

	#  Accumulate TOTAL ping counts for this host
	$glbPingsReceivedCountByHostHs{$hostNm} += $pingReceivedCt;
	$glbPingsSentCountByHostHs{$hostNm} += $pingsSentCt;

	#  post new sum for this traversal for this host
	postNewTraversalToFIFO($hostNm, $pingReturnTimesSum, $pingReceivedCt, $qualityStr);

	#  IFF new state for host, record it
	my $setNewStatus = 0;	# not yet we didn't
	if($glbCurrentStatusByHostHs{$hostNm} ne $qualityStr) {
		#  save current state and prior
		$glbPriorStatusByHostHs{$hostNm} = $glbCurrentStatusByHostHs{$hostNm};
		# time at which we entered prior state
		$glbTimeEnteredPriorStatusByHostHs{$hostNm} =
			$glbTimeEnteredCurrentStatusByHostHs{$hostNm};
		DebugMsg("PRIOR: StatChg $hostNm=[$glbPriorStatusByHostHs{$hostNm}]");
		#  set new current state
		$glbCurrentStatusByHostHs{$hostNm} = $qualityStr;
		# time at which we entered curr state
		$glbTimeEnteredCurrentStatusByHostHs{$hostNm} = timeNow();
		DebugMsg("CURR: StatChg $hostNm=[$glbCurrentStatusByHostHs{$hostNm}]");
		$setNewStatus = 1; 	# OK, now we did
	}

	#  Set color for host if different from last
	if($setNewStatus) {
		foreach ($qualityStr) {
			/$CONST_QUAL_ALL/ and do {
				### choose between returning to service (gry) and ALL (grn)
				if($glbPriorStatusByHostHs{$hostNm} !~ /$CONST_QUAL_ALL|$CONST_QUAL_STARTUP|$CONST_QUAL_RESTART/) {
					###  set host ReturnedToService (GREY)
				 	setHostColor($glbBtnListByHostHs{$hostNm},
					             $glbConfigDataHs{'changedcolor'});
				} else {
					###  set host GOOD (GREEN)
				 	setHostColor($glbBtnListByHostHs{$hostNm},
					             $glbConfigDataHs{'allpacketsbackcolor'});
				}
				last;
			};
			/$CONST_QUAL_SOME/ and do {
				###  set host PARTIAL (ORANGE)
		 		setHostColor($glbBtnListByHostHs{$hostNm},
			             $glbConfigDataHs{'somepacketsbackcolor'});
				last;
			};
			/$CONST_QUAL_NONE/ and do {
				###  set host UNRESPONSIVE (RED)
		 		setHostColor($glbBtnListByHostHs{$hostNm},
				             $glbConfigDataHs{'nopacketsbackcolor'});
				last;
			};
			/$CONST_QUAL_UNKNOWN/ and do {
				###  set host UNKNOWN (BLACK)
		 		setHostColor($glbBtnListByHostHs{$hostNm},
				             $glbConfigDataHs{'errorcolor'});
				last;
			};
			Msg(-err,"accumulateTraversalWithPings($hostNm) Unknown quality string [$qualityStr]");
		}
	}
}


### ---------------------------------------------------------------------
###  accumulateTraversalHostUnknown -
###
sub accumulateTraversalHostUnknown($$)
{
	my $hostNm = shift;			# host to which this string applies
	my $pingsSentCt = shift;	# number of entries to be placed in pingRecord

	my $CONST_HOST_NOT_FOUND = 0;

	###  Synthesize ping return record
	my $pingRecord = "";
	if($pingsSentCt > 1) {
		$pingRecord = "- " x ($pingsSentCt - 1);
	}
	$pingRecord .= "-";		# add final ping non-response

	###  now let single routine handle this return, too
	accumulateTraversalWithPings($hostNm, $pingsSentCt, $pingRecord, $CONST_HOST_NOT_FOUND)
}


### ---------------------------------------------------------------------
###  accumulateStatsFromFpingData - process data returned from fping(1)
###
sub accumulateStatsFromFpingData(@)
{
	my @rawFPingDataAr = @_;
	my %hostListHs = ();
	my $hostListCt = ();

	###  NOTE: $glbNbrPackets is our required number of
	###    responses for each host

	my @summaryResultsAr = ();

	my %pingReturnCtrsByHostHs = ();

	###  NOTE: host list is passed first (then sep) then fping output

	my $inHostList = 1;
	foreach my $line (@rawFPingDataAr) {
		if($inHostList) {
			###  have new host entry (we should have ping results for this host!
			if($line ne $CONST_HOST_SEP) {
				my $hostNm = $line;
				if(exists $glbHostByFQDNHs{$line}) {
					$hostNm = $glbHostByFQDNHs{$line};
				} else {
					FatalMsg("accumStats...() unable to locate sort name for FQDN [$line]");
				}
				$hostListHs{$hostNm} = 1;	# add new host
				DebugMsg("ASFFD() host=[$hostNm]");
			} else {
				$inHostList = 0;
				$hostListCt = (keys %hostListHs);
				DebugMsg("* Pinging $hostListCt hosts in list");
				map { $pingReturnCtrsByHostHs{$_} = 0; } (keys %hostListHs);
				next;
			}
		} else {
			###  we have the full host list, now let's process real
			###   fping(1m) results data
			if($line =~ /^\s*$/) {
				next;	#  skip blank lines
			} elsif($line =~ /Host Unreachable.*sent\sto\s(\w+)\s/i) {
				###  host names to mark as partially avail...
				my $deadHost = $1;
				DebugMsg("LINE: Dead host[$deadHost] = [$line]");
				###  count the number of pings sent to this host
				###   --- NO, SUMMARY WILL ARRIVE FOR THESE, HANDLE IT, INSTEAD ---
			} elsif($line =~ /^([^\s]+)\saddress not found/i) {
				###  host names to turn black (bad names)
				my $unkHost = $1;
				$unkHost = $glbHostByFQDNHs{$unkHost};	# xlate to label text
				DebugMsg("LINE: Unk host[$unkHost] = [$line]");
				###  count the number of pings sent to this host
				accumulateTraversalHostUnknown($unkHost, $glbNbrPackets);
			} elsif($line =~ /^.*:\s+\[.*bytes.*\(.*$/) {
				###  host-ok : [0], 64 bytes, 0.81 ms (0.81 avg, 0% loss)
				###  is ping results for one ping
				###  \$1 is hostname
				my $hostNm = $line;
				$hostNm =~ s/\s+:\s+.*$//;
				$hostNm = $glbHostByFQDNHs{$hostNm};	# xlate to label text
				###  \$2 is ping attempt nbr
				my $attemptNbr = $line;
				$attemptNbr =~ s/^[^\[]+\[//;
				$attemptNbr =~ s/\],.*$//;
				###  \$3 is response time ex:(0.03 ms)
				my $responseTime = $line;
				$responseTime =~ s/^.*bytes,\s+//;
				$responseTime =~ s/\s\(.*$//;
				DebugMsg("LINE: #${attemptNbr} host=[${hostNm}],dur=[${responseTime}]\n\t - line=[$line]");
			} elsif($line =~ /^([^\s]+)\s+:.*$/) {
				my $hostNm = $1;
				my $resultsRecord = $line;
				$resultsRecord =~ s/${hostNm}\s*:\s+//;
				$hostNm = $glbHostByFQDNHs{$hostNm};	# xlate to label text
				###  count the number of pings sent to this host
				DebugMsg("LINE: Summary line=[$line]");
				accumulateTraversalWithPings($hostNm, $glbNbrPackets, $resultsRecord);
			} else {
				DebugMsg("LINE: ??? line=[$line]");
			}
		}
	}

    ### ---------------------------------------------------------------------
    ###  Example fping(8) v2.2b2-3 (at time of this writing) output
    ### ---------------------------------------------------------------------
    # $ fping -C 5 -b 36 host-ok host-off
    # RET_CODE=1
    # --- stdout ---
    # host-ok : [0], 64 bytes, 0.81 ms (0.81 avg, 0% loss)
    # host-ok : [1], 64 bytes, 0.43 ms (0.62 avg, 0% loss)
    # host-ok : [2], 64 bytes, 0.42 ms (0.55 avg, 0% loss)
    # host-ok : [3], 64 bytes, 0.44 ms (0.52 avg, 0% loss)
    # host-ok : [4], 64 bytes, 0.43 ms (0.50 avg, 0% loss)
    # --------------
    # --- stderr ---
    # ICMP Host Unreachable from 10.0.0.4 for ICMP Echo sent to host-off (10.0.0.6)
    # ICMP Host Unreachable from 10.0.0.4 for ICMP Echo sent to host-off (10.0.0.6)
    # ICMP Host Unreachable from 10.0.0.4 for ICMP Echo sent to host-off (10.0.0.6)
    #
    # host-ok : 0.1 0.3 0.2 0.4 0.3
    # host-off : - - - - -
    # --------------
    ### ---------------------------------------------------------------------
    # $ fping -C 5 -b 36 host-ok host-off host-bad
    # RET_CODE=2
    # --- stdout ---
    # host-ok : [0], 64 bytes, 0.48 ms (0.48 avg, 0% loss)
    # host-ok : [1], 64 bytes, 0.42 ms (0.45 avg, 0% loss)
    # host-ok : [2], 64 bytes, 0.46 ms (0.45 avg, 0% loss)
    # host-ok : [3], 64 bytes, 0.44 ms (0.45 avg, 0% loss)
    # host-ok : [4], 64 bytes, 0.44 ms (0.44 avg, 0% loss)
    # --------------
    # --- stderr ---
    # host-bad address not found
    # ICMP Host Unreachable from 10.0.0.4 for ICMP Echo sent to host-off (10.0.0.6)
    # ICMP Host Unreachable from 10.0.0.4 for ICMP Echo sent to host-off (10.0.0.6)
    # ICMP Host Unreachable from 10.0.0.4 for ICMP Echo sent to host-off (10.0.0.6)
    # host-ok : 0.8 0.2 0.6 0.4 0.4
    # host-off : - - - - -
    # --------------
    #
    ### ---------------------------------------------------------------------
    # # $ fping -C 5 -b 36 host-ok
    # RET_CODE=0
    # --- stdout ---
    # host-ok : [0], 64 bytes, 0.44 ms (0.44 avg, 0% loss)
    # host-ok : [1], 64 bytes, 0.44 ms (0.44 avg, 0% loss)
    # host-ok : [2], 64 bytes, 0.45 ms (0.44 avg, 0% loss)
    # host-ok : [3], 64 bytes, 0.44 ms (0.44 avg, 0% loss)
    # host-ok : [4], 64 bytes, 0.59 ms (0.47 avg, 0% loss)
    # --------------
    # --- stderr ---
    # host-ok : 0.4 0.4 0.5 0.4 0.9
    # --------------
    ### ---------------------------------------------------------------------
    # # $ fping -C 100 -b 36 host-c  # (host appears after ping starts)
    # RET_CODE=1
    # --- stdout ---
    #    STATS NOT SHOWN
    # --------------
    # --- stderr ---
    #    ERRORS NOT SHOWN
    # host-c : - - - - - - - - - - - - - - - - - - - - - - - - 1092.3 134.1 1.6 \\
    # 1.5 1.9 1.4 1.6 1.5 1.2 1.0 1.5 1.6 1.3 1.7 1.4 1.4 1.2 1.6 1.3 1.0 1.1 1.3 \\
    # 1.2 3.6 1.8 1.5 1.9 1.7 1.1 1.7 1.9 1.0 1.9 1.9 1.1 1.2 1.0 1.6 1.1 1.9 \\
    # 1.9 1.7 1.0 1.7 1.0 1.3 1.2 1.9 1.1 1.0 1.3 1.8 1.4 1.7 1.3
    # --------------
    ### ---------------------------------------------------------------------
    # # $ fping -C 20 filer
    # filer : [0], 84 bytes, 0.75 ms (0.75 avg, 0% loss)
    # filer : [1], 84 bytes, 0.50 ms (0.62 avg, 0% loss)
    # filer : [2], 84 bytes, 0.48 ms (0.57 avg, 0% loss)
    # filer : [3], 84 bytes, 0.48 ms (0.55 avg, 0% loss)
    # filer : [4], 84 bytes, 0.48 ms (0.53 avg, 0% loss)
    # filer : [5], 84 bytes, 0.52 ms (0.53 avg, 0% loss)
    #   NOTE: here we break the connection to see what happens...
    #     What happens is no output in this section, which is later
    #     confirmed by '-' chars in final summary line.
    # filer : [17], 84 bytes, 0.49 ms (0.52 avg, 61% loss)
    # filer : [18], 84 bytes, 0.45 ms (0.51 avg, 57% loss)
    # filer : [19], 84 bytes, 0.45 ms (0.51 avg, 55% loss)
    #
    # filer : 0.75 0.50 0.48 0.48 0.48 0.52 - - - - - - - - - - - 0.49 0.45 0.45
    ### ---------------------------------------------------------------------
}

### ---------------------------------------------------------------------
###  pingHostsAndAccumulateStats - ping one or more hosts and hand-off
###                                data to be processed
###
sub pingHostsAndAccumulateStats(@)
{
	my @hostLabelTextAr = @_;

	if(@hostLabelTextAr < 1) {
		DebugMsg("Aborting ping, no active hosts!");
		return;	###  No hosts to ping!
	}

	my %hostnameHs = ();
	foreach my $hostLabelText (@hostLabelTextAr) {
		if(exists  $glbFQDNByHostHs{$hostLabelText}) {
			$hostnameHs{$glbFQDNByHostHs{$hostLabelText}} = 1;
		} else {
			FatalMsg("pingHosts...() Failed to find FQDN for [$hostLabelText]");
		}
	}
	my @hostListAr = (keys %hostnameHs);
	my $hostLst = join " ",@hostListAr;

	###  force display update (makes it harder to catch button pressed...)
	$glbEvaluationStatusMsg	= "Ping";
	$MW->idletasks();   ###  force display update

	#Z#  TODO need? fork of task at 120 sec periods to run fping(8) to
	###    get host status data...

	#
	#my $cmdStr = "$FPING_CMD -c 5 host host host host host, etc."
	#   NOTE: 5 is a setting, and host.. is list of current buttons...
	#
	my @stdoutAr = ();
	my @stderrAr = ();
	my $cmdStr = "$FPING_CMD -C $glbNbrPackets -t $glbTimeoutMillSecs $hostLst";
	if($glbEnableDiagnosticLogging) {
		# we are getting fewer ping results than expected, log this to help us see why...
		LogMsg("DIAGLOG: FPING_CMD=[$cmdStr]");
	}
	my $retCode = DoCmdRetOutput($cmdStr,\@stdoutAr,\@stderrAr);
	#  NOTE: fping returns the following error codes which are OK
	#          rc=0: all OK,
	#          rc=1: some unreachable & some ok,
	#          rc=2: some not found & some not reachable & some ok
	#   Also: summaries arrive in STDERR output
	if($retCode < 0 || $retCode > 2) {
		Msg(-err,"$FPING_CMD failed retCode=[$retCode]");
		map { print STDERR "$NAME(ERR): $_\n";  } @stderrAr;
		map { print STDERR "$NAME(OUT): $_\n";  } @stdoutAr;
		FatalMsg("Aborted");
	}


	#
	#  We have good fping(1) data let's process it
	#
	accumulateStatsFromFpingData(@hostListAr, $CONST_HOST_SEP,
	                             @stdoutAr, @stderrAr);

	#
	#  In case of down hosts, we have no ping data but add to fifo counts
	#    do stats show how long we've been down
	#
	foreach my $hostNm (keys %glbBtnDownedByHostHs) {
		postNewTraversalToFIFO($hostNm, 0.0, 0, $CONST_QUAL_DOWN);
	}

	###  force display update (makes it harder to catch button pressed...)
	$glbEvaluationStatusMsg	= "IDLE";
	$glbGeneralStatusMsg = "";
	$MW->idletasks();   ###  force display update
}


### ---------------------------------------------------------------------
###  setPostedDialog - if a dialog is showing, mark it busy/unbusy
###
sub	setPostedDialog($)
{
	my $desiredBusyState = shift;

	if($glbDlgIsCurrentlyPosted) {
		if($desiredBusyState eq $CONST_DLG_BUSY) {
			#$glbPostedDialog->Busy(-recurse => 1); # kills OK button after IDLE????
			$glbPostedDialog->Busy();
		} else {
			$glbPostedDialog->Unbusy();
		}
		$MW->idletasks();   ###  force display update
	}
}


### ---------------------------------------------------------------------
###  pingEmAll - iterate over hosts pinging each and gathering stats
###
sub pingEmAll()
{
	#  account for any late color changes
	$MW->idletasks();   ###  force display update

	#  enter ping mode
	$glbEvaluationStatusMsg	= "Ping";
	$glbGeneralStatusMsg = "Pinging active hosts...";

	$glbButtonGrid->Busy(-recurse => 1);
	$glbMenuBar->Busy();
	setPostedDialog($CONST_DLG_BUSY);

	$MW->idletasks();   ###  force display update

	###  issue the ping command (currently active hosts)
	###  and process ping-returned data
	pingHostsAndAccumulateStats((keys %glbBtnActiveByHostHs));

	#  Accumulate program-overall stats
	$glbTotalTraversalCount++;
	$glbTotalPingsSentCount += $glbNbrPackets;

	#  return to normal user interaction
	setPostedDialog($CONST_DLG_UNBUSY);
	$glbMenuBar->Unbusy();
	$glbButtonGrid->Unbusy();

	$MW->idletasks();   ###  force display update
}


### ---------------------------------------------------------------------
###  setupPingLoop - we're starting (or restarting) ping-loop
###
my $glbPingLoopId = 0;	# default to not yet created...
###
sub setupPingLoop()
{
	#my $LOOP_INTERVAL_120MINS = (2 * 60  * 1000);	# 2 minutes...
	#my $LOOP_INTERVAL_50SEC = (50 * 1000);	# 50 seconds...
	my $loopInterval = $glbSleepSecs * 1000;

	$glbPingLoopId = $MW->repeat($loopInterval ,\&pingEmAll);
}


### ---------------------------------------------------------------------
###  killPingLoop - we're shutting down (or we've stopped loop), kill ping loop
###
sub killPingLoop()
{
	if($glbPingLoopId) {
		$MW->afterCancel($glbPingLoopId);
		$glbPingLoopId = 0;	# reset to not yet created...
	} else {
		DebugMsg("killPingLoop() - already killed!");
	}
}


### --------------------------------------------------------------------------
###  mvDown2Active - move a node from down back to active
###
sub mvDown2Active($)
{
	my $hostNm = shift;

	#  IFF node is DOWN...
	if(exists $glbBtnDownedByHostHs{$hostNm}) {
		DebugMsg(" - moving host [$hostNm] from down to active");
		#  Add to active list
		$glbBtnActiveByHostHs{$hostNm} = $glbBtnDownedByHostHs{$hostNm};
		#  Remove from down list
		delete $glbBtnDownedByHostHs{$hostNm};
		###  now reset to default color!
	 	setHostColor($glbBtnActiveByHostHs{$hostNm},
		             $glbDefaultBtnColor);
		#  set new current state
		$glbCurrentStatusByHostHs{$hostNm} = $CONST_QUAL_RESTART;
		# time at which we entered curr state
		$glbTimeEnteredCurrentStatusByHostHs{$hostNm} = timeNow();
		DebugMsg("CURR: StatChg $hostNm=[$glbCurrentStatusByHostHs{$hostNm}]");
	} else {
		Msg(-war,"Attempt to activate host [$hostNm] 2nd time!");
	}
}


### --------------------------------------------------------------------------
###  mvActive2Down - move a node from active to down
###
sub mvActive2Down($)
{
	my $hostNm = shift;

	#  IFF node is Active...
	if(exists $glbBtnActiveByHostHs{$hostNm}) {
		DebugMsg(" - moving host [$hostNm] from active to down");
		#  Add to down list
		$glbBtnDownedByHostHs{$hostNm} = $glbBtnActiveByHostHs{$hostNm};
		#  Remove from active list
		delete $glbBtnActiveByHostHs{$hostNm};
		###  now color as down!
 		setHostColor($glbBtnDownedByHostHs{$hostNm},
		             $glbConfigDataHs{'ignoredcolor'});
		#  set new current state
		$glbCurrentStatusByHostHs{$hostNm} = $CONST_QUAL_DOWN;
		# time at which we entered curr state
		$glbTimeEnteredCurrentStatusByHostHs{$hostNm} = timeNow();
		DebugMsg("CURR: StatChg $hostNm=[$glbCurrentStatusByHostHs{$hostNm}]");
	} else {
		Msg(-war,"Attempt to down host [$hostNm] 2nd time!");
	}
}


### --------------------------------------------------------------------------
###  showNeed2SelectHostFirst - popup advisory dialog if neglected to select
###                             host
###
sub showNeed2SelectHostFirst($)
{
	my $actionId = shift;

	my $actionStr = "";
	my $purposeStr = "";
	foreach ($actionId) {
		/$CONST_FILE_ACTION_RECHECK/ and do {
			$actionStr = "File->Recheck";
			$purposeStr = "recheck one or more hosts";
			last;
		};
		/$CONST_FILE_ACTION_DOWN/ and do {
			$actionStr = "File->Down";
			$purposeStr = "mark one or more hosts down";
			last;
		};
		Msg(-err,"showNeed2SelectHostFirst($actionId) Unknown action!");
	}

	my $dlgTitleStr = "INFO please select host";
	my $dlgTextStr = "You selected $actionStr.\n\nIn order to $purposeStr,\n" .
                    "you must first select one or more hosts by\n" .
                    "clicking on one or more host buttons.\n" .
                    "\n" .
                    "  NOTE: clicking a host button a second time clears\n" .
                    "        the selection (the selection toggles.)\n";

	my $DIALOG_NEED_HOST = $MW->Dialog(
		-title          => $dlgTitleStr,
		-bitmap         => 'info',
		-justify        => 'left',
		-font           => 'fixed',
		-text           => $dlgTextStr,
		-wraplength     => 0,
		-default_button => 'OK',
		-buttons        => ['OK']
	);

	#  show that we a posting a non-modal dialog
	$glbDlgIsCurrentlyPosted = 1;
	$glbPostedDialog = $DIALOG_NEED_HOST;

	#  now put up dialog for user to see
	$DIALOG_NEED_HOST->Show();

	#  show that we a now removing non-modal dialog
	$glbDlgIsCurrentlyPosted = 0;
}


### --------------------------------------------------------------------------
###  markSelectedHostsDown - user clicked on host buttons, then picked
###                          File->Down.  This sets selected hosts to IGNORE
###                          (moves them to down list from active!)
###
sub markSelectedHostsDown(;$)
{
	DebugMsg("markSelectedHostsDown() - ENTRY");

	my $singleHostNm = shift;

	my %selectedHostsHs = ();

	if(defined $singleHostNm) {
		$selectedHostsHs{$singleHostNm} =
		        $glbBtnListByHostHs{$singleHostNm};
	} else {
		%selectedHostsHs = %glbBtnSelectedByHostHs;
	}

	###  iff we have entries selected, do...
	if((keys %selectedHostsHs) > 0) {
		foreach my $hostNm (keys %selectedHostsHs) {
			###  If really selected... (not by popup menu...)
			if(exists $glbBtnSelectedByHostHs{$hostNm}) {
				###  toggle selection state, removing from the list, too
				toggleHostSelection($hostNm);	#  Use toggle to un-select!
			}
			###  move to down status, if not already down
			if(!exists $glbBtnDownedByHostHs{$hostNm}) {
				mvActive2Down($hostNm);
			}
		}
	} else {
		showNeed2SelectHostFirst($CONST_FILE_ACTION_DOWN);
	}
}


### --------------------------------------------------------------------------
###  markSingleHostDown - user right-clicked on a single host button then
###                       picked down.  This deactivates future pings for
###                       this host.
sub markSingleHostDown()
{
	DebugMsg("markSingleHostsDown() - ENTRY");

	markSelectedHostsDown($glbCurrentRightMouseHost);	#  Now, mark host down...
}


### --------------------------------------------------------------------------
###  clearAllHosts - user picked 'Edit->Clear Selection'.  So... we unselect
###                    all selected hosts!
sub clearAllHosts()
{
	foreach my $hostNm (keys %glbBtnSelectedByHostHs) {
		###  toggle selection state, removing from the list, too
		toggleHostSelection($hostNm);	#  Use toggle to un-select!
	}
}


### --------------------------------------------------------------------------
###  selectAllHosts - user picked 'Edit->Select All'.  So... we move all hosts
###                    to selected list and post color changes accordingly
sub selectAllHosts()
{
	### foreach host
	foreach my $hostNm (keys %glbBtnListByHostHs) {
		###	if not already selected...
		if(!exists $glbBtnSelectedByHostHs{$hostNm}) {
			###  toggle selection state, adding to list, too
			toggleHostSelection($hostNm);	#  Hey! use toggle to un-select!
		}
	}
}


### --------------------------------------------------------------------------
###  recheckAllHosts - user picked 'File->Recheck All'.  So... we move any from
###                    down list back to active status!  We also unselect all
###                    selected!
sub recheckAllHosts()
{
	DebugMsg("recheckAllHosts() - ENTRY");

	$MW->idletasks();   ###  force display update (so menu shadows go away!)

	###  unselect selected-hosts before resetting selection list!
	if((keys %glbBtnSelectedByHostHs) > 0) {
		###  for each selected host...
		foreach my $hostNm (keys %glbBtnSelectedByHostHs) {
			toggleHostSelection($hostNm);	#  Hey! use toggle to un-select!
		}
	}
	###  iff we have down-entries, do...
	if((keys %glbBtnDownedByHostHs) > 0) {
		foreach my $hostNm (keys %glbBtnDownedByHostHs) {
			mvDown2Active($hostNm);
		}
		%glbBtnDownedByHostHs = ();		# clear our downed list
	}

	###  Clear old prior history allowing node to go green
	foreach my $hostNm (keys %glbBtnActiveByHostHs) {
		$glbPriorStatusByHostHs{$hostNm} = $CONST_QUAL_RESTART;
		$glbCurrentStatusByHostHs{$hostNm} = $CONST_QUAL_RESTART;
		DebugMsg("PRIOR: RechkAll $hostNm=[$glbPriorStatusByHostHs{$hostNm}]");
		$glbTimeEnteredPriorStatusByHostHs{$hostNm} =
		   $glbTimeEnteredCurrentStatusByHostHs{$hostNm} = timeNow();
	}

	###  issue the ping command (all hosts, after making all active again)
	###  and process ping-returned data
	$glbGeneralStatusMsg = "Rechecking all hosts...";
	pingHostsAndAccumulateStats((keys %glbBtnActiveByHostHs));
}


### --------------------------------------------------------------------------
###  recheckSelectedHosts - user clicked on host buttons, then picked
###                         File->Recheck.  This reactivates selected
###                         hosts if they were downed!
###                          (moves them to active list from down list!)
###
sub recheckSelectedHosts()
{
	DebugMsg("recheckSelectedHosts() - ENTRY");

	$MW->idletasks();   ###  force display update (so menu shadows go away!)

	my @hostsToPingAr = ();

	###  iff we have entries, do...
	if((keys %glbBtnSelectedByHostHs) > 0) {
		###  for each selected host...
		foreach my $hostNm (keys %glbBtnSelectedByHostHs) {
			###  Mark this host as needing ping
			push @hostsToPingAr, $hostNm;
			###  unselect... our button
			toggleHostSelection($hostNm);	#  let's use toggle to un-select!
			###  if was marked down, awaken it
			if(exists $glbBtnDownedByHostHs{$hostNm}) {
				###  add to active list
				mvDown2Active($hostNm);
			}

			###  Clear  prior/current history allowing node to go green
			$glbPriorStatusByHostHs{$hostNm} = $CONST_QUAL_RESTART;
			$glbCurrentStatusByHostHs{$hostNm} = $CONST_QUAL_RESTART;
			DebugMsg("PRIOR: RchkSlctd $hostNm=[$glbPriorStatusByHostHs{$hostNm}]");
			$glbTimeEnteredPriorStatusByHostHs{$hostNm} =
			   $glbTimeEnteredCurrentStatusByHostHs{$hostNm} = timeNow();
		}

		###  issue the ping command (selected hosts)
		###  and process ping-returned data
		$glbGeneralStatusMsg = "Rechecking selected hosts...";
		pingHostsAndAccumulateStats(@hostsToPingAr);
	} else {
		showNeed2SelectHostFirst($CONST_FILE_ACTION_RECHECK);
	}
}


### --------------------------------------------------------------------------
###  recheckSingleHost - user right-clicked on a host button then
###                      picked recheck.  This reactivates the selected
###                      host if it was downed!
###                       (moves it to the active list from down list!)
sub recheckSingleHost()
{
	DebugMsg("recheckSingleHost() - ENTRY");

	$MW->idletasks();   ###  force display update (so menu shadows go away!)

	my $singleHost = $glbCurrentRightMouseHost;

	#  capture prior selections
	my %holdSelectedHostsHs = %glbBtnSelectedByHostHs;

	%glbBtnSelectedByHostHs = ();		#  empty it
	$glbBtnSelectedByHostHs{$singleHost} = $glbBtnListByHostHs{$singleHost};

	recheckSelectedHosts();		#  Now, recheck selected...

	#  restore prior selections
	%glbBtnSelectedByHostHs = %holdSelectedHostsHs;
}


### --------------------------------------------------------------------------
###  showInfoFor1stSelectedHost - user clicked on one or more host buttons
###                               then picked File->Info.  This displays a status
###                               dialog showing ping history for only the first host
###                               in the list and app runtime info.
###
sub showInfoFor1stSelectedHost()
{
	my $hostNm = "";

	# iff have more than one host, select first host only!
	if((keys %glbBtnSelectedByHostHs) > 0) {
		$hostNm = (keys %glbBtnSelectedByHostHs)[0];
	}

	my $dlgTitleStr = "";
	my $dlgTextStr = "";

	###  setup overall application status
	my $appStatsTxt = "TkPing started: $glbApplicationStartTimeStr\n" .
					  "\t$glbTotalTraversalCount traversals, at $glbNbrPackets pings each, " .
                      "for a total\n" .
					  "\tof $glbTotalPingsSentCount pings sent to each active host\n" .
					  "\t(which is any host not marked down.)\n";

	###  setup application configuration status
	my $appConfigTxt = "TkPing Configuration:\n" .
                       " Packets: A traversal is $glbNbrPackets packets sent " .
					   "to each active host.\n" .
                       "   Sleep: TkPing waits $glbSleepSecs Secs. between traversals\n" .
                       " Timeout: TkPing waits $glbTimeoutMillSecs mSec. for " .
					   "initial response from host.\n" .
					   "   (for more on timeout see fping(8) manpage -t{n} option)\n" .
                       "\n" .
                       " Configuration Data Read From:\n" .
                       "  .host file: $glbHostsFspec\n" .
                       "  .conf file: $glbConfFspec";

	# if have specific host...
	if($hostNm ne "") {
		### display host specific form...
		my $failedPings = $glbPingsSentCountByHostHs{$hostNm} -
							$glbPingsReceivedCountByHostHs{$hostNm};

		# set dialog title
		$dlgTitleStr = "Info for $hostNm";

		#  Layout text for dialog
		#    - App Overall
		#    - host status
		#    - pings for host
		#    - traversals for host
		#    - Configuration settings

		###  setup ping statuses for dialog
		my $percentSuccess = 0.0;
		if($glbPingsSentCountByHostHs{$hostNm} > 0) {
			$percentSuccess = ($glbPingsReceivedCountByHostHs{$hostNm} /
							   $glbPingsSentCountByHostHs{$hostNm}) * 100;
		}
		my $pingCtsFormatted = sprintf(" %8d  %8d    %2.2f",
		                               $glbPingsSentCountByHostHs{$hostNm},
		                               $glbPingsReceivedCountByHostHs{$hostNm},
		                               $percentSuccess);

		###  setup current quality message for dialog (host status)
		my $qualityTxt = "";
		my $qualityStr = $glbCurrentStatusByHostHs{$hostNm};
		foreach ($qualityStr) {
			/$CONST_QUAL_DOWN/ and do {
				$qualityTxt = "$hostNm has been MARKED DOWN";
				last;
			};
			/$CONST_QUAL_RESTART/ and do {
				$qualityTxt = "$hostNm has been MARKED UP (redetermining current status)";
				last;
			};
			/$CONST_QUAL_ALL/ and do {
				$qualityTxt = "Receiving ALL packets from $hostNm";
				last;
			};
			/$CONST_QUAL_SOME/ and do {
				$qualityTxt = "Receiving only SOME packets from $hostNm";
				last;
			};
			/$CONST_QUAL_NONE/ and do {
				$qualityTxt = "Receiving NO packets from $hostNm";
				last;
			};
			/$CONST_QUAL_UNKNOWN/ and do {
				$qualityTxt = "$hostNm is not found on network";
				last;
			};
			Msg(-err,"showInfoFor1stSelectedHost($hostNm) Unknown quality string [$qualityStr]");
		}
		my $hostStatusTxt = "$qualityTxt\n" .
                            "  since $glbTimeEnteredCurrentStatusByHostHs{$hostNm}\n";

		#  IFF host is marked down, add to above findings....
		my $extraPingTxt = "\n";
		if($glbBtnDownedByHostHs{$hostNm}) {
			$extraPingTxt = "\n   (This was prior to it being marked down.)\n\n";
		}

		###  setup current interval statuses for dialog
		my %intervalStatsTxtHs = ();
		foreach my $intervalId (@glbTraversalCountIntervalsAr) {

			my $hostKey = "${hostNm}${intervalId}";

			my $traversalsCt = $glbTraversalEntryCountByHostHs{$hostNm};

			my $traversalAvg = 0.0;
			if($glbTraversalPingCountByHostKeyHs{$hostKey} > 0) {
				$traversalAvg = $glbTraversalRunningSumByHostKeyHs{$hostKey} /
                                $glbTraversalPingCountByHostKeyHs{$hostKey};
			}

			$intervalStatsTxtHs{$intervalId} = sprintf("%6d    %8.2f    %3d  %3d  %3d   %3d    %3d",
			                           $intervalId,
		                               $traversalAvg,
									   $glbTraversalCountAllByHostKeyHs{$hostKey},
									   $glbTraversalCountSomeByHostKeyHs{$hostKey},
									   $glbTraversalCountNoneByHostKeyHs{$hostKey},
									   $glbTraversalCountUnknownHostByHostKeyHs{$hostKey},
									   $glbTraversalCountDownByHostKeyHs{$hostKey});
		}

		$dlgTextStr = "$hostStatusTxt" .
					  "\n" .
                      "Pings:       Sent  Received   Percent\n" .
                      "\t$pingCtsFormatted\n" .
                      "$extraPingTxt" .
                      "Traversals:  Count   Avg (mSec)   All Some None Unknown Down\n" .
                      "\t   $intervalStatsTxtHs{$glbTraversalCountIntervalsAr[0]}\n" .
                      "\t   $intervalStatsTxtHs{$glbTraversalCountIntervalsAr[1]}\n" .
                      "\t   $intervalStatsTxtHs{$glbTraversalCountIntervalsAr[2]}\n" .
                      "\t   $intervalStatsTxtHs{$glbTraversalCountIntervalsAr[3]}\n" .
                      "\n" .
					  "$appStatsTxt" .
                      "\n" .
                      "$appConfigTxt";
	} else {
		### show general form...

		# set dialog title
		$dlgTitleStr = "TkPing - General Info";

		#  Layout text for dialog
		#    - App Overall
		#    - Configuration settings
		$dlgTextStr = "$appStatsTxt" .
                      "\n" .
                      "   NOTE: select host then File->Info to see details\n" .
                      "    specific to that host (or use right-mouse menu\n" .
                      "    on host button)\n" .
                      "\n" .
                      "$appConfigTxt";
	}

	# craft dialog
	my $DIALOG_INFO = $MW->Dialog(
		-title          => $dlgTitleStr,
		-bitmap         => 'info',
		-justify        => 'left',
		-font           => 'fixed',
		-text           => $dlgTextStr,
		-wraplength     => 0,
		-default_button => 'OK',
		-buttons        => ['OK']
	);

	#  show that we a posting a non-modal dialog
	$glbDlgIsCurrentlyPosted = 1;
	$glbPostedDialog = $DIALOG_INFO;

	# now put up dialog for user to see
	$DIALOG_INFO->Show();

	#  show that we a now removing non-modal dialog
	$glbDlgIsCurrentlyPosted = 0;

	### --TEST--TEST--TEST--TEST--TEST--
	#  if host specific, unslect host
	#if($hostNm ne "") {
	#	toggleHostSelection($hostNm);	#  Use toggle to un-select!
	#}
	### --TEST--TEST--TEST--TEST--TEST--
}


### --------------------------------------------------------------------------
###  showInfoForSingleHost - user right-clicked on a single host button then
###                          picked info.  This displays a status dialog showing
###                          ping history for this host and app runtime info.
sub showInfoForSingleHost()
{
	my $singleHost = $glbCurrentRightMouseHost;

	#  capture prior selections
	my %holdSelectedHostsHs = %glbBtnSelectedByHostHs;

	%glbBtnSelectedByHostHs = ();		#  empty it
	$glbBtnSelectedByHostHs{$singleHost} = $glbBtnListByHostHs{$singleHost};

	showInfoFor1stSelectedHost();

	#  restore prior selections
	%glbBtnSelectedByHostHs = %holdSelectedHostsHs;
}


### --------------------------------------------------------------------------
###  toggleHostSelection - [button press] invert selection color, if selects
###                        add to the list of selections, else remove from the
###                        list.
###
sub toggleHostSelection($)
{
#	my $btnObj = shift;
	my $hostNm = shift;

	#  if the code is broken (inactive buttons get pressed)
	#  we may come thru here with "" hostnames!
	if(!defined $hostNm || $hostNm eq "") {
		my $hostTxt = (defined $hostNm) ? $hostNm : "{undef}";
		DebugMsg("Entered toggleHostSelection() with hostNm=[$hostTxt], abort select.");
		return;		# exit without doing anything
	}

	###  if already selected...
	if(exists $glbBtnSelectedByHostHs{$hostNm}) {
		###  determine prior color....
		my $restoreColor = (exists $glbPriorBtnColorByHostHs{$hostNm}) ?
		             $glbPriorBtnColorByHostHs{$hostNm} :
					 $glbDefaultBtnColor;
		###  decolor host (revert to prior color)!
		setHostColor($glbBtnSelectedByHostHs{$hostNm},
					 $restoreColor);
		###  deselect host!
		delete $glbBtnSelectedByHostHs{$hostNm};
	} else {
		###  select host!
		if(!exists $glbBtnListByHostHs{$hostNm}) {
			FatalMsg("[CODE] hostNm[$hostNm] NOT found in master button list!");
		}
		#  record our hostname and button object as selected
		$glbBtnSelectedByHostHs{$hostNm} = $glbBtnListByHostHs{$hostNm};
		###  color host as selected!
		$glbPriorBtnColorByHostHs{$hostNm} =
		   setHostColor($glbBtnSelectedByHostHs{$hostNm},$CONST_SELECTED_COLOR);
	}
}


### ---------------------------------------------------------------------
###  loadParmData - load our configuration data including color, rates and
###                 durations, etc.
###
sub loadParmData($)
{
	my $fSpec = shift;

	my @rawParmsDataAr = File2Array($fSpec);
	my %actualParmDataHs = ();

	my %lcParmListHs = ();
	foreach my $parmId (keys %glbValidParametersHs) {
		my $parm = lc $parmId;
		$lcParmListHs{$parm} = 1;
	}

	foreach (@rawParmsDataAr) {
		$_ =~ s/#.*$//g;	#  remove comments
		$_ =~ s/\s+$//g;	#  remove trailing white-space
		$_ =~ s/^\s+//g;	#  remove leading white-space
		if($_ =~ /^\s*$|^$/) {
			next;	# skip blank lines
		}
		if($_ =~ /^[^:\s]+:\s+/) {
			my ($parmNm, $value) = split /:\s+/,$_,2;
			my $lcParmNm = lc $parmNm;	#  force to lc
			if(!exists $lcParmListHs{$lcParmNm}) {
				Msg(-err,"Ignoring BAD(unknown) parm [$parmNm]=[$value]");
				next;
			}
			$value = lc $value;	#  force to lc
			$value =~ s/0x/#/;	#  put back the # color-value lead-in
			$actualParmDataHs{$lcParmNm} = $value;
			DebugMsg("parmLoader() $lcParmNm=[$value]");
			next;   # skip paramter overrides, too
		}
		DebugMsg("parmLoader() skipping host data [$_]");
	}
	###  Now declare text-color and active-text-color settings
	###   iff not given in file just loaded
	my @colorKeyAr = (grep /Color$/i, (keys %actualParmDataHs));
	foreach my $colorKey (grep !/TextColor$/i, @colorKeyAr) {
		my $reasonPrefix = $colorKey;
		$reasonPrefix =~ s/Color$//i;
		my $color = $actualParmDataHs{$colorKey};
		my ($nml, $actv) = ($color =~ /black|#000000/i) ? ("white","red") : ("black", "white");
		my $keyNm = lc "${reasonPrefix}TextColor";
		if(!exists $actualParmDataHs{$keyNm}) {
			$actualParmDataHs{$keyNm} = $nml;
			DebugMsg("+$keyNm: $nml");
		}
		$keyNm = lc "${reasonPrefix}ActiveTextColor";
		if(!exists $actualParmDataHs{$keyNm}) {
			$actualParmDataHs{$keyNm} = $actv;
			DebugMsg("+$keyNm: $actv");
		}
	}
	return %actualParmDataHs;
}


### ---------------------------------------------------------------------
###  loadHostData - load list of hosts to be monitored, maybe even placement
###                 and labellinbg data as well
###
sub loadHostData($)
{
	my $fSpec = shift;

	my @rawHostsDataAr = File2Array($fSpec);
	my @actualHostDataAr = ();

	foreach (@rawHostsDataAr) {
		$_ =~ s/#.*$//g;	#  remove comments
		$_ =~ s/\s+$//g;	#  remove trailing white-space
		$_ =~ s/^\s+//g;	#  remove leading white-space
		if($_ =~ /^\s*$|^$/) {
			next;	# skip blank lines
		}
		if($_ =~ /^[^:\s]+:\s+/) {
			DebugMsg("hostLoader() skipping parm [$_]");
			next;   # skip parameter overrides, too
		}
		push @actualHostDataAr, $_;
	}
	return @actualHostDataAr;
}


### ---------------------------------------------------------------------
###  timeNow - get, format, and return current data/time
###
sub timeNow()
{
	return GetDateTime('HH:MM_DDDc_DD_MMM_YYYY');
}


###
### ---------------------------------------------------------------------
#Z#                      * End of Subroutines
### =====================================================================

DebugMsg("Acting on request");

$glbApplicationStartTimeStr = timeNow();

###
#Z#  - load our host data
###
my @rawHostsDataAr = loadHostData($glbHostsFspec);
my $rawHostsDataCt = @rawHostsDataAr;

if($rawHostsDataCt < 1) {
	Msg(-err,"No hosts given");
	Msg(-inf,"Please add hosts to ~/.tkpingrc or provide -nodes option");
	FatalMsg("Aborting...");
}

###
#Z#  - load our color-config data
###
%glbConfigDataHs = loadParmData($glbConfFspec);
#Z#  --- FIXME add default color for <message> text
if(!exists $glbConfigDataHs{'messagetextcolor'}) {
	$glbConfigDataHs{'messagetextcolor'} = "white";
}

###
#Z# - determine nbr of rows & cols from host data
###
my $colOverrideCt = (grep /<nextcolumn>/i,@rawHostsDataAr);
if($colOverrideCt > 0) {
	$colOverrideCt++;	#  make one-relative (vs. zero)
	$glbNbrColumns = $colOverrideCt;	#  force use of desired column count
	#  calculate desired nbr rows since columns are specified!
	my $maxRowCt = 0;
	my $currRowCt = 0;
	foreach (@rawHostsDataAr) {
		/<nextcolumn>/i and do {
			if($currRowCt > $maxRowCt) {
				$maxRowCt = $currRowCt;
			}
			$currRowCt = 0;
			next;
		};
		$currRowCt++
	}
	if($currRowCt > 0) {
		if($currRowCt > $maxRowCt) {
			$maxRowCt = $currRowCt;
		}
	}
	#  now set desired row count
	$glbNbrRows = $maxRowCt + 1;	###  BUG +1 fixes draw problem!
} else {
	if($glbNbrColumns < 2) {
		$glbNbrRows = $rawHostsDataCt + 1;  #  always leave last blank line
		$glbNbrColumns = 1;	# always at least one column
	} else {
#		$glbNbrRows = int(($rawHostsDataCt / $glbNbrColumns) + 1);
	}
}


###
#Z#  - If we need a GUI, generate/display it!
###
if(!$glbModeCommandLineOnly) {

	my $topWindowWidth = ($glbNbrColumns * 72) + 20;
	### ---------------------------------------------------------------------
	###   setup the GUI (top window...)
	###
	#$MW = MainWindow->new(-width => $topWindowWidth);
	$MW = MainWindow->new();
	$MW->title("$NAME - Ver $VERSION");
	my $FONT = '-*-Helvetica-Medium-B-Normal--*-140-*-*-*-*-*-*';


	# ------------------------------------------------------------
	#  Create the Menu Pane
	#
	$glbMenuBar = $MW->Frame(-relief => 'raised', -borderwidth => 1);

	$glbMenuBar->grid(-sticky => 'nsew')->pack(-fill => 'x');

	$glbOnHostMenu = $MW->Menu(
	    qw/-tearoff 0 -menuitems/ =>
	    [
	     [Button => '~Recheck', -command => \&recheckSingleHost],
	     [Button => '~Info', -command => \&showInfoForSingleHost],
	     [Button => '~Down', -command => \&markSingleHostDown],
		]);


	### OnBtn3HostBtn - if right mouse button pressed, pop-up the menu
	sub OnBtn3HostBtn
	{
		my ($button, $hostName) = @_;
		$glbCurrentRightMouseHost = $hostName;
		#print STDOUT "hostname=[$hostName]\n";

		### --TEST--TEST--TEST--TEST--TEST--
		#Z# toggleHostSelection($hostName);
		#Z#  TEST FIXME?: need to hook un-right-mouse to clear
		#Z#           selection! (when no menu item picked)
		### --TEST--TEST--TEST--TEST--TEST--


		$glbOnHostMenu->Popup(-popover => $button, -popanchor => "sw");
	}

	###  setup File menu
	my $fileMenu = $glbMenuBar->Menubutton(
	    qw/-text File -tearoff 0 -underline 0 -menuitems/ =>
	    [
	     [Button => '~Recheck', -command => \&recheckSelectedHosts],
	     [Button => 'Recheck ~All', -command => \&recheckAllHosts],
	     [Button => '~Info', -command => \&showInfoFor1stSelectedHost],
	     [Button => '~Down', -command => \&markSelectedHostsDown],
	     [Separator => ''],
	     [Button    => '~Quit', -command => \&onQuit],
	    ])->grid(qw/-row 0 -column 0 -sticky w/)->pack(-side => 'left');

	###  setup Edit menu
	my $editMenu = $glbMenuBar->Menubutton(
	    qw/-text Edit -tearoff 0 -underline 0 -menuitems/ =>
	    [
	     [Button => '~Select All', -command => \&selectAllHosts],
	     [Button => '~Clear Selection', -command => \&clearAllHosts],
	    ])->grid(qw/-row 0 -column 0 -sticky w/)->pack(-side => 'left');

	###  setup Help menu
	my $helpMenu = $glbMenuBar->Menubutton(
	    qw/-text Help -tearoff 0 -underline 0 -menuitems/ =>
	    [
	     [Button    => "~About"],
	    ])->grid(qw/-row 0 -column 2/)->pack(-side => 'right');


	###  configure ABOUT dialog
	my $DIALOG_ABOUT = $MW->Dialog(
	    -title          => "About $NAME",
	    -bitmap         => 'info',
	    -default_button => 'OK',
	    -buttons        => ['OK'],
	    -font           => 'fixed',
		-wraplength     => 0,
	    -justify        => 'center',
	    -text           => $DESCRIPTION
	);

	###  connect ABOUT dialog to menu system
	$helpMenu->entryconfigure("About",
						      -command => [$DIALOG_ABOUT => 'Show']);


	# ------------------------------------------------------------
	#Z#  - Create the StatusText Pane (bottom row of window)
	#
	#$glbGeneralStatusMsg = "";
	$glbEvaluationStatusMsg = "OK";

	my $statusBar = $MW->Frame(
	                       -relief => 'flat',
	                       -borderwidth => 2,
	                       -height => 20,
	                     )->pack(-expand => 'no',
	                             -fill => 'x',
								 -anchor => 's',
								 -side => 'bottom',
								 -padx => 0,
								 -pady => 1);
	my $statusFrame = $statusBar->Frame(-relief => 'sunken',
								 -borderwidth => 1)->pack(-expand => 'yes',
								                          -anchor => 's',
								                          -side => 'left',
														  -fill => 'x',
														  -pady => '0',
														  -padx => '1');

	my $status2Frame = $statusBar->Frame(-relief => 'sunken',
								 -borderwidth => 1)->pack(-expand => 'no',
								                          -anchor => 's',
								                          -side => 'right',
														  -fill => 'none',
														  -pady => '0',
														  -padx => '1');

	my $generalStatus = $statusFrame->Label(-textvariable => \$glbGeneralStatusMsg,
									 -width => '25',
								     -anchor => 'w')->grid(-sticky => 'ew')->pack(-side => 'left');

	my $evalStatus = $status2Frame->Label(-textvariable => \$glbEvaluationStatusMsg,
									 -width => '5',
								     -anchor => 'c')->grid(-sticky => 'ew')->pack(-side => 'right');

	# ------------------------------------------------------------
	#Z#  - Create the button grid
	#
	$glbNbrRows--;
my %glbCanvasBtnIdByBtnObjHs = ();

    #$glbButtonGrid = $MW->Frame->pack(-expand => 0, -fill => 'none');
    $glbButtonGrid = $MW->Scrolled('Canvas',
	                               -scrollbars => 'osoe',
								   -height => ($glbNbrRows * 35) + 19,
								   -width => ($glbNbrColumns * 135) + 5,
	                       )->pack(-expand => 'yes', -fill => 'both');

	my $btnCanvas = $glbButtonGrid->Subwidget('canvas');

	my $rawDataSlotNbr = 0;
	my $waitingForColumnChange = 0;
	for(my $column = 0; $column < $glbNbrColumns; $column++) {
	    for(my $row = 0; $row <= $glbNbrRows; $row++) {
			#  get data for this button
			my ($hostOrType, $labelText);
			if($rawDataSlotNbr + 1 > $rawHostsDataCt) {
				$hostOrType = "<blank>";	###  ran out! force empty extras!
			} else {
				($hostOrType, $labelText) = split /\s+/,$rawHostsDataAr[$rawDataSlotNbr],2;
			}
			my $styleChoice = "";
			my $foregroundColor = "black";
			my $activeForegroundColor = "white";
			my $state = "active";
			foreach ($hostOrType) {
				/<blank>/i and do {
					$styleChoice = "flat";
					$state = "disabled";
					$labelText = "";	#  nope no hostname here!
					last;
				};
				/<nextcolumn>/i and do {
					$waitingForColumnChange = 1;
					$rawDataSlotNbr++;
					last;
				};
				/<message>/i and do {
					$styleChoice = "flat";
					$state = "disabled";
					$foregroundColor = $glbConfigDataHs{'messagetextcolor'};
					$activeForegroundColor = $glbConfigDataHs{'messagetextcolor'};
					last;
				};
				# have hostname
				$styleChoice = "raised";
			}
			if($waitingForColumnChange) {
				$styleChoice = "flat";
				$state = "disabled";
				$labelText = "";	#  nope no hostname here, either!
			}

			#  create new buttons, one for each row,col position
			my $button = $btnCanvas->Button(
			               -text		=> "$labelText",
						   -relief		=> "$styleChoice",
						   -foreground	=> "$foregroundColor",
						   -activeforeground	=> "$activeForegroundColor",
						   -disabledforeground 	=> "$foregroundColor",
						   -state		=> "$state",
						   -height		=> 1,
						   -width		=> 14,
						   -default		=> 'normal',
						   -command		=> [ \&toggleHostSelection, $labelText ]
						   );

			#  place button on canvas at specific coords
			$glbCanvasBtnIdByBtnObjHs{$button} =
				$btnCanvas->createWindow(($column * 135) + 70,
				                         ($row * 35) + 25,
										 -window => $button);


			#  if is host button (not label/spacer) then...
			if($styleChoice eq "raised") {
				#  bind a popup menu via right-mouse to this button
            	if($state ne "disabled") {
					$button->bind('<Button-3>', [\&OnBtn3HostBtn, $labelText]);
				}

				#  keep track of our button ID's
				$glbBtnListByHostHs{$labelText} = $button;

				#  keep track of our fully qualified host names
				$glbFQDNByHostHs{$labelText} = $hostOrType;
				$glbHostByFQDNHs{$hostOrType} = $labelText;

				#  setup our list of active hosts (have button-id ready)
				$glbBtnActiveByHostHs{$labelText} = $button;
			}

			$rawDataSlotNbr++ if(!$waitingForColumnChange);
	  	}
	  	$waitingForColumnChange = 0;
	}

	#  tell scrolled canvas how big the drawn areas is so it
	#   can setup scroll bars appropriately...
	$glbButtonGrid->configure(-scrollregion => [ $btnCanvas->bbox("all") ] );

	if(defined $opt_debug) {
		$glbGeneralStatusMsg = sprintf("oc=%d, c=%d, r=%d",
	   	                               $colOverrideCt,
									   $glbNbrColumns,
									   $glbNbrRows);
	}

} ##  end if($glbModeCommandLineOnly)


# ------------------------------------------------------------
#Z# - do work, GUI or NOTgui
#
if(!$glbModeCommandLineOnly) {
	#  display the initial status message
	if($glbGeneralStatusMsg eq "") {
		$glbGeneralStatusMsg = "Ping loop enabled";
	}

	###
	###  init status counters
	###
	foreach my $hostNm (keys %glbBtnActiveByHostHs) {
		$glbPingsSentCountByHostHs{$hostNm}  = 0;
		$glbPingsReceivedCountByHostHs{$hostNm} = 0;
	}

	# start our ping loop...
	setupPingLoop();

	# do initial ping (instead of waiting)
	pingEmAll();

	#  allow user interaction!!!
	MainLoop();
}


###
#Z# -  We're done!!!
###
doExit(0,"Normal exit at end");

### ---------------------------------------------------------------------
# These lines must be at the end of the file.  They help emacs users to
# prevent tabs being introduced into our source files.
#
# GNU emacs magic
# Local Variables:
# local-write-file-hooks:((lambda () (untabify (point-min) (point-max)) nil))
# tab-width:4
# tab-stop-list:(4 8 12 16 20 24 28 32 36)
# End:
##-----------------------------------------------------------------------
