#!/bin/sh
# -*- tab-width:  4 -*- ;; Emacs
# vi: set tabstop=4     :: Vi/ViM
#
# Revision: 1.2
# Created: June 27th, 2011
# Last Modified: January 11th, 2012
############################################################ COPYRIGHT
#
# Devin Teske (c)2011-2012. All Rights Reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
############################################################ INFORMATION
#
# Description:
#
#    Translation of FreeBSD's tzsetup(8) from C program based on dialog(3) to
#    sh(1) shell script based on dialog(1). Goals/reasons of this project are
#    as follows:
#
#    1. Act as a drop-in replacement for FreeBSD's tzsetup(8).
#    2. Provide a Graphical User Interface (GUI) via Xdialog(1) -- an X11-
#       enabled drop-in replacement for dialog(1).
#    3. To bring tzsetup(8) to Linux (replacing tzselect(8)) and other systems
#       that support dialog(1) and/or Xdialog(1).
#    4. Provide a single interface for both Linux and FreeBSD users without
#       needing to ship two separate binaries.
#
# Command Usage:
#
#    tzdialog [-enrsvX] [-C chroot_directory] [zoneinfo_file | zoneinfo_name]
#
#    OPTIONS:
#    	-e   Only return success on exit if user selects a timezone AND
#    	     the selected timezone was successfully installed. By default
#    	     (without this flag), success is always returned unless an
#    	     error has occurred.
#    	-n   Do not create or copy files.
#    	-r   Reinstall the zoneinfo file installed last time. The name is
#    	     obtained from /var/db/zoneinfo.
#    	-s   Skip the initial question about adjusting the clock if
#    	     not set to UTC.
#    	-v   Verbose. Enable extra output when installing the zone file.
#    	-X   Enable the use of Xdialog(1) instead of dialog(1).
#
# Dependencies (sorted alphabetically):
#
#    Xdialog(1)*   awk(1)    cat(1)        dialog(1)   grep(1)
#    id(1)         ln(1)     printf(1)**   rm(1)       sort(1)
#    strings(1)    stty(1)   uname(1)      which(1)
#
# * Optional
# ** Is a shell-builtin on some releases
#
############################################################ CONFIGURATION

#
# Default directory to store dialog(1) temporary files
#
: ${DIALOG_TMPDIR:="/tmp"}

#
# OS Glue
#
UNAME_S=$( uname -s )
UNAME_R=$( uname -r )

#
# Standard pathnames
#
_PATH_ZONETAB="/usr/share/zoneinfo/zone.tab"
_PATH_ZONEINFO="/usr/share/zoneinfo"
_PATH_LOCALTIME="/etc/localtime"
_PATH_DB="/var/db/zoneinfo"
case "$UNAME_S" in
Linux|Darwin|CYGWIN_NT-*)
	_PATH_ISO3166="/usr/share/zoneinfo/iso3166.tab"
	;;
FreeBSD)
	_PATH_ISO3166="/usr/share/misc/iso3166"
	_PATH_WALL_CMOS_CLOCK="/etc/wall_cmos_clock"
	;;
*)
	for _PATH_ISO3166 in \
		/usr/share/zoneinfo/iso3166.tab \
		/usr/share/misc/iso3166 \
	; do
		[ -f "$_PATH_ISO3166" ] && break
	done
	[ -f "$_PATH_ISO3166" ] || die \
		"%s: %s: No such file or directory" \
		"$progname" "$_PATH_ISO3166"
esac

#
# List of worldly continents/oceans
#
export CONTINENTS="
	africa
	america
	antarctica
	arctic
	asia
	atlantic
	australia
	europe
	indian
	pacific
	utc
"

#
# Directory name of each continent/ocean (in _PATH_ZONEINFO)
#
export continent_africa_name="Africa"
export continent_america_name="America"
export continent_antarctica_name="Antarctica"
export continent_arctic_name="Arctic"
export continent_asia_name="Asia"
export continent_atlantic_name="Atlantic"
export continent_australia_name="Australia"
export continent_europe_name="Europe"
export continent_indian_name="Indian"
export continent_pacific_name="Pacific"
export continent_utc_name="UTC"

#
# Menu text of each continent/ocean
#
export continent_africa_title="Africa"
export continent_america_title="America -- North and South"
export continent_antarctica_title="Antarctica"
export continent_arctic_title="Arctic Ocean"
export continent_asia_title="Asia"
export continent_atlantic_title="Atlantic Ocean"
export continent_australia_title="Australia"
export continent_europe_title="Europe"
export continent_indian_title="Indian Ocean"
export continent_pacific_title="Pacific Ocean"
export continent_utc_title="UTC"

############################################################ GLOBALS

#
# Global exit status variables
#
SUCCESS=0
FAILURE=1

#
# Program name
#
progname="${0##*/}"

#
# Default name of dialog(1) utility
#
DIALOG="dialog"

#
# Settings used while interacting with dialog(1)
#
# NOTE: export is required for awk(1) `ENVIRON' visibility
#
export DIALOG_MENU_TAGS="1234567890abcdefghijklmnopqrstuvwxyz"

#
# Not all implementations of dialog(1) support the `--defaultno' option.
# NOTE: Initially assumed to be unsupported but tested later in MAIN
# NOTE: Disable the test by setting CHECK_DIALOG_FOR_DEFAULTNO to NULL
# NOTE: Passing `-X' will implicitly disable both of the following options
#
DIALOG_ENABLE_DEFAULTNO=
CHECK_DIALOG_FOR_DEFAULTNO=1

#
# Declare that we are fully-compliant with Xdialog(1) by unset'ing all
# compatibility settings.
#
unset XDIALOG_HIGH_DIALOG_COMPAT
unset XDIALOG_FORCE_AUTOSIZE
unset XDIALOG_INFOBOX_TIMEOUT

#
# Xdialog(1) size considerations
# Pick a sensible fall-back in-case `Xdialog --print-maxsize' fails.
#
#XDIALOG_MAX_SIZE="20 78"   # for 720x400 (9:5)
#XDIALOG_MAX_SIZE="24 69"   # for 640x480 (4:3)
XDIALOG_MAX_SIZE="31 86"    # for 800x600 (4:3)
#XDIALOG_MAX_SIZE="32 90"   # for 832x624 (4:3)
#XDIALOG_MAX_SIZE="40 111"  # for 1024x768 (4:3)
#XDIALOG_MAX_SIZE="46 126"  # for 1152x864 (4:3)
#XDIALOG_MAX_SIZE="51 140"  # for 1280x960 (4:3)
#XDIALOG_MAX_SIZE="54 140"  # for 1280x1024 (5:4)
#XDIALOG_MAX_SIZE="64 175"  # for 1600x1200 (4:3)

#
# Options
#
REALLYDOIT=1
REINSTALL=
USEDIALOG=1
SKIPUTC=
USE_XDIALOG=
VERBOSE=
TZ_OR_FAIL=
CHROOTENV=

