#!/usr/bin/perl -w

use PDA::Pilot;
use DBI;
use Data::Dumper;
use Getopt::Long;
use Time::Local;

use strict;
# 
# Übertragung mit 57600 Baud
#
$ENV{'PILOTRATE'} = 9600*6;


my ($debug, $simulate, $replace, $help);

GetOptions("d|debug" => \$debug,
	   "s|simulate" => \$simulate,
	   "r|replace=s" => \$replace,
	   "h|help" => \$help);

if ($help) {
  print <<USAGE;
 Benutzung: $0 
           (-d|--debug) (-s|--simulate) (-r|--replace=[host|pilot])

 Adressen zwischen Pilot und Host synchronisieren.

 -d|--debug        Alle Aktionen protokollieren
 -s|--simulate     Alle Aktionen protokollieren, aber nicht ausführen
 -r|--replace      host:  Daten auf dem Host durch Palm-Daten ersetzen
                   pilot: Daten auf dem Palm durch Host-Daten ersetzen
 -h|--help         Diese Hilfe anzeigen

 Wenn -r nicht angegeben ist, bleiben Adressen, die seit der letzten 
 Synchronisierung auf beiden Rechnern geändert wurden, unverändert.
USAGE
exit(0);
} # if help

if ($replace) {
  die "-r|--replace kann nur die Werte 'host' und 'pilot' annehmen" 
    unless $replace =~ /host/ || $replace =~ /pilot/;
}

#
# Alle Feldnamen des Piloten
#
my @pilot_entries = ("Last Name", "First Name","Company",
		     "Phone1","Phone2","Phone3","Phone4","Phone5",
		     "Address","City","State","Zip","Country","Title",
		     "Custom1","Custom2","Custom3","Custom4","Note");

# 
# Zuordnung von Feldern im PalmPilot zu DB-Feldern
# Bei den Telefonfeldern des Piloten benutzen wir hier
# die "Labels", also nicht "Phone1..n".
#
my %pilot_to_DB = ("Last Name"  => "name",
                   "First Name" => "vorname",
                   "Company"    => "firma",
                   "Work"       => "telgesch",
                   "Home"       => "telpriv",
                   "Fax"        => "fax",
                   "E-mail"     => "email",
                   "Mobile"     => "telhandy",
                   "Other"      => "telandere",
                   "Address"    => "strasse",
                   "City"       => "ort",
                   "Zip"        => "plz",
                   "Country"    => "land",
                   "Title"      => "titel"  ,
                   "Pager"      => "telhandy",
                   "Main"       => "telgesch",
                   "State"      => "bundland",
		   "Custom1"    => "anrede",
		   "category"   => "kategorie");

#
# Konstanten für DB-Zugriff: Treibername, Datenbank, 
# Benutzer, Passwort und Tabelle
#
my $RDBMS = "mysql";
my $DB    = "ck";
my $USER  = "ck";
my $PWD   = undef;
my $TABLE = "adressen";
#
# Name des Primary Key in der DB. Später für UPDATE etc. erforderlich
#
my $primary_key = "nummer"; 
#
# DB-Feld zur Speicherung der Kategorie-Nr.
#
my $category = $pilot_to_DB{'category'};

#
# DB-Feld mit Timestamp der letzten Änderung
#
my $timestamp = "lastmod";

#
# Verbindung zum Piloten herstellen
#
my $socket = PDA::Pilot::openPort("/dev/pilot");

print "Bitte HotSync starten ...\n";

#
# Ein paar Daten auslesen, vor allem Telefontypen (@phone_labels) 
# und Kategorien (@category_labels)
#
my $dlp = PDA::Pilot::accept($socket);

my $uinfo = $dlp->getUserInfo();
my $lastsync = $uinfo->{'successfulSyncDate'};

print "last synched on " , scalar localtime($lastsync), "\n" 
  if $debug || $simulate;
my $db = $dlp->open("AddressDB");

$dlp->getStatus();

my $app = $db->getAppBlock;
my $dbi = $dlp->getDBInfo(0,0,1);


my @phone_labels = @{$app->{'phoneLabel'}};

my @default_phone;

@default_phone = (0, 1, 2, 4, 7);

my @category_labels = @{$app->{'categoryName'}};

print "phonelabels: @phone_labels\n" if $debug || $simulate;

