# 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.
#
# SyncMemo conduit for PilotManager
# Originally written by Alan Harder, 4/97
# See ChangeLog for list of modifications and contributors.

package SyncMemo;

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

#   $gHomeDirectory;		# base directory for files
#   $gOnlyAscii;		# whether to limit files to "ascii text"
#   $OnlyFilesToPilot;		# whether to do pilot->file transfers
#   $gMemoUpdater;		# script for updating memo files
#   $gUseUpdater;		# whether or not to run update script
#   $gPostUpdater;		# post update script
#   $gUsePostUpdater;		# whether or not to run post update script
#   $gUseFilemerge;		# whether to use filemerge to merge changes
#   $gBlowAwayPilot;		# nuke all pilot memos and copy over files
my ($gConfigDialog);		# configuration dialog
my ($gCheckDialog);		# dialog about illegal config values
my ($gDismissBtn);		# dismiss button on config dialog
my ($gFilemergeCb);		# "Use Filemerge" checkbutton
my ($gDefSyncMenu);		# Default Sync Type menu
my ($gInfoDialog);		# Sync Type info dialog
my ($gEmulInfoDialog);		# Old version emulation config info dialog
my ($gEmulMenu);		# Old version Emulation mode menu
my ($gYesNoDialog);		# yes/no dialog
my ($RCFILE);			# configuration file
my ($DBFILE);			# record of files/checksums
my ($PREFS);			# configuration variables
my $VERSION = $PilotMgr::VERSION;		# SyncMemo version

sub conduitInit
{
    $RCFILE = "SyncMemo/SyncMemo.prefs";
    $DBFILE = $PREFS->{"DBFILE"} = "SyncMemo/SyncMemo.db";
    my $prefFileExists = &loadPrefs;

    $PREFS->{"gHomeDirectory"} = "$ENV{HOME}/.pilotmgr/SyncMemo"
	unless (defined($PREFS->{"gHomeDirectory"}));
    $PREFS->{"gOnlyAscii"} = 1
	unless (defined($PREFS->{"gOnlyAscii"}));
    $PREFS->{"OnlyFilesToPilot"} = 0
	unless (defined($PREFS->{"OnlyFilesToPilot"}));
    $PREFS->{"gMemoUpdater"} = "$ENV{HOME}/.pilotmgr/SyncMemo/update"
	unless (defined($PREFS->{"gMemoUpdater"}));
    $PREFS->{"gUseUpdater"} = 0
	unless (defined($PREFS->{"gUseUpdater"}));
    $PREFS->{"gPostUpdater"} = ""
	unless (defined($PREFS->{"gPostUpdater"}));
    $PREFS->{"gUsePostUpdater"} = 0
	unless (defined($PREFS->{"gUsePostUpdater"}));
    $PREFS->{"gUseFilemerge"} = 0
	unless (defined($PREFS->{"gUseFilemerge"}));
    $PREFS->{"gBlowAwayPilot"} = 0
	unless (defined($PREFS->{"gBlowAwayPilot"}));
    $PREFS->{'UseDefaultSyncType'} = 0
	unless (defined($PREFS->{'UseDefaultSyncType'}));
    $PREFS->{'DefaultSyncType'} = 'Full'
	unless (defined($PREFS->{'DefaultSyncType'}));
    $PREFS->{'useSkipStart'} = 0
	unless (defined($PREFS->{'useSkipStart'}));
    $PREFS->{'useSkipEnd'} = 0
	unless (defined($PREFS->{'useSkipEnd'}));
    $PREFS->{'skipStart'} = '#'
	unless (defined($PREFS->{'skipStart'}));
    $PREFS->{'skipEnd'} = '~'
	unless (defined($PREFS->{'skipEnd'}));
    $PREFS->{'privateHandler'} = 'Default Handling'
	unless (defined($PREFS->{'privateHandler'}));
    $PREFS->{'privatePerms'} = '640'
	unless (defined($PREFS->{'privatePerms'}));
    # If the user is upgrading from an earlier version, leave
    # emulateBrokenFullSink undefined until later; if they're starting
    # from scratch, assume they want the new behavior.
    $PREFS->{'emulateBrokenFullSync'} = 'Normal behavior' if ! $prefFileExists;
}

sub conduitQuit
{
    &savePrefs;
}

sub conduitInfo
{
    return { "database" =>
		{
		    "name" => "MemoDB",
		    "creator" => "memo",
		    "type" => "DATA",
		    "flags" => 0,
		    "version" => 0,
		},
	     "version" => $VERSION,
	     "author" => "Alan Harder",
	   };
}

sub checkConfig
{
    # Require ASCI-only skpiStart and skipEnd strings.  For
    # explanation, see comment beginning "The config dialog
    # requires..." in &checkFiles.

    my $ok = 1;
    if ($] > 5.007) {
	require Encode;
	if ($PREFS->{'useSkipStart'}) {
	    eval { $PREFS->{'skipStart'} =
		     Encode::encode("ascii", $PREFS->{'skipStart'}, Encode::FB_CROAK()) };
	    if ($@)
	    {
		$gCheckDialog->Show;
		$ok = 0;
	    }
	}
	if ($PREFS->{'useSkipEnd'}) {
	    eval { $PREFS->{'skipEnd'} =
		     Encode::encode("ascii", $PREFS->{'skipEnd'}, Encode::FB_CROAK()) };
	    if ($@)
	    {
		$gCheckDialog->Show;
		$ok = 0;
	    }
	}
    }
    $gConfigDialog->withdraw if $ok;
}