#
# Dummy vars (populated dynamically)
#
COUNTRIES= # list of 2-character country codes created by read_iso3166_table

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

# have $anything
#
# A wrapper to the `type' built-in. Returns true if argument is a valid shell
# built-in, keyword, or externally-tracked binary, otherwise false.
#
have()
{
	type "$@" > /dev/null 2>&1
}

# quietly $command [ $arguments ... ]
#
# Run a command quietly (quell any output to stdout or stderr).
#
quietly()
{
	"$@" > /dev/null 2>&1
}

# fprintf $fd $fmt [ $opts ... ]
#
# Like printf, except allows you to print to a specific file-descriptor. Useful
# for printing to stderr (fd=2) or some other known file-descriptor.
#
fprintf()
{
	local fd=$1
	[ $# -gt 1 ] || return $FAILURE
	shift 1
	printf "$@" >&$fd
}

# eprintf $fmt [ $opts ... ]
#
# Print a message to stderr (fd=2).
#
eprintf()
{
	fprintf 2 "$@"
}

# die [ $fmt [ $opts ... ]]
#
# Optionally print a message to stderr before exiting with failure status.
#
die()
{
	local fmt="$1"
	[ $# -gt 0 ] && shift 1
	[  "$fmt"  ] && eprintf "$fmt\n" "$@"

	exit $FAILURE
}

# usage
#
# Prints a short syntax statement and exits.
#
usage()
{
	local optfmt="\t%-11s%s\n"

	eprintf "Usage: %s [-enrsvX] [-C %s] [%s | %s]\n" "$progname" \
	        "chroot_directory" "zoneinfo_file" "zoneinfo_name"

	eprintf "OPTIONS:\n"
	eprintf "$optfmt" "-e" \
	        "Only return success on exit if user selects a timezone AND"
	eprintf "$optfmt" "" \
	        "the selected timezone was successfully installed. By default"
	eprintf "$optfmt" "" \
	        "(without this flag), success is always returned unless an"
	eprintf "$optfmt" "" \
	        "error has occurred."
	eprintf "$optfmt" "-n" \
	        "Do not create or copy files."
	eprintf "$optfmt" "-r" \
	        "Reinstall the zoneinfo file installed last time. The name is"
	eprintf "$optfmt" "" \
	        "obtained from /var/db/zoneinfo."
	eprintf "$optfmt" "-s" \
	        "Skip the initial question about adjusting the clock if"
	eprintf "$optfmt" "" \
	        "not set to UTC."
	eprintf "$optfmt" "-v" \
	        "Verbose. Enable extra output when installing the zone file."
	eprintf "$optfmt" "-X" \
	        "Enable the use of Xdialog(1) instead of dialog(1)."

	die
}

# longest_line_length
#
# Simple wrapper to an awk(1) script to print the length of the longest line of
# input (read from stdin). Supports the newline escape-sequence `\n' for
# splitting a single line into multiple lines.
#
longest_line_length_awk='
BEGIN { longest = 0 }
{
	if (split($0, lines, /\\n/) > 1)
	{
		for (n in lines)
		{
			len = length(lines[n])
			longest = ( len > longest ? len : longest )
		}
	}
	else
	{
		len = length($0)
		longest = ( len > longest ? len : longest )
	}
}
END { print longest }
'
longest_line_length()
{
	awk "$longest_line_length_awk"
}

# number_of_lines
#
# Simple wrapper to an awk(1) script to print the number of lines read from
# stdin. Supports newline escape-sequence `\n' for splitting a single line into
# multiple lines.
#
number_of_lines_awk='
{
	NR += gsub(/\\n/, "\n")
}
END { print NR }
'
number_of_lines()
{
	awk "$number_of_lines_awk"
}

###############################################################################
############################# TIME-ZONE FUNCTIONS #############################
###############################################################################

# read_iso3166_table
#
# Read the ISO 3166 country code database in _PATH_ISO3166:
# 	/usr/share/misc/iso3166 on FreeBSD
# 	/usr/share/zoneinfo/iso3166.tab on Linux, Mac OS X, and Cygwin
#
# The format of this file on FreeBSD is:
# 	two	three	number	name
#
# The format of this file on Linux, Mac OS X, and Cygwin is:
# 	two	name
#
# With each of the following elements (described below) being separated by a
# single tab character:
#
# 	two      ISO 3166 2-character country code
# 	three    ISO 3166 3-character country code (if provided)
# 	number   ISO 3166 numeric country code (if provided)
# 	name     Human-readable country name (may contain spaces)
#
# Variables created by this function:
#
# 	COUNTRIES
# 		A space-separated list of 2-character country codes.
# 	country_CODE_name
# 		The country `name' (as described above).
#
# where CODE is the 2-character country code.
#
# This function is a two-parter. Below is the awk(1) portion of the function,
# afterward is the sh(1) function which utilizes the below awk script.
#
read_iso3166_table_awk='
# Variables that should be defined on the invocation line:
# 	-v progname="progname"
# 	-v uname_s="$(uname -s)"
#
BEGIN {
	lineno = 0
	failed = 0
}
function die(fmt, argc, argv)
{
	printf "die \"%%s: %s\" \"%s\"", fmt, progname
	for (n = 1; n <= argc; n++)
		printf " \"%s\"", argv[n]
	print ""
	failed++
	exit 1
}
function add_country(tlc, name)
{
	if (country_name[tlc])
	{
		argv[1] = lineno
		argv[2] = tlc
		argv[3] = name
		die(FILENAME \
		    ":%d: country code \\`%s'\'' multiply defined: %s",
		    3, argv)
	}

	country_name[tlc] = name
}
function print_country_name(tlc)
{
	name = country_name[tlc]
	gsub(/"/, "\\\"", name)
	printf "country_%s_name=\"%s\"\n", tlc, name
	printf "export country_%s_name\n", tlc
}
/^#/ {
	lineno++
	next
}
!/^#/ {
	lineno++

	# Split the current record (on TAB) into an array
	split($0, line, /\t/)

	# Get the ISO3166-1 (Alpha 1) 2-letter country code
	tlc = line[1]

	#
	# Validate the two-character country code
	#
	if (length(tlc) != 2)
	{
		argv[1] = lineno
		die(FILENAME ":%d: invalid format", 1, argv)
	}
	if (!match(tlc, /^[A-Z][A-Z]$/))
	{
		argv[1] = lineno
		argv[2] = tlc
		die(FILENAME ":%d: invalid code \\`%s'\''", 2, argv)
	}

	#
	# Calculate the substr start-position of the name
	#
	name_start = 0
	n = 4
	if (uname_s ~ /Linux|Darwin|CYGWIN_NT-.*/)
		n = 2
	else if (FILENAME ~ /\.tab$/)
		n = 2
	while (--n)
	{
		#
		# Validate field-length of 2nd/3rd columns while we are here
		#
		if (n > 1  && length(line[n]) != 3)
		{
			argv[1] = lineno
			die(FILENAME ":%d: invalid format", 1, argv)
		}

		name_start += length(line[n]) + 1
	}

	# Get the name field
	name = substr($0, name_start + 1)

	add_country(tlc, name)
}
END {
	list = ""
	for (tlc in country_name)
	{
		list = list (length(list) > 0 ? " " : "") tlc
		print_country_name(tlc)
	}
	printf "COUNTRIES=\"%s\"\n", list
	print "export COUNTRIES"
}
'
read_iso3166_table()
{
	eval $( awk -v progname="$progname"   \
	            -v uname_s="$UNAME_S"     \
	            "$read_iso3166_table_awk" \
	            "$_PATH_ISO3166" )
}

