#!/usr/bin/perl -w
#
# $Id: EditAB.pl,v 2.2 1999/02/08 23:31:36 bodo Exp $
#
# graphical interface to data created by
# SyncAB, part of PilotManager.
# Database functions.
#
# (C) 1998, 1999 Bodo Bellut bodo@garfield.ping.de
#                      http://www.ping.de/sites/garfield/pilotmgr.html
#
# Partly based on SyncAB and PilotManager.
#
# SyncAB contains the following notices:
# -----cut-----
# Address Book conduit for PilotManager
# 3/17/98 Alan.Harder@Sun.COM
# http://www.moshpit.org/pilotmgr
# -----cut-----
#
# PilotManager contains the following notices:
# -----cut-----
# Copyright (c) 1997 Sun Microsystems, Inc.
# All rights reserved.
# 
# Permission is hereby granted, without written agreement and without
# license or royalty fees, to use, copy, modify, and distribute this
# software and its documentation for any purpose, provided that the
# above copyright notice and the following two paragraphs appear in
# all copies of this software.
# 
# IN NO EVENT SHALL SUN MICROSYSTEMS, INC. BE LIABLE TO ANY PARTY FOR
# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
# OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF SUN
# MICROSYSTEMS, INC. HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# 
# SUN MICROSYSTEMS, INC. SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.  THE SOFTWARE PROVIDED
# HEREUNDER IS ON AN "AS IS" BASIS, AND SUN MICROSYSTEMS, INC. HAS NO
# OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
# MODIFICATIONS.
# -----cut-----
#
# The new code is placed under the GPL,
# so the following applies:
# -----cut-----
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
# -----cut-----
#
# This is a standalone program and not part of PilotManager, although
# it uses part of the data created by it, and is intended to be used
# alongside with it.
#

use TkAB;


my ($RCFILE, $APPINFO_FILE);
my ($gConfigDialog, $gFileLabel, $gFileEntry);
my ($gDB);
my ($gAktRecord);
my ($gDataChanged) = 0;
my ($gAlreadyLoaded) = 0;
my ($gReader, $gWriter);
my ($gDataFile);
my ($gEntryMap) = ({ 'lastname' => 0,
		     'firstname' => 1,
		     'company' => 2,
		     'phone1' => 3,
		     'phone2' => 4,
		     'phone3' => 5,
		     'phone4' => 6,
		     'phone5' => 7,
		     'address' => 8,
		     'city' => 9,
		     'state' => 10,
		     'zip' => 11,
		     'country' => 12,
		     'title' => 13,
		     'custom1' => 14,
		     'custom2' => 15,
		     'custom3' => 16,
		     'custom4' => 17,
		     'note' => 18,
		     'whichphone' => 'showPhone',
		     'phonetypes' => 'phoneLabel',
		     'category' => 'category',
		     'rolo_id' => 'rolo_id',
		     'updatetop' => 'updatetop',
		     'private' => 'secret'});
my ($gCSVorder) = ([ 'rolo_id', 'lastname', 'firstname', 'company',
		     'phone1', 'phone2', 'phone3', 'phone4', 'phone5',
		     'address', 'city', 'state', 'zip', 'country', 'title',
		     'custom1', 'custom2', 'custom3', 'custom4', 'note',
		     'whichphone', 'phonetypes', 'category', 'private' ]);

sub init
{
# Modify paths here
    $RCFILE = "$ENV{HOME}/.pilotmgr/SyncAB/SyncAB.prefs";
    $APPINFO_FILE = "$ENV{HOME}/.pilotmgr/SyncAB/pilot.appinfo";
# Nothing to modify beyond this line
    &loadPrefs;

    $PREFS->{'syncType'} = 'CSV'
	unless (defined $PREFS->{'syncType'});
    $PREFS->{'CSVFile'} = "$ENV{HOME}/.csvAddr"
	unless (defined $PREFS->{'CSVFile'});
    $PREFS->{'vCardFile'} = "$ENV{HOME}/.vCards"
	unless (defined $PREFS->{'vCardFile'});
    $PREFS->{'vCardDir'} = "$ENV{HOME}/.dt/Addresses"
	unless (defined $PREFS->{'vCardDir'});
    $PREFS->{'RoloFile'} = "$ENV{HOME}/.rolo"
	unless (defined $PREFS->{'RoloFile'});
}


