##
# Generic pilot sychronizing routine.
# Alan Harder <Alan.Harder@Sun.Com>
#
# Assumptions-
# 1. File side records have some sort of unique Id.
# 2. Pilot records don't need any translation (may add hooks for this later).
# 3. Records can contain scalars, hash refs and array refs, to any depth.
#    AppInfo record can contain _only_ scalars and arrays of scalars.
#
# Required parameters
#	Dlp	  = The $dlp parameter passed into the conduitSync method.
#
#	DbInfo    = Hash ref containing info about the pilot database.  Must
#		    have the following fields: name, creator, type, flags,
#		    version.  Ex: 'MemoDB, 'memo', 'DATA', 0, 0
#
#	ReqFields = Array ref listing the key fields in the records.
#		    These are the fields that will be compared to determine
#		    if two records are the same.
#
#	InfoFields= Array ref listing the key fields in the appinfo
#		    structure.  These fields will be compared and updated
#		    between the pilot and file sides.
#
#	IdField   = Name of the field used to store unique Id in file records.
#
#	MasterFile= Full path filename of conduit db file
#
#	Datafile  = Full path filename of datafile
#
#	NameHook  = Used for PilotMgr log output describing actions.
#		    This method takes a record hash as parameter and
#		    returns a "pretty name" string for the output.
#
#	ReadHook  = Code ref for method to read data from the file side.
#		    Subroutine should take data filename as a parameter and
#		    return a hash ref in the following format:
#			'__RECORDS' => Array ref containing all records
#				       Order will be maintained in case it is
#				       important to your app.
#
#			'__APPINFO' => Array ref containing appinfo data.
#				       (can be undef if no appinfo exists)
#
#			<REC_ID> => <index>   Mappings of your IdField values
#					      to the index that record can be
#					      found at in the __RECORDS list.
#
#	WriteHook = Code ref for method to write data back to file side.
#		    Subroutine parameters are data filename and Hash ref
#		    containing records (hash returned from ReadHook).
#
#       IdHook    = Code ref to generate a new unique record id for the file
#                   side.  Parameters are the hash from ReadHook and the pilot
#                   record for which an id is needed.  Note that generated ids
#                   must be guaranteed unique.
#                   
#
#	TranslateHook= (optional)
#		    A translate hook is require for conduits not using one
#		    of the five builtin databases (MemoDB, DatebookDB,
#		    ToDoDB, AddressDB, ExpenseDB).  Records will be returned
#		    from the pilot in 'raw' format.  This hook is used to
#		    read the raw data and fill in the hash with the relevant
#		    fields.  The parameters to this hook are the hash
#		    containing the raw data (key=='raw') and 0 for
#		    raw->fields, 1 for fields->raw.
#
#	AppInfoHook= (optional)Code ref to subroutine which translates to
#		    and from a 'raw' appinfo from pilot and the expanded
#		    format.  Parameters are appinfo hash and 0 for
#		    raw->expanded, 1 for expanded->raw.
#
#	CancelFlag= (optional)Scalar ref for cancel flag.  If value gets
#		    set to a non-empty string before actual sychronization
#		    begins then whole operation gets cancelled.  During
#		    the sync a cancel will abort all file changes, but
#		    pilot changes up to that point have already been written.
#		    Upon a cancel the new value of the cancel flag gets
#		    printed to the pilotmgr log (should inform user of cancel).
#
#       ExtendedArgs= (optional) Hash ref containing arguments added
#                   to later versions of the PilotSync API. Currently
#                   defined are:
#
#                   - A FullSync flag that should be set to a nonzero
#                     value if a full sync (reading all pilot records
#                     and ignoring the 'modified' flag in the read pilot
#                     records) should be performed. The necessity of a
#                     full sync can be detected automatically by
#                     comparing the last sync date of the local
#                     application with the date the pilot thinks the
#                     last sync was. In this way syncing to multiple
#                     desktops without data corruption can now be
#                     supported.
#
#                   - A FilterHook CODE ref containing a routine called
#                     for every record read from the pilot. The routine
#                     gets the record read from the pilot as its only
#                     parameter. It should return 1 if the record should
#                     be synced and 0 if the record should not be synced
#                     to the application. In this way filtering of
#                     private records etc. can be implemented. Note that
#                     it is typically a good idea to perform a full sync
#                     (see the FullSync flag above) if the filtering
#                     rules are changed (i.e., if a different set of
#                     records is filterd).
#
#                   - Debug: debugging messages will be logged if this is 1.
#
package PilotSync;

use Data::Dumper;
use Carp;
use strict;