# read_zones
#
# Read the zone descriptions database in _PATH_ZONETAB:
# 	/usr/share/zoneinfo/zone.tab on all OSes
#
# The format of this file (on all OSes) is:
# 	code	coordinates	TZ	comments
#
# With each of the following elements (described below) being separated by a
# single tab character:
#
# 	code
# 		The ISO 3166 2-character country code.
# 	coordinates
# 		Latitude and logitude of the zone's principal location in ISO
# 		6709 sign-degrees-minutes-seconds format, either +-DDMM+-DDDMM
# 		or +-DDMMSS+-DDDMMSS, first latitude (+ is north), then long-
# 		itude (+ is east).
# 	TZ
# 		Zone name used in value of TZ environment variable.
# 	comments
# 		Comments; present if and only if the country has multiple rows.
#
# Required variables [from the CONFIGURATION section (above)]:
#
# 	CONTINENTS
# 		Space-separated list of continents.
# 	continent_*_name
# 		Directory element in _PATH_ZONEINFO for the continent
# 		represented by *.
#
# Required variables [created by read_iso3166_table]:
#
# 	country_CODE_name
# 		Country name of the country represented by CODE, the 2-
# 		character country code.
#
# Variables created by this function:
#
# 	country_CODE_nzones
# 		Either set to `-1' to indicate that the 2-character country
# 		code has only a single zone associated with it (and therefore
# 		you should query the `country_CODE_*' environment variables),
# 		or set to `0' or higher to indicate how many zones are assoc-
# 		iated with the given country code. When multiple zones are
# 		configured for a single code, you should instead query the
# 		`country_CODE_*_N' environment variables (e.g., `echo
# 		$country_AQ_descr_1' prints the description of the first
# 		timezone in Antarctica).
# 	country_CODE_filename
# 		The ``filename'' portion of the TZ value that appears after the
# 		`/' (e.g., `Hong_Kong' from `Asia/Hong_Kong' or `Isle_of_Man'
# 		from `Europe/Isle_of_Man').
# 	country_CODE_cont
# 		The ``continent'' portion of the TZ value that appears before
# 		the `/' (e.g., `Asia' from `Asia/Hong_Kong' or `Europe' from
# 		`Europe/Isle_of_Man').
# 	country_CODE_descr
# 		The comments associated with the ISO 3166 code entry (if any).
#
# 	NOTE: CODE is the 2-character country code.
# 	
# This function is a two-parter. Below is the awk(1) portion of the function,
# afterward is the sh(1) function which utilizes the below awk script.
#
read_zones_awk='
# Variables that should be defined on the invocation line:
# 	-v progname="progname"
#
BEGIN {
	lineno = 0
	failed = 0

	#
	# Initialize continents array/map (name => id)
	#
	split(ENVIRON["CONTINENTS"], array, /[[:space:]]+/)
	for (item in array)
	{
		cont = array[item]
		if (!cont) continue
		name = ENVIRON["continent_" cont "_name"]
		continents[name] = cont
	}
}
function die(fmt, argc, argv)
{
	printf "die \"%%s: %s\" \"%s\"", fmt, progname
	for (n = 1; n <= argc; n++)
		printf " \"%s\"", argv[n]
	print ""
	failed++
	exit 1
}
function find_continent(name)
{
	return continents[name]
}
function add_zone_to_country(lineno, tlc, descr, file, cont)
{
	#
	# Validate the two-character country code
	#
	if (!match(tlc, /^[A-Z][A-Z]$/))
	{
		argv[1] = lineno
		argv[2] = tlc
		die(FILENAME ":%d: country code \\`%s'\'' invalid", 2, argv)
	}
	if (!ENVIRON["country_" tlc "_name"])
	{
		argv[1] = lineno
		argv[2] = tlc
		die(FILENAME ":%d: country code \\`%s'\'' unknown", 2, argv)
	}

	#
	# Add Zone to an array that we will parse at the end
	#
	if (length(descr) > 0)
	{
		if (country_nzones[tlc] < 0)
		{
			argv[1] = lineno
			die(FILENAME ":%d: conflicting zone definition",
			    1, argv)
		}

		n = ++country_nzones[tlc]
		country_cont[tlc,n] = cont
		country_filename[tlc,n] = file
		country_descr[tlc,n] = descr
	}
	else
	{
		if (country_nzones[tlc] > 0)
		{
			argv[1] = lineno
			die(FILENAME ":%d: zone must have description",
			    1, argv)
		}
		if (country_nzones[tlc] < 0)
		{
			argv[1] = lineno
			die(FILENAME ":%d: zone multiply defined", 1, argv)
		}

		country_nzones[tlc] = -1
		country_cont[tlc] = cont
		country_filename[tlc] = file
	}
}
function print_country_code(tlc)
{
	nz = country_nzones[tlc]

	printf "country_%s_nzones=%d\n", tlc, nz
	printf "export country_%s_nzones\n", tlc

	if (nz < 0)
	{
		printf "country_%s_cont=\"%s\"\n", tlc, country_cont[tlc]
		printf "export country_%s_cont\n", tlc
		printf "country_%s_filename=\"%s\"\n",
		       tlc, country_filename[tlc]
	}
	else
	{
		n = 0
		while ( ++n <= nz )
		{
			printf "country_%s_cont_%d=\"%s\"\n",
			       tlc, n, country_cont[tlc,n]
			printf "export country_%s_cont_%d\n", tlc, n
			printf "country_%s_filename_%d=\"%s\"\n",
			       tlc, n, country_filename[tlc,n]
			printf "country_%s_descr_%d=\"%s\"\n",
			       tlc, n, country_descr[tlc,n]
		}
	}
}
/^#/ {
	lineno++
	next
}
!/^#/ {
	lineno++

	#
	# Split the current record (on TAB) into an array
	#
	if (split($0, line, /\t/) < 2)
	{
		argv[1] = lineno
		die(FILENAME ":%d: invalid format", 1, argv)
	}

	# Get the ISO3166-1 (Alpha 1) 2-letter country code
	tlc = line[1]

	#
	# Validate the two-character country code
	#
	if (length(tlc) != 2)
	{
		argv[1] = lineno
		argv[2] = tlc
		die(FILENAME ":%d: invalid country code \\`%s'\''", 2, argv)
	}

	# Get the TZ field
	tz = line[3]

	#
	# Validate the TZ field
	#
	if (!match(tz, "/"))
	{
		argv[1] = lineno
		argv[2] = tz
		die(FILENAME ":%d: invalid zone name \\`%s'\''", 2, argv)
	}

	#
	# Get the continent portion of the TZ field
	#
	contbuf = tz
	sub("/.*$", "", contbuf)

	#
	# Validate the continent
	#
	cont = find_continent(contbuf)
	if (!cont)
	{
		argv[1] = lineno
		argv[2] = contbuf
		die(FILENAME ":%d: invalid region \\`%s'\''", 2, argv)
	}

	#
	# Get the filename portion of the TZ field
	#
	filename = tz
	sub("^[^/]*/", "", filename)

	#
	# Calculate the substr start-position of the comment
	#
	descr_start = 0
	n = 4
	while (--n)
		descr_start += length(line[n]) + 1

	# Get the comment field
	descr = substr($0, descr_start + 1)

	add_zone_to_country(lineno, tlc, descr, filename, cont)
}
END {
	if (failed) exit failed
	for (tlc in country_nzones)
		print_country_code(tlc)
}
'
read_zones()
{
	eval $( awk -v progname="$progname" \
	            "$read_zones_awk"       \
	            "$_PATH_ZONETAB" )
}