my %category_numbers; # Schlüssel: Category-Namen, Werte: Kategorienummer
my %number_categories; # Schlüssel: Kategoriennummer
my $i = 0;
foreach (@category_labels) {
  next unless $_;
  $category_numbers{$_} = $app->{'categoryID'}[$i];
  $number_categories{$app->{'categoryID'}[$i]} = $i;
  $i++;
}

print "\%category_numbers: " . 
  Dumper(%category_numbers) .
  "\n\@category_labels: @category_labels\n" 
  if $simulate;

$i = 0;
my %recordlist;
my $r;
my %deleted = ();
#
# Alle Daten aus dem Pilot-AdressDB holen und in lokalem Hash speichern
#
while (defined($r = $db->getRecord($i++))) {
  
  # Eintrag ist auf dem Piloten gelöscht: 
  # ID als Schlüssel und Archivflag als Wert in %deleted speichern

  if ($r->{'deleted'}) {
    $deleted{$r->{'id'}} = $r->{'archived'};
    next;
  }
  print Dumper($r) if $debug ;
  my @entries = @{$r->{'entry'}};
  my $id = $r->{'id'};
  my $j=0;
  my ($val,$field);
  $recordlist{$id} = {};
  my %entrylist = ();
  my @fieldnames = ();
  foreach $val (@entries) {
    $field = $pilot_entries[$j];
    $field = findphone($r,$1) 
      if ($field =~ /Phone(\d)/) ;
    #
    # Bereits vorhandene Einträge nicht überschreiben!
    #
    $entrylist{$field} = $val unless
      $entrylist{$field};
    $j++;
  }
  $entrylist{'modified'} = $r->{'modified'};
  $entrylist{'phoneLabel'} = $r->{'phoneLabel'};
  $entrylist{'showPhone'} = $r->{'showPhone'};
  $entrylist{'category'} = $r->{'category'};
  $recordlist{$id} = \%entrylist;
}				# while defined $r

print "Got " . scalar(keys %recordlist) . " records from Pilot\n",
  " delete list: " , Dumper(%deleted)
  if $debug || $simulate;

#print Dumper(%recordlist) if $debug;
$dlp->tickle; 

foreach (sort keys %recordlist) {
  print Dumper ($recordlist{$_});
}  

$dlp->tickle; 

my $dbh = DBI->connect("DBI:$RDBMS:$DB","$USER", $PWD);

#
# alle vom Piloten gelöschten Einträge, die kein Backup-Bit gesetzt haben,
# vom Host entfernen
#

my @ids = grep { ! $deleted{$_} } keys %deleted ;
my $query;
if (scalar(@ids)) {
  $query = "DELETE FROM $TABLE WHERE $primary_key IN ( ".
  join (',', @ids) . ")";

  $dbh->do($query) unless $simulate;

  print "Deleting: $query\n" if $debug || $simulate;
}

#
# Jetzt alle als gelöscht markierten Einträge vom Piloten beseitigen,
# für die das Archivbit nicht gesetzt ist. Von denen *mit* Archivbit bleiben
# Rudimente auf dem Piloten erhalten, so dass beim nächsten Sync nicht wieder
# das Original vom Host übertragen wird.
#
foreach (@ids) {
  $db->deleteRecord($_) unless $simulate;
  print "$_ deleted from pilot\n" if $simulate || $debug;
}

#
# Hash für DB-Datensätze des Host vorbereiten
# Schlüssel sind die Feldnamen,
# Werte sind zunächst undefiniert
#

my %DB_record = map { ($_, undef) } values(%pilot_to_DB);
$DB_record{$primary_key} = undef;
$DB_record{$category} = undef; 
$DB_record{$timestamp} = undef;

# @DB_fields enthält die Schlüssel aus %DB_record in einer definierten
# Reihenfolge, so dass man dieses Feld benutzen kann, um die Spalten 
# der DB-Abfrage an die korrekten Einträge in %DB_record binden kann.

my @DB_fields = sort keys(%DB_record);

$query = "SELECT " . join (',',(@DB_fields)) . 
   " FROM $TABLE order by $primary_key";


#     Jetzt über alle Einträge der Host-DB laufen

my $sth = $dbh->prepare($query);
$sth->execute();
$i = 1;
foreach (@DB_fields) {
   $sth->bind_col($i++,\$DB_record{$_});
}

