#!/usr/bin/perl -w
###
# wat - what a todo
###
# $Id: wat,v 1.5 2001/07/29 15:00:07 danny Exp $
###
#
# WAT
# ===
# WAT syncs a Palm todo list with a todo list held as a text file.
# Changes in either will be mirrored in the other.

#####
# External requirements: PDA::Pilot, Data::Dumper;
#####
use PDA::Pilot;
use Data::Dumper;

#####
# Global settings
#####
$VERSION  = "0.01";
if (($#ARGV == 0) && ($ARGV[0]="-V")) {
        print "wat $VERSION\n";
        exit
    }
$TODOFILE = "~/.wat";
$TODOFILE =~
s{^~([^/]*)}{$1?(getpwname($1))[7]:($ENV{HOME}||($ENV{LOGDIR}||(getpwuid($>)))[7])}ex;

$DEBUG=$^D;
$DONT_SHOW_COMPLETED = 1;
$PILOT_DEV="/dev/pilot";
# open todo file
# parse todo file into data structure

my ( $pc_inorder, $pc_byid ) = parse_todofile($TODOFILE);

# structure of todo file
# <X for done> <priority> description
# memo data
# %id number

# set autoflush
select( ( select(STDOUT), $| = 1 )[0] );

# open connection to palm
print "Now press the HotSync button\n";
while (!(open FILE, ">$PILOT_DEV")) {
}
close FILE;
my $socket = PDA::Pilot::openPort("$PILOT_DEV");
my $dlp = PDA::Pilot::accept($socket);
my $db  = $dlp->open("ToDoDB");

# for each record on the palm
#   load into a database (seems to get confused if we try
#                         getting records *and* changing them)
#
# for each record in the palm db
#   if it's completed (and we're not processing completed), skip
#   if it's new , add it to the pc database
#   if it's existent, check it's the same as the pc db
#       if it's different
#                syncronize the two records
#   mark record as "seen" in data structure

my $pc_record;
my $palm_db = [];
print "Loading ToDo Items: ";
while ( defined( my $palm_record = $db->getRecord( $i++ ) ) ) {
    if ( $DONT_SHOW_COMPLETED && $palm_record->{'complete'} ) {
        print ".";
        next;
    }
    push @$palm_db, $palm_record;
    print "#";
}
print "\nSyncing ToDo Items:\n";

foreach $palm_record (@$palm_db) {
    my $id = $palm_record->{'id'};
    $pc_record = $pc_byid->{"$id"};
    if ( !defined $pc_record ) {    # new record on palm?
        if ($palm_record->{'deleted'}) { # removed?
            next;
        }
        print "PC append: $id:$palm_record->{'description'}\n";
        unshift @$pc_inorder, $palm_record;
        $palm_record->{'seen'} = 1;
        $$pc_byid{$id} = $palm_record;
        next;
    }
    sync_record( $palm_record, $pc_record );
    $pc_record->{'seen'} = 1;
}

#
# for each record in the pc data structure that hasn't been seen
#   if it's got an id number, mark it as "deleted"
#   if it hasn't, add it to palm

foreach $pc_record (@$pc_inorder) {
    if ( defined $pc_record->{'seen'} ) { next; }
    if ( $pc_record->{'id'} ) {
        print "PC   delete: $pc_record->{'description'}\n ";
        $pc_record->{'deleted'} = 1;
    }
    else {
        print "Palm adding: $pc_record->{'description'}\n";
        my $new_record = $db->newRecord(0,0,1);
        foreach $k ( keys %$pc_record ) {
            if ($k eq "seen") { next; };
            $new_record->{$k} = $pc_record->{$k};
            }
         # Set record consistently returns with a
         # "" not numeric warning, but I can't see why.
         # Still, it seems to work, so silence it.
         no warnings; 
         $pc_record->{'id'}=$db->setRecord($new_record);
    }
}

# write the data structure back out (taking care to preserve order)
write_todofile( $pc_inorder, "$TODOFILE.new" );
system "/bin/cp", "-f",$TODOFILE, "$TODOFILE~";
system "/bin/mv", "$TODOFILE.new", "$TODOFILE";

# sync_record
# in: $palm_record - palm data
#     $pc_record  - pc data
# out: $pc_record is updated to reflect changes in both
#
# here are the rules
# pc has precedence
# a tick in the palm "complete" category gives precedence to palm
# a tick in the palm "modified" status gives precedence to palm
# a tick in the palm "deleted" status gives precedence to palm
sub sync_record {
    my ( $palm_record, $pc_record ) = @_;
    my $needs_syncing;

    if ($DEBUG) {
        print STDERR "SYNCING RECORDS $pc_record->{'description'}\n";
    }
    foreach $k ( keys %$pc_record ) {
        next if ( !defined $palm_record->{$k} );

        if ( $palm_record->{$k} ne $pc_record->{$k} ) {
            next if ( $k eq "seen" );
            $needs_syncing = 1;
        }
    }
    return unless $needs_syncing;

    my $precedence = "pc";
    if ( $palm_record->{'complete'} eq 1 ) { $precedence = "palm"; }
    if ( $palm_record->{'modified'} eq 1 ) { $precedence = "palm"; }
    if ( $palm_record->{'deleted'} eq 1 )  { $precedence = "palm"; }

    if ( $pc_record->{'complete'} eq 1 ) { $precedence = "pc"; }
    if ( $pc_record->{'deleted'} eq 1 )  { $precedence = "pc"; }

    if ( $precedence eq "palm" ) {
        print STDERR "PC  change: $pc_record->{'description'}\n";
        foreach $k ( keys %$palm_record ) {
            $pc_record->{$k} = $palm_record->{$k};
        }
    }

    if ( $precedence eq "pc" ) {
        print STDERR "Palm change: $pc_record->{'description'}\n";
        foreach $k ( keys %$pc_record ) {
            next if ( $k eq "seen" );
            $palm_record->{$k} = $pc_record->{$k};
        }
        $db->setRecord($palm_record);
    }
}

# write_todofile
# in: $order_of_ids - an array of pointers to record hashes (with a
# significant order)
#     $todo_fname - filename to write to (allow us to write to a tmp file)
#
# outputs a (hopefully) parseable todo file. can be set with
# DONT_SHOW_COMPLETED to not save finished itesm
#
sub write_todofile {
    my ( $order_of_ids, $todo_fname ) = @_;
    open TODO, ">", "$todo_fname" or die "$!: Couldn't open $todo_fname\n";

    foreach $i (@$order_of_ids) {
        if ( $i->{'deleted'} ) {
            next;
        }
        if ( $i->{'complete'} ) {
            if ($DONT_SHOW_COMPLETED) { next; }
            print TODO "X ";
        }
        else {
            print TODO "  ";
        }
        $i->{'description'} =~ s/\n/\\/g;
        print TODO "$i->{'priority'} ";
        print TODO "$i->{'description'}\n";
        if ( $i->{'note'} ) { print TODO "$i->{'note'}\n"; }
        print TODO "%$i->{'id'}\n";
    }
}

sub TEST_writetodofile {
    my ( $r, @m );
    while ( defined( $r = $db->getRecord( $i++ ) ) ) {
        push @m, $r;
    }
    write_todofile( \@m, $TODOFILE );
}

# parse_todofile
# in: $todo_fname - filename to read from
# out: ref to array of pointers to record hashes (with significant order)
#      ref to hash of pointers to record hashes (indexed by id)
sub parse_todofile {
    my $todo_fname = shift;
    if (! -e $todo_fname) { 
        open TODO, ">$todo_fname" or die "$!: Couldn't create $todo_fname";
    }
    open TODO, "<$todo_fname" or die "$!: Couldn't open $todo_fname";
    my $inside_memo;
    my $memo = "";
    my $new_todo;
    my @todo_by_order;
    my %todo_by_id;

    while (<TODO>) {
        if (/^[CD X] [0-9]/) {    #top line of a todo entry
            $new_todo = {};
            parse_topline( $new_todo, $_ );
            $inside_memo = 1;
            $memo        = "";
            next;
        }

        if (/^%([0-9]*)$/) {    # idnumber (and end of todo entry
            $new_todo->{'id'} = $1?$1:0;
            chomp( $new_todo->{'note'} = $memo );
            $todo_by_id{"$1"} = $new_todo;
            push @todo_by_order, $new_todo;
            undef $inside_memo;
            next;
        }

        if ($inside_memo) {
            $memo .= $_;
            next;
        }
    }
    return ( \@todo_by_order, \%todo_by_id );
}

sub TEST_parse_todofile {
    my ( $todo_byorder, undef )= parse_todofile("/home/danny/tmp/todo");
    print Dumper($todo_byorder);
    write_todofile( $todo_byorder, "/home/danny/tmp/todo.copy" );
}

# parse_topline
# in: $r = ref to todo record hash
#     $line = top line of text entry in form
#      <X|D| > <0-9> <Description>
sub parse_topline {
    my ( $r, $line ) = @_;
    my ( $status, $priority, $desc );
    ( $status, $priority, $desc ) = ( $line =~ /^([XD ]) ([0-9]+) (.*)$/ );
    if ( $status eq "X" ) {
        $r->{'complete'} = 1;
    }
    elsif ( $status eq "D" ) {
        $r->{'deleted'} = 1;    # TODO TODO TODO FIXME
    }
    elsif ( $status eq " " ) {
        $r->{'complete'} = 0;
    }
    $desc =~ s/\\/\n/g;
    $r->{'priority'}    = $priority?$priority:0;
    $r->{'description'} = $desc;
}

sub TEST_parse_topline {
    my $r = {};
    parse_topline( $r, "  1 Hello this is a description" );
    print Dumper($r);
    $r = {};
    parse_topline( $r, "X 3 Tooty wooty big boy cuty" );
    print Dumper($r);
    $r = {};
    parse_topline( $r, "D 99 This is a weird, but acceptable entry" );
    print Dumper($r);
}