# sort_countries
#
# Sorts alphabetically the 2-character country codes listed in $COUNTRIES based
# on the name of each country.
#
# This function is a two-parter. Below is the awk(1) portion of the function,
# afterward is the sh(1) function which utilizes the below awk script.
#
sort_countries_awk='
{
	split($0, array, /[[:space:]]+/)
	for (item in array)
	{
		tlc = array[item]
		print ENVIRON["country_" tlc "_name"] " " tlc
	}
}
'
sort_countries()
{
	COUNTRIES=$( echo "$COUNTRIES" | awk "$sort_countries_awk" |
	             	sort | awk '{print $NF}' )
	export COUNTRIES
}

# make_menus
#
# Creates the tag/item ordered-pair list environment variables for the
# continent and country menus.
#
# Required variables [from the CONFIGURATION section (above)]:
#
# 	CONTINENTS
# 		Space-separated list of continents.
# 	continent_*_title
# 		Desired menu text for the continent represented by *.
#
# Required variables [created by read_iso3166_table]:
#
# 	COUNTRIES
# 		Space-separated list of 2-character country codes.
# 	country_*_name :: when country_*_nzones < 0
# 		Desired menu text for the country-zone represented by *, the 2-
# 		character country code.
#
# Required variables [created by read_zones]:
#
# 	country_*_nzones
# 		Number of zones for the country represented by *, the 2-
# 		character country code. Should be -1 if the country has only
# 		one single zone, otherwise 1 or greater to indicate how many
# 		zones the country has.
# 	country_*_cont :: when country_*_nzones < 0
# 		Principal continent (or ocean) in which the country-zone
# 		represented by *, the 2-character country code, resides.
# 	country_*_cont_N :: when country_*_nzones > 0
# 		Principal continent (or ocean) in which zone-N of the country
# 		represented by * resides, the 2-character country code.
# 	country_*_descr_N :: when country_*_nzones > 0
# 		Desired submenu text for zone-N of the country represented by
# 		*, the 2-character country code.
#
# Variables created by this function:
#
# 	continent_menu_list
# 		Menu-list of continents.
# 	continent_*_nitems
# 		Number of items associated with the continent represented by *,
# 		the continent identifier.
# 	continent_*_tlc_N
# 		2-character country code of the Nth item in the continent menu
# 		for the continent represented by *, the continent identifier.
# 	continent_*_menu_list
# 		Menu-list of countries/zones for each continent represented by
# 		*, the continent identifier.
# 	country_*_menu_list
# 		For countries that have multiple zones, this is the submenu-
# 		list of zones for said country represented by *, the 2-
# 		character country code.
#
# This function is a two-parter. Below is the awk(1) portion of the function,
# afterward is the sh(1) function which utilizes the below awk script.
#
make_menus_awk='
function add_zone_n_to_country_menu(tlc, n)
{
	zone_title = ENVIRON["country_" tlc "_descr_" n]
	gsub(/'\''/, "'\''\\'\'''\''", zone_title)
	country_menu_list[tlc] = country_menu_list[tlc] \
		( length(country_menu_list[tlc]) > 0 ? "\n" : "" ) \
		n " '\''" zone_title "'\''"
}
BEGIN {
	#
	# First, count up all the countries in each continent/ocean.
	# Be careful to count those countries which have multiple zones
	# only once for each.  NB: some countries are in multiple
	# continents/oceans.
	#
	i = split(ENVIRON["COUNTRIES"], countries, /[[:space:]]+/)
	for (cp = 1; cp <= i; cp++)
	{
		tlc = countries[cp]
		title = ENVIRON["country_" tlc "_name"]
		gsub(/'\''/, "'\''\\'\'''\''", title)
		nzones = ENVIRON["country_" tlc "_nzones"]
		if (!nzones)
		{
			# Country has no zones
			continue
		}
		else if (nzones < 0)
		{
			# Country has only one zone
			cont = ENVIRON["country_" tlc "_cont"]
			nitems = ++continent_nitems[cont]
			continent_tlc[cont,nitems] = tlc
			continent_title[cont,nitems] = title
		}
		else
		{
			# Country has one or more zones
			for (n = 1; n <= nzones; n++)
			{
				add_zone_n_to_country_menu(tlc, n)
				cont = ENVIRON["country_" tlc "_cont_" n]
				for (x = 1; x < n; x++)
				{
					contx = ENVIRON["country_"tlc"_cont_"x]
					if (cont == contx) break
				}
				if (x == n)
				{
					nitems = ++continent_nitems[cont]
					continent_tlc[cont,nitems] = tlc
					continent_title[cont,nitems] = title
				}
			}
		}
	}
}
END {
	tags = ENVIRON["DIALOG_MENU_TAGS"]
	cont_menu_list = ""
	tagn = 0

	#
	# Assemble the menu items in the menu list for each continent/ocean.
	#
	i = split(ENVIRON["CONTINENTS"], array, /[[:space:]]+/)
	for (item = 1; item <= i; item++)
	{
		cont = array[item]
		if (!cont) continue

		if (++tagn >= length(tags)) break
		tag = substr(tags, tagn, 1)
		cont_menu_list = cont_menu_list \
			( length(cont_menu_list) > 0 ? "\n" : "" ) \
			"'\''" tag "'\'' '\''" \
			ENVIRON["continent_" cont "_title"] "'\''"

		nitems = continent_nitems[cont]
		printf "continent_%s_nitems=%d\n", cont, nitems

		menu_list = ""
		for (n = 1; n <= nitems; n++)
		{
			printf "continent_%s_tlc_%d=%s\n",
			       cont, n, continent_tlc[cont,n]

			title = continent_title[cont,n]
			menu_list = menu_list \
				( length(menu_list) > 0 ? "\n" : "" ) \
				n " '\''" title "'\''"
		}

		gsub(/"/, "\\\"", menu_list)
		printf "continent_%s_menu_list=\"%s\"\n", cont, menu_list
	}

	gsub(/"/, "\\\"", continent_menu_list)
	printf "continent_menu_list=\"%s\"\n", cont_menu_list
	print "export continent_menu_list"

	#
	# Dump the submenus of countries with multiple zones
	#
	for (tlc in country_menu_list)
	{
		menu_list = country_menu_list[tlc]
		gsub(/"/, "\\\"", menu_list)
		printf "country_%s_menu_list=\"%s\"\n", tlc, menu_list
	}
}
'
make_menus()
{
	eval $( :| awk "$make_menus_awk" )
}

