#!/usr/bin/perl -w

use strict;

##
## IMPORTANT !!!
##
## The libs that must be manually installed from CPAN using perl -MCPAN -eshell or perl package manager (ppm) are:
##
## Date::Calc
## DateTime::TimeZone
##
## All other libs should exist w/i the core mh lib/site dir structure
##

use lib '../lib', '../lib/site';
use iCal::Parser;
use DateTime;
use Data::Dumper;
use Digest::MD5 qw(md5 md5_hex);
use Date::Calc qw(Delta_Days);
use LWP::Simple;
use LWP::UserAgent;
use vsDB;
use vsLock;

# todo
# reminder logic
# verify todos (uninitialized string message?)
# verify locking works as expected

my $progname = "ical2vsdb";
my $progver  = "v1.0 07-08-24";
my $DB = 0;

my $days_before = 0;
my $days_after = 0;

my $config_file= "";
my $vsdb_cal_file = "calendar.tab";
my $vsdb_todo_file = "tasks.tab";
my $sleep_time = 900;                #poll cycle default is 15 minutes
my $md5file = "";

$config_file = $ARGV[0] if $ARGV[0];

&help if ((lc $config_file eq "-h") or (lc $config_file eq "--h") or ($config_file eq ""));

print "iCal to vsDB Misterhouse import ($progname $progver) starting...\n";
print "Using configuration File: $config_file\n";

my @ical_data = ();
my @master_cal;
my @master_todo;
my @changed_icals;
my $output_dir = "";
my $count = 0;
my $date_format = 'ddmm';


#------------------- Init ---------------------------

$count = &init;

if ($count == 0) {
	die "Error with Config File. Exiting";
}

# overwrite any initialized output dir if passed via the command line

&read_md5file if ($md5file ne "");

print "Processing $count iCals";
print " up to $days_after future days" if $days_after;
print " back $days_before in the past" if $days_before;
print "\nTarget vsdb directory is :$output_dir\n";
print "MD5 temp file is:$md5file\n" if ($md5file ne "");


#------------- Main Processing Loop -----------------
#BEGIN
while (1) {

 print "\nBeginning scan for calendars...\n";

 for (my $loop = 0; $loop < scalar (@ical_data); $loop++) {

  print "loop=$loop\n" if ($DB > 2);

   my $process = 0;
   my @cal_data;

   my $calendar_loc = $ical_data[$loop]->{loc};

# -------------- Calendar Scan -----------------------


   if (lc $ical_data[$loop]->{method} eq "file") {
        print "Opening File $calendar_loc...\n";
	my $in;
	open (FILE,$calendar_loc);	
	while (<FILE>) {
	   $in .= $_;
	   if (/END:VCALENDAR/) {
		push(@cal_data,$in);
		$in = "";
	   }
	}
	close (FILE);
	$process = 1 if @cal_data;
        print "@cal_data\n\n\n" if ($DB >2);

    } elsif ((lc $ical_data[$loop]->{method} eq "http") or (lc $ical_data[$loop]->{method} eq "webcal")) {
        print "Fetching http://$calendar_loc via LWP...\n";
        my $ua = new LWP::UserAgent;
	my $req = new HTTP::Request GET => "http://" . $calendar_loc;
        if ($ical_data[$loop]->{username}) {
           $req->authorization_basic($ical_data[$loop]->{username},$ical_data[$loop]->{password});
        }
        my $results = $ua->request($req);
        if (!($results->is_success)) {
           print "The calendar specified by $calendar_loc could not be retrieved: " . $results->status_line . "\n";
        } else {
           my $data = $results->content;
	   push(@cal_data,$data);
	   $process = 1;
        }
        print "@cal_data\n\n\n" if ($DB > 2);

   } else {

	print "Unknown method $ical_data[$loop]->{method}\n";
        $process=0;
   }
	

#-------- iCal Processing (if changed) -----------------

  if ($process) {

    my $data_string = join('',@cal_data);
    my $digest = md5_hex($data_string);
    print "Stored Hash=$ical_data[$loop]->{hash} File Hash=$digest\n" if ($DB > 2);


    if (!($ical_data[$loop]->{hash}) or ($ical_data[$loop]->{hash} ne $digest)) {
       print "Processing iCal $calendar_loc ...";
       print "[$ical_data[$loop]->{options}]" if $DB;
       $ical_data[$loop]->{hash} = $digest;
       my ($data_info, $data_cals, $data_todos) = parse_cal(@cal_data, $ical_data[$loop]->{options});

       push (@changed_icals, @$data_info[0]);
       push (@master_cal, @$data_cals);
       push (@master_todo, @$data_todos);
       print "done\n";
       print "Calendar Info\n" if $DB;
       print @$data_info[0] . "," . @$data_info[1] . "\n" if $DB;
       } else {
       print "iCal $calendar_loc hasn't changed in poll period, skipping processing....\n";
       }
    }
 }

#--------------- Update the vsdb database ------------------

  if (@changed_icals) {
       print "Updating databases for @changed_icals ...";

#print Dumper @master_cal;
       update_db("$output_dir/$vsdb_cal_file",\@changed_icals,\@master_cal);
       update_db("$output_dir/$vsdb_todo_file",\@changed_icals,\@master_todo);
       print "done\n";
  }

#--------------- Empty data structures and sleep ------------
  @master_cal = ();
  @master_todo = ();
  @changed_icals = ();
  if ($sleep_time) {
     sleep $sleep_time;
  } else {
     last;
  }
}

