#!/usr/bin/perl
#
# ColdSync conduit. Converts Palm Memos to a plain text format and
# back again.
#
#	Copyright (C) 2000, Andrew Arensburger.
#	You may distribute this file under the terms of the Artistic
#	License, as specified in the README file.
#
# $Id: memo-text,v 1.4 2001/02/18 06:50:39 arensb Exp $

# XXX - Write pod.

# XXX - If memo contains NUL, truncate it.

# XXX - Would be nice to replace \x95 with some other character (and
# back).

# XXX - This conduit can't create the database from scratch. It
# should.

# XXX - Headers
#	"Where: <location>" - Specifies location of memos
#	"Scheme: [all|categories|files]"
#		all - All memos in one file. "Where" must be that file
#		categories - "Where" is a directory. Each file in that
#			directory contains all memos in one category.
#		files - "Where" is a directory. It contains a set of
#			directories named after categories. Each file in
#			"Where"/<foo> is a single memo
#	"Skip: N" - Skip N lines at the beginning of the file. Those lines
#		should be preserved. This is for XPostIt notes.

#	"Delete: [yes|no|archive|expunge]" - Default is "no": don't delete
#		anything, just add/update existing memos. "yes" and
#		"archive" are synonyms.

# XXX - "__END__" terminates the file. That way, you can put Emacs
# magic lines at the end.

use strict;
use Palm::Memo;
use ColdSync;

# Set default values
%HEADERS = (
	File		=> "$ENV{HOME}/Memos",
	Delete		=> "no",		# [yes|no|archive|expunge]
						# XXX - Same as todo-text

	# XXX - Should be able to specify multiple schemes:
	# - One file containing all memos
	# - One file per category
	# - One file per memo

);

my $VERSION = (qw( $Revision: 1.4 $ ))[1];	# Cute hack for conduit version

ConduitMain(
	"fetch"		=> \&DoFetch,
	"dump"		=> \&DoDump,
);

