package SyncAB;
# Address Book conduit for PilotManager
# Originally written by Alan Harder, with assistance from Steve Swales.
# See ChangeLog for list of modifications and contributors.

# CSV file format.  Each line contains:
# id,lastname,firstname,company,phone1,phone2,phone3,phone4,phone5,
# address,city,state,zip,country,title,custom1,custom2,custom3,custom4,
# note,whichphone,phonetypes,category,private
#
# Text items follow these rules:
# \n encodes a newline
# \\ encodes a backslash
# If the item contains a double quote (") or comma (,) then the entire
# item is enclose in a pair of double quotes ("item") and inside this
# string two double quotes ("") encodes a single double quote (").
#
# Examples:  One\nTwo,One\\nTwo,"One,Two","One ""Two"""
# These items expand to: One   One\nTwo   One,Two   One "Two"
#                        Two
#
# Other items:
# id = unique integer id
# whichphone = [0-4]  which phone value is shown on list screen on pilot
# phonetypes = # # # # #  five digits, each from 0-7
#              specifies index of phonetype for phone1 thru phone5
#              text values for 0-7 can be found in last 8 lines of
#              ~/.pilotmgr/SyncAB/pilot.appinfo file
# category = integer category index, 0-15.
#            text values for 0-15 are lines 1-16 of pilot.appinfo
# private = 1 for private, blank (empty string) for non-private
#


use PilotSync;
use Tk;
use TkUtils;
use Data::Dumper;
use Carp;

my $VERSION = $PilotMgr::VERSION;
my ($RCFILE, $APPINFO_FILE, $CANCEL);
my ($gConfigDialog, $gFileLabel, $gFileEntry, $gCategories);
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',
		  'fullname' => 'fullname',	# only for vcards
		 );
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' );
my @gPhoneTypes = ( ['WORK', 0],	# Work
		    ['HOME', 1],	# Home
		    ['FAX', 2],		# Fax
		    ['PREF', 5],	# Main
		    ['PAGER', 6],	# Page
		    ['CELL', 7],	# Mobile
		    ['INTERNET', 4],	# (Email)
		  );

sub conduitInit
{
    $RCFILE = "SyncAB/SyncAB.prefs";
    $APPINFO_FILE = "SyncAB/pilot.appinfo";
    &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'});
    $PREFS->{'PrivateRecs'} = 1
	unless (defined $PREFS->{'PrivateRecs'});
    $PREFS->{'FullSync'} = 0
	unless (defined $PREFS->{'FullSync'});
    $PREFS->{'EnabledCategories'} = [ 1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1 ]
	unless (defined $PREFS->{'EnabledCategories'});
}

sub conduitQuit
{
    &savePrefs;
}

sub conduitInfo
{
    return { 'database' =>
		{
		    'name' => 'AddressDB',
		    'creator' => 'addr',
		    'type' => 'DATA',
		    'flags' => 0,
		    'version' => 0,
		},
	     'version' => $VERSION,
	     'author' => 'Alan Harder',
	   };
}