print "Process Completed.\n";
#END

#-----------------------------------------------------------------
   


sub update_db {

       my $filename = $_[0];
       my $icals = $_[1];
       my $data = $_[2];
       my $count = 0;

print "vsdb file = $filename\n\n" if $DB;
print "icals to be processed = @$icals\n\n" if $DB;

#print Dumper @icals;
#print Dumper $data;
#print "\n\n\n\n";

  my ($objDB) = new vsDB(file => $filename);
#  $objDB->Open;

# --- lock the datafile locking code ----
my ($objLock) = new vsLock(-warn => 1, -max => 5, delay => 1);

if (!$objLock->lock($filename)) { print "Problem locking file";
} else {

 if (!$objDB->Open) { print "Problem opening file!";

 } else {
# ---	
  while (!$objDB->EOF) {
     my $delete = 0;
     foreach my $ical (@$icals) {
	#print $objDB->FieldValue('EVENT') . "ical=$ical\n";
	$delete = $ical if ($objDB->FieldValue('SOURCE') =~ m/ical=$ical/i);
        }
     if ($delete) {
	print "Deleting " . $objDB->FieldValue("ID") . " source=$delete\n" if $DB;
	$objDB->Delete();
     } else {
        $objDB->MoveNext;
     }
  }

  #Add the new data
  $objDB->MoveLast;
  foreach my $item (@{$data}) {
    $objDB->AddNew;
    my ($newId) = $objDB->Max("ID") || 0;
    $newId = int($newId) + 1;
    $objDB->FieldValue("ID",$newId);
    while (my $key = each %{$item}) {
      print "Adding $key,$item->{$key}\n" if ((defined $item->{$key}) and ($DB > 2));
      $objDB->FieldValue("$key","$item->{$key}") if defined $item->{$key};
    }
  }
  $objDB->Commit;
  $objDB->Close;
  $objLock->unlock($filename)
  }
 }
# flush the hashes if md5file is enabled
&flush_md5file if ($md5file ne "");
}