while ($sth->fetch()) {
   $dlp->tickle;
   my $id = $DB_record{$primary_key};
   my $k;
   my $modsec = stamptoepoch($DB_record{$timestamp});

   # Eintrag fehlt auf dem Piloten und ist nicht in der Liste der
   # gelöschten enthalten -> Kopieren

   if (! exists($recordlist{$id}) && !exists($deleted{$id})) { 
     
     host_to_pilot($db,$DB_record{$primary_key},\%DB_record);

   } else { 
     #
     # Eintrag existiert auf Piloten und dem Host
     #
      my $r = $recordlist{$id};
      $dlp->tickle; # Piloten kitzeln, es könnte etwas dauern
      #
      # Eintrag auf Pilot geändert
      #
      if ($r->{'modified'}) {
	#
	# Letzte Synchronisierung *nach* Änderung dieses
	# Eintrags auf dem Host:
	# Host-Eintrag seit Synchro unverändert, also
	# vom Piloten kopieren
	#
	if ($lastsync > $modsec) {
          pilot_to_host($dbh, $id, \%DB_record, $r);
	} else {
	  # 
	  # Eintrag auf Pilot geändert, aber seit der letzten Synchro
	  # auch auf dem Host. Je nach Setzung 
	  #
          if ($replace =~ /host/) {
            $db->deleteRecord($id);
	    pilot_to_host($dbh, $id,\%DB_record, $r);
	  } elsif ($replace =~ /pilot/) {
	    host_to_pilot($db,$DB_record{$primary_key},\%DB_record);
	  } else {
	    warn "Datensatz $DB_record{$primary_key} auf Pilot und Host ".
	      "geändert, aber 'replace'-Parameter nicht angegeben.\n" .
		"Datensatz nicht synchronisiert";
	  }
	}
      } elsif ($modsec > $lastsync) { 
	#
	# Eintrag auf Piloten nicht geändert
	# Wenn auf dem Host Veränderung seit letzter Synchro,
	# zum Piloten kopieren
	#
	print "Updating record for $id on Pilot ",
        "Hosttime: $DB_record{$timestamp} ", 
	"Palmtime: " , scalar localtime($lastsync),
	"modsec:   $modsec\n",
	"lastsync: $lastsync\n",
	"\n",
	"host is : ", Dumper(%DB_record), "\n",
	"pilot is : ", Dumper($r) if $debug || $simulate;

	$db->deleteRecord($id) unless $simulate;
	print "Deleted record for id=$id from Pilot\n" if $debug || $simulate;
	
	host_to_pilot($db,$id,\%DB_record);
      } elsif ( (! $DB_record{$category}) && $r->{'category'}) {
	$query = "update $TABLE set $category = ".
	  $category_numbers{$category_labels[$r->{'category'}]} ." WHERE " .
	    "$primary_key= $id";
	$dbh->do($query) unless $simulate;
	print "$query\n" if $simulate || $debug;
      }
      #
      # Diesen Eintrag aus der Liste der Pilotadressen löschen
      #
      delete($recordlist{$id});
    }
 } # while $sth->fetch

print Dumper(%recordlist) if $debug;


# Alle Einträge, die jetzt noch in %recordlist enthalten sind,
# fehlen in der Host-Datenbank. 
# Für jeden von ihnen drei Schritte
# Neuen Eintrag in Host-DB erzeugen
# Alten Eintrag in Palm-DB löschen
# Daten mit dem neu vergebenen Primary Key in Palm anlegen

my $k;
foreach $k (keys %recordlist) {
  $dlp->tickle;
  my $r = $recordlist{$k};
  $db->deleteRecord($k) unless $simulate;
  print "Deleted record for id=$k from Pilot\n",
  Dumper($r), "\n" if $debug || $simulate;
  my @values = ();
  my %fields = ();

  my $p;
  foreach (@pilot_entries) { # loop over all fields
    $p = $_;
    $p = findphone($r,$1) if (/^Phone(\d)$/);

    my $val = $r->{$p};
    
    if ($val && exists($pilot_to_DB{$p})) {
      $val =~ s/^\s+//;
      $val =~ s/\s+$//;
      $fields{$pilot_to_DB{$p}} = $_ eq "category" ? $val :  "'$val'" ;
    }
  }
  #
  # Pathologischer Fall: Leerer Datensatz
  #
  next unless scalar(keys %fields);
  my $query = "insert into adressen ( " . 
    join(',',sort (keys %fields)) . ") values (" .
    join(',',  map { $fields{$_}} sort (keys %fields))  . ")";
	 
  print "Inserting: $query\n" if $debug || $simulate;

  $dbh->do($query) unless $simulate;

  my $copy = $db->newRecord;
  $copy->{'id'}         = $sth->{'mysql_insertid'};
  $copy->{'entry'}      = $r->{'entry'};
  $copy->{'phoneLabel'} = $r->{'phoneLabel'};
  $copy->{'showPhone'}  = $r->{'showPhone'};
  $copy->{'category'}   = $r->{'category'};

  print Dumper($copy) . "\n\n" if $debug || $simulate;
  $db->setRecord($copy) unless $simulate;
}