sub conduitConfigure
{
    my ($this, $wm) = @_;
    my ($frame, $obj, $subfr, @objs);
    my ($appinfo) = readAppInfoFile ();
    my ($i) = (0);

    unless (defined $gConfigDialog and $gConfigDialog->Exists)
    {
	$gConfigDialog = $wm->Toplevel(-title => "Configuring SyncAB");
	$gConfigDialog->withdraw;
	$gConfigDialog->transient($wm);
	$frame = $gConfigDialog->Frame(-relief => 'ridge', -bd => 2);

	$frame->Label(-text =>
			"SyncAB v$VERSION\n" . &conduitInfo->{'author'})->pack;

	$subfr = $frame->Frame;
	@objs = TkUtils::Radiobuttons($subfr, \$PREFS->{'syncType'},
				      'CSV', 'vCard single file',
				      'vCard one per file', 'Rolo');
	$objs[0]->configure(-command => sub{
	    $gFileLabel = 'CSV file:';
	    $gFileEntry->configure(-textvariable => \$PREFS->{'CSVFile'});
	});
	$objs[1]->configure(-command => sub{
	    $gFileLabel = 'vCard file:';
	    $gFileEntry->configure(-textvariable => \$PREFS->{'vCardFile'});
	});
	$objs[2]->configure(-command => sub{
	    $gFileLabel = 'vCard dir:';
	    $gFileEntry->configure(-textvariable => \$PREFS->{'vCardDir'});
	});
	$objs[3]->configure(-command => sub{
	    $gFileLabel = 'Rolo file:';
	    $gFileEntry->configure(-textvariable => \$PREFS->{'RoloFile'});
	});
	$subfr->pack(-fill => 'x', -expand => 1);

	$subfr = $frame->Frame;
	$obj = $subfr->Label(-textvariable => \$gFileLabel, -width => 10);
	$obj->pack(-side => 'left', -anchor => 'e');

	$gFileEntry = $subfr->Entry(-relief => 'sunken', -width => 40);
	$gFileEntry->pack(-fill => 'x', -expand => 1);
	$subfr->pack(-fill => 'x', -expand => 1);

        $subfr = $frame->Frame (-label => 'Sync Options');
        $obj = TkUtils::Checkbutton
            ( $subfr
            , 'Private records from Pilot'
            , \$PREFS->{'PrivateRecs'}
            );
	$obj->pack (-side => 'left', -anchor =>  'w', -fill => 'x');
        $subfr->pack(-anchor => 'c');

        $subfr = $frame->Frame (-label => 'Enabled Categories');
	for ($i=0; $i<4; $i++)
	{
	    my $sf = $subfr->Frame;
	    for ($j=0; $j<4; $j++)
	    {
		my $idx = $j+4*$i;
		$gCategories->[$idx] = $sf->Checkbutton
		    ( -text     => "[$idx]: $appinfo->{'categoryName'}[$idx]"
		    , -variable => \$PREFS->{'EnabledCategories'}[$idx]
		    , -width    => 16
		    , -anchor   => 'w'
		    );
		$gCategories->[$idx]->pack (-side => 'top', -anchor => 'w');
	    }
	    $sf->pack (-side => 'left', -fill => 'x');
	}
        $subfr->pack(-anchor => 'c');

        $subfr = $frame->Frame (-label => 'Options for next Sync');
        $obj = TkUtils::Checkbutton
            ($subfr, 'Make next Sync a Full Sync', \$PREFS->{'FullSync'});
        $obj->pack (-side => 'left', -anchor =>  'w', -fill => 'x');
        $subfr->pack(-anchor => 'c');

	$obj = TkUtils::Button($frame, 'Dismiss',
			       sub{ $gConfigDialog->withdraw });
	$obj->pack;

	$frame->pack(-fill => 'x', -expand => 1, -anchor => 'n');
	PilotMgr::setColors($gConfigDialog);
    }
    for ($i=0; $i<16; $i++)
    {
	$gCategories->[$i]->configure
	    (-text => "[$i]: $appinfo->{'categoryName'}[$i]");
    }

    if ($PREFS->{'syncType'} eq 'Rolo')
    {
	$gFileLabel = 'Rolo file:';
	$gFileEntry->configure(-textvariable => \$PREFS->{'RoloFile'});
    }
    elsif ($PREFS->{'syncType'} eq 'CSV')
    {
	$gFileLabel = 'CSV file:';
	$gFileEntry->configure(-textvariable => \$PREFS->{'CSVFile'});
    }
    elsif ($PREFS->{'syncType'} eq 'vCard single file')
    {
	$gFileLabel = 'vCard file:';
	$gFileEntry->configure(-textvariable => \$PREFS->{'vCardFile'});
    }
    elsif ($PREFS->{'syncType'} eq 'vCard one per file')
    {
	$gFileLabel = 'vCard dir:';
	$gFileEntry->configure(-textvariable => \$PREFS->{'vCardDir'});
    }

    $gConfigDialog->Popup(-popanchor => 'c', -overanchor => 'c',
			  -popover => $wm);
}

sub PrivacyFilter
{
    my ($rec) = @_;
    return !($rec->{'secret'});
}

sub CategoryFilter
{
    my ($rec) = @_;
    return $PREFS->{'EnabledCategories'}[$rec->{'category'}];
}

sub CategoryAndPrivacyFilter
{
    return PrivacyFilter (@_) && CategoryFilter (@_);
}