# OCEANP $cont
#
# Returns "1" if the first argument is an ocean, otherwise NULL.
#
OCEANP()
{
	case "$1" in
	arctic|atlantic|indian|pacific)
		echo 1
	esac
}

# find_continent $title
#
# Returns continent identifier given continent title.
#
find_continent()
{
	local cont
	for cont in $CONTINENTS; do
		if [ "$1" = "$( continent $cont title )" ]; then
			echo "$cont"
			return $SUCCESS
		fi
	done
	return $FAILURE
}

# continent $cont $property
#
# Returns a single property of a given continent. Available properties are:
#
# 	name        Directory name of continent/ocean as it appears in
# 	            _PATH_ZONEINFO.
# 	title       Menu text of this continent/ocean to be displayed in the
# 	            continent-selection menu.
# 	nitems      Number of submenu items associated with this
# 	            continent/ocean.
# 	tlc_N       2-character country code of the Nth submenu item associated
# 	            with this continent displayed in the country-selection menu
# 	            (which appears after continent selection).
# 	menu_list   Menu-list of regions for this continent.
#
continent()
{
	local cont="$1" property="$2"
	eval echo \"\${continent_${cont}_$property}\"
}

# country $code $property
#
# Returns a single property of a given country. Available properties are:
#
# 	name         Name of the country as read from _PATH_ISO3166.
# 	nzones       Number of zones within the country (-1 if country has
# 	             only a single zone).
# 	filename     The filename portion of the TZ field (after the `/') as
# 	             read from _PATH_ZONETAB.
# 	cont         The principal continent in which the country lies (appears
# 	             before the `/' in the TZ field of _PATH_ZONETAB).
# 	filename_N   Like filename, but for the Nth zone when the country has
# 	             multiple zones (nzones > 0).
# 	cont_N       Like cont, but for the Nth zone when the country has
# 	             multiple zones (nzones > 0).
# 	descr_N      Like name, but for the Nth zone when the country has
# 	             multiple zones (nzones > 0)
#
country()
{
	local code="$1" property="$2"
	eval echo \"\${country_${code}_$property}\"
}

# set_zone_utc
#
# Resets to the UTC timezone.
#
set_zone_utc()
{
	confirm_zone "" || return $FAILURE
	install_zoneinfo_file ""
}

# install_zoneinfo_file $filename
#
# Installs a zone file to _PATH_LOCALTIME.
#
install_zoneinfo_file()
{
	local zoneinfo_file="$1"
	local copymode title msg err size

	if [ -L "$_PATH_LOCALTIME" ]; then
		copymode=
	elif [ ! -e "$_PATH_LOCALTIME" ]; then
		# Nothing there yet...
		copymode=1
	else
		copymode=1
	fi

	if [ "$VERBOSE" ]; then
		if [ "$copymode" ]; then
			msg="Copying $zoneinfo_file to $_PATH_LOCALTIME"
		else
			msg="Creating symbolic link $_PATH_LOCALTIME"
			msg="$msg to ${zoneinfo_file:-(UTC)}"
		fi
		if [ "$USEDIALOG" ]; then
			size=$( dialog_infobox_size "" "$msg" )
			eval $DIALOG \
				${USE_XDIALOG:+--no-buttons} \
				--infobox \"\$msg\" $size
		else
			printf "%s\n" "$msg"
		fi
	fi

	if [ "$REALLYDOIT" ]; then
		title="Error"
		if [ ! "$zoneinfo_file" ]; then
			err=$( rm -f "$_PATH_LOCALTIME" 2>&1 )
			if [ "$err" ]; then
				if [ "$USEDIALOG" ]; then
					size=$( dialog_buttonbox_size \
					        	"$title" "$err" )
					eval $DIALOG \
						--title \"\$title\" \
						--msgbox \"\$err\" $size
				else
					eprintf "%s\n" "$err"
				fi
				return $FAILURE
			fi
			err=$( rm -f "$_PATH_DB" 2>&1 )
			if [ "$err" ]; then
				if [ "$USEDIALOG" ]; then
					size=$( dialog_buttonbox_size \
					        	"$title" "$err" )
					eval $DIALOG \
						--title \"\$title\" \
						--msgbox \"\$err\" $size
				else
					eprintf "%s\n" "$err"
				fi
				return $FAILURE
			fi
			return $SUCCESS
		fi

		if [ "$copymode" ]; then
			quietly rm -f "$_PATH_LOCALTIME"
			err=$( umask 222 && : 2>&1 > "$_PATH_LOCALTIME" )
			if [ "$err" ]; then
				if [ "$USEDIALOG" ]; then
					size=$( dialog_buttonbox_size \
					        	"$title" "$err" )
					eval $DIALOG --title \"\$title\" \
					             --msgbox \"\$err\" $size
				else
					eprintf "%s\n" "$err"
				fi
				return $FAILURE
			fi
			err=$( cat "$zoneinfo_file" 2>&1 > "$_PATH_LOCALTIME" )
			if [ "$err" ]; then
				if [ "$USEDIALOG" ]; then
					size=$( dialog_buttonbox_size \
					        	"$title" "$err" )
					eval $DIALOG --title \"\$title\" \
					             --msgbox \"\$err\" $size
				else
					eprintf "%s\n" "$err"
				fi
				return $FAILURE
			fi
		else
			err=$( ( :< "$zoneinfo_file" ) 2>&1 )
			if [ "$err" ]; then
				if [ "$USEDIALOG" ]; then
					size=$( dialog_buttonbox_size \
					        	"$title" "$err" )
					eval $DIALOG --title \"\$title\" \
					             --msgbox \"\$err\" $size
				else
					eprintf "%s\n" "$err"
				fi
				return $FAILURE
			fi
			quietly rm -f "$_PATH_LOCALTIME"
			err=$( ln -s "$zoneinfo_file" "$_PATH_LOCALTIME" 2>&1 )
			if [ "$err" ]; then
				if [ "$USEDIALOG" ]; then
					size=$( dialog_buttonbox_size \
					        	"$title" "$err" )
					eval $DIALOG --title \"\$title\" \
					             --msgbox \"\$err\" $size
				else
					eprintf "%s\n" "$err"
				fi
				return $FAILURE
			fi
		fi
	fi

	if [ "$VERBOSE" ]; then
		title="Done"
		if [ "$copymode" ]; then
			msg="Copied timezone file from $zoneinfo_file"
			msg="$msg to $_PATH_LOCALTIME"
		else
			msg="Created symbolic link from $_PATH_LOCALTIME"
			msg="$msg to $zoneinfo_file"
		fi
		if [ "$USEDIALOG" ]; then
			size=$( dialog_buttonbox_size "$title" "$msg" )
			eval $DIALOG \
				--title \"\$title\" \
				--msgbox \"\$msg\" $size
		else
			printf "%s\n" "$msg"
		fi
	fi

	return $SUCCESS
}