my $VERSION = $PilotMgr::VERSION;
my ($master_db, $file_db, $pilot_db);
my ($gIdField, $gIdHook, $gReqFields, $gTranslateHook, $gNameHook);
my ($FILECHANGE, $PILOTCHANGE, $FILEDELETE, $PILOTDELETE) = (1,4,2,8);
my (@gNotSynced);
my ($DEBUG) = 0;

sub doSync
{
    my ($dlp, $dbinfo, $reqFields, $infoFields, $idField,
	$masterFile, $dataFile, $nameHook, $readHook, $writeHook, $idHook,
	$translateHook, $appinfoHook, $cancelFlag, $ExtendedArgs) = @_;

    $gIdField = $idField;
    $gIdHook = $idHook;
    $gReqFields = $reqFields;
    $gTranslateHook = $translateHook;
    $gNameHook = $nameHook;
    @gNotSynced = ();
    $ExtendedArgs = {} unless defined $ExtendedArgs;
    $DEBUG = $ExtendedArgs->{'Debug'} if defined $ExtendedArgs->{'Debug'};
    my ($filt) = $ExtendedArgs->{'FilterHook'};

    # Open or create database
    #
    my ($pilot_dbhandle, $appinfo);
    $pilot_dbhandle = &openDB($dlp, $dbinfo);

    unless (defined $pilot_dbhandle)
    {
	PilotMgr::msg("Unable to open '$dbinfo->{name}'.  Aborting!");
	return;
    }

    # Set display on pilot
    #
    $dlp->getStatus();					PilotMgr::update();

    # Read inputs
    #
    $dlp->tickle;
    $dlp->watchdog(20);

    $master_db = &readMaster($masterFile);		PilotMgr::update();
    $file_db = &$readHook($dataFile);			PilotMgr::update();

    $dlp->watchdog(0);
    $dlp->tickle;

    $pilot_db = (%$master_db and not $ExtendedArgs->{'FullSync'})
	      ? &readPilotChanges($dlp, $pilot_dbhandle, $translateHook, $filt)
	      : &readPilotAll($dlp, $pilot_dbhandle, $translateHook, $filt);

    # Check if the sync has been cancelled
    #
    PilotMgr::update();
    if (defined $cancelFlag and $$cancelFlag)
    {
	$pilot_dbhandle->close();
	PilotMgr::msg($$cancelFlag);
	PilotMgr::msg('No changes written to pilot or desktop.');
	croak('cancel');
    }

    &fakeDeletedPilotRecords ($pilot_dbhandle) if $ExtendedArgs->{'FullSync'};

    # Do sync
    #
    &syncAppInfo($pilot_dbhandle, $infoFields, $appinfoHook);
    &doFastSync($dlp, $pilot_dbhandle, $cancelFlag);

    if (defined $cancelFlag and $$cancelFlag)
    {
	$pilot_dbhandle->close();
	PilotMgr::msg($$cancelFlag);
	PilotMgr::msg('Cancel during sync!  All file modifications listed ' .
	    'above will NOT be saved, but pilot changes above have already ' .
	    'been written.');
	croak('cancel');
    }

    # Write dbs
    #
    $dlp->tickle;
    $dlp->watchdog(20);

    &writeMaster($masterFile, $master_db);
    &$writeHook($dataFile, $file_db);

    $dlp->watchdog(0);
    $dlp->tickle;

    # Clear flags
    #
    $pilot_dbhandle->purge();
    $pilot_dbhandle->resetFlags();

    # If there were any unmergeable changes, mark pilot records still dirty
    #
    foreach (@gNotSynced)
    {
	my $rec = $pilot_dbhandle->getRecordByID($_);
	die PilotMgr::dlpErrno2Str($pilot_dbhandle->errno()) if !defined($rec);
	$rec->{'modified'} = 1;
	$pilot_dbhandle->setRecord($rec);
    }

    # Close db
    #
    $pilot_dbhandle->close();
}

# If we are doing a full sync, we have to loop over the master database
# to find the deleted pilot records (they are not in the pilot database
# with a 'deleted' flag in this case). We simulate the fast-sync
# behaviour by generating fake pilot records for all deleted records
# (they just contain the 'deleted' flag). Afterwards we can do a normal
# doFastSync.
sub fakeDeletedPilotRecords
{
    my ($dbh) = @_;
    my ($id, $mrec);
    while (($id, $mrec) = each %$master_db)
    {
	next unless (ref $mrec && $id =~ /[0-9]+/);
	unless (defined ($pilot_db->{$id}))
	{
	    $pilot_db->{$id} = $dbh->newRecord();
	    $pilot_db->{$id}{'deleted'} = 1;
	    $pilot_db->{$id}{'id'}      = $id;
	    # There might be a dangling master record. Insert the id
	    # just in case.
	    $master_db->{$gIdField . '_' . $mrec->{$gIdField}} = $id;
	}
    }
}