sub conduitSync
{
    my ($this, $dlp, $info) = @_;
    my ($idField, $file, $reader, $writer);
    my ($OptionFullSync) = $PREFS->{'FullSync'};
    my ($OptionFilter)
	= $PREFS->{'PrivateRecs'}
	? \&CategoryFilter
	: \&CategoryAndPrivacyFilter
	;

    if (!exists $PREFS->{'lastSyncType'} or
	$PREFS->{'syncType'} ne $PREFS->{'lastSyncType'})
    {
	# Full reset if changing sync type
	rename "SyncAB/addr.db", "SyncAB/addr.db.bak";
	$OptionFullSync = 1;
    }

    $idField = 'rolo_id';
    if ($PREFS->{'syncType'} eq 'Rolo')
    {
	$file = $PREFS->{'RoloFile'};
	$reader = \&readRolo;
	$writer = \&writeRolo;
    }
    elsif ($PREFS->{'syncType'} eq 'CSV')
    {
	$file = $PREFS->{'CSVFile'};
	$reader = \&readCSV;
	$writer = \&writeCSV;
    }
    elsif ($PREFS->{'syncType'} eq 'vCard one per file')
    {
	$file = $PREFS->{'vCardDir'};
	$reader = \&readVCardsMultipleFiles;
	$writer = \&writeVCardsMultipleFiles;
    }
    elsif ($PREFS->{'syncType'} eq 'vCard single file')
    {
	$file = $PREFS->{'vCardFile'};
	$reader = \&readVCardsOneFile;
	$writer = \&writeVCardsOneFile;
    }
    else
    {
	PilotMgr::msg(
	    "SyncAB does not yet support type $PREFS->{'syncType'}\n");
	return;
    }

    $CANCEL = 0;

    # Determine if we are doing a fast or a full sync. This is given as
    # an option to PilotSync, so a version that is capable of full sync
    # is needed. Note that FullSync may also be set as a preference, so
    # this overrides the users choice only if FullSync was not selected.
    $OptionFullSync = 1
        unless (  $PREFS->{'LastSync'}
               && $PREFS->{'LastSync'} == $info->{'successfulSyncDate'}
               );
    PilotMgr::msg
	("Doing a " . ($OptionFullSync ? "full" : "fast") . " sync.\n");

    PilotSync::doSync
	( $dlp
        , &conduitInfo->{'database'}
	, ['entry', 'phoneLabel', 'showPhone', 'category', 'secret']
	, ['categoryName', 'phoneLabel', 'label']
	, $idField
	, "SyncAB/addr.db"
	, $file
	, \&titleString
	, $reader
	, $writer
	, \&newRoloId
	, undef
	, undef
	, \$CANCEL
	, { 'FullSync' => $OptionFullSync, 'FilterHook' => $OptionFilter }
	);

    $PREFS->{'lastSyncType'} = $PREFS->{'syncType'} unless ($CANCEL);
    $PREFS->{'LastSync'}  = $info->{"thisSyncDate"} unless ($CANCEL);
    $PREFS->{'FullSync'} = 0 unless ($CANCEL);
    &savePrefs;
}

sub conduitCancel
{
    $CANCEL = 'SyncAB Cancelled!';
}

sub loadPrefs
{
    $PREFS = {}, return unless (-r "$RCFILE");
    use vars qw($PREFS);
    do "$RCFILE";
}

sub savePrefs
{
    $Data::Dumper::Purity = 1;
    $Data::Dumper::Deepcopy = 1;
    $Data::Dumper::Indent = 0;

    if (open(FD, ">$RCFILE"))
    {
	print FD Data::Dumper->Dumpxs([$PREFS], ['PREFS']), "1;\n";
	close FD;
    }
    else
    {
	PilotMgr::msg("Unable to save preferences to $RCFILE!");
    }
}

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

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