# install_zoneinfo $zoneinfo
#
# Install a zoneinfo file relative to _PATH_ZONEINFO. The given $zoneinfo
# will be written to _PATH_DB (usable later with the `-r' flag).
#
install_zoneinfo()
{
	local zoneinfo="$1"
	local rv

	install_zoneinfo_file "$_PATH_ZONEINFO/$zoneinfo"
	rv=$?

	# Save knowledge for later
	if : 2> /dev/null > "$_PATH_DB"; then
		cat <<-EOF > "$_PATH_DB"
		$zoneinfo
		EOF
	fi

	return $rv
}

 
###############################################################################
############################ DIALOG SIZE FUNCTIONS ############################
###############################################################################

# dialog_infobox_size $title $prompt
#
# Not all versions of dialog(1) perform auto-sizing of the width and height of
# `--infobox' boxes sensibly.
#
# This function helps solve this issue by taking as arguments (in order of
# appearance) the title and prompt, returning the optimal width and height for
# the box (not exceeding the actual terminal width or height).
#
# Newline character sequences (``\n'') in $prompt are expanded as-is done by
# dialog(1).
#
# Output is in the format of "height width".
#
dialog_infobox_size()
{
	local title="$1" prompt="$2" n=0
	local min_width max_size

	if [ "$USE_XDIALOG" ]; then
		min_width=35
		max_size="$XDIALOG_MAX_SIZE" # see CONFIGURATION
	else
		min_width=24
		min_rows=0
		max_size="$( stty size )" # usually "24 80"
	fi

	local max_height="${max_size%%[$IFS]*}"
	local max_width="${max_size##*[$IFS]}"
	local height width=$min_width

	#
	# Bump width for long titles (but don't exceed terminal width).
	#
	n=$(( ${#title} + 4 ))
	if [ $n -gt $width -a $n -gt $min_width ]; then
		# Add 16.6% width for Xdialog(1)
		[ "$USE_XDIALOG" ] && n=$(( $n + $n / 6 ))

		if [ $n -lt $max_width ]; then
			width=$n
		else
			width=$max_width
		fi
	fi

	#
	# Bump width for long prompts (if not already at maximum width).
	#
	if [ $width -lt $max_width ]; then
		n=$( echo "$prompt" | longest_line_length )
		n=$(( $n + 4 ))

		# Add 16.6% width for Xdialog(1)
		[ "$USE_XDIALOG" ] && n=$(( $n + $n / 6 ))

		if [ $n -gt $width -a $n -gt $min_width ]; then
			if [ $n -lt $max_width ]; then
				width=$n
			else
				width=$max_width
			fi
		fi
	fi

	#
	# Set height based on number of rows in prompt
	#
	height=$( echo "$prompt" | number_of_lines )
	height=$(( $height + 2 ))

	# Return both
	echo "$height $width"
}

# dialog_buttonbox_size $title $prompt
#
# Not all versions of dialog(1) perform auto-sizing of the width and height of
# `--msgbox' and `--yesno' boxes sensibly.
#
# This function helps solve this issue by taking as arguments (in order of
# appearance) the title and prompt, returning the optimal width and height for
# the box (not exceeding the actual terminal width or height).
#
# Newline character sequences (``\n'') in $prompt are expanded as-is done by
# dialog(1).
#
# Output is in the format of "height width".
#
dialog_buttonbox_size()
{
	local title="$1" prompt="$2"
	local size="$( dialog_infobox_size "$title" "$prompt" )"
	local height="${size%%[$IFS]*}"
	local width="${size##*[$IFS]}"

	# Add height to accomodate the buttons
	height=$(( $height + 3 ))

	# Adjust for clipping with Xdialog(1) on Linux/GTK2
	[ "$USE_XDIALOG" ] && height=$(( $height + 1 ))

	# Return both
	echo "$height $width"
}

# dialog_menu_size $title $prompt $tag1 $item1 $tag2 $item2 ...
#
# Not all versions of dialog(1) perform auto-sizing of the width and height of
# `--menu' boxes sensibly.
#
# This function helps solve this issue by taking as arguments (in order of
# appearance) the title, prompt, and list of tag/item pairs, returning the
# optimal width and height for the menu (not exceeding the actual terminal
# width or height).
#
# Newline character sequences (``\n'') in $prompt are expanded as-is done by
# dialog(1).
#
# Output is in the format of "height width rows".
#
dialog_menu_size()
{
	local title="$1" prompt="$2" n=0
	local min_width min_rows max_size

	if [ "$USE_XDIALOG" ]; then
		min_width=35
		min_rows=1
		max_size="$XDIALOG_MAX_SIZE" # see CONFIGURATION
	else
		min_width=24
		min_rows=0
		max_size="$( stty size )" # usually "24 80"
	fi

	local max_width="${max_size##*[$IFS]}"
	local max_height="${max_size%%[$IFS]*}"
	local box_size="$( dialog_infobox_size "$title" "$prompt" )"
	local box_height="${box_size%%[$IFS]*}"
	local box_width="${box_size##*[$IFS]}"
	local max_rows=$(( $max_height - 8 ))
	local height width=$box_width rows=$min_rows

	shift 2 # title/prompt

	#
	# Bump width for long menu items (if not already at maximum width) and
	# calculate the number of rows (not to exceed terminal maximum height).
	#
	while [ $# -ge 2 ]; do
		local tag="$1" item="$2"
		shift 2 # tag/item

		if [ $width -lt $max_width ]; then
			n=$(( ${#tag} + ${#item} + 11 ))

			# Add 16.6% width for Xdialog(1)
			[ "$USE_XDIALOG" ] && n=$(( $n + $n / 6 ))

			if [ $n -gt $width -a $n -gt $min_width ]; then
				if [ $n -lt $max_width ]; then
					width=$n
				else
					width=$max_width
				fi
			fi
		fi

		if [ $rows -lt $max_rows ]; then
			rows=$(( $rows + 1 ))
		fi
	done

	# Fix rows and set height
	[ $rows -gt 0 ] || rows=1
	if [ "$USE_XDIALOG" ]; then
		height=$(( $rows + $box_height + 7 ))
	else
		height=$(( $rows + $box_height + 4 ))
	fi

	# Return all three
	echo "$height $width $rows"
}

###############################################################################
########################### DIALOG YESNO FUNCTIONS ############################
###############################################################################

# confirm_zone $filename
#
# Prompt the user to confirm the new timezone data. The first (and only)
# argument should be the pathname to the zoneinfo file, either absolute or
# relative to `/usr/share/zoneinfo' (e.g., "America/Los_Angeles").
#
# The return status is 0 if "Yes" is chosen, 1 if "No", and 255 if Esc is
# pressed (see dialog(1) for additional details).
# 
confirm_zone()
{
	local filename="$1"
	local title="Confirmation"
	local tm_zone="$( TZ="$filename" date +%Z )"
	local prompt="Does the abbreviation \`$tm_zone' look reasonable?"
	local height=5 width=72

	if [ "$UNAME_S" = "FreeBSD" -a ! "$USE_XDIALOG" ]; then
		case "$UNAME_R" in
		[12345678].*) height=4
		esac
	fi
	
	$DIALOG --title "$title" --yesno "$prompt" $height $width
}

###############################################################################
############################ DIALOG MENU FUNCTIONS ############################
###############################################################################

# dialog_menutag
#
# Obtain the menutag chosen by the user from the most recently displayed
# dialog(1) menu and clean up any temporary files.
#
dialog_menutag()
{
	local tmpfile="$DIALOG_TMPDIR/dialog.menu.$$"

	[ -f "$tmpfile" ] || return $FAILURE

	cat "$tmpfile" 2> /dev/null
	quietly rm -f "$tmpfile"

	return $SUCCESS
}

# dialog_menutag2item $tag_chosen $tag1 $item1 $tag2 $item2 ...
#
# To use the `--menu' option of dialog(1) you must pass an ordered list of
# tag/item pairs on the command-line. When the user selects a menu option the
# tag for that item is printed to stderr.
#
# This function allows you to dereference the tag chosen by the user back into
# the item associated with said tag.
#
# Pass the tag chosen by the user as the first argument, followed by the
# ordered list of tag/item pairs (HINT: use the same tag/item list as was
# passed to dialog(1) for consistency).
#
# If the tag cannot be found, NULL is returned.
#
dialog_menutag2item()
{
	local tag="$1" tagn item
	shift 1

	while [ $# -gt 0 ]; do
		tagn="$1"
		item="$2"
		shift 2

		if [ "$tag" = "$tagn" ]; then
			echo "$item"
			return $SUCCESS
		fi
	done
	return $FAILURE
}

# dialog_menu_root
#
# Display the dialog(1)-based application root menu.
#
dialog_menu_root()
{
	local title="Time Zone Selector"
	local prompt="Select a region"
	local size

	size=$( eval dialog_menu_size \"\$title\" \"\$prompt\" \
	                              $continent_menu_list )
	eval $DIALOG \
		--title \"\$title\" \
		--menu \"\$prompt\" $size \
		$continent_menu_list \
		2> "$DIALOG_TMPDIR/dialog.menu.$$"
}

###############################################################################
################################# MAIN SOURCE #################################
###############################################################################
[ "`id -u`" = "0" ] || die "Must run as root!"

#
# Process command-line arguments
#
while getopts C:enrsvX flag; do
	case "$flag" in
	C) CHROOTENV="$OPTARG";;
	e) TZ_OR_FAIL=1;;
	n) REALLYDOIT=;;
	r) REINSTALL=1
	   USEDIALOG=;;
	s) SKIPUTC=1;;
	v) VERBOSE=1;;
	X) USE_XDIALOG=1;;
	\?) usage;;
	esac