sub init2
{
    my ($this, $dlp, $info) = @_;
#    my ($idField);

    if ($PREFS->{'syncType'} eq 'Rolo')
    {
#	$idField   = 'rolo_id';
	$gDataFile = $PREFS->{'RoloFile'};
	$gReader   = \&readRolo;
	$gWriter   = \&writeRolo;
    }
    elsif ($PREFS->{'syncType'} eq 'CSV')
    {
#	$idField   = 'rolo_id';
	$gDataFile = $PREFS->{'CSVFile'};
	$gReader   = \&readCSV;
	$gWriter   = \&writeCSV;
    }
    else
    {
#	$idField = 'rolo_id';
	die "SyncAB does not yet support type $PREFS->{'syncType'}\n";
    }

}

sub loadFile
{
    &storeAktRecord if ($gAlreadyLoaded == 1);

    if ($gDataChanged == 1) {
	my ($userAnswer) = TkAB::askUser("Something's changed. Save?", ("Save", "Load", "Cancel"));
	
	return if ($userAnswer eq "Cancel");

	if ($userAnswer eq "Save") {
	    &$gWriter($gDataFile, $gDB);
	}
    }

    $gDB = &$gReader($gDataFile);
    TkAB::updateAIData($gDB->{'__APPINFO'});
    TkAB::displayRecord($gDB);
    TkAB::updateListView;
    $gAktRecord = 0;
    $gDataChanged = 0;
    $gAlreadyLoaded = 1;
}

sub saveFile
{
    &storeAktRecord if ($gAlreadyLoaded == 1);

    return unless $gDataChanged == 1;

    if (TkAB::askUser("Really save?", ("No", "Yes")) eq "Yes") {
	&$gWriter($gDataFile, $gDB);
	$gDataChanged = 0;
    }
}

sub quit
{
    &storeAktRecord if ($gAlreadyLoaded == 1);

    return 1 unless $gDataChanged == 1;

    my ($userAnswer) = TkAB::askUser("Something's changed. Save?", ("Save", "Quit", "Cancel"));
	
    return 0 if ($userAnswer eq "Cancel");

    if ($userAnswer eq "Save") {
	&$gWriter($gDataFile, $gDB);
    }
    return 1;
}

sub updateAI
{
    my ($update) = @_;
    my (@newCustomNames)   = @{$update->{'customNames'}};
    my (@newCategoryNames) = @{$update->{'categoryNames'}};
    my ($ai) = $gDB->{'__APPINFO'};
    my ($i);
    
    if (scalar(@newCustomNames))
    {
	$ai->{'label'}[14] = $newCustomNames[0];
	$ai->{'label'}[15] = $newCustomNames[1];
	$ai->{'label'}[16] = $newCustomNames[2];
	$ai->{'label'}[17] = $newCustomNames[3];
	$gDataChanged = 1;
    }

    if (scalar(@newCategoryNames))
    {
	for ($i=0; $i <= $#{$ai->{'categoryName'}}; $i++) {
	    $ai->{'categoryName'}[$i] = defined($newCategoryNames[$i])?$newCategoryNames[$i]:"";
	}
	$gDataChanged = 1;
    }
}

sub displayAktRecord
{
    TkAB::displayRecord($gDB, $gAktRecord);
}

sub storeAktRecord
{
    my ($newRec) = &TkAB::getAktRecord;
    return unless defined $newRec;
    
    $newRec->{'rolo_id'} = $gDB->{'__RECORDS'}[$gAktRecord]->{'rolo_id'};
    
    $newRec->{'entry'}[18] =~ s/^\n$//s;  # Ugh, Tk::Text ALWAYS leaves "\n"
                                          #      even when user clears area.

    $gDB->{'__RECORDS'}[$gAktRecord] = $newRec;
    
    TkAB::updateListView;

    $gDataChanged = 1;
}