sub titleString
{
    my ($rec) = @_;
    my ($str, $str2) = ('');

    $str2 = $rec->{'entry'}->[$gEntryMap{'firstname'}];
    $str = $str2 if (&isgood($str2));

    $str2 = $rec->{'entry'}->[$gEntryMap{'lastname'}];
    if (&isgood($str2))
    {
	$str .= ' ' if (length $str);
	$str .= $str2;
    }
    return $str if (length $str);

    $str2 = $rec->{'entry'}->[$gEntryMap{'company'}];
    return $str2 if (&isgood($str2));

    return '-Unnamed-';
}

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;
		}
	    }
	}
	# 'secret' field value must be 1 or ''
	$rec->{'secret'} = (exists $rec->{'secret'} and $rec->{'secret'})?1:'';
	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"))
    {
	# Don't do a sync if master data file exists (then we'll end up
	# deleting all records!)
	if (-f "SyncAB/addr.db")
	{
	    PilotMgr::msg(
		"**ERROR: Unable to open $CSVFILE.  Aborting SyncAB!");
	    croak("NODATA");
	}
	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 '');

	    &setRecVal($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 setRecVal
{
    my ($rec, $fld, $val) = @_;

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

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

    &writeAppInfoFile($db->{'__APPINFO'});
    unless (open(FD, ">$CSVFILE"))
    {
	PilotMgr::msg("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;
}

sub writeVCardsOneFile
{
    my ($VCARDFILE, $db) = @_;
    my ($rec);

    &writeAppInfoFile($db->{'__APPINFO'});

    unless (open(FD, ">$VCARDFILE"))
    {
	PilotMgr::msg("Unable to write to $VCARDFILE.  Help!");
	return;
    }

    foreach $rec (@{$db->{'__RECORDS'}})
    {
	&writeVCard($rec, FD);
	print FD "\n";
    }

    close(FD);
}

sub writeVCardsMultipleFiles
{
    my ($VCARDDIR, $db) = @_;
    my ($rec, $cat, $fn, @dirlist, $dir, @filelist, $file);

    &writeAppInfoFile($db->{'__APPINFO'});

    # yikes, scary! delete all old files!
    # vCard files are stored in subdirectories named by category.
    # Each SyncAB owned directory has a ".pilotmgr" file in it.
    #
    opendir DIR, "$VCARDDIR";
    @dirlist = readdir DIR;
    closedir DIR;
    foreach $dir (@dirlist)
    {
	next if ($dir =~ /^\.\.?$/);		# skip . and ..
	if (-d "$VCARDDIR/$dir" and -f "$VCARDDIR/$dir/.pilotmgr")
	{
	    opendir DEL, "$VCARDDIR/$dir";
	    @filelist = readdir DEL;
	    closedir DEL;
	    foreach $file (@filelist)
	    {
		if ($file ne '.pilotmgr' and -f "$VCARDDIR/$dir/$file")
		{
		    unlink "$VCARDDIR/$dir/$file";
		}
	    }
	}
    }

    foreach $rec (@{$db->{'__RECORDS'}})
    {
	($cat, $fn) = &vCardFileName($rec, $db->{'__APPINFO'});
	unless (-d "$VCARDDIR/$cat")
	{
	    mkdir "$VCARDDIR/$cat", 0755;
	    open(FD, ">$VCARDDIR/$cat/.pilotmgr") and close(FD);
	}
	if (-f "$VCARDDIR/$cat/$fn")
	{
	    # file already exists
	    $_ = 1;
	    while (-f "$VCARDDIR/$cat/${fn}_$_") { $_++ }
	    $fn .= "_$_";
	}
	unless (open(FD, ">$VCARDDIR/$cat/$fn"))
	{
	    PilotMgr::msg("** Error opening $VCARDDIR/$cat/$fn for write!");
	    next;
	}
	&writeVCard($rec, FD);
	close(FD);
    }
}

sub vCardFileName
{
    my ($rec, $appinfo) = @_;

    my $fn = $rec->{'fullname'};
    $fn = &titleString($rec) unless (&isgood($fn));

    # remove newlines, and anything after them.
    $fn =~ s/\n.*//g;
    # remove spaces from beginning and end of line.
    $fn =~ s/^\s*(.*?)\s*$/$1/;
    # replace multiple spaces with a single space.
    $fn =~ s/\s\s+/ /g;
    # replace characters we don't want in filenames.
    $fn =~ tr|'"<>[]/|_______|s;

    my $cat = $appinfo->{'categoryName'}->[ $rec->{$gEntryMap{'category'}} ];
    $cat = 'PilotDB' unless (&isgood($cat));

    $cat =~ s/\n.*//g;
    $cat =~ s/^\s*(.*?)\s*$/$1/;
    $cat =~ s/\s\s+/ /g;
    $cat =~ tr|'"<>[]/|_______|s;

    return ($cat, $fn);
}

sub writeVCard
{
    my ($rec, $FD) = @_;
    my ($val, $val2, $i);

    #XXX: need to handle newlines or semicolons in ADR and N fields!!

    #sdtname requires a ADR type, HOME/WORK.. we'll default to HOME
    my $defaultAddrPlace = 'HOME';

    print $FD "BEGIN:VCARD\n";

    # FN is just for looks, doesn't store actual data:
    &printEncodedString('FN',
	defined($rec->{'fullname'}) ? $rec->{'fullname'} : &titleString($rec),
	$FD);

    ($val = $rec->{'entry'}->[$gEntryMap{'lastname'}]) =~ s/;/\\;/g;
    ($val2 = $rec->{'entry'}->[$gEntryMap{'firstname'}]) =~ s/;/\\;/g;
    if (defined $val || defined $val2)
    {
	#XXX: use &printEncodedString here?
	print $FD 'N:';
	print $FD $val if (defined $val);
	print $FD ';';
	print $FD $val2 if (defined $val2);
	print $FD "\n";
    }

    $val = $rec->{'entry'}->[$gEntryMap{'company'}];
    &printEncodedString('ORG', $val, $FD) if (defined $val);

    $val = $rec->{'entry'}->[$gEntryMap{'title'}];
    &printEncodedString('TITLE', $val, $FD) if (defined $val);

    print $FD 'ADR;';
    $val = $rec->{'addrTypeInfo'};		# vCard info like HOME or WORK
    $val = $defaultAddrPlace unless (defined $val);
    print $FD "$val;X-pilot-field=addr:;;";

    ($val = $rec->{'entry'}->[$gEntryMap{'address'}]) =~ s/;/\\;/g;
    $val =~ s/\n/\\n/g;		#XXX- not right- what to do with newlines?
    print $FD $val if (defined $val);
    print $FD ';';

    ($val = $rec->{'entry'}->[$gEntryMap{'city'}]) =~ s/;/\\;/g;
    print $FD $val if (defined $val);
    print $FD ';';

    ($val = $rec->{'entry'}->[$gEntryMap{'state'}]) =~ s/;/\\;/g;
    print $FD $val if (defined $val);
    print $FD ';';

    ($val = $rec->{'entry'}->[$gEntryMap{'zip'}]) =~ s/;/\\;/g;
    print $FD $val if (defined $val);
    print $FD ';';

    ($val = $rec->{'entry'}->[$gEntryMap{'country'}]) =~ s/;/\\;/g;
    print $FD $val if (defined $val);
    print $FD "\n";

    foreach $i (1..5)
    {
	$val = $rec->{'entry'}->[$gEntryMap{"phone$i"}];
	$val2 = $rec->{'phoneLabel'}->[$i-1];

	# Unless phonetype is equal to default, need to record even empty
	# value, to get phonetype recorded..
	next if (!defined $val and $val2 == ($i-1));
	$val = '' unless (defined $val);

	#XXX: might want to look at prefs and see what types these
	#     *really* are in case they've been changed..
	if ($val2 == 4)
	{
	    print $FD 'EMAIL;INTERNET';
	}
	else
	{
	    print $FD 'TEL;',
		      (@_=grep($_->[1] == $val2, @gPhoneTypes))?$_[0]->[0]:'';
	}

	&printEncodedString(";X-pilot-field=phone$i", $val, $FD);
    }

    foreach $i (1..4)
    {
	$val = $rec->{'entry'}->[$gEntryMap{"custom$i"}];
	next unless (defined $val);

	&printEncodedString("NOTE;X-pilot-field=custom$i", $val, $FD);
    }

    $val = $rec->{'entry'}->[$gEntryMap{'note'}];
    &printEncodedString('NOTE;X-pilot-field=note', $val, $FD)
	if (defined $val);

    print $FD "X-pilot-id:$rec->{rolo_id}\n",
	      "X-pilot-category:$rec->{category}\n",
	      "X-pilot-show-phone:$rec->{showPhone}\n";
    print $FD "X-pilot-private:$rec->{secret}\n"
	if (exists $rec->{'secret'} and length $rec->{'secret'});

    print $FD "END:VCARD\n";
}

sub printEncodedString
{
    # print string value to vcard. use Quoted-Printable if necessary
    # XXX: this needs to be more complete to encode all control chars,etc too
    #
    my ($hdr, $val, $fd) = @_;

    print $fd $hdr if ($hdr);
    if ($val =~ /\n/)
    {
	$val =~ s/=/=3D/g;
	$val =~ s/\n/=0A/g;
	print $fd ";ENCODING=QUOTED-PRINTABLE";
    }
    print $fd ":$val\n";
}

sub readVCardsOneFile
{
    my ($VCARDFILE) = @_;
    my ($max_id, $db, $rec, $i) = (-1, { '__RECORDS' => [], 'NEXT_ID' => 0 });

    $db->{'__APPINFO'} = &readAppInfoFile;
    unless (open(FD, "<$VCARDFILE"))
    {
	# Don't do a sync if master data file exists (then we'll end up
	# deleting all records!)
	if (-f "SyncAB/addr.db")
	{
	    PilotMgr::msg(
		"**ERROR: Unable to open $VCARDFILE.  Aborting SyncAB!");
	    croak("NODATA");
	}
	return $db;
    }

    while (<FD>)
    {
	if ( /^\s*BEGIN\s*:\s*VCARD\s*$/ )
	{
	    $rec = &readVCard(FD);
	    push(@{$db->{'__RECORDS'}}, $rec);

	    if (defined $rec->{'rolo_id'})
	    {
		$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;

    foreach $i ($[..$#{$db->{'__RECORDS'}})
    {
	$rec = $db->{'__RECORDS'}->[$i];
	unless (defined $rec->{'rolo_id'})
	{
	    $rec->{'rolo_id'} = $db->{'NEXT_ID'}++;
	    $db->{ $rec->{'rolo_id'} } = $i;
	}
    }

    return $db;
}

sub readVCardsMultipleFiles
{
    my ($VCARDDIR) = @_;
    my ($max_id, $db, $rec, @dirlist, $dir, @filelist, $file, $cat) =
	(-1, { '__RECORDS' => [], 'NEXT_ID' => 0 });

    $db->{'__APPINFO'} = &readAppInfoFile;

    # vCard files are stored in subdirectories named by category.
    # Each SyncAB owned directory has a ".pilotmgr" file in it.
    unless (opendir DIR, "$VCARDDIR")
    {
	PilotMgr::msg(
	    "**ERROR: Unable to open dir $VCARDDIR.  Aborting SyncAB!");
	croak("BADDIR");
    }
    @dirlist = readdir DIR;
    closedir DIR;
    foreach $dir (@dirlist)
    {
	next if ($dir =~ /^\.\.?$/);		# skip . and ..
	if (-d "$VCARDDIR/$dir" and -f "$VCARDDIR/$dir/.pilotmgr")
	{
	    opendir DAT, "$VCARDDIR/$dir";
	    @filelist = readdir DAT;
	    closedir DAT;
	    foreach $file (@filelist)
	    {
		if ($file ne '.pilotmgr' and -f "$VCARDDIR/$dir/$file")
		{
		    unless (open(FD, "<$VCARDDIR/$dir/$file"))
		    {
			PilotMgr::msg("** Unable to read $VCARDDIR/$dir/$file");
			next;
		    }
		    do { $_ = <FD> }
			until (/^\s*BEGIN\s*:\s*VCARD\s*$/i or eof(FD));
		    close(FD), next if (eof(FD));
		    $rec = &readVCard(FD);
		    close(FD);
		    push(@{$db->{'__RECORDS'}}, $rec);

		    if (defined $rec->{'rolo_id'})
		    {
			$db->{ $rec->{'rolo_id'} } = $#{$db->{'__RECORDS'}};
			$max_id = $rec->{'rolo_id'}
			    if ($rec->{'rolo_id'} > $max_id);
		    }
		}
	    }
	}
    }
    $db->{'NEXT_ID'} = $max_id+1;

    foreach $i ($[..$#{$db->{'__RECORDS'}})
    {
	$rec = $db->{'__RECORDS'}->[$i];
	unless (defined $rec->{'rolo_id'})
	{
	    $rec->{'rolo_id'} = $db->{'NEXT_ID'}++;
	    $db->{ $rec->{'rolo_id'} } = $i;
	}
    }

    return $db;
}

sub readVCard
{
    my ($FD) = @_;
    my $encodeMatch = '(^|;)\s*ENCODING\s*=\s*QUOTED-PRINTABLE\s*(;|$)';
    my $pilotMatch = '(^|;)\s*X-pilot-field\s*=\s*(\S+?)\s*(;|$)';
    my %fieldMap = ('FN'	=> 'fullname',
		    'ORG'	=> 'company',
		    'TITLE'	=> 'title',
		    'id'	=> 'rolo_id',
		    'category'  => 'category',
		    'show-phone'=> 'whichphone',
		    'private'   => 'private');

    my ($rec, $field, $extra, $item);
    $rec = { 'entry' => [],
	     'showPhone' => 0,
	     'phoneLabel' => [0,1,2,3,4],
	     'secret' => '' };
    $rec->{'entry'}->[18] = undef;  # ensure right array length

    while (<$FD>)
    {
	s/\015?\n$//;
	last if ( /^\s*END\s*:\s*VCARD\s*$/ );

	if ( /^\s*(FN|ORG|TITLE)\s*(;[^:]*)?:(.*)$/i )
	{
	    ($field = $1) =~ tr/a-z/A-Z/;
	    $extra = $2;
	    $val = $3;
	    $val = &decodeQuotedPrintable($val, $FD)
		if ($extra =~ /$encodeMatch/i);

	    &setRecVal($rec, $gEntryMap{$fieldMap{$field}}, $val);
	}
	elsif ( /^\s*N\s*(;[^:]*)?:(.*)$/i )
	{
	    $extra = $1;
	    $val = $2;

	    &popFields($rec, $val, 'lastname', 'firstname');
	    #XXX: do anything with remaining fields? (ie suffix, etc)
	}
	elsif ( /^\s*ADR\s*(;[^:]*)?:(.*)$/i )
	{
	    $extra = $1;
	    $val = $2;

	    unless ($extra =~ /$pilotMatch/i and ($field=$2) =~ /^addr$/i)
	    {
		# Not the ADR entry for pilot
		#XXX: save this somewhere so it won't be lost,
		#     or maybe select it for pilot data if there is none
		next;
	    }

	    &popFields($rec, $val, 'SKIP', 'SKIP',  #XXX use first values?
			'address', 'city', 'state', 'zip', 'country');
	    $rec->{'address'} =~ s/\\n/\n/g;  # XXX not right - newlines?!?
	}
	elsif ( /^\s*(TEL|EMAIL|NOTE)\s*(;[^:]*)?:(.*)$/i )
	{
	    $extra = $2;
	    $val = $3;
	    $val = &decodeQuotedPrintable($val, $FD)
		if ($extra =~ /$encodeMatch/i);

	    unless ($extra =~ /$pilotMatch/i)
	    {
		# Not a pilot entry
		#XXX: save this somewhere so it won't be lost,
		#     or maybe assign to a pilot entry...
		next;
	    }
	    ($field = $2) =~ tr/A-Z/a-z/;

	    &setRecVal($rec, $gEntryMap{$field}, $val) if (length $val);

	    if ($field =~ /^phone(\d)$/)
	    {
		$val = $1;
		@_ = grep($extra =~ /(^|;)\s*$_->[0]\s*(;|$)/i, @gPhoneTypes);
		$rec->{'phoneLabel'}->[$val-1] = @_ ? $_[0]->[1] : 3;
						  # default val == 3 == OTHER
	    }
	}
	elsif ( /^\s*X-pilot-(.*)\s*(;[^:]*)?:(.*)$/i )
	{
	    $field = $fieldMap{$1};
	    next unless (defined $field);
	    $val = $3;
	    # Value for private must be 1 or ''
	    $val = $val ? 1 : ''  if ($field eq 'private');

	    $rec->{$gEntryMap{$field}} = $val;
	}
	else
	{
	    #XXX: save data somewhere so it won't be lost
	}
    }

    return $rec;
}

sub popFields
{
    my ($rec, $val, @fields) = @_;
    my ($field, $item);

    foreach $field (@fields)
    {
	($item = $1) =~ s/\\;/;/g  if ($val =~ s/^(.*?(^|[^\\]))(;|$)//);

	next if ($field eq 'SKIP');
	$item = undef unless (length $item);	#XXX I think I want this
	&setRecVal($rec, $gEntryMap{$field}, $item);
    }
}

sub decodeQuotedPrintable
{
    my ($val, $FD) = @_;

    while ($val =~ s/=$//)
    {
	$val .= <$FD>;
	$val =~ s/\015?\n$//;
    }
    #XXX: should decode all =## things
    $val =~ s/=0[Aa]/\n/g;
    $val =~ s/=3[Dd]/=/g;

    return $val;
}

#XXX test code XXX
#my $db = &readVCardsMultipleFiles("$ENV{HOME}/.dt/Addresses");
#foreach (@{$db->{'__RECORDS'}}) { print Dumper($_) }

1;