sub conduitConfigure
{
    my ($this, $wm) = @_;
    my ($frame, $obj, $subfr);

    unless (defined($gConfigDialog))
    {
	$gConfigDialog = $wm->Toplevel(-title => "Configuring SyncMemo");
	$gConfigDialog->transient($wm);

	$gYesNoDialog = $wm->Dialog(-title => "Do Something",
				    -text =>
					"Are you sure?      \n(No undo!)     ",
				    -justify => "center",
				    -buttons => ["Yes", "No"],
				    -default_button => "No",
				    -popover => $gConfigDialog,
				    -overanchor => 'c', -popanchor => 'c');
	PilotMgr::setColors($gYesNoDialog);

	$gInfoDialog = $wm->Toplevel(-title => "SyncMemo");
	$gInfoDialog->transient($wm);
	$gInfoDialog->withdraw;
	$frame = $gInfoDialog->Frame;
	($obj) = TkUtils::Text($frame, "Sync Type Info");
	$obj->insert('end',
		"Sometimes SyncMemo will popup a dialog asking whether " .
		"a full or fast sync should be performed.  The purpose of " .
		"this dialog is to make sure all memo changes get noticed " .
		"during the hotsync.  There are two cases this might not " .
		"happen:\n 1. You have restored a backed up MemoDB file- any " .
		"and all memos may now be totally different, so a full sync " . 
		"is in order!\n 2. You have synced to another machine- when " .
		"you do this the memos you had changed will be updated to " .
		"THAT machine, and then cleared so that when SyncMemo does " .
		"its next sync it doesn't know the memo has changed.  The " .
		"only way to get all the changes is to do a full sync " .
		"(load ALL the memos and look at them to see if they have " .
		"changed).\nIf you find that you see this dialog a lot and " .
		"always give the same answer you can use the Default Sync " .
		"Type option in the SyncMemo configuration dialog to specify ".
		"a default answer.  HOWEVER, if you choose Fast Sync as your ".
		"default YOU must be aware of when the above conditions " .
		"occur and manually select Reset SyncMemo from the config " .
		"dialog to force a full sync.");
	$obj->configure(-height => 20, -state => 'disabled');
	$obj->pack(-expand => 1, -fill => 'both');
	$obj = TkUtils::Button($frame, "Dismiss", sub{$gInfoDialog->withdraw});
	$obj->pack;
	$frame->pack(-expand => 1, -fill => 'both');
	PilotMgr::setColors($gInfoDialog);

	$gCheckDialog = $wm->Dialog(-title => "Configuration Problem",
				    -text => "The \"starting with\" and \"ending with\" fields ".
				             "must contain only ASCII characters.",
				    -justify => 'center',
				    -buttons => ["OK"],
				    -default_button => "OK",
				    -popover => $gConfigDialog,
				    -overanchor => 'c', -popanchor => 'c',
				   );
	PilotMgr::setColors($gCheckDialog);

	$gEmulInfoDialog = $wm->Toplevel(-title => "SyncMemo");
	$gEmulInfoDialog->transient($wm);
	$gEmulInfoDialog->withdraw;
	$frame = $gEmulInfoDialog->Frame;
	($obj) = TkUtils::Text($frame, "Old Version Emulation Info");
	$obj->insert('end',
	   "Earlier versions of SyncMemo had a quirk whereby " .
	   "if a file was deleted on the workstation and unchanged " .
	   "on the Pilot, a full sync would cause the file to be recreated " .
	   "on the workstation.  If you used previous versions and have " .
	   "come to depend on that behavior, you can choose to emulate " .
	   "it; otherwise, choose the \"normal behavior\" which ".
           "is to propagate the deletion to the Pilot.");
	$obj->configure(-height => 10, -state => 'disabled');
	$obj->pack(-expand => 1, -fill => 'both');
	$obj = TkUtils::Button($frame, "Dismiss", sub {$gEmulInfoDialog->withdraw});
	$obj->pack;
	$frame->pack(-expand => 1, -fill => 'both');
	PilotMgr::setColors($gEmulInfoDialog);

	$frame = $gConfigDialog->Frame(-relief => "ridge", -bd => 4);
	$obj = TkUtils::Label($frame, "SyncMemo Settings");
	$obj->pack(-anchor => "c");

	$subfr = $frame->Frame;
	$obj = $subfr->Label(-text => "Memo directory: ");
	$obj->pack(-side => "left", -anchor => "nw", -fill => "x");

	$obj = $subfr->Entry(-textvariable => \$PREFS->{"gHomeDirectory"},
			     -relief => "sunken", -width => 60);
	$obj->pack(-side => "right", -anchor => "w", -fill => "x");
	$subfr->pack(-anchor => "e");

	$subfr = $frame->Frame;
	$obj = $subfr->Label(-text => "Skip files:");
	$obj->pack(-side => "left", -anchor => "nw", -fill => "x");

	$obj = TkUtils::Checkbutton($subfr, "starting with",
				    \$PREFS->{"useSkipStart"});
	$obj->pack(-side => "left", -anchor => "w", -fill => "x");

	$obj = $subfr->Entry(-textvariable => \$PREFS->{"skipStart"},
			     -relief => "sunken", -width => 15);
	$obj->pack(-side => "left", -anchor => "w", -fill => "x");

	$obj = TkUtils::Checkbutton($subfr, "ending with",
				    \$PREFS->{"useSkipEnd"});
	$obj->pack(-side => "left", -anchor => "w", -fill => "x");

	$obj = $subfr->Entry(-textvariable => \$PREFS->{"skipEnd"},
			     -relief => "sunken", -width => 16);
	$obj->pack(-side => "left", -anchor => "w", -fill => "x");
	$subfr->pack(-anchor => "e");

	$subfr = $frame->Frame;
	TkUtils::LabelRadiobuttons($subfr, "Private memos:",
				   \$PREFS->{"privateHandler"},
				   "Default Handling",
				   "Skip", "Use permissions:");
	$obj = $subfr->Entry(-textvariable => \$PREFS->{"privatePerms"},
			     -relief => "sunken", -width => 4);
	$obj->pack;
	$subfr->pack;

	$subfr = $frame->Frame;
	$obj = TkUtils::Checkbutton($subfr, "Only sync text files",
				    \$PREFS->{"gOnlyAscii"});
	$obj->pack(-side => "left", -fill => "x");

	$gFilemergeCb = TkUtils::Checkbutton($subfr, "Use graphical file merge program",
					     \$PREFS->{"gUseFilemerge"});
	$gFilemergeCb->configure(-state => "disabled")
	    if ($PREFS->{"OnlyFilesToPilot"});
	$gFilemergeCb->pack(-side => 'right', -fill => 'x');

	$obj = TkUtils::Checkbutton($subfr, 'Only Sync Files->Pilot',
				    \$PREFS->{'OnlyFilesToPilot'});
	$obj->configure(-command => sub{
	    $gFilemergeCb->configure(-state => (($gFilemergeCb->cget('-state')
		eq 'normal') ? 'disabled' : 'normal')) });
	$obj->pack(-side => "left", -fill => "x");
	$subfr->pack;
	$frame->pack(-side => "top", -fill => "x");

	$frame = $gConfigDialog->Frame(-relief => "ridge", -bd => 4);
	$obj = TkUtils::Label($frame, "Update Scripts");
	$obj->pack(-anchor => "c");

	$subfr = $frame->Frame;
	$obj = $subfr->Label(-text => "Pre-Update script: ");
	$obj->pack(-side => "left", -anchor => "nw", -fill => "x");

	$obj = $subfr->Entry(-textvariable => \$PREFS->{"gMemoUpdater"},
			     -relief => "sunken", -width => 60);
	$obj->pack(-side => "right", -anchor => "w", -fill => "x");
	$subfr->pack(-anchor => "e");

	$subfr = $frame->Frame;
	$obj = $subfr->Label(-text => "Post-Update script: ");
	$obj->pack(-side => "left", -anchor => "nw", -fill => "x");

	$obj = $subfr->Entry(-textvariable => \$PREFS->{"gPostUpdater"},
			     -relief => "sunken", -width => 60);
	$obj->pack(-side => "right", -anchor => "w", -fill => "x");
	$subfr->pack(-anchor => "e");

	$subfr = $frame->Frame;
	$obj = TkUtils::Checkbutton($subfr,"Run pre-update script before sync",
				    \$PREFS->{"gUseUpdater"});
	$obj->pack(-side => "left", -fill => "x");

	$obj = TkUtils::Checkbutton($subfr,"Run post-update script after sync",
				    \$PREFS->{"gUsePostUpdater"});
	$obj->pack(-fill => "x");
	$subfr->pack;

	$subfr = $frame->Frame;
	$gDismissBtn = TkUtils::Button($gConfigDialog, "Dismiss",
		sub{  &savePrefs; &checkConfig });

	$obj = TkUtils::Button($subfr, "Run Pre-Update Script Now",
		sub{ if (-x $PREFS->{"gMemoUpdater"}) {
			$gDismissBtn->configure(-state => "disabled");
			my ($curs) = ($gConfigDialog->cget("-cursor"));
			$gConfigDialog->configure(-cursor => "watch");
			$gConfigDialog->update;
			system "$PREFS->{gMemoUpdater}";
			$gConfigDialog->configure(-cursor => $curs);
			$gDismissBtn->configure(-state => "normal");
		     } });
	$obj->pack(-side => "left", -anchor => "w", -fill => "x");

	$obj = TkUtils::Button($subfr, "Run Post-Update Script Now",
		sub{ if (-x $PREFS->{"gPostUpdater"}) {
			$gDismissBtn->configure(-state => "disabled");
			my ($curs) = ($gConfigDialog->cget("-cursor"));
			$gConfigDialog->configure(-cursor => "watch");
			$gConfigDialog->update;
			system "$PREFS->{gPostUpdater}";
			$gConfigDialog->configure(-cursor => $curs);
			$gDismissBtn->configure(-state => "normal");
		     } });
	$obj->pack(-side => "left", -anchor => "w", -fill => "x");
	$subfr->pack;
	$frame->pack(-side => "top", -fill => "x");

	$frame = $gConfigDialog->Frame(-relief => "ridge", -bd => 4);
	$obj = TkUtils::Label($frame, "Special Options");
	$obj->pack(-anchor => "center");
	$obj = TkUtils::Label($frame,
	    "These options allow special actions to be performed on your " .
	    "next hotsync.\nA Reset will cause SyncMemo to reexamine ALL " .
	    "memos on the Pilot instead of just\nthose that have changed.  " .
	    "Blowing away either the Pilot or Files will cause ALL memos\n" .
	    "in that database to be deleted and replaced with the contents " .
	    "of the other database.");
	$obj->configure(-justify => "left");
	$obj->pack(-anchor => "center");

	$subfr = $frame->Frame;
	$obj = TkUtils::Button($subfr, "Reset SyncMemo",
	    sub{ $gYesNoDialog->configure(-title => "Reset SyncMemo");
		 unlink ($PREFS->{"DBFILE"}) if ($gYesNoDialog->Show eq "Yes");
		});
	$obj->pack(-side => "left", -anchor => "w", -fill => "x");
	$obj = TkUtils::Button($subfr, "Overwrite Pilot",
	    sub{ $gYesNoDialog->configure(-title => "Overwrite Pilot");
		 if ($gYesNoDialog->Show eq "Yes")
		 {
		     unlink($PREFS->{"DBFILE"});
		     $PREFS->{"gBlowAwayPilot"} = 1;
		 }
		});
	$obj->pack(-side => "left", -anchor => "w", -fill => "x");
	$obj = TkUtils::Button($subfr, "Overwrite Files",
	    sub{ $gYesNoDialog->configure(-title => "Overwrite Files");
		 if ($gYesNoDialog->Show eq "Yes")
		 {
		     unlink($PREFS->{"DBFILE"});
		     &cleanDir($PREFS->{"gHomeDirectory"}, 1);
		 }
		});
	$obj->pack(-side => "left", -anchor => "w", -fill => "x");
	$subfr->pack;
	$frame->pack(-side => 'top', -fill => 'x');

	$frame = $gConfigDialog->Frame(-relief => 'ridge', -bd => 4);
	$obj = TkUtils::Label($frame, 'Advanced Options');
	$obj->pack(-anchor => 'center');
	$subfr = $frame->Frame;
	$obj = TkUtils::Checkbutton($subfr, 'Sync type defaults to ',
				    \$PREFS->{'UseDefaultSyncType'});
	$obj->pack(-side => 'left', -fill => 'x');
        $gDefSyncMenu = TkUtils::Menu($subfr,
	    $PREFS->{"DefaultSyncType"},
	    sub{ ($PREFS->{"DefaultSyncType"} = $_[0]) =~ s|.*/ ||;
		 $gDefSyncMenu->configure(-text => $PREFS->{'DefaultSyncType'});
	       },
            'Fast', [], 'Full', []);
	$subfr->pack;
	$subfr = $frame->Frame;
	$obj = TkUtils::Label($subfr,
	    "This option allows you to skip the popup dialog during the " .
	    "sync which asks if\na Fast or Full sync should be performed.  " .
	    "Only activate this option if you know\nwhat you are doing.");
	$obj->configure(-justify => 'left');
	$obj->pack(-side => 'left', -anchor => 'center');
	$obj = TkUtils::Button($subfr, "More\nInfo",
	    sub{ $gInfoDialog->Popup(-popanchor => 'c', -overanchor => 'c',
				     -popover => $gConfigDialog);
		});
	$obj->pack(-anchor => 'center');
	$subfr->pack;

	$subfr = $frame->Frame;
	$obj = TkUtils::Label($subfr, 'Emulation of old SyncMemo versions');
	$obj->pack(-side => 'left', -anchor => 'c', -fill => 'x');
	my $initlabel;
	if (defined($PREFS->{'emulateBrokenFullSync'})) {
	    $initlabel = $PREFS->{'emulateBrokenFullSync'};
	} else {
	    $initlabel = "Click to set mode";
	}
        $gEmulMenu = TkUtils::Menu($subfr,
	    $initlabel,
	    sub{ ($PREFS->{"emulateBrokenFullSync"} = $_[0]) =~ s|.*/ ||;
		 $gEmulMenu->configure(-text => $PREFS->{'emulateBrokenFullSync'});
	       },
            'Emulate old behavior', [], 'Normal behavior', []);
	$gEmulMenu->pack(-side => 'left', -anchor => 'w');
	$obj = TkUtils::Button($subfr, "More\nInfo",
	    sub{ $gEmulInfoDialog->Popup(-popanchor => 'c', -overanchor => 'c',
				     -popover => $gConfigDialog);
		});
	$obj->pack(-side => 'left', -anchor => 'w');
	$subfr->pack;


	$frame->pack(-side => 'top', -fill => 'x');

	$gDismissBtn->pack(-side => 'bottom', -anchor => 'c');
	PilotMgr::setColors($gConfigDialog);
    }

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

sub cleanDir
{
    my ($dir, $top) = @_;
    my ($sub, @dirlist);

    opendir DIR, $dir;
    @dirlist = readdir DIR;
    closedir DIR;
    foreach $sub (@dirlist)
    {
	next if $sub =~ /^\.\.?$/;
	$sub = "$dir/$sub";

	# If we're in the top directory, don't clean files only
	# clean directories.
	#
	next if ($top and -f $sub);

	if (-d $sub)
	{
	    &cleanDir($sub, 0);
	    rmdir($sub);
	}

	elsif (-f $sub)
	{
	    unlink $sub;
	}
    }
}

sub conduitSync
{
    my ($this, $dlp, $info) = @_;
    my (%oldsum, %newsum, %piid, %pifile, $record, %pilot, $fullsync);
    my ($appinfo, $cnt,$ret, $file,$fname, $a1,$a2,$a3, $i, $lastsync);
    my (@cats, %catcase, %catindx, $cat, @cat_ids,
	$updates, $filetext, $pilot_dbhandle);
    my ($dbinfo) = (&conduitInfo->{database});
    my ($dbname) = ($dbinfo->{name});

    # Special case!  "Overwrite Pilot"
    if ($PREFS->{"gBlowAwayPilot"})
    {
	$PREFS->{"gBlowAwayPilot"} = 0;
	&savePrefs;
	PilotMgr::msg(
	    "Overwrite Pilot option: deleting and recreating $dbname!");
	$ret = $dlp->delete($dbname);
	if ($ret < 0)
	{
	    $ret = PDA::Pilot::errorText($ret);  
	    PilotMgr::msg("Error $ret while deleting $dbname.. Aborting!");
	    return;
	}
	$pilot_dbhandle = $dlp->create($dbname, $dbinfo->{creator},
				       $dbinfo->{type}, $dbinfo->{flags},
				       $dbinfo->{version});
    }
    else
    {

        if (! defined($PREFS->{'emulateBrokenFullSync'})) {
	    PilotMgr::msg("\nYou have upgraded to a new version of " .
			  "SyncMemo.  Please go to the SyncMemo " .
			  "configuration screen and make a choice " .
			  "for the \"Emulation of old SyncMemo " .
			  "Versions\" option.\n\n");
	}

	# Open or create DB:
	$pilot_dbhandle = $dlp->open($dbname);

	if (!defined($pilot_dbhandle))
	{
	    PilotMgr::msg("Pilot database '$dbname' does not exist.\n" .
			  "Creating it...");

	    $pilot_dbhandle = $dlp->create($dbname, $dbinfo->{creator},
					   $dbinfo->{type}, $dbinfo->{flags},
					   $dbinfo->{version});
	}
    }

    if (!defined($pilot_dbhandle))
    {
	PilotMgr::msg("Unable to create '$dbname'.  Aborting!");
	return;
    }

    # Let the user know what we're doing
    #
    $dlp->getStatus();

    $appinfo = $pilot_dbhandle->getAppBlock();
    @cats = @{$appinfo->{categoryName}};
    @cat_ids = @{$appinfo->{categoryID}};
    foreach $i ($[..$#cats)
    {
	next unless (length($cats[$i])>0);
	$cats[$i] =~ tr|/|+|;
	$cat = $cats[$i];
	$cat =~ tr/A-Z/a-z/;
	$catcase{$cat} = $cats[$i];	# translate lowercase to anycase
	$catindx{$cats[$i]} = $i;	# translate anycase to index
    }

    # Run pre-update script
    if ($PREFS->{"gUseUpdater"})
    {
	PilotMgr::status("Running pre-update script", 0);
	&runScript($dlp, $PREFS->{"gMemoUpdater"}, "memo update");
	PilotMgr::status("Running pre-update script", 100);
    }

    # Read DBFILE
    ($a1, $a2, $a3, $lastsync) = &readDBFile;
    %oldsum = %$a1;   %piid = %$a2;   %pifile = %$a3;

    $fullsync = 1;
    if ($lastsync > 0)
    {
	if ($lastsync != $info->{"successfulSyncDate"})
	{
	    if ($PREFS->{'UseDefaultSyncType'})
	    {
		$i = $PREFS->{'DefaultSyncType'} . ' Sync';
	    }
	    else
	    {
		$dlp->watchdog(20);
		$i = PilotMgr::askUser("Your last sync was either " .
		    "unsuccessful or did not include SyncMemo. If you have " .
		    "done any memo sync operations to another machine or " .
		    "restored an old MemoDB file you should do a full sync " .
		    "now.  Otherwise a fast sync is still safe...",
		'Fast Sync', 'Full Sync');
		$dlp->watchdog(0);
	    }
	    unless ($i =~ /full/i)
	    {
		$fullsync = 0;
	    }
	}
	else
	{
	    $fullsync = 0;
	}
    }

    # Check files
    %newsum = &checkFiles($dlp, %oldsum);

    # Read pilot changes
    unless ($PREFS->{'OnlyFilesToPilot'})
    {
	if ($fullsync)
	{
	    %pilot = &readAllMemos($dlp, $pilot_dbhandle);
	} else {
	    %pilot = &readChangedMemos($dlp, $pilot_dbhandle);
	}

	# Check for renamed categories
	if (defined($PREFS->{"categories"}))
	{
	    for ($i=0; $i < 16; $i++)
	    {
		if ($cats[$i] && $PREFS->{"categories"}->[$i] &&
		    ($cats[$i] ne $PREFS->{"categories"}->[$i]))
		{
		    # Category renamed! Add records to be processed:
		    #(This could be more efficient by just renaming the dir)
		    PilotMgr::msg("Processing Category Change: " .
				  $PREFS->{"categories"}->[$i] . " -> " .
				  $cats[$i]);
		    %pilot = &readMemosInCategory($pilot_dbhandle,
						  $i, {%pilot});
		}
	    }
	}
	$PREFS->{"categories"} = [@cats];
    }

    #compare and update
    $cnt = 0;
    $updates = "";

    my ($count, $count_max) = (0, scalar(keys %newsum));

    my $savedsum;

    foreach $file (keys(%newsum))
    {
	unless ($count % &fake_ceil($count_max / 20))
	{
	    PilotMgr::status("Synchronizing Memos", 
			     int (100 * $count / $count_max));
	}
	$dlp->tickle unless (++$count % 40);

	$savedsum = $oldsum{$file};

	if (!defined($oldsum{$file}) ||
	    $newsum{$file} ne $oldsum{$file})
	{
	    # Do update
	    $cnt++;
	    unless (open(FD, "<$file"))
	    {
		PilotMgr::msg("Error reading $file.");
		$newsum{$file} = $oldsum{$file};
		delete $oldsum{$file};
		next;
	    }
	    $filetext = join('', <FD>);
	    close(FD);
	    # determine category based on containing directory...
	    if ($file =~ m|/([^/]*)/[^/]*$|)
	    {
		$cat = $a3 = $1;
		$cat =~ tr/A-Z/a-z/;
		if (!defined($catcase{$cat}))
		{
		    for ($a1 = 0; $a1 < 16; $a1++)
		    {
			if (!length($cats[$a1]))
			{
			    $cats[$a1] = $a3;
			    $appinfo->{categoryName} = [@cats];
			    $catcase{$cat} = $a3;
			    $catindx{$a3} = $a1;
			    $a3 = 129;
			    foreach $a2 (@cat_ids)
			    {
				$a3 = $a2 + 1 if ($a2 >= $a3);
			    }
			    $cat_ids[$a1] = $a3;
			    $appinfo->{categoryID} = [@cat_ids];
			    PilotMgr::msg("Creating new Pilot category '" .
				$catcase{$cat} . "' with id " . $a3 . ".");
			    $i = $pilot_dbhandle->setAppBlock($appinfo);
			    if ($i < 0)
			    {
				$i = PDA::Pilot::errorText($i);
				PilotMgr::msg("** Error $i creating new " .
				    " category $catcase{$cat} on Pilot.");
			    }
			    last;
			}
		    }
		}
		$cat = $catcase{$cat};
		$cat = defined($catindx{$cat}) ? $catindx{$cat} : "";
	    }
	    $record = &fileToNeutral($piid{$file}, $cat, $filetext);
	    $fname = $file;
	    $fname =~ s|^$PREFS->{"gHomeDirectory"}/||;
	    if (defined($record->{pilot_id}) &&
		defined($pilot{$record->{pilot_id}}))
	    {
		if ($pilot{$record->{pilot_id}} eq "DELETED")
		{
		    PilotMgr::msg("Memo $fname ($record->{pilot_id}) " .
			"was deleted on the Pilot but modified on the " .
			"workstation.. recreating on Pilot..");
		}
		elsif (&recsDiffer($record, $pilot{$record->{pilot_id}}))
		{
		    $cat = $cats[$pilot{$record->{pilot_id}}->{category}];
		    if ($fname !~ m|$cat/[^/]+$|)
		    {
			# Category changed too!
			PilotMgr::msg("(Category Change: $fname)");
			unlink($file);
			delete $piid{$file};
			delete $oldsum{$file};
			delete $newsum{$file};
			$fname =~ s|^.*/||g;
			$file = "$PREFS->{gHomeDirectory}/$cat";
			$record->{category} = defined($catindx{$cat})
						? $catindx{$cat} : "";
			mkdir($file, 0755) unless (-d $file);
			if (-e "$file/$fname") {
			    $file = &mktemp($file);
			} else {
			    $file .= "/$fname";
			}
			$fname = "$cat/$fname";
			$piid{$file} = $record->{pilot_id};
		    }
		    if (&cksum($pilot{$record->{pilot_id}}->{text}) ne
			$savedsum) {
			PilotMgr::msg("Memo $fname ($record->{pilot_id}) " .
				      "modified on Pilot and workstation..");
			&mergeRecs($dlp, $record,
				   $pilot{$record->{pilot_id}});
			PilotMgr::msg("Update File: $fname");
			open(FD, ">$file");
			$filetext =  &neutralToFile($record);
			print FD $filetext;
			close(FD);
			$ret = &cksum($filetext);
			if ($ret =~ /^(\d+)\s+(\d+)/) {
			    $newsum{$file} = "$1 $2";
			} else {
			    PilotMgr::msg("Sum Error: couldn't retrieve checksum ",
					  "for $fname, using zero sum. (sum='$ret')");
			    $newsum{$file} = "0 0";
			}
			if ($pilot{$record->{pilot_id}}->{"private"} and
			    $PREFS->{"privateHandler"} eq "Use permissions:" and
			    $PREFS->{"privatePerms"} > 0) {
			    chmod oct($PREFS->{"privatePerms"}), $file;
			}
			$updates .= "$file\n";
		    }
		}
		delete $pilot{$record->{pilot_id}};
	    }
	    PilotMgr::msg("Update Pilot: $fname.");
	    $ret = &writeMemo($record, $dlp, $pilot_dbhandle);
	    if ($ret < 0)
	    {
		$ret = PDA::Pilot::errorText($ret);
		PilotMgr::msg("Error $ret writing $file to pilot.");
		$dlp->log("SyncMemo: Error $ret writing $file to pilot.");
		$newsum{$file} = $oldsum{$file};
		delete $oldsum{$file};
		next;
	    }
	    $piid{$file} = $ret;
	} elsif (defined($piid{$file})) {
	    if (($fullsync && 
		 defined($pilot{$piid{$file}}) && 
		 &cksum(&neutralToFile($pilot{$piid{$file}})) eq $oldsum{$file}) ||
		(!$fullsync &&
		 !defined($pilot{$piid{$file}})))
	    {
		# unchanged locally and unchanged on pilot; no action.
		delete $pilot{$piid{$file}} if defined $pilot{$piid{$file}};
	    } elsif ($fullsync && !defined($pilot{$piid{$file}}))
	    {
		# unchanged locally and deleted and purged on pilot
		# (this can happen if you sync with multiple
		# workstations).  Arrange for file to be deleted in
		# %pilot loop.
		$pilot{$piid{$file}} = "DELETED";
		$cnt++;
	    }
	    # Remaining case: The file is old, unchanged, has a pilot
	    # id, and exists and is modified (but not deleted) on the
	    # pilot. this will be handled during the %pilot loop.
	}
	delete $oldsum{$file} if (defined($oldsum{$file}));
    }
    # check for files to delete
    $count = 0;
    foreach $file (keys(%oldsum))
    {
	$dlp->tickle unless (++$count % 40);

	$fname = $file;
	$fname =~ s|^$PREFS->{"gHomeDirectory"}/||;

	if (defined($pilot{$piid{$file}}))
	{
	    if ($pilot{$piid{$file}} eq "DELETED") {
	        # deleted on both sides
		delete $pilot{$piid{$file}};
	    } elsif ($fullsync == 1 &&
		     &cksum(&neutralToFile($pilot{$piid{$file}})) eq 
		     $oldsum{$file})
	    {
		# Deleted on workstation and unchanged on pilot.  Delete it for
		# good, unless we're emulating the old, broken full sync behavior.
	        if (!defined($PREFS->{'emulateBrokenFullSync'}) ||
		    $PREFS->{'emulateBrokenFullSync'} eq 'Emulate old behavior') {
		    PilotMgr::msg("Memo $fname ($piid{$file}) " .
		        "was deleted on the workstation and unmodified " .
			"on the Pilot... recreating workstation file.");
		    next;
		} else {
		    delete $pilot{$piid{$file}};
		}
	    } else {
		PilotMgr::msg("Memo $fname ($piid{$file}) " .
		    "was deleted on the workstation but modified on the " .
		    "Pilot.. recreating workstation file..");
		next;
	    }
	}
	PilotMgr::msg("Delete Pilot: $fname ($piid{$file})");
	$ret = &deleteRecord($dlp, $pilot_dbhandle, $piid{$file});
	if ($ret < 0)
	{
	    $ret = PDA::Pilot::errorText($ret);
	    PilotMgr::msg("Error $ret deleting pilot record $piid{$file}!!");
	    $dlp->log("SyncMemo: Error $ret deleting pilot record " .
		      $piid{$file});
	}
	$cnt++;
    }

    # Check pilot changes
    unless ($PREFS->{'OnlyFilesToPilot'})
    {
	$count = 0;

	# First %pilot loop: handle deletes only.  In the case where a
	# memo has been deleted on the pilot and a new one created on
	# the pilot with the same first line, handling the deletion
	# before the creation lets us reuse the first line as the
	# filename, rather than causing a spurious collision that
	# results in using a filename like Memo0000.txt.

	foreach $i (keys(%pilot))
	{
	    $dlp->tickle unless (++$count % 20);

	    $cnt++;
	    if ($pilot{$i} eq "DELETED")
	    {
		if (defined($pifile{$i}))
		{
		    $file = $fname = $pifile{$i};
		    $fname =~ s|^$PREFS->{"gHomeDirectory"}/||;
		    PilotMgr::msg("Delete File: $fname.");
		    unlink($file);
		    delete $newsum{$file};
		    delete $piid{$file};
		    delete $pifile{$i};
		    $updates .= "$file\n";
		} else {
		    PilotMgr::msg("Memo $i deleted on Pilot but file not " .
				  "found.. ignored.");
		}
		delete $pilot{$i};
	    }
	}

	# Second %pilot loop: handle memos modified or created on the
	# pilot.

	foreach $i (keys(%pilot))
	{
	    $dlp->tickle unless (++$count % 20);

	    $cnt++;

	    $file = "$PREFS->{gHomeDirectory}/" .
	      $cats[$pilot{$i}->{category}];
	    if (defined($pifile{$i}))
	    {
		$fname = $pifile{$i};
		$ret = "Update";
		if ($fname !~ m|^$file/[^/]+$|)
		{
		    # Changed Category!  Delete in old location..
		    unlink($fname);
		    delete $piid{$fname};
		    delete $newsum{$fname};
		    $fname =~ s|^$PREFS->{"gHomeDirectory"}/||;
		    PilotMgr::msg("Delete File: $fname.");
		    $fname =~ s|^.*/||g;
		    mkdir($file, 0755) unless (-d $file);
		    if (-e "$file/$fname") {
			$file = &mktemp($file);
		    } else {
			$file .= "/$fname";
		    }
		    $fname = $file;
		    $ret = "New Category/Update";
		} else {
		    $file = $fname;
		}
	    } else {
		mkdir($file, 0755) unless (-d $file);
		$fname = &makeFilename($pilot{$i});
		$fname =~ s/\n.*//g;
		$fname =~ tr| '"<>()[]/;\n|____________|s;
		$file = $fname = &mktemp(($a1=$file), $fname);
		if (open(FD, ">$file"))
		{
		    close(FD);
		    unlink($file);
		} else {
		    # in case some weird character causes a filename prob
		    $file = $fname = &mktemp($a1);
		}
		$ret = "Create new";
	    }
	    $fname =~ s|^$PREFS->{"gHomeDirectory"}/||;
	    unless (open(FD, ">$file"))
	    {
		PilotMgr::msg("$ret File: $fname.\n" .
			      "Error opening $file for write!!");
		next;
	    }
	    $filetext = &neutralToFile($pilot{$i});
	    print FD $filetext;
	    close(FD);
	    if ($PREFS->{"gOnlyAscii"} && ($ret eq "Create new"))
	    {
		unless (&isPilotText($file))
		{
		    PilotMgr::msg("Skipping non-textfile from pilot (" .
				  $cats[$pilot{$i}->{category}] . ", $i, $fname)");
		    unlink($file);
		    next;
		}
	    }
	    if ($pilot{$i}->{"private"} and
		$PREFS->{"privateHandler"} eq "Use permissions:" and
		$PREFS->{"privatePerms"} > 0)
	    {
		chmod oct($PREFS->{"privatePerms"}), $file;
	    }
	    PilotMgr::msg("$ret File: $fname.");
	    $ret = &cksum($filetext);
	    $ret = '' unless defined $ret;
	    if ($ret =~ /^(\d+)\s+(\d+)/)
	    {
		$newsum{$file} = "$1 $2";
	    }
	    else
	    {
		PilotMgr::msg("Sum Error: couldn't retrieve checksum ",
			      "for $fname, using zero sum. (sum='$ret')");
		$newsum{$file} = "0 0";
	    }
	    $piid{$file} = $i;
	    $pifile{$i} = $file;
	    $updates .= "$file\n";
	}
    }
    &cleanupPilot($dlp, $pilot_dbhandle)
	unless ($PREFS->{"OnlyFilesToPilot"});

    $pilot_dbhandle->close();

    PilotMgr::msg("All files up to date.") if ($cnt == 0 && %newsum);
    PilotMgr::msg("No Memo files found.") if ($cnt == 0 && !%newsum);

    # Run post-update script
    if ($PREFS->{"gUsePostUpdater"})
    {
	PilotMgr::status("Running post-update script", 0);
	&runScript($dlp, $PREFS->{"gPostUpdater"}, "post update", $updates);
	PilotMgr::status("Running post-update script", 100);
    }

    # Write back to DBFILE
    unless (open(FD, ">$DBFILE"))
    {
	PilotMgr::msg("Error opening $DBFILE for write.");
	return;
    }
    print FD $info->{"thisSyncDate"}, "\n";
    foreach $file (keys(%newsum))
    {
	print FD "$newsum{$file} $file $piid{$file}\n";
    }
    close(FD);
}

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

sub loadPrefs
{
    my ($lines);

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

    # For some reason, we need to reference $PREFS here
    # or the preferences won't get loaded properly.
    #
    $PREFS;
}

sub savePrefs
{
    my ($var);

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

    if (open(FD, ">$RCFILE"))
    {
	if (defined &Data::Dumper::Dumpxs)
	{
	    print FD Data::Dumper->Dumpxs([$PREFS], ['PREFS']);
	}
	else
	{
	    print FD Data::Dumper->Dump([$PREFS], ['PREFS']);
	}

	print FD "1;\n";
	close(FD);
    }
    else
    {
	PilotMgr::msg("Unable to save preferences to $RCFILE!");
    }
}

sub readDBFile
{
    my (%sum, %id, %fi, $line, @lines);
    my ($lastsync) = (0);

    %sum = %id = %fi = ();
    if (open(FD, "<$DBFILE"))
    {
	@lines = <FD>;
	close(FD);
	if ($lines[0] =~ /^(\d+)$/)
	{
	    $lastsync = $1;
	    shift(@lines);
	}
	foreach $line (@lines)
	{
	    if ($line =~ /^(\d+ \d+) (.+) (\d+)$/)
	    {
		$sum{$2} = $1;
		$id{$2} = $3;
		$fi{$3} = $2;
	    }
	}
    }

    return ({%sum}, {%id}, {%fi}, $lastsync);
}

sub checkFiles
{
    my ($dlp, %oldsum) = @_;
    my (%sum, $sum, $file, $dir, @list, @dirlist);
    my ($skipStart, $skipEnd) = (quotemeta $PREFS->{'skipStart'},
				 quotemeta $PREFS->{'skipEnd'});

    opendir DIR, $PREFS->{'gHomeDirectory'};
    @dirlist = readdir DIR;
    closedir DIR;
    foreach $dir (@dirlist)
    {
	next if $dir =~ /^\.\.?$/ or not -d "$PREFS->{gHomeDirectory}/$dir";
	opendir DIR, "$PREFS->{gHomeDirectory}/$dir";
	@_ = readdir DIR;
	closedir DIR;


	if ($] > 5.007)
	{
	    require Encode;
	    if ($PREFS->{'useSkipStart'}) {
		$skipStart = Encode::encode("ascii", $skipStart, Encode::FB_CROAK());
	    }
	    if ($PREFS->{'useSkipEnd'}) {
		$skipEnd   = Encode::encode("ascii", $skipEnd,   Encode::FB_CROAK());
	    }
	    # encode will croak if the strings aren't ascii; I enforce
	    # ascii-ness in the config dialog.
	}

	foreach $file (@_)
	{
	    next unless -f "$PREFS->{gHomeDirectory}/$dir/$file";
	    next if ($PREFS->{'useSkipStart'} and $file =~ /^$skipStart/);
	    next if ($PREFS->{'useSkipEnd'} and $file =~ /$skipEnd$/);
	    push(@list, "$PREFS->{gHomeDirectory}/$dir/$file");

	    # The config dialog requires the skipStart and skipEnd
	    # strings to be ASCII-only, because a bug in perl 5.8.0
	    # (and I don't know what other versions) makes the above
	    # regexp matching fail if they have the utf-8 flag set,
	    # even if they contain only ascii characters.
	    #
	    # Here's a test script that excercises the bug:
	    #
	    #   my $a = 'foo';
	    #   print(($a =~ /$a$/) ? 1 : 0);
	    #   print "\n";
	    #
	    #   utf8::upgrade($a);
	    #   print(($a =~ /$a$/) ? 1 : 0);
	    #   print "\n";
	    #
	    # This should print two 1s, and on perl 5.8.6 it does, but
	    # on perl 5.8.0 it prints 1 the first time and 0 the
	    # second.

	}
    }

    my ($count, $count_max) = (0, scalar(@list));
    PilotMgr::status("Reading File System Memos", 0);

    foreach $file (@list)
    {
	unless ($count % &fake_ceil($count_max / 20))
	{
	    PilotMgr::status("Reading File System Memos",
			     int(100 * $count / $count_max));
	}
	$dlp->tickle unless (++$count % 20);
	
	if ($PREFS->{"gOnlyAscii"})
	{
	    unless (&isPilotText($file))
	    {
		my $fname = $file;
		$fname =~ s|^$PREFS->{"gHomeDirectory"}/||;
		PilotMgr::msg("Skipping non-textfile $fname.");
		next;
	    }
	}

	my $filetext = "";
	if (-f $file and open(IN, "<$file")) {
	    $filetext = join('', <IN>);
	    close(IN);
	} else {
	    warn "Sum error for $file: $!";
	}

	$sum = &cksum($filetext);
	$sum = '' unless defined $sum;
	if ($sum =~ /^(\d+)\s+(\d+)/)
	{
	    $sum{$file} = "$1 $2";
	}
	else
	{
	    $sum{$file} = $oldsum{$file} if (defined $oldsum{$file});
	    $file =~ s|^.*/([^/]+/[^/]+)$|$1|;
	    PilotMgr::msg("Sum Error: couldn't retrieve checksum for $file, ",
			  "using old sum.  (sum='$sum')");
	}
    }
    PilotMgr::status("Reading File System Memos", 100);

    return (%sum);
}

sub readAllMemos
{
    my ($sock, $dbhandle) = @_;
    my ($i, $record, $id, $count_max);
    my (%db);
    my $errstr;

    $i = 0;
    PilotMgr::status("Reading Pilot Memos [full sync]", 0);
    $count_max = $dbhandle->getRecords();

    while (1)
    {
	$record = $dbhandle->getRecord($i);
	if (!defined($record))
	{
	    PilotMgr::checkErrNotFound($dbhandle);
	    last;
	}

	unless ($i % &fake_ceil($count_max / 20))
	{
	    PilotMgr::status("Reading Pilot Memos [full sync]",
			     int(100 * $i / $count_max));
	}

	$i++;

	next if ($record->{"deleted"} or $record->{"archived"} or
		 $record->{"busy"} or
		 $record->{"secret"} && $PREFS->{"privateHandler"} eq "Skip");

	$id = $record->{"id"};
	$db{$id} = &pilotToNeutral($id, $record->{"category"}, $record);
    }
    PilotMgr::status("Reading Pilot Memos [full sync]", 100);

    return(%db);
}

sub readChangedMemos
{
    my ($sock, $dbhandle) = @_;
    my ($i, $record, $id);
    my (%db);
    my $errstr;

    PilotMgr::status("Reading Pilot Memos [fast sync]", 0);
    while (1)
    {
	$record = $dbhandle->getNextModRecord();
	if (!defined($record))
	{
	    PilotMgr::checkErrNotFound($dbhandle);
	    last;
	}

	next if ($record->{"secret"} and
		 $PREFS->{"privateHandler"} eq "Skip");

	$id = $record->{"id"};

	if ($record->{"deleted"} or $record->{"archived"})
	{
	    $db{$id} = "DELETED";
	}
	else
	{
	    $db{$id} = &pilotToNeutral($id, $record->{"category"}, $record);
	}
    }
    PilotMgr::status("Reading Pilot Memos [fast sync]", 100);

    return(%db);
}

sub readMemosInCategory
{
    my ($dbhandle, $categ, $current_recs) = @_;
    my ($record, $id);
    my (%db) = (%$current_recs);
    my $errstr;

    while (1)
    {
	$record = $dbhandle->getNextRecord($categ);
	if (!defined($record))
	{
	    PilotMgr::checkErrNotFound($dbhandle);
	    last;
	}

	$id = $record->{"id"};
	next if (defined($db{$id}));

	if ($record->{"deleted"} or $record->{"archived"})
	{
	    $db{$id} = "DELETED";
	}
	else
	{
	    $db{$id} = &pilotToNeutral($id, $record->{"category"}, $record);
	}
    }

    return(%db);
}

sub recsDiffer
{
    my ($a, $b) = @_;
    my (%seen, $key);

    foreach $key (keys %$a, keys %$b)
    {
	next if $seen{$key}++;
	next if (&isSpecialSyncField($key));
	return 1 unless (defined $a->{$key} and defined $b->{$key} and
			 $a->{$key} eq $b->{$key});
    }
    return 0;
}

sub writeMemo
{
    my ($record, $dlp, $pilot_dbhandle) = @_;
    my ($pi_rec, $id);

    $record->{pilot_id} ||= 0;
    $record->{category} ||= 0;

    $pi_rec = &neutralToPilot($record, $pilot_dbhandle);

    $pi_rec->{attr} ||= 0; # Why isn't this set?
    $pi_rec->{category} ||= 0; # Why isn't this set?

    $dlp->tickle;
    $id = $pilot_dbhandle->setRecord($pi_rec);

    return $id;
}

sub deleteRecord
{
    my ($dlp, $pilot_dbhandle, $record_id) = @_;
    my ($ret);

    $ret = $pilot_dbhandle->deleteRecord($record_id);

    return ($ret);
}

sub cleanupPilot
{
    my ($dlp, $pilot_dbhandle) = @_;
    $pilot_dbhandle->purge();
    $pilot_dbhandle->resetFlags();
}

sub fileMerge
{
    my ($dlp, $text1, $text2) = @_;
    my ($file1, $file2, $outfile, $outtext);
    my $ret;

    $file1 = &mktemp("/tmp", "File.txt");
    open(FD, ">$file1") || return;
    print FD $text1;
    close(FD);
    $file2 = &mktemp("/tmp", "Pilot.txt");
    open(FD, ">$file2") || do { unlink($file1); return };
    print FD $text2;
    close(FD);

    $outfile = &mktemp("/tmp", "Merge.txt");
    $dlp->watchdog(20);
    $ret = system "filemerge $file1 $file2    $outfile";
    $ret = system "gtkdiff   $file1 $file2 -o $outfile"  if ($ret == -1);
    $ret = system "tkdiff    $file1 $file2 -o $outfile"  if ($ret == -1);
    $dlp->watchdog(0);
    unlink($file1);
    unlink($file2);

    open(FD, "<$outfile") || return;
    $outtext = join('', <FD>);
    close(FD);
    unlink($outfile);

    return $outtext;
}

sub runScript
{
    my ($dlp, $cmdline, $name, $input_text) = @_;
    my ($script, $args, $bg, $tmp, $tmpfile);

    $bg = '&' if ($cmdline =~ s/\s*&$//);
    ($script, $args) = split(' ', $cmdline, 2);
    $name .= " " if ($name);
    if (-x $script)
    {
	PilotMgr::msg("Running ${name}script.");
	$dlp->watchdog(20);
	if ($input_text)
	{
	    $tmpfile = &mktemp("/tmp");
	    open(FD, ">$tmpfile") || ($tmpfile = "");
	    print FD $input_text;
	    close(FD);
	}
	if (defined $tmpfile) {
	    system "$script $args < $tmpfile $bg";
	} else {
	    system "$script $args < /dev/null $bg";
	}
	unlink($tmpfile) if (defined $tmpfile);
	$dlp->watchdog(0);
    } else {
	PilotMgr::msg("Error running ${name}script.");
    }
}

sub mktemp
{
    my ($dir, $basename) = @_;
    my ($fname, $fext) = ("Memo0000", "");

    $basename ||= "";
    return ("$dir/$basename") if ($basename && (! -e "$dir/$basename"));
    $fext = $1 if ($basename =~ /(\.\w+)$/);
    $fext ||= ".txt";

    while (1)
    {
        return ("$dir/$fname$fext") if (! -e "$dir/$fname$fext");
        $fname++;
    }
}

sub fake_ceil
{
    my ($val) = (int($_[0]));

    return 1 if ($val == 0);
    return $val;
}

sub isPilotText
{
    open(FD, "<$_[0]") or return 0;
    while (read(FD, $_, 4096))
    {
	close FD, return 0 if /[^\x09-\x7F\x83\x91-\x95\x99\xA1-\xFF]/;
    }
    close FD;
    return 1;
}

sub cksum
{
    my ($filetext) = @_;
    my ($checksum, $blocks) = (0);
    my $length = length($filetext);
    my $offset ;
    for ($offset = 0; $offset < $length; $offset += 4096) {
	$_ = substr($filetext, $offset, 4096);
	$checksum = ($checksum + unpack("%32C*", $_)) % 65535;
    }
    $checksum = 65535 if ($checksum == 0);
    $blocks = int(($length + 511)/512);
    return "$checksum $blocks";
}

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

sub pilotToNeutral
{
    my ($id, $category, $pi_rec) = @_;
    my ($record);

    $record = {
	pilot_id => $id,
	category => $category,
    };
    #Copy all relevant fields here:
    $record->{text} = $pi_rec->{text};
    $record->{private} = $pi_rec->{secret} if (exists $pi_rec->{secret});

    return($record);
}

sub neutralToPilot
{
    my ($record, $db_handle) = @_;
    my ($pi_rec) = ($db_handle->newRecord());

    $pi_rec->{id} = $record->{pilot_id};
    $pi_rec->{category} = $record->{category};
    #Copy all relevant fields here:
    $pi_rec->{text} = $record->{text};
    $pi_rec->{secret} = $record->{private} if (exists $record->{private});

    return $pi_rec;
}

sub fileToNeutral
{
    my ($id, $category, $filetext) = @_;
    my ($record);

    $record = { 'private' => 0 };
    $record->{pilot_id} = $id if ($id);
    $record->{category} = $category if (length($category) > 0);

    #Parse file and fill all relevant fields here:
    $record->{text} = $filetext;
#XXX set private to 1 if prefs is in attr mode and attrs match..
#    if this is added, remove "private" from isSpecialSyncField and
#    add ability to change perms on an existing file... (fully sync this field)

    return($record);
}

sub neutralToFile
{
    my ($record) = @_;
    my ($filetext) = ("");

    #Pull out record fields and encode into filetext:
    $filetext = $record->{text};

    return($filetext);
}

sub isSpecialSyncField
{
    my ($field) = @_;

    # Fields that should NOT be compared between Pilot and file.
    #     (ie info stored only on one side or the other, but not both)
    #     "pilot_id" should always be one of these fields.
    # return ($field eq "pilot_id" || $field eq "createdate");

    return ($field eq "pilot_id" or $field eq "private");
}

sub mergeRecs
{
    my ($dlp, $record, $pirec) = @_;
    my ($ret);

    #Merge method.  Modify fields in $record to get final result.

    $ret = "";
    if ($PREFS->{"gUseFilemerge"})
    {
	PilotMgr::msg("Running merge program..");
	$ret = &fileMerge($dlp, $record->{text}, $pirec->{text});
	PilotMgr::msg("Using merged text.") if ($ret);
    }
    unless ($ret)
    {
	PilotMgr::msg("Concatenating both versions!  Merge " .
	    "changes before your next hotsync!");
	$ret = $record->{text} .
	       "\n--- ^^ File ^^ === Version Separator === vv Pilot vv ---\n" .
	       $pirec->{text};
    }
    $record->{text} = $ret;
}

sub makeFilename
{
    my ($record) = @_;
    my ($filename) = ("");

    #Make a potential filename from the given record:
    my $extension;
    if ($record->{text} =~ /^\s*<HTML>/i)
    {
	$extension = ".html";
	if ($record->{text} =~ /<TITLE\s*[^>]*>\s*(.*)<\/TITLE\s*[^>]*>/si)
	{
	    # Try to use the title of HTML docs
	    $filename = $1;
	}
	elsif ($record->{text} =~ /(\s*<[^>]*>)*\s*([^<]*)\s*/si)
	{
	    # Use first non-html text
	    $filename = $2;
	}
    }
    unless ($filename)
    {
        $extension = ".txt";
	$filename = $record->{text};
	$filename =~ s/\n.*//g;
    }
    $filename =~ s/\s+$//;
    $filename =~ s/[\W-]+/_/g;
    $filename = substr($filename, 0, 28);
    $filename .= $extension if length($filename) > 0;
    return($filename);
}

1;