sub DoFetch
{
	local $/;

	# XXX - Create the output database if necessary. This can
	# happen if OutputDB wasn't specified, or if this is a first
	# sync and the database doesn't exist.
	open IN, "< $HEADERS{File}" or
		die "401 Can't open \"$HEADERS{File}\": $!\n";
	$/ = "\nMemo";

	# Read the headers.
	my $header = <IN>;
	my @catlines;
	my @categories;
	my @uniqueIDs;
	my %cat2num;		# Maps category names to category number

	chomp $header;		# Remove trailing "\nMemo ";

	# Find the category lines in the header
	@catlines = grep { /^\s*Categories:/i .. /^\s*END/i }
		split /\n/, $header;
	if ($catlines[0] !~ /^\s*Categories:/i)
	{
		# XXX - This can't happen. @catlines might be empty,
		# but $catlines[0] has to be "\s*Categories:".
		warn "Missing Categories: line in header\n";
	} else {
		shift @catlines;	# Discard ".Categories"
	}
	if ($catlines[$#catlines] !~ /^\s*END/i)
	{
		warn "Missing END line in header\n";
	} else {
		pop @catlines;			# Discard ".END"
	}
	# XXX - Keep only the first 16 categories. Warn if there are more.

	# Initialize the categories
	my $i = 0;
	for (@catlines)
	{
		my $name;		# Category name
		my $id;			# Category ID

		if (!/^\s*(\d+):\s*(.*)/)
		{
			warn "Malformed category line: \"$_\", line $.\n";
			next;
		}
		$id = $1;
		$name = $2;

		$name =~ s/\s*$//;	# Trim trailing whitespace
		$cat2num{$name} = $i;	# Remember category number, for later
		push @categories, $name;
		push @uniqueIDs, $id;
		$i++;
	}
	@{$PDB->{appinfo}{categories}} = @categories;
	@{$PDB->{appinfo}{uniqueIDs}} = @uniqueIDs;

	# Now read the memos themselves
	my $memo;
	while ($memo = <IN>)
	{

		# The following substitution may seem odd (why not
		# just use "chomp $memo"?) We also want to remove the
		# trailing \n on the last memo, otherwise it'll grow
		# by a \n each time we sync.
		$memo =~ s/\Memo$//;
		$memo =~ s/\n$//;

		my $record;
		my $id = 0;			# Memo's ID
		my $category = "Unfiled";	# Memo's category name
		my $private = 0;		# Is the memo private?
		my $top;			# First line of the memo

		if ($memo !~ /\n/)
		{
			warn "Malformed memo $.\n";
			next;
		}
		$top = $`;
		$memo = $';
		$top =~ s/^\s+//;	# Trim leading whitespace
		$top =~ s/\s+$//;	# Trim trailing whitespace
		$memo =~ s/\\(.)/$1/g;	# Remove extraneous backslashes

		# XXX - Would be nice to allow \039 or \xf3 style
		# escapes. However, make sure it plays nice with the
		# other substitutions.

		my $topfield;		# Item in the top line

		# This ugly line splits $top at the commas. The
		# parenthesized expression in there is to prevent
		# splits at escaped commas: "\,".
		foreach $topfield (split /\s*(?<!\\),\s*/, $top)
		{
			if ($topfield =~ /^ID:\s*(\d+)/i)
			{
				$id = $1;
			} elsif ($topfield =~ /^Category:\s*(.*)/i)
			{
				$category = $1;
			} elsif (lc($topfield) eq "private")
			{
				$private = 1;
			} else {
				warn "Invalid memo attribute: \"$topfield\"\n";
				next;
			}
		}

		# Look up the record, or create a new one
		if ($id == 0 or
		    ($record = $PDB->findRecordByID($id)) == undef)
		{
			# Either the ID wasn't specified, it was given
			# as 0, or it doesn't exist in the database.
			# Either way, we need to create a new record.
			$record = $PDB->append_Record;
			$record->{id} = 0;	# XXX - Assign a new ID?
			$record->{attributes}{dirty} = 1;
		}

		# Mark this memo as having been seen. We do it right
		# away so that it won't be deleted if, for some
		# reason, the rest of this loop aborts, but the
		# deletion phase happens anyway.
		$record->{_seen} = 1;

		# See if the record's category has changed
		if ($cat2num{$category} != $record->{category})
		{
			$record->{category} = $cat2num{$category};
			$record->{attributes}{dirty} = 1;
		}

		# See if the "private" flag has changed.
		# The "xor" here effectively converts the two sides of
		# the expressions into booleans before comparing them.
		# The "xor" basically acts as "ne" or "!=", but is
		# more robust.
		if ($private xor $record->{attributes}{private})
		{
			# The "private" flag has changed
			$record->{attributes}{private} = $private;
			$record->{attributes}{dirty} = 1;
		}

		# The important part: see if the text of the memo has
		# changed.
		if ($memo ne $record->{data})
		{
			$record->{data} = $memo;
			$record->{attributes}{dirty} = 1;
		}
	}

	close IN;

	# XXX - Delete any records that haven't been seen (don't have
	# a "_seen" member. But only if the headers say to do so.
}

sub DoDump
{
	my $i;

	# XXX - Read the original output file. Copy the first
	# $HEADERS{Skip} lines.

	open OUT, "> $HEADERS{File}" or
		die "401 Can't open \"$HEADERS{File}\": $!\n";

	# Dump category names.
	print OUT "Categories:\n";
	for ($i = 0; $i <= $#{$PDB->{appinfo}{categories}}; $i++)
	{
		my $category = $PDB->{appinfo}{categories}[$i]{name};
		my $cat_id = $PDB->{appinfo}{categories}[$i]{id};

		next if $category eq "";
		$category =~ s/,/\\,/g;		# Escape commas
		print OUT "    $cat_id: $category\n";
	}
	print OUT "END\n";

	my $record;

	foreach $record (@{$PDB->{records}})
	{
		my @f;

		print OUT "Memo ";
		print OUT "ID: ", $record->{id};
		print OUT ", Category: ",
			$PDB->{appinfo}{categories}[$record->{category}]{name};
		print OUT ", Private" if $record->{attributes}{private};
		print OUT "\n";

		my $text = $record->{data};

		# Escape any inconvenient characters in the memo text
		$text =~ s/\\/\\\\/g;
		$text =~ s/^Memo/\\Memo/gm;	# Escape leading "Memo"
		print OUT $text, "\n";
	}

	close OUT;
}