sub parse_cal {

#returned value is an array of hashes
# @outcal[0]->{field}

  my @strings = $_[0];
  my $options = $_[1];
  my (@out_info, @out_cals, @out_todos);

  print "options=$options\n" if $DB;
  my $opt_speak_cal = 0;
  my $opt_speak_todo = 0;
  my $opt_holiday = 0;
  my $opt_vacation = 0;

  $opt_speak_cal = 1 if $options =~ /speak_cal/i;
  $opt_speak_todo = 1 if $options =~ /speak_todo/i;
  $opt_holiday = 1 if $options =~ /holiday/i;
  $opt_vacation = 1 if $options =~ /vacation/i;
  # the following is a bit of a hack as the options format doesn't implement a delimitter
  #   and, therefore, the optional source name can't have embedded spaces, *AND*
  #   it has to be the last option.  REALLY NEED TO MAKE OPTIONS PROPERLY PARSABLE
  my ($opt_sourcename) = $options =~ /name=(\S+)/;

  my $cal = iCal::Parser->new->parse_strings(@strings);

  my $calname = $cal->{cals}->[0]->{'X-WR-CALNAME'};
  $calname = $opt_sourcename if $opt_sourcename;

  my $caldesc = $cal->{cals}->[0]->{'X-WR-CALDESC'};
  $caldesc = "none" if !$caldesc;
  my ($lsec,$lmin,$lhour,$lday,$lmon,$lyear,$t4,$t5,$t6) = localtime();
  $lyear += 1900;
  $lmon++;
  $lsec = "00" if ($lsec == 0);
  $lmin = "00" if ($lmin == 0);
  my $source = "ical=$calname";
  if ($date_format =~ /ddmm/i) {
     $source .= " sync=$lday" . "/" . $lmon . "/" . "$lyear $lhour" . ":" . $lmin . ":" . $lsec;
  } else {
     $source .= " sync=$lmon" . "/" . $lday . "/" . "$lyear $lhour" . ":" . $lmin . ":" . $lsec;
  }
  push @out_info, $calname;
  push @out_info, $caldesc;
  my $count = 0;

  #print Dumper($cal->{events});

  while (my $year = each %{$cal->{events}}) {
    while (my $month = each %{$cal->{events}->{$year}}) {
       while (my $day = each %{$cal->{events}->{$year}->{$month}}) {
	  while (my $uid = each %{$cal->{events}->{$year}->{$month}->{$day}}) {
		my $delta;

		#is this event in range?
		if ($days_before or $days_after) {
		   $delta  = Delta_Days($lyear,$lmon,$lday, $year,$month,$day);
		   #print "db:$delta ical=$year-$month-$day local=$lyear-$lmon-$lday $cal->{events}->{$year}->{$month}->{$day}->{$uid}->{SUMMARY}\n" if $DB;
		}

		#if not then skip the item
		next if ((($days_before) and ($days_before > $delta)) or
		         (($days_after) and ($days_after < $delta)));		


		my $event_ref = $cal->{events}->{$year}->{$month}->{$day}->{$uid};
		my $starttime = "12:00 am"; # starttime and endtime are initialized the same if all day
                my $endtime = "12:00 am";
		#What is the time of the event, or is it all day?
		if (not defined ($event_ref->{allday})) { 
		   my $starthour = $event_ref->{DTSTART}->{local_c}->{hour};
		   my $am_pm = ($starthour >= 12) ? "pm" : "am";
		   $starthour = ($starthour + 11) % 12 + 1;
		   $starttime = sprintf("%2s", $starthour) . ":" . 
                         sprintf("%02s", $event_ref->{DTSTART}->{local_c}->{minute}) . " $am_pm";
		   my $endhour = $event_ref->{DTEND}->{local_c}->{hour};
		   $am_pm = ($endhour >= 12) ? "pm" : "am";
		   $endhour = ($endhour + 11) % 12 + 1;
		   $endtime = sprintf("%2s", $endhour) . ":" . 
                         sprintf("%02s", $event_ref->{DTEND}->{local_c}->{minute}) . " $am_pm";
		}

                my $reminder = '';
		if ($event_ref->{VALARM}) {
                   foreach my $alarm (@{$event_ref->{VALARM}}) {
                      if ($alarm->{when} && $event_ref->{DTSTART}) {
                         my $duration = $event_ref->{DTSTART} - $alarm->{when};
                         $reminder .= ($reminder) ? ',' : '';
                         if ($duration->delta_days) {
                            $reminder .= $duration->delta_days . 'd';
                         } else {
                            $reminder .= $duration->delta_minutes . 'm';
                         }
                      }
                   }
                }
		#MH Specific attributes
		my $holiday = "off";
		$holiday = "on" if $opt_holiday;
		my $vacation = "off";
		$vacation = "on" if $opt_vacation;

                my $description = $event_ref->{DESCRIPTION} || '';
                if ($description) {
                   $description =~ s/\*\~//g;
                   $description =~ s/\\n\*?//g;
                   $description =~ s/\\//g;
                }
                my $location = $event_ref->{LOCATION};
                $description .= ' location: ' . $location if $location && $description !~ /$location/;

		#package all the ical data up in a hash

		$out_cals[$count]->{DATE} = $year . "." . $month . "." . $day;
		$out_cals[$count]->{TIME} = $starttime;
		$out_cals[$count]->{EVENT} = $event_ref->{SUMMARY};
		$out_cals[$count]->{CATEGORY} = $event_ref->{CATEGORIES};
		$out_cals[$count]->{DETAILS} = $description;
		$out_cals[$count]->{HOLIDAY} = $holiday;
		$out_cals[$count]->{VACATION} = $vacation;
		$out_cals[$count]->{SOURCE} = $source;
		$out_cals[$count]->{REMINDER} = $reminder;
		$out_cals[$count]->{ENDTIME} = $endtime;

		$count++;
	    }

	}
     }
  }
  print "db: $count calendar records processed\n" if $DB;


  my @todos = $cal->{todos};
  $count = 0;
  foreach my $todo (@{$cal->{todos}}) {
      #print Dumper $todo;
      my $completed = "No";
      $completed = "Yes" if ($todo->{STATUS} && $todo->{STATUS} eq "COMPLETED");
      my $duedate = '';
      my $duetime = '';
      my $startdate = '';
      my $starttime = '';
      if ($todo->{DUE}->{local_c}->{day} and $todo->{DUE}->{local_c}->{month} and $todo->{DUE}->{local_c}->{year}) {
        if ($date_format =~ /ddmm/i) {
            $duedate = $todo->{DUE}->{local_c}->{day} . "/" . $todo->{DUE}->{local_c}->{month} 
                      . "/" . $todo->{DUE}->{local_c}->{year};
        } else {
            $duedate = $todo->{DUE}->{local_c}->{month} . "/" . $todo->{DUE}->{local_c}->{day} 
                      . "/" . $todo->{DUE}->{local_c}->{year};
        }
        if ($todo->{DTSTART}->{local_c}->{day} and $todo->{DTSTART}->{local_c}->{month} and $todo->{DTSTART}->{local_c}->{year}) {
           if ($date_format =~ /ddmm/i) {
              $startdate = $todo->{DTSTART}->{local_c}->{day} . "/" . $todo->{DTSTART}->{local_c}->{month}
                      . "/" . $todo->{DTSTART}->{local_c}->{year};
           } else {
              $startdate = $todo->{DTSTART}->{local_c}->{month} . "/" . $todo->{DTSTART}->{local_c}->{day}
                      . "/" . $todo->{DTSTART}->{local_c}->{year};
           }
        }
	my $endhour = $todo->{DUE}->{local_c}->{hour};
	my $am_pm = ($endhour >= 12) ? "pm" : "am";
	$endhour = ($endhour + 11) % 12 + 1;
	$duetime = sprintf("%2s", $endhour) . ":" . sprintf("%02s", $todo->{DUE}->{local_c}->{minute}) . " $am_pm";
        $duetime = '' if $duetime =~ /0:00/;
 	my $starthour = $todo->{DTSTART}->{local_c}->{hour};
	$am_pm = ($starthour >= 12) ? "pm" : "am";
	$starthour = ($starthour + 11) % 12 + 1;
	$starttime = sprintf("%2s", $starthour) . ":" . sprintf("%02s", $todo->{DTSTART}->{local_c}->{minute}) . " $am_pm";
        $starttime = '' if $duetime =~ /0:00/;
      }
      my $reminder = '';
      if ($todo->{VALARM}) {
         foreach my $alarm (@{$todo->{VALARM}}) {
            if ($alarm->{when} && $duedate) {
                my $duration = $todo->{DUE} - $alarm->{when};
                my $delta = '';
                if ($duration->delta_days) {
                   $delta = $duration->delta_days . 'd';
                } else {
                   $delta = $duration->delta_minutes . 'm';
                }
                if ($reminder !~ /$delta/) { # prevent duplicates
                   $reminder .= ($reminder) ? ',' : '';
                   $reminder .= $delta;
                }
            }
         }
      }

      my $speak = "No";
      $speak = "Yes" if $opt_speak_todo;

      my $notes = $todo->{DESCRIPTION};
      $notes = "" if !$notes;

      $out_todos[$count]->{Complete} = $completed;
      $out_todos[$count]->{Description} = $todo->{SUMMARY};
      $out_todos[$count]->{DueDate} = $duedate . (($duetime) ? ' ' . $duetime : '');
      $out_todos[$count]->{AssignedTo} = $calname;
      $out_todos[$count]->{Notes} = $notes;
      $out_todos[$count]->{SPEAK} = ($reminder) ? 'Yes' : $speak; # override speak logic if an explicit reminder exists
      $out_todos[$count]->{SOURCE} = $source;
      $out_todos[$count]->{REMINDER} = $reminder;
      $out_todos[$count]->{STARTDATE} = $startdate . (($starttime) ? ' ' . $starttime : '');
      $out_todos[$count]->{CATEGORY} = $todo->{CATEGORIES};
      $count++;
  }
  print "db: $count todo records processed\n" if $DB;
  return (\@out_info, \@out_cals, \@out_todos);
}