# 
# Kategorien-Tabelle aktualisieren. Erst alle Einträge löschen,
# dann die aktuellen Werte vom Piloten kopieren
#
if (!$simulate) {
   $dbh->do("delete from kategorien");
   $query = "insert into kategorien values (?,?)";
   $sth = $dbh->prepare($query);
   foreach (keys %category_numbers) {
     $sth->execute($category_numbers{$_},$_);
   }
} else {
  foreach (keys %category_numbers) {
    print "insert into kategorien values ($category_numbers{$_}, $_)\n";
  }
}


$dbh->disconnect;

if (! $simulate) {
  $dlp->log("MySQL-Sync done");
  $db->resetFlags();
  $uinfo->{'lastSyncDate'} = time();
  $uinfo->{'successfulSyncDate'} = time();
  $dlp->setUserInfo($uinfo);
}
$dlp->close();
undef $db;
undef $dlp;


sub findphone { # expect address record on input (or at least a hash reference with the
                # key "phoneLabel") and the digit following "Phone"
  my ($r,$no) = @_;
  my $ind = $r->{'phoneLabel'}[$no - 1];
  return $phone_labels[ $ind ];
}


sub stamptoepoch {
  my $modtime = shift;
  return timelocal(substr($modtime,12,2),
		   substr($modtime,10,2),
		   substr($modtime,8,2),
		   substr($modtime,6,2),
		   substr($modtime,4,2) - 1,
		   substr($modtime,0,4) - 1900);
}


sub pilot_to_host {
  my ($dbh, $id, $hostrec, $pilotrec) = @_;

  my @values = ();
  foreach (@pilot_entries) { # über alle Felder laufen
    $k = $_;
    
    $k = findphone($pilotrec,$1) if (/^Phone(\d)$/);
    my $f = $pilot_to_DB{$k}; # $f = DB-Feld, 
    #$k = Feld in pilotrec{$id}
    #
    # Pilot-Felder ohne Entsprechung auf dem Host auslassen
    #
    next unless $f;
    
    # Werte der einzelnen Felder vergleichen
    # Unterschiedliche  werden per push in @values gepackt
    
    if ($r->{$k} && ($hostrec->{$f} ne $r->{$k})) {
      push @values, "$f = '$r->{$k}'";
    } # if DB_record ...
  } # foreach @pilot_entries
  
  if ((!$hostrec->{$category}) ||
      $hostrec->{$category} != $pilotrec->{"category"}) {
    push @values, 
    "$category = $category_numbers{$category_labels[$pilotrec->{'category'}]}";
  }
  #
  # Pathologischen Fall abfangen: Das Modified-Flag war zwar
  # gesetzt, es wurde aber nix geändert
  #
  if ($#values >= 0) {
    #
    # Insert into DB on host
    #
    my $query = "update adressen set " . join(',',@values) . 
      " where $primary_key = $id";
    print "$query\n" if $debug || $simulate;
    $dbh->do($query) unless $simulate;
  }
}
  
sub host_to_pilot {
  my ($db, $id, $record) = @_;
  my (@entry,$k);
  my $newr = $db->newRecord;

  print "host_to_pilot, id: $id, record: ", Dumper($record) , "\n"
     if $debug || $simulate;
  foreach (@pilot_entries) {
    $k = $_;
    $k = $phone_labels[$1-1] if /^Phone(\d)$/;
    my $f = $pilot_to_DB{$k};
    push @entry, ($f && $record->{$f} ? $record->{$f} : '');
  }
  
  $newr->{'id'} = $id;
  $newr->{'entry'} = \@entry;
  $newr->{'phoneLabel'} = \@default_phone;
  $newr->{'showPhone' } = 0;
  $newr->{'category'} = $record->{$category} ? $number_categories{$record->{$category}} : 0;
  print "New Pilot is: ", Dumper($newr) , "\n\n" 
	  if $debug || $simulate;
  $db->setRecord($newr) unless $simulate;
}