sub openDB
{
    my ($dlp, $dbinfo) = @_;
    my ($dbname, $dbh) = ($dbinfo->{'name'});

    eval
    {
	$dbh = $dlp->open($dbname);
    };
    if ($@ =~ /read-only value/)
    {
	PilotMgr::msg("Pilot database '$dbname' does not exist.\n" .
		      "Creating it...");

	$dbh = $dlp->create($dbname, $dbinfo->{'creator'},
			    $dbinfo->{'type'}, $dbinfo->{'flags'},
			    $dbinfo->{'version'});
    }
    elsif ($@)
    {
	croak($@);
    }

    return $dbh;
}

sub readMaster
{
    my ($fname) = @_;
    return {} unless (-r "$fname");

    use vars qw($masterdb);
    do "$fname";
    $masterdb = {} unless defined $masterdb;
    return $masterdb;
}

sub writeMaster
{
    my ($fname, $masterdb) = @_;

    $Data::Dumper::Purity = 1;
    $Data::Dumper::Deepcopy = 1;
    $Data::Dumper::Indent = 0;

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

    print FD (defined &Data::Dumper::Dumpxs
		? Data::Dumper->Dumpxs([$masterdb], ['masterdb'])
		: Data::Dumper->Dump([$masterdb], ['masterdb'])), "1;\n";

    close(FD);
}

sub readPilotChanges
{
    my ($dlp, $dbh, $translateHook, $FilterHook) = @_;
    my ($db, $i, $msg, $rec) = ({}, 1, "Reading Pilot Changes");

    PilotMgr::status($msg, 0);
    while (1)
    {
	$rec = $dbh->getNextModRecord();
	if (!defined($rec))
	{
	    PilotMgr::checkErrNotFound($dbh);
	    last;
	}

	$rec->{'deleted'} = 1
	    if (ref $FilterHook eq 'CODE' && ! &$FilterHook ($rec));

	&$translateHook($rec) if (defined $translateHook);

	$db->{$rec->{'id'}} = $rec;

	PilotMgr::status($msg, $i++);
	$i = 96 if ($i > 100);
    }
    PilotMgr::status($msg, 100);

    return $db;
}

sub readPilotAll
{
    my ($dlp, $dbh, $translateHook, $FilterHook) = @_;
    my ($db, $i, $msg, $rec, $max) = ({}, 0, "Reading All Pilot Records");

    PilotMgr::status($msg, 0);
    $max = $dbh->getRecords();
    while (1)
    {
	$rec = $dbh->getRecord($i++);
	if (!defined($rec))
	{
	    PilotMgr::checkErrNotFound($dbh);
	    last;
	}

	$rec->{'deleted'} = 1
	    if (ref $FilterHook eq 'CODE' && ! &$FilterHook ($rec));

	&$translateHook($rec) if (defined $translateHook);
	$db->{$rec->{'id'}} = $rec;

	PilotMgr::status($msg, int(100 * $i / $max));
    }
    PilotMgr::status($msg, 100);

    return $db;
}

################################