sub init {

  my $count = 0;
  open (CFGFILE,$config_file) || die "Error: Cannot open config file $config_file!";

  while (<CFGFILE>) {
	s/#.*//;
	next if /^(\s)*$/; #skip blank lines
	my $line = $_;
	chomp($line);
	my ($type, $url, $options) = split(/\t/,$line);
	$options = "" if !$options;
        print "db: type=$type, url=$url, options=$options\n" if ($DB > 2);	

	if (lc $type eq "output_dir" ) {
	  $output_dir = $url;
	
	} elsif (lc $type eq "days_before" ) {
	  $days_before=0-$url;

	} elsif (lc $type eq "days_after" ) {
	  $days_after=$url;

	} elsif (lc $type eq "sleep_delay" ) {
	  $sleep_time=$url;

	} elsif (lc $type eq "md5file" ) {
	  $md5file=$url;

	
	} else {

	  $ical_data[$count]->{type} = $type;
	  $ical_data[$count]->{options} = $options;
	  $ical_data[$count]->{hash} = "none";
	  
	  my ($method, $loc) = split(/:\/\//,$url);
          if ($loc) {
             my ($username, $password, $uri) = $loc =~ /^(\S+):(\S+)@(\S+)/i;
             if ($username and $password) {
                print "parsed username: $username and password: $password and uri: $uri\n" if ($DB > 2);
                $loc = $uri;
                $ical_data[$count]->{username} = $username;
                $ical_data[$count]->{password} = $password;
             }
          }
          print "db: method=$method, location=$loc\n" if ($DB > 2);
	  $ical_data[$count]->{method} = $method;
	  $ical_data[$count]->{loc} = $loc;
	  $count++;
	  }
  }
  close(CFGFILE);

  # overwrite output_dir if passed in from command line
  $output_dir = $ARGV[1] if $ARGV[1];
  $date_format = $ARGV[2] if $ARGV[2];
  $md5file = 'ical2vsdb.md5' unless $md5file;
  $md5file = $output_dir . "/" . $md5file if ($md5file ne "");

# do some sanity checking...

  print "Error: No iCals to Process!\n" if ($count == 0 );

  if (! -e "$output_dir/$vsdb_cal_file") {
    print "Error: Target Calendar file not found at $output_dir/$vsdb_cal_file!\n";
    $count = 0;
  }

  if (! -e "$output_dir/$vsdb_todo_file") {
    print "Error: Target Todo file not found at $output_dir/$vsdb_todo_file!\n";
    $count = 0;
  }

  return ($count);
}

sub help {

  print "usage $progname CONFIGURATION_FILE OUTPUT_DIR\n";
  print "\n";
  die;
}

sub read_md5file {

  if (!(-e $md5file)) {
     open (MD5FILE,"> $md5file");
     close (MD5FILE);
  }
  if ( -e $md5file ) {
    my $count = 0;
    my %md5;
    open (MD5FILE,$md5file) || die "Error: Cannot read md5 file $md5file!";

    while (<MD5FILE>) {
	my $line = $_;
	chomp($line);
	my ($loc, $hash) = split(/\t/,$line);
	$md5{$loc} = $hash;
    }

    for (my $loop = 0; $loop < scalar (@ical_data); $loop++) {
      $ical_data[$loop]->{hash} = $md5{$ical_data[$loop]->{loc}};
      print "reading $ical_data[$loop]->{hash}=$md5{$ical_data[$loop]->{loc}}\n" if $DB;
    }
    close (MD5FILE);
  }
}

sub flush_md5file {

  my %md5;
  open (MD5FILE,">$md5file") || die "Error: Cannot write md5 file $md5file!";

  for (my $loop = 0; $loop < scalar (@ical_data); $loop++) {
   print MD5FILE "$ical_data[$loop]->{loc}\t$ical_data[$loop]->{hash}\n";
   print "writing $ical_data[$loop]->{loc}\t$ical_data[$loop]->{hash}\n" if $DB;
  }
  
  close (MD5FILE);
}