sub gotoRecord
{
    my ($newRecNo) = @_;

    if ((defined($gDB)) &&
	(defined($newRecNo)) &&
	($newRecNo =~ m/^\d+$/) &&
	($newRecNo >= 0) &&
	($newRecNo <= $#{$gDB->{'__RECORDS'}}))
    {
	&storeAktRecord;
	$gAktRecord = $newRecNo;
	displayAktRecord;
    }
    else
    {
	TkAB::tellUser("No data loaded or index out of bounds!");
    }
}

sub searchRecord
{
    my ($pattern) = @_;

    if ((defined($gDB)) &&
        (defined($pattern)))
    {
	if ($gAktRecord < $#{$gDB->{'__RECORDS'}}) {
	    for ($i=$gAktRecord+1; $i <= $#{$gDB->{'__RECORDS'}}; $i++) {
		if ($gDB->{'__RECORDS'}[$i]->{'entry'}[0] =~ m/$pattern/i) {
		    &storeAktRecord;
		    $gAktRecord = $i;
		    displayAktRecord;
		    return;
		}
	    }
	}
	for ($i=0; $i <= $gAktRecord; $i++) {
	    if ($gDB->{'__RECORDS'}[$i]->{'entry'}[0] =~ m/$pattern/i) {
		&storeAktRecord;
		$gAktRecord = $i;
		displayAktRecord;
		return;
	    }

	}
	TkAB::tellUser("Pattern not found!");
    }
    else
    {
	TkAB::tellUser("No data loaded or no pattern specified!");
    }
        
}

sub newRecord
{
    my ($rec);

    &storeAktRecord;

    $rec = { 'entry' => [],
	     'showPhone' => 0,
	     'phoneLabel' => [0,1,2,3,4],
	     'category' => $gDB->{'__RECORDS'}[$gAktRecord]->{'category'},
	     'rolo_id' => &newRoloId($gDB),
	     'secret' => '' };
    $rec->{'entry'}->[18] = undef;  # ensure right array length
    
    push(@{$gDB->{'__RECORDS'}}, $rec);
    $gDB->{ $rec->{'rolo_id'} } = $#{$gDB->{'__RECORDS'}};
    
    $gAktRecord = $gDB->{ $rec->{'rolo_id'} };
    
    $gDataChanged = 1;
    &displayAktRecord;
}

sub deleteRecord
{
    return unless(TkAB::askUser("Delete this record?", ("No", "Yes")) eq "Yes");

    my ($rec) = $gDB->{'__RECORDS'}[$gAktRecord];

    delete($gDB->{ $rec->{'rolo_id'} });

    splice(@{$gDB->{'__RECORDS'}}, $gAktRecord, 1);

    $gAktRecord-- if ($gAktRecord > $#{$gDB->{'__RECORDS'}});
    TkAB::updateListView;
    $gDataChanged = 1;
    &displayAktRecord;
}

sub firstRecord
{
    &storeAktRecord;
    
    $gAktRecord = 0;
    &displayAktRecord;
    
}

sub lastRecord
{
    &storeAktRecord;
    
    $gAktRecord = $#{$gDB->{'__RECORDS'}};
    &displayAktRecord;
}

sub forwardRecord
{
    return unless ($gAktRecord < $#{$gDB->{'__RECORDS'}});
    
    &storeAktRecord;
    
    $gAktRecord++;
    &displayAktRecord;
}

sub backwardRecord
{
    return unless ($gAktRecord > 0);
    
    &storeAktRecord;
    
    $gAktRecord--;
    &displayAktRecord;
}

sub getAllRecs
{
    my ($catName) = @_;
    my ($catNo) = -1;
    my (@recList);
    my (@recs) = @{$gDB->{'__RECORDS'}};
    my ($ai) = $gDB->{'__APPINFO'};
    my ($rec);
    my (@entry);
    my ($entry1, $entry2, $entry3, $entry4, $entry5, $entryFull);
    my ($i);
    my (@pl);

    if (defined($catName) && ($catName ne "All")) {
	for ($catNo = 0; $gDB->{'__APPINFO'}->{'categoryName'}[$catNo] ne $catName; $catNo++)
	    { ; }
    }
    
    for ($i=0; $i <= $#{$gDB->{'__RECORDS'}}; $i++) {
	$rec = $recs[$i];
	@pl = $rec->{'phoneLabel'};
	@entry = @{$rec->{'entry'}};
	
	next if (($catNo != -1) && ($catNo != $rec->{'category'}));
	
	if (defined($entry[0]) && length($entry[0]) > 0) {
	    $entry1 = $entry[0];
	    
	    if (defined($entry[1]) && length($entry[1]) > 0) {
		$entry2 = $entry[1];
	    } else {
		$entry2 = "";
	    }
	    
	} elsif (defined($entry[1]) && length($entry[1]) > 0) {
	    $entry1 = $entry[1];
	    $entry2 = "";
	} else {
	    $entry1 = $entry[2];
	    $entry2 = "";
	}
	
	if (defined($entry[$rec->{'showPhone'} + 3]) && length($entry[$rec->{'showPhone'} + 3]) > 0) {
	    $entry3 = $entry[$rec->{'showPhone'} + 3] . " ";
	    $entry4 = substr($ai->{'phoneLabel'}[$rec->{'showPhone'}], 0, 1);
	} else {
	    $entry3 = "";
	    $entry4 = "";
	}
	
	$entryFull = $entry1 . ", " . $entry2 . "  " . $entry3 . " " . $entry4;
	if (length($entryFull) <= 33) {
	    $entry5 = " " x (33 - length($entryFull));
	    $entryFull = $entry1 . ", " . $entry2 . "  " . $entry5 . $entry3 . " " . $entry4;
	} else {
	    my ($restWidth) = 33 - length($entry3 . $entry4) - 5;
	    my ($highMaxWidth) = int $restWidth * 2 / 3;
	    my ($lowMaxWidth)  = $restWidth - $highMaxWidth;
	    my ($highWidth, $lowWidth);
	    
	    if (length($entry2) == 0) {
		$entry1 = substr($entry1, 0 , $restWidth);
	    } else {
		if (length($entry1) > $highMaxWidth) {
		    my ($pos) = index($entry2, " ");
		    if ($pos > -1) {
			$lowWidth = $pos;
			if ($lowWidth < $lowMaxWidth) {
			    $lowMaxWidth  = $lowWidth;
			    $highMaxWidth = $restWidth - $lowMaxWidth;
			}
		    }
		} else {
		    $highMaxWidth = length($entry1);
		    $lowMaxWidth  = $restWidth - $highMaxWidth;
		}
		if (length($entry1) > $highMaxWidth) {
		   $entry1 = substr($entry1, 0, $highMaxWidth - 3) . "...";
		} else {
		    $entry1 = substr($entry1, 0, $highMaxWidth);
		}
		$entry2 = substr($entry2, 0, $lowMaxWidth);
	    }
	    $entryFull = $entry1 . ", " . $entry2 . "  " . $entry3 . " " . $entry4;
	    if (length($entryFull) <= 33) {
		$entry5 = " " x (33 - length($entryFull));
		$entryFull = $entry1 . ", " . $entry2 . "  " . $entry5 . $entry3 . " " . $entry4;
	    }
	}
	push(@recList, $entryFull);
    }
    return sort @recList;
}

sub getAllCategories
{
    return (@{$gDB->{'__APPINFO'}->{'categoryName'}});
}

sub categoryExists
{
    my ($catName) = @_;
    my ($i);

    return 0 unless defined($catName);
    
    for ($i = 0; $i <= $#{$gDB->{'__APPINFO'}->{'categoryName'}}; $i++) {
	return 1 if ($gDB->{'__APPINFO'}->{'categoryName'}[$i] eq $catName);
    }
    return 0;
}

sub categoryUsed
{
    my ($catName) = @_;
    my ($catNo);
    my ($i);
    
    for ($catNo = 0; $gDB->{'__APPINFO'}->{'categoryName'}[$catNo] ne $catName; $catNo++)
	{ ; }
    
    for ($i=0; $i <= $#{$gDB->{'__RECORDS'}}; $i++) {
	$gDB->{'__RECORDS'}[$i]->{'category'} == $catNo and return 1;
    }
    return 0;
}

sub refile
{
    my ($oldName, $newName) = @_;
    my ($oldNo, $newNo);
    my ($i);
    
    for ($oldNo = 0; $gDB->{'__APPINFO'}->{'categoryName'}[$oldNo] ne $oldName; $oldNo++)
	{ ; }

    for ($newNo = 0; $gDB->{'__APPINFO'}->{'categoryName'}[$newNo] ne $newName; $newNo++)
	{ ; }

    ($oldNo < $newNo) and $newNo--;
    return if ($oldNo == $newNo);

    for ($i=0; $i <= $#{$gDB->{'__RECORDS'}}; $i++) {
	($gDB->{'__RECORDS'}[$i]->{'category'} == $oldNo) and ($gDB->{'__RECORDS'}[$i]->{'category'} = $newNo);
    }
}


sub loadPrefs
{
    my ($lines);

    open(FD, "<$RCFILE") || return;
    $lines = join('', <FD>);
    close(FD);
    eval $lines;
}

sub newRoloId
{
    my ($db) = @_;

    return $db->{'NEXT_ID'}++;
}


sub readAppInfoFile
{
    # AppInfo file used by Rolo and CSV formats
    #
    my ($ai, $s) =
	({ 'categoryName' => [], 'label' => [], 'phoneLabel' => [] });

    open(FD, "<$APPINFO_FILE") or return $ai;
    scalar(<FD>);	# read off comment line

    foreach (1..16)
    {
	chomp($s = <FD>);
	push(@{$ai->{'categoryName'}}, $s);
    }
    foreach (1..22)
    {
	chomp($s = <FD>);
	push(@{$ai->{'label'}}, $s);
    }
    foreach (1..8)
    {
	chomp($s = <FD>);
	push(@{$ai->{'phoneLabel'}}, $s);
    }

    close(FD);
    return $ai;
}

sub writeAppInfoFile
{
    my ($ai) = @_;

    open(FD, ">$APPINFO_FILE") or return;
    print FD <<EOW;
WARNING- If you edit this file it will modify your pilot on the next sync!
EOW

    foreach $_ (@{$ai->{'categoryName'}},
		@{$ai->{'label'}},
		@{$ai->{'phoneLabel'}})
    {
	print FD "$_\n";
    }

    close(FD);
}

sub readRolo
{
    my ($ROLOFILE) = @_;
    my ($db, $rec) = ({ 'nonPilot' => [],
			'isPilot' => [],
			'__RECORDS' => [],
			'NEXT_ID' => 0
		      });

    $db->{'__APPINFO'} = &readAppInfoFile;
    open(FD, "<$ROLOFILE") || return $db;

    while (<FD>)
    {
	$rec = { 'topsect' => '' };

	while ($_ !~ /^\*PILOT\*$/ && $_ !~ /^\014/)
	{
	    $rec->{'topsect'} .= $_;
	    $_ = <FD>;
	}

	if ( /^\014/ )
	{
	    push(@{$db->{'isPilot'}}, -1);
	    push(@{$db->{'nonPilot'}}, $rec);
	    next;
	}

	$rec->{'entry'} = [];
	$rec->{'entry'}->[18] = undef;  # ensure right array length
	$rec->{'phoneLabel'} = [0,1,2,3,4];
	$rec->{'showPhone'} = 0;

	for ($_ = <FD>; $_ !~ /^\014/; $_ = <FD>)
	{
	    if ($_ =~ /^([^:]*): ?(.*)$/)
	    {
		$field = $1;
		$value = $2;
		$field =~ tr/A-Z/a-z/;
		$value =~ s/\\n/\n/g;	# translate newlines

		unless (defined $gEntryMap->{$field})
		{
		    print "skipping bad field '$field' in rolo record.\n";
		    next;
		}
		$field = $gEntryMap->{$field};

		if ($field =~ /^\d+$/)
		{
		    $rec->{'entry'}->[$field] = $value;
		}
		elsif ($field eq 'phoneLabel')
		{
		    $rec->{$field} = [split(/ /, $value)];
		}
		else
		{
		    $rec->{$field} = $value;
		}
	    }
	}
	push(@{$db->{'isPilot'}}, $rec->{'rolo_id'});
	push(@{$db->{'__RECORDS'}}, $rec);
	$db->{ $rec->{'rolo_id'} } = $#{$db->{'__RECORDS'}};

	$db->{'NEXT_ID'} = $rec->{'rolo_id'} + 1
	    if ($rec->{'rolo_id'} >= $db->{'NEXT_ID'});
    }
    close(FD);
    return $db;
}

sub writeRolo
{
    my ($ROLOFILE, $db) = @_;
    my ($rec, $which);

    &writeAppInfoFile($db->{'__APPINFO'});
    unless (open(FD, ">$ROLOFILE"))
    {
	PilotMgr::msg("Unable to write to $ROLOFILE.  Help!");
	return;
    }

    foreach $which (@{$db->{'isPilot'}})
    {
	if ($which < 0)
	{
	    # non-pilot rec
	    $rec = shift @{$db->{'nonPilot'}};
	    print FD $rec->{'topsect'}, "\014\n";
	}

	next unless (defined $db->{'__RECORDS'}->[0] &&
		     $which eq $db->{'__RECORDS'}->[0]->{'rolo_id'});
	$rec->{'topsect'} = &makeTopSect($rec, $db->{'__APPINFO'})
	    unless exists $rec->{'topsect'};
	&writeRec(FD, shift @{$db->{'__RECORDS'}});
    }

    while (defined ($rec = shift @{$db->{'__RECORDS'}}))
    {
	$rec->{'topsect'} = &makeTopSect($rec, $db->{'__APPINFO'})
	    unless exists $rec->{'topsect'};
	&writeRec(FD, $rec);
    }

    close(FD);
}

sub writeRec
{
    my ($fd, $rec) = @_;
    my ($key, $val);

    print $fd $rec->{'topsect'} if defined ($rec->{'topsect'});
    print $fd "*PILOT*\n";
    foreach $key (keys %$gEntryMap)
    {
        $val = $gEntryMap->{$key};
        if ($val =~ /^\d+$/)
        {
            next unless (defined ($val = $rec->{'entry'}->[$val]));
            $val =~ s/\n/\\n/g; # translate newlines
            print $fd "$key: $val\n";
        }
        else
        {
            # shouldn't be any newlines to translate down here..
            next unless (defined ($val = $rec->{$val}));
            # for phoneLabel field:
            $val = join(' ', @$val) if (ref($val) eq 'ARRAY');
            print $fd "$key: $val\n";
        }
    }
    print $fd "\014\n";
}

sub isgood
{
    return (defined $_[0] and length($_[0]) > 0);
}

sub makeTopSect
{
    my ($rec, $ai) = @_;
    my ($topsect, $boo, $val, $val2, $i, @phonetypes) = ("", 0);

    $val = $rec->{'entry'}->[ $gEntryMap->{'lastname'} ];
    $val2 = $rec->{'entry'}->[ $gEntryMap->{'firstname'} ];
    $i = $rec->{'entry'}->[ $gEntryMap->{'company'} ];
    if (&isgood($val))
    {
	$topsect .= "$val2 " if (&isgood($val2));
	$topsect .= $val;
    }
    elsif (&isgood($val2))
    {
	$topsect .= $val2;
    }
    elsif (&isgood($i))
    {
	$topsect .= $i;
	$boo = 1;
    }

    $topsect .= "\n";
    $val = $rec->{'entry'}->[ $gEntryMap->{'title'} ];
    $topsect .= $val . "\n" if (&isgood($val));
    $val = $rec->{'entry'}->[ $gEntryMap->{'company'} ];
    $topsect .= $val . "\n" if (!$boo and &isgood($val));
    $topsect .= "\n";

    $val = $rec->{ $gEntryMap->{'phonetypes'} };
    @phonetypes = @$val if (defined $val and ref($val) eq 'ARRAY');

    foreach $i (1..5)
    {
	$val = $rec->{'entry'}->[ $gEntryMap->{"phone$i"} ];
	if (&isgood($val))
	{
	    $topsect .= @phonetypes ? $ai->{'phoneLabel'}->[$phonetypes[$i-1]]
				    : "phone$i";
	    $topsect .= ": $val\n";
	}
    }
    $topsect .= "\n";

    $val = $rec->{'entry'}->[ $gEntryMap->{'address'} ];
    $topsect .= $val . "\n" if (&isgood($val));
    $boo = 0;
    $val = $rec->{'entry'}->[ $gEntryMap->{'city'} ];
    if (&isgood($val))
    {
	$topsect .= $val;
	$boo = 1;
    }
    $val = $rec->{'entry'}->[ $gEntryMap->{'state'} ];
    if (&isgood($val))
    {
	$topsect .= ", " if ($boo);
	$boo = 0;
	$topsect .= "$val  ";
    }
    $val = $rec->{'entry'}->[ $gEntryMap->{'zip'} ];
    if (&isgood($val))
    {
	$topsect .= ', ' if ($boo);
	$topsect .= $val;
    }
    $topsect .= "\n";
    $boo = 0;
    foreach $i (1..4)
    {
	$val = $rec->{'entry'}->[ $gEntryMap->{"custom$i"} ];
	if (&isgood($val))
	{
	    $topsect .= $ai->{'label'}->[$i+13] . ": $val\n";
	    $boo++;
	}
    }
    $topsect .= "\n" if ($boo);

    $val = $rec->{'entry'}->[ $gEntryMap->{'note'} ];
    $topsect .= $val . "\n" if (&isgood($val));

    return $topsect;
}


sub readCSV
{
    my ($CSVFILE) = @_;
    my ($max_id, $db) = (-1, { '__RECORDS' => [] , 'NEXT_ID' => 0 });
    my ($rec, $key, $fld, $val);

    $db->{'__APPINFO'} = &readAppInfoFile;
    unless (open(FD, "<$CSVFILE"))
    {
	TkAB::tellUser("Warning: Unable to open $CSVFILE");
	return $db;
    }

    while (<FD>)
    {
	$rec = { 'entry' => [],
		 'showPhone' => 0,
		 'phoneLabel' => [0,1,2,3,4] };
	$rec->{'entry'}->[18] = undef;  # ensure right array length

	foreach $key (@$gCSVorder)
	{
	    $fld = $gEntryMap->{$key};
	    ($val, $_) = &popCSV($_);
	    $val = &CSVToStr($val);
	    $val = undef if ($val eq '');

	    if ($fld =~ /^\d+$/)
	    {
		$rec->{'entry'}->[$fld] = $val;
	    }
	    elsif ($fld eq 'phoneLabel')
	    {
		$rec->{$fld} = [split(/ /, $val)];
	    }
	    else
	    {
		$rec->{$fld} = $val;
	    }
	}

	# Value for "secret" field must be '' or '1'.
	# Convert any perl "false" value to '' and any "true" value to 1:
	#
	$rec->{'secret'} =
	    (defined $rec->{'secret'} and $rec->{'secret'}) ? 1 : '';

	push(@{$db->{'__RECORDS'}}, $rec);
	$db->{ $rec->{'rolo_id'} } = $#{$db->{'__RECORDS'}};

	$max_id = $rec->{'rolo_id'} if ($rec->{'rolo_id'} > $max_id);
    }
    close(FD);
    $db->{'NEXT_ID'} = $max_id+1;
    return $db;
}

sub writeCSV
{
    my ($CSVFILE, $db) = @_;
    my ($rec, $key, $val, @fields);

    &writeAppInfoFile($db->{'__APPINFO'});
    unless (open(FD, ">$CSVFILE"))
    {
	TkAB::tellUser("Unable to write to $CSVFILE.  Help!");
	return;
    }

    foreach $rec (@{$db->{'__RECORDS'}})
    {
	@fields = ();
	foreach $key (@$gCSVorder)
	{
	    $val = $gEntryMap->{$key};
	    if ($val =~ /^\d+$/)
	    {
		if (defined ($val = $rec->{'entry'}->[$val]))
		{
		    $val = &StrToCSV($val);
		}
	    }
	    else
	    {
		if (defined ($val = $rec->{$val}))
		{
		    $val = join(' ', @$val) if (ref($val) eq 'ARRAY');
		    $val = &StrToCSV($val);
		}
	    }

	    $val = '' unless (defined $val);
	    push(@fields, $val);
	}

	print FD join(',', @fields), "\n";
    }

    close(FD);
}

sub StrToCSV
{
    my ($str) = @_;

    $str =~ s/(\\*)(n|\n)/'\\' x (2*length($1)) . ($2 eq 'n' ? 'n' : '\\n')/ge;
    if ($str =~ /[,"]/)
    {
	$str =~ s/"/""/g;
	$str = '"' . $str . '"';
    }

    return $str;
}

sub popCSV
{
    my ($str) = @_;

    if ($str =~ s/^("([^"]|"")*")(,|$)//)
    {
	return($1, $str);
    }
    elsif ($str =~ s/^(.*?)(,|$)//)
    {
	return($1, $str);
    }

    return($str, '');
}

sub CSVToStr
{
    my ($str) = @_;

    if ($str =~ /^"(.*)"$/)
    {
	$str = $1;
	$str =~ s/""/"/g;
    }
    $str =~ s/((\\\\)*)(\\)?n/'\\' x (length($1)\/2) . ($3 ? "\n" : 'n')/ge;

    return $str;
}

##################################################
# Main code
##################################################

my ($DEBUG) = 0;

while ($_ = shift)
{
    if ($_ eq '-d')
    {
	$DEBUG = 1;
    }
}

if (! $DEBUG) {
    $SIG{__WARN__} = sub { };
}


&init;
&init2;

TkAB::createGUI;
Tk::MainLoop;