sub doFastSync
{
    my ($dlp, $dbh, $cancel) = @_;
    my ($frec, $mrec, $prec, $id, $i, $tcl);

    # Check file records first
    #
    $tcl = time;
    for ($i=$[; $i < @{$file_db->{'__RECORDS'}}; $i++)
    {
	$frec = $file_db->{'__RECORDS'}->[$i];

	# Find matching records in master and pilot, if any
	#
	$id = $master_db->{$gIdField . '_' . $frec->{$gIdField}};
	$mrec = defined $id ? $master_db->{$id} : undef;
	$prec = defined $id ? $pilot_db->{$id} : undef;

	unless (defined $mrec)
	{
	    # Try to match to existing pilot record
	    #
	    $prec = &findPilotMatch($frec);
	    $id = $prec->{'id'} if (defined $prec);
	}

	# Sync record - method returns any change in size to
	# $file_db->{records} array so we can keep this loop straight..
	#
	$i += &syncRecord($mrec, $frec, $prec, $dlp, $dbh);

	delete $pilot_db->{$id} if (defined $prec);
	$id = $master_db->{$gIdField . '_' . $frec->{$gIdField}};
	$master_db->{$id}->{'__GOT_SYNCED'} = 1 if (defined $id);

	if (time - $tcl >= 20)
	{
	    $dlp->tickle;
	    $tcl = time;
	}

	PilotMgr::update();
	return if (defined $cancel and $$cancel);
    }

    # find deleted file records
    #
    $frec = undef;

    # Copy values of %$master_db into an array before iterating over
    # them, because syncRecord might delete some of them from the hash
    # during the loop, which causes a "Use of freed value in
    # iteration" error.
    my @master_db_values = values %$master_db;
    foreach $mrec (@master_db_values)
    {
	next unless (ref $mrec eq 'HASH');

	if (defined $mrec->{'__GOT_SYNCED'})
	{
	    delete $mrec->{'__GOT_SYNCED'};
	    next;
	}

	$id = $master_db->{$gIdField . '_' . $mrec->{$gIdField}};
	$prec = defined $id ? $pilot_db->{$id} : undef;

	&syncRecord($mrec, $frec, $prec, $dlp, $dbh, $FILEDELETE);

	PilotMgr::update();
	return if (defined $cancel and $$cancel);
    }

    # remaining pilot changes
    $tcl = time;
    foreach $prec (values %$pilot_db)
    {
	# Find matching records in master and file, if any
	#
	$mrec = $master_db->{$prec->{'id'}};
	$frec = defined($mrec) ?
	    $file_db->{'__RECORDS'}->[$file_db->{$mrec->{$gIdField}}] : undef;

	&syncRecord($mrec, $frec, $prec, $dlp, $dbh);

	if (time - $tcl >= 20)
	{
	    $dlp->tickle;
	    $tcl = time;
	}

	PilotMgr::update();
	return if (defined $cancel and $$cancel);
    }
}

sub syncRecord
{
    my ($mrec, $frec, $prec, $dlp, $dbh, $rule) = @_;
    $rule = 0 unless (defined $rule);

    $rule |= $FILECHANGE
	if (defined $frec and (!defined $mrec || &recsDiffer($frec, $mrec)));

    # The if postcondition used to check only for the pilot record being
    # present (being defined). This is not enough for full sync: Here
    # all records are read from the pilot and we have to check if they
    # really differ. This may slightly modify the behaviour in case the
    # user changes something on the pilot and then changes it back: The
    # pilot sets the flag that the record changed and the record is
    # treated as a changed record. In case the same record is deleted in
    # the File, the record from the Pilot would be recreated in the File
    # for the old behaviour. It would be deleted for the new behaviour
    # because the record would not be marked as changed.
    if (defined ($prec))
    {
	$rule |= $PILOTDELETE if ($prec->{'deleted'});
	$rule |= $PILOTCHANGE
	    if (  !$prec->{'deleted'}
	       && &recsDiffer ($prec, $mrec)
	       );
    }

    ##
    # Apply rule...
    #
    # Rules:
    #   0: Pilot=unchanged    File=unchanged    * No action
    #   1: Pilot=unchanged    File=changed/new  * Update Pilot
    #   2: Pilot=unchanged    File=deleted      * Delete Pilot
    #   4: Pilot=changed/new  File=unchanged    * Update File
    #   8: Pilot=deleted      File=unchanged    * Delete File
    #  10: Pilot=deleted      File=deleted      * Just remove master rec
    #
    #   5: Pilot=changed/new  File=changed/new  * Merge changes, Update Both
    #   6: Pilot=changed/new  File=deleted      * Restore File ?
    #   9: Pilot=deleted      File=changed/new  * Restore Pilot ?
    #
    my ($file_sizechange, $id) = (0);
    return 0 if ($rule == 0);

    if ($rule == 10)
    {
	delete $master_db->{$prec->{'id'}};
	delete $master_db->{$gIdField . '_' . $mrec->{$gIdField}};
	# The following is needed in case of a full sync (in this case
	# a faked pilot record with 'deleted' flag exists and must be
	# deleted, otherwise it is processed again when iterating over
	# remaining pilot records.
	delete $pilot_db->{$id};
    }

    if ($rule == 5)
    {
	unless (&recsDiffer($frec, $prec))
	{
	    # both changed, but they're still the same- just update master
	    unless (defined $mrec)
	    {
		$master_db->{$prec->{'id'}} = $frec;
		$master_db->{$gIdField . '_' . $frec->{$gIdField}} =
		    $prec->{'id'};
		PilotMgr::msg("Matching '" . &$gNameHook($prec) . "' " .
			      "(pilot $prec->{id}, file $frec->{$gIdField})");
	    }
	    else
	    {
		&copyRec($frec, $mrec);
	    }
	    return 0;
	}

	# Attempt to automatically merge data
	#
	my ($mergeOk, $fchange, $pchange) = &mergeRec($mrec, $frec, $prec);
	my ($pname, $fname) = (&$gNameHook($prec), &$gNameHook($frec));
	$pname .= " / $fname" if ($pname ne $fname);

	if ($mergeOk)
	{
	    PilotMgr::msg(
		"Pilot/File changes successfully merged for: $pname");
	}
	else
	{
	    push(@gNotSynced, $prec->{'id'});
	    PilotMgr::msg("Both Pilot and File records changed for: $pname\n" .
			  "** Unable to merge all changes!\n** Please edit " .
			  'records to make them the same.');
	}

	if ($pchange)
	{
	    &$gTranslateHook($prec, 1) if (defined $gTranslateHook);
	    debugDump([$prec], ['prec']) if $DEBUG;
	    $dbh->setRecord($prec);
	    PilotMgr::msg('Update Pilot: ' . &$gNameHook($prec));
	}
	if ($fchange)
	{
	    PilotMgr::msg('Update File: ' . &$gNameHook($frec));
	}
    }

    if ($rule == 1 or $rule == 9)
    {
	# Update Pilot
	#
	$prec = &copyRec($frec, (defined($prec) ? $prec : $dbh->newRecord()));
	$prec->{'id'} = $master_db->{$gIdField . '_' . $frec->{$gIdField}};
	$prec->{'id'} ||= 0;
	$prec->{'secret'} ||= 0;
	$prec->{'category'} ||= 0;

	# going to restore a deleted pilot rec with file changes:
	$prec->{'deleted'} = 0 if ($rule ==9);

	&$gTranslateHook($prec, 1) if (defined $gTranslateHook);

	debugDump([$prec], ['prec']) if $DEBUG;

	$id = $dbh->setRecord($prec);
	PilotMgr::msg('Update Pilot: ' . &$gNameHook($frec));

	$master_db->{$id} = $frec;
	$master_db->{$gIdField . '_' . $frec->{$gIdField}} = $id
	    unless (defined $mrec);
    }

    if ($rule == 4 and defined $frec and !&recsDiffer($frec, $prec))
    {
	# Pilot reports record modified, but it's really still the same..
	# (this can happen after using Installer to restore a db)
	#
	$rule = -1;
    }

    if ($rule == 4 or $rule == 6)
    {
	# Update File
	#
	unless (defined $frec)
	{
	    $frec = {};
	    $frec->{$gIdField} = &$gIdHook($file_db, $prec);
	    push(@{$file_db->{'__RECORDS'}}, $frec);
	    $file_db->{$frec->{$gIdField}} = $#{$file_db->{'__RECORDS'}};
	    $file_sizechange = 1;
	}

	&copyRec($prec, $frec);
	PilotMgr::msg('Update File: ' . &$gNameHook($prec));

	$master_db->{$prec->{'id'}} = $frec;
	$master_db->{$gIdField . '_' . $frec->{$gIdField}} = $prec->{'id'}
	    unless (defined $mrec);
    }

    if ($rule == 2)
    {
	# Delete Pilot
	#
	$id = $master_db->{$gIdField . '_' . $mrec->{$gIdField}};

	$dbh->deleteRecord($id);
	PilotMgr::msg('Delete Pilot: ' . &$gNameHook($mrec));

	delete $master_db->{$id};
	delete $master_db->{$gIdField . '_' . $mrec->{$gIdField}};
	# The following is needed in case of a full sync (in this case
	# an unchanged pilot record exists and must be deleted,
	# otherwise it is reinserted into the file).
	delete $pilot_db->{$id};
    }

    if ($rule == 8 and defined $mrec)
    {
	# Delete File
	#
	$id = $file_db->{$mrec->{$gIdField}};
	splice(@{$file_db->{'__RECORDS'}}, $id, 1);
	delete $file_db->{$mrec->{$gIdField}};
	$file_sizechange = -1;
	PilotMgr::msg('Delete File: ' . &$gNameHook($mrec));

	for (; $id < @{$file_db->{'__RECORDS'}}; $id++)
	{
	    $file_db->{ $file_db->{'__RECORDS'}->[$id]->{$gIdField} }--;
	}

	delete $master_db->{$gIdField . '_' . $mrec->{$gIdField}};
	delete $master_db->{$prec->{'id'}};
    }

    return $file_sizechange;
}

sub copyRec
{
    my ($src, $dst) = @_;
    my ($fld, $val);

    foreach $fld (@$gReqFields)
    {
	unless (exists $src->{$fld})
	{
	    delete ($dst->{$fld});
	    next;
	}
	$val = $src->{$fld};
	if (ref $val eq 'HASH')		{ $dst->{$fld} = &copyHash($val);   }
	elsif (ref $val eq 'ARRAY')	{ $dst->{$fld} = &copyArray($val);  }
	else				{ $dst->{$fld} = $val;		    }
    }
    return $dst;
}

sub copyHash
{
    my ($src) = @_;
    my ($h, $fld, $val) = ({});

    foreach $fld (keys %$src)
    {
	$val = $src->{$fld};
	if (ref $val eq 'HASH')		{ $h->{$fld} = &copyHash($val);    }
	elsif (ref $val eq 'ARRAY')	{ $h->{$fld} = &copyArray($val);   }
	else				{ $h->{$fld} = $val;		   }
    }
    return $h;
}

sub copyArray
{
    my ($src) = @_;
    my ($a, $val) = ([]);

    foreach $val (@$src)
    {
	if (ref $val eq 'HASH')		{ push(@$a, &copyHash($val));	}
	elsif (ref $val eq 'ARRAY')	{ push(@$a, &copyArray($val));	}
	else				{ push(@$a, $val);		}
    }
    return $a;
}

sub recsDiffer
{
    my ($rec1, $rec2) = @_;
    my ($fld);

    foreach $fld (@$gReqFields)
    {
	return 1 if valuesDiffer($rec1->{$fld}, $rec2->{$fld});
    }
    return 0;
}

sub valuesDiffer
{
    my ($val1, $val2) = @_;
    return 1 if (defined $val1 ^ defined $val2);
    return 0 if !defined($val1);
    return 1 if (ref $val1 ne ref $val2		   or
		 ref $val1 eq 'HASH'  &&
		 &hashDiffer($val1, $val2)  or
		 ref $val1 eq 'ARRAY'  &&
		 &arrayDiffer($val1, $val2) or
		 !ref $val1  &&
		 $val1 ne $val2		   );
    return 0;
}

sub hashDiffer
{
    my ($h1, $h2) = @_;
    my ($fld);

    foreach $fld (keys %$h1)
    {
	return 1 if (!exists $h1->{$fld}  or
		     defined $h1->{$fld} ^ defined $h2->{$fld});
	next unless (defined $h1->{$fld});
	return 1 if (ref $h1->{$fld} ne ref $h2->{$fld}			   or
		     ref $h1->{$fld} eq 'HASH'  &&
			&hashDiffer($h1->{$fld}, $h2->{$fld})		   or
		     ref $h1->{$fld} eq 'ARRAY'  &&
			&arrayDiffer($h1->{$fld}, $h2->{$fld})		   or
		     !ref $h1->{$fld}  &&  $h1->{$fld} ne $h2->{$fld}	   );
    }
    return 0;
}

sub arrayDiffer
{
    my ($a1, $a2) = @_;
    my ($i);

    return 1 if ($#$a1 ne $#$a2);
    for ($i=0; $i < @$a1; $i++)
    {
	return 1 if (defined $a1->[$i] ^ defined $a2->[$i]);
	next unless (defined $a1->[$i]);
	return 1 if (ref $a1->[$i] ne ref $a2->[$i]			or
		     ref $a1->[$i] eq 'HASH'  &&
			&hashDiffer($a1->[$i], $a2->[$i])		or
		     ref $a1->[$i] eq 'ARRAY'  &&
			&arrayDiffer($a1->[$i], $a2->[$i])		or
		     !ref $a1->[$i]  &&  $a1->[$i] ne $a2->[$i]		);
    }
    return 0;
}

sub findPilotMatch
{
    my ($frec) = @_;
    my ($prec);

    foreach $prec (values %$pilot_db)
    {
	return $prec unless (&recsDiffer($frec, $prec));
    }
    return undef;
}

sub mergeRec
{
    my ($mrec, $frec, $prec) = @_;
    return &mergeHash($mrec, $frec, $prec, @$gReqFields);
}

sub mergeHash
{
    my ($m, $f, $p, @fields) = @_;
    my ($fld);
    my ($ok, $fchng, $pchng) = (1, 0, 0);

    @fields = keys %$m unless (@fields);
    foreach $fld (@fields)
    {
	my ($mval, $fval, $pval) = ();
	$mval = $m->{$fld} if defined ($m->{$fld});
	$fval = $f->{$fld} if defined ($f->{$fld});
	$pval = $p->{$fld} if defined ($p->{$fld});

	# Special case: One of the fields (they might be hashes!) is
	# undefined. This can happen for e.g., datebook alarms. An
	# undefined hash (no alarm) is ok, while an empty hash will
	# result in a core dump for the datebook case.
	if (!defined ($f->{$fld}) && !defined ($p->{$fld}))
	{
	    delete ($m->{$fld});
	}
	elsif (!defined ($p->{$fld}) && !valuesDiffer($m->{$fld}, $f->{$fld}))
	{
	    delete ($f->{$fld});
	    delete ($m->{$fld});
	    $fchng = 1;
	}
	elsif (!defined ($f->{$fld}) && !valuesDiffer($m->{$fld}, $p->{$fld}))
	{
	    delete ($p->{$fld});
	    delete ($m->{$fld});
	    $pchng = 1;
	}
	elsif (ref $mval eq 'HASH')
	{
	    $f->{$fld} = $fval = {} unless (defined $fval);
	    $p->{$fld} = $pval = {} unless (defined $pval);
	    @_ = &mergeHash($mval, $fval, $pval);
	    $ok &= $_[0];
	    $fchng |= $_[1];
	    $pchng |= $_[2];
	}
	elsif (ref $mval eq 'ARRAY')
	{
	    $f->{$fld} = $fval = [] unless (defined $fval);
	    $p->{$fld} = $pval = [] unless (defined $pval);
	    @_ = &mergeArray($mval, $fval, $pval);
	    $ok &= $_[0];
	    $fchng |= $_[1];
	    $pchng |= $_[2];
	}
	else
	{
	    if ($mval eq $fval and $fval ne $pval)
	    {
		$f->{$fld} = $m->{$fld} = $pval;
		$fchng = 1;
	    }
	    elsif ($mval eq $pval and $pval ne $fval)
	    {
		$p->{$fld} = $m->{$fld} = $fval;
		$pchng = 1;
	    }
	    elsif ($fval ne $pval)
	    {
		$ok = 0;
	    }
	    elsif ($fval ne $mval)
	    {
		$m->{$fld} = $fval;
	    }
	}
    }
    return ($ok, $fchng, $pchng);
}

sub mergeArray
{
    my ($m, $f, $p) = @_;
    my ($i, $mval, $fval, $pval);
    my ($ok, $fchng, $pchng) = (1, 0, 0);

    for ($i=$[; $i < @$m; $i++)
    {
	$mval = $m->[$i];
	$fval = $f->[$i];
	$pval = $p->[$i];
	if (ref $mval eq 'HASH')
	{
	    $f->[$i] = $fval = {} unless (defined $fval);
	    $p->[$i] = $pval = {} unless (defined $pval);
	    @_ = &mergeHash($mval, $fval, $pval);
	    $ok &= $_[0];
	    $fchng |= $_[1];
	    $pchng |= $_[2];
	}
	elsif (ref $mval eq 'ARRAY')
	{
	    $f->[$i] = $fval = [] unless (defined $fval);
	    $p->[$i] = $pval = [] unless (defined $pval);
	    @_ = &mergeArray($mval, $fval, $pval);
	    $ok &= $_[0];
	    $fchng |= $_[1];
	    $pchng |= $_[2];
	}
	else
	{
	    if ($mval eq $fval and $fval ne $pval)
	    {
		$f->[$i] = $m->[$i] = $pval;
		$fchng = 1;
	    }
	    elsif ($mval eq $pval and $pval ne $fval)
	    {
		$p->[$i] = $m->[$i] = $fval;
		$pchng = 1;
	    }
	    elsif ($fval ne $pval)
	    {
		$ok = 0;
	    }
	    elsif ($fval ne $mval)
	    {
		$m->[$i] = $fval;
	    }
	}
    }
    return ($ok, $fchng, $pchng);
}

sub syncAppInfo
{
    my ($dbh, $InfoFields, $translateHook) = @_;
    my ($writePi, $pappi, $fappi, $mappi, $fld) = (0);

    $pappi = $dbh->getAppBlock();
    $fappi = $file_db->{'__APPINFO'};

    return unless (defined $pappi or defined $fappi);

    &$translateHook($pappi, 0) if (defined $translateHook and defined $pappi);

    $mappi = $master_db->{'__APPINFO'};
    $master_db->{'__APPINFO'} = $mappi = {} unless (defined $mappi);
    $mappi->{'__GOT_SYNCED'} = 1;

    $file_db->{'__APPINFO'} = $fappi = {} unless (defined $fappi);
    unless (defined $pappi)
    {
	$pappi = bless {}, 'PDA::Pilot::AppBlock';
	$writePi=1;
    }

    foreach $fld (@$InfoFields)
    {
	if (ref $pappi->{$fld} eq 'ARRAY' or ref $fappi->{$fld} eq 'ARRAY')
	{
	    # for array values
	    #
	    my ($size, $i);
	    $size = @{$fappi->{$fld}} if defined($fappi->{$fld});
	    $size = @{$pappi->{$fld}} if defined($pappi->{$fld});

	    $pappi->{$fld} = [] unless (defined $pappi->{$fld});
	    $fappi->{$fld} = [] unless (defined $fappi->{$fld});
	    $mappi->{$fld} = [] unless (defined $mappi->{$fld});

	    for ($i=0; $i < $size; $i++)
	    {
		$writePi |= &aiCheck($pappi, $fappi, $mappi, $fld, $i);
	    }
	}
	else
	{
	    # for scalar values
	    #
	    $writePi |= &aiCheck($pappi, $fappi, $mappi, $fld);
	}
    }

    if ($writePi)
    {
	&$translateHook($pappi, 1) if (defined $translateHook);
	PilotMgr::msg("Updating Pilot AppInfo..");
	$dbh->setAppBlock($pappi);
    }
}

sub aiCheck
{
    my ($pappi, $fappi, $mappi, $fld, $i) = @_;
    my ($pichange, $pval, $fval, $mval) = (0);

    $pval = $pappi->{$fld};
    $fval = $fappi->{$fld};
    $mval = $mappi->{$fld};
    if (defined $i)
    {
	$pval = $pval->[$i];
	$fval = $fval->[$i];
	$mval = $mval->[$i];
    }

    # Pilot   Master  File
    # undef   any     undef	-> huh?
    # def \\  any  // def	-> copy to master (pilot==file)
    # undef   any     def	-> use file
    # def     any     undef	-> use pilot
    # def  == def     def	-> use file
    # def     def ==  def	-> use pilot
    # def  != any !=  def	-> merge? do nothing for now

    return unless (defined $pval or defined $fval);

    my $peqf = defined $pval && defined $fval && ($pval eq $fval);
    my $feqm = defined $fval && defined $mval && ($fval eq $mval);
    my $peqm = defined $pval && defined $mval && ($pval eq $mval);

    if (ref $pval eq 'HASH' && ref $fval eq 'HASH') {
	$peqf = hashEqual($pval, $fval);
    }

    if (ref $fval eq 'HASH' && ref $mval eq 'HASH') {
	$feqm = hashEqual($fval, $mval);
    }

    if (ref $pval eq 'HASH' && ref $mval eq 'HASH') {
	$peqm = hashEqual($pval, $mval);
    }

    if (defined $pval and defined $fval and $peqf)
    {
	if (defined $i) { $mappi->{$fld}->[$i] = $fval; }
	else		{ $mappi->{$fld} = $fval;	}
    }
    elsif (&empty($fval) or (defined $mval and $feqm))
    {
	if (defined $i) { $mappi->{$fld}->[$i] = $fappi->{$fld}->[$i] = $pval;}
	else		{ $mappi->{$fld} = $fappi->{$fld} = $pval;	      }
    }
    elsif (&empty($pval) or (defined $mval and $peqm))
    {
	if (defined $i) { $mappi->{$fld}->[$i] = $pappi->{$fld}->[$i] = $fval;}
	else		{ $mappi->{$fld} = $pappi->{$fld} = $fval;	      }
	$pichange = 1;
    }
    else
    {
	PilotMgr::msg("AppInfo field $fld" .
	    ((defined $i) ? "[$i]" : '') .
	    " changed on both pilot and file!\n" .
	    "Not updating either side.. please change them to be the same.\n" .
	    "(pilot=$pval, file=$fval)");
    }

    return $pichange;
}

sub empty
{
    return (!defined $_[0] or !length($_[0]));
}

sub hashEqual {
    my $a = shift;
    my $b = shift;

    foreach my $key (keys %{$a}) {
	return undef if (!exists $b->{$key}) || ($a->{$key} ne $b->{$key});
    }

    foreach my $key (keys %{$b}) {
	return undef if (!exists $a->{$key});
    }

    return 1;
}

sub debugDump {
    my ($items, $names) = @_;
    $Data::Dumper::Purity = 1;
    $Data::Dumper::Deepcopy = 1;
    $Data::Dumper::Indent = 0;
    print STDERR
      ( defined &Data::Dumper::Dumpxs
	? Data::Dumper->Dumpxs($items, $names)
	: Data::Dumper->Dump($items, $names)
      ), "\n";
}

1;