done
shift $(( $OPTIND - 1 ))

#
# Process `-C chroot_directory' command-line argument
#
if [ "$CHROOTENV" ]; then
	_PATH_ZONETAB="$CHROOTENV$_PATH_ZONETAB"
	_PATH_ISO3166="$CHROOTENV$_PATH_ISO3166"
	_PATH_ZONEINFO="$CHROOTENV$_PATH_ZONEINFO"
	_PATH_LOCALTIME="$CHROOTENV$_PATH_LOCALTIME"
	_PATH_DB="$CHROOTENV$_PATH_DB"
	_PATH_WALL_CMOS_CLOCK="$CHROOTENV$_PATH_WALL_CMOS_CLOCK"
fi

#
# Process `-r' command-line option
#
if [ "$REINSTALL" ]; then
	[ -f "$_PATH_DB" -a -r "$_PATH_DB" ] ||
		die "Cannot open %s for reading. Does it exist?" "$_PATH_DB"
	zonefile=$( cat "$_PATH_DB" ) ||
		die "Error reading %s." "$_PATH_DB"
	[ "$zonefile" ] ||
		die "Unable to determine %s zoneinfo file. Check %s" \
		    "earlier installed" "$_PATH_DB"
	install_zoneinfo "$zonefile"
	exit $?
fi

#
# If the arguments on the command-line do not specify a file,
# then interpret it as a zoneinfo name
#
if [ $# -ge 1 ]; then
	zoneinfo="$1"

	if [ ! -f "$zoneinfo" ]; then
		USEDIALOG=
		install_zoneinfo "$zoneinfo"
		exit $?
	fi

	# FALLTHROUGH
fi

#
# Trap signals so we can recover gracefully
#
trap 'die' SIGINT SIGTERM SIGPIPE SIGXCPU SIGXFSZ \
           SIGFPE SIGTRAP SIGABRT SIGSEGV
[ "$SHELL" = "$0" ] &&
	trap 'die' SIGSTOP SIGTSTP SIGTTIN SIGTTOU
trap '' SIGALRM SIGPROF SIGUSR1 SIGUSR2 SIGHUP SIGVTALRM

#
# Process `-X' command-line option
#
if [ "$USE_XDIALOG" ]; then
	DIALOG=Xdialog

	# Disable the use of `--defaultno' since Xdialog(1) has it's own
	# `--dialog-no' option which will be triggered by $USE_XDIALOG
	#
	DIALOG_ENABLE_DEFAULTNO=
fi

#
# Probe Xdialog(1) for maximum height/width constraints
#
if [ "$USE_XDIALOG" ]; then
	maxsize=$( $DIALOG --print-maxsize 2>&1 ) \
	&& XDIALOG_MAX_SIZE=$(
		set -- ${maxsize##*:}

		height=${1%,}
		width=$2

		echo $height $width
	)
	unset maxsize
fi

#
# DIALOG fixup (FreeBSD-9.0 dialog(1) and Linux dialog(1) support the
# `--defaultno' command-line argument.
#
if [ "$CHECK_DIALOG_FOR_DEFAULTNO" -a ! "$USE_XDIALOG" ]; then
	have $DIALOG ||
		die "%s: %s: No such file or directory" "$progname" "$DIALOG"

	DIALOG=$( which $DIALOG )
	data=$( strings "$DIALOG" ) || die

	if echo "$data" | grep -q defaultno; then
		DIALOG_ENABLE_DEFAULTNO=1
	else
		DIALOG_ENABLE_DEFAULTNO=
	fi
fi

#
# Process the UTC option
#
if [ "$_PATH_WALL_CMOS_CLOCK" -a ! "$SKIPUTC" ]; then
	title="Select local or UTC (Greenwhich Mean Time) clock"

	msg="Is this machine's CMOS clock set to UTC? "
	msg="$msg If it is set to local time,\nor you"
	msg="$msg don't know, please choose NO here!"

	if [ "$USE_XDIALOG" ]; then
		height=7 width=77
	else
		height=7 width=72

		# If dialog(1) supports --defaultno, then it also has a bug
		# in which it counts the newline in the wrapping. A simple
		# fix is to ``add one'' when using such a beast.
		#
		# Includes Linux, FreeBSD 9 (but not FreeBSD 8.1 or older)
		# and possibly others -- anyone using cdialog really.
		#
		[ "$DIALOG_ENABLE_DEFAULTNO" ] && width=73
	fi

	$DIALOG --title "$title"                        \
	        ${DIALOG_ENABLE_DEFAULTNO:+--defaultno} \
	        ${USE_XDIALOG:+--default-no}            \
	        --yesno "$msg" $height $width
	result=$?

	if [ $result -eq 0 ]; then
		# User chose YES
		[ "$REALLYDOIT" ] &&
			quietly rm -f "$_PATH_WALL_CMOS_CLOCK"
	else
		# User chose NO, pressed ESC (or Ctrl-C), or closed box
		[ "$REALLYDOIT" ] &&
			( umask 222 && :> "$_PATH_WALL_CMOS_CLOCK" )
	fi

	[ ! "$USE_XDIALOG" ] && $DIALOG --clear
fi

#
# Process optional default zone argument
#
if [ $# -ge 1 ]; then
	default="$1"

	title="Default timezone provided"
	msg="\nUse the default \`$default' zone?"
	size=$( dialog_buttonbox_size "$title" "$msg" )

	eval $DIALOG --title \"\$title\" --yesno \"\$msg\" $size
	result=$?

	if [ $result -eq 0 ]; then
		# User chose YES
		install_zoneinfo_file "$default"
		result=$?
		[ ! "$USE_XDIALOG" ] && $DIALOG --clear
		exit $result
	fi

	[ ! "$USE_XDIALOG" ] && $DIALOG --clear
fi

#
# Override the user-supplied umask
#
umask 022

#
# Read databases and perform initialization
#
read_iso3166_table # creates $COUNTRIES and $country_*_name
read_zones         # creates $country_*_{descr,cont,filename}
sort_countries     # sorts the countries listed for each continent
make_menus         # creates $continent_menu_list and $continent_*_menu_list

#
# Launch application root menu
#
NEED_CONTINENT=1
NEED_COUNTRY=1
while :; do
	if [ "$NEED_CONTINENT" ]; then
		dialog_menu_root # prompt the user to select a continent/ocean
		retval=$?
		mtag=$( dialog_menutag )

		if [ $retval -ne 0 ]; then
			[ ! "$USE_XDIALOG" ] && $DIALOG --clear
			[ "$TZ_OR_FAIL" ] && die
			exit $SUCCESS
		fi

		NEED_CONTINENT=

		continent=$( eval dialog_menutag2item \"\$mtag\" \
		                  	$continent_menu_list )
		cont=$( find_continent "$continent" )
		cont_title=$( continent $cont title )
		nitems=$( continent $cont nitems )
		isocean=$( OCEANP $cont )
	fi

	if [ "$NEED_COUNTRY" ]; then
		if [ "$cont_title" = "UTC" ]; then
			if set_zone_utc; then
				break
			else
				NEED_CONTINENT=1
				continue
			fi
		fi

		#
		# Short cut -- if there's only one country, don't post a menu.
		#
		if [ $nitems -eq 1 ]; then
			tag=1
		else
			#
			# It's amazing how much good grammar really matters...
			#
			if [ ! "$isocean" ]; then
				title="Countries in $cont_title"
				prompt="Select a country"
			else
				title="Islands and groups in the $cont_title"
				prompt="Select an island or group"
			fi

			#
			# Calculate size of menu
			#
			menu_list=$( continent $cont menu_list )
			size=$( eval dialog_menu_size \"\$title\"  \
			                              \"\$prompt\" \
			                              $menu_list )

			#
			# Launch the country selection menu
			#
			eval $DIALOG \
				--title \"\$title\" \
				--menu \"\$prompt\" $size \
				$menu_list \
				2> "$DIALOG_TMPDIR/dialog.menu.$$"
			retval=$?
			tag=$( dialog_menutag )

			if [ $retval -ne 0 ]; then
				NEED_CONTINENT=1
				continue # back to main menu
			fi
		fi

		# Get the country code from the user's selection 
		tlc=$( continent $cont tlc_$tag )

		NEED_COUNTRY=
	fi

	#
	# If the selection has only one zone (nzones == -1),
	# just set it.
	#
	nzones=$( country $tlc nzones )
	if [ $nzones -lt 0 ]; then
		real_cont=$( country $tlc cont )
		real_continent=$( continent $real_cont name )
		name=$( country $tlc name )
		filename=$( country $tlc filename )

		if ! confirm_zone "$real_continent/$filename"; then
			[ $nitems -eq 1 ] && NEED_CONTINENT=1
			NEED_COUNTRY=1
			continue
		fi
	else
		title="$( country $tlc name ) Time Zones"
		prompt="Select a zone which observes the same"
		prompt="$prompt time as your locality."
		menu_list=$( country $tlc menu_list )
		size=$( eval dialog_menu_size \"\$title\" \"\$prompt\" \
		                              $menu_list )

		#
		# Launch the zone selection menu
		# NOTE: This is as deep as we go
		#
		eval $DIALOG \
			--title \"\$title\" \
			--menu \"\$prompt\" $size \
			$menu_list \
			2> "$DIALOG_TMPDIR/dialog.menu.$$"
		retval=$?
		n=$( dialog_menutag )

		if [ $retval -ne 0 ]; then
			[ $nitems -eq 1 ] && NEED_CONTINENT=1
			NEED_COUNTRY=1
			continue
		fi

		real_cont=$( country $tlc cont_$n )
		real_continent=$( continent $real_cont name )
		name=$( country $tlc name )
		filename=$( country $tlc filename_$n )

		confirm_zone "$real_continent/$filename" || continue
	fi

	[ $retval -eq 0 ] || continue # back to main menu

	if ! install_zoneinfo "$real_continent/$filename"; then
		[ $nzones -lt 0 ] && NEED_COUNTRY=1
	else
		break
	fi
done

[ ! "$USE_XDIALOG" ] && $DIALOG --clear

################################################################################
# END
################################################################################
#
# $Header$
#
# $Copyright: 2011 Devin Teske. All Rights Reserved. $
#
# $Log$
#
################################################################################
