#!/usr/bin/perl
#
# ical-sort  --  smart chronological sort of iCalendar files
#                 (useful for ownCloud backups and diffs)
#
# Dr. Andy Spiegl, (owncloud.andy@spiegl.de)
#
################################################################################
#
# Copyright 2013 Dr. Andy Spiegl, (owncloud.andy@spiegl.de)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# http://www.opensource.org/licenses/mit-license.php
#
################################################################################
#
# History:
#
# v0.1  2013-05-22: spinoff of ownCloudCalendar-backup
#
############################################
#
# Usage:
#  ical-sort <INPUTFILE>
#
# Output:
#  sorted outputfile: $INPUTFILE.sorted
#
############################################
#
# DESCRIPTION:
#  sort tags inside VEVENTs alphabetically
#  sort list of VEVENTs chronologically:
#   - single events sorted by DTEND
#   - repeated DAILY events sorted by UNTIL or DTEND + INTERVAL*COUNT
#   - repeated WEEKLY events sorted by UNTIL or DTEND + INTERVAL*COUNT weeks
#   - repeated MONTHLY events sorted by UNTIL or DTEND + INTERVAL*COUNT months
#   - repeated YEARLY events sorted by UNTIL or DTEND + INTERVAL*COUNT years
#   - repeated events without "UNTIL" are put at the end
#
############################################

my $VERSION = "0.1";

use strict;
use warnings;

use Date::Manip;

# security for shell calls:
$ENV{'PATH'} = '/bin:/usr/bin';
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};


############################################
# some self detection
############################################
my $self = $0; $self =~ s|.*/||;

my $TMPDIR = $ENV{'TEMP'} || $ENV{'TMPDIR'} || "/tmp";


############################################
# configurable VARIABLES
############################################

my $debug = 0;


############################################
# main program
############################################

my $INPUTFILE = shift or die "ERROR: no inputfile specified";
my $OUTPUTFILE = $INPUTFILE .".sorted";

&Date_Init("DateFormat=non-US");

if (! open (OCFILE, $INPUTFILE))
{
  print "ERROR while reading inputfile \"$INPUTFILE\": $!\n";
  exit 11;
}

my @calendarICAL = <OCFILE>;
close OCFILE;

# sort the VCF lines alphabetically (for better diffs)
my $calendarICAL = &ical_make_pretty(@calendarICAL);

# save to new file
print "Writing output file \"$OUTPUTFILE\"\n"  if $debug > 0;
if (not open(OUTPUTFILE, ">$OUTPUTFILE"))
{
  print "ERROR: can't write \"$OUTPUTFILE\": $!";
  exit 21;
}
print OUTPUTFILE $calendarICAL;
close OUTPUTFILE;

exit;


# sort (see DESCRIPTION above) and put newlines between VEVENT blocks
sub ical_make_pretty()
{
  my (@calendar) = @_;

  my ($line, $i, %ical, $vevent, %vevents, %vevents2, $sortdate, $dateErr, $interval);
  my $calendar='';
  my $header='';
  my $footer='';
  my $num=0;
  my $index=0;
  my $repeatedEvent='';
  my $insideEvent=0;
  my $insideAlarm=0;
  my $alarm='';
  my $eventend = new Date::Manip::Date;

  foreach $line (@calendar)
  {
    $line =~ s/\r$//;
    chomp $line;

    if (not $insideEvent)
    {
      next if $line =~ /^$/;      # skip empty lines outside VEVENTs

      if ($line eq 'BEGIN:VEVENT')
      {
        $insideEvent = 1;
      }
      else
      {
        # e.g. VCALENDAR properties
        if ($num == 0)
        {
          $header .= $line . "\n";
        }
        else
        {
          $footer .= $line . "\n";
        }
      }
    }

    else # inside VEVENT area
    {
      $repeatedEvent = $line  if $line =~ /^RRULE:/;

      # DTEND;VALUE=DATE:19670118
      if ($line =~ /^DTEND;VALUE=DATE:(.*)/)
      {
        my $d = $1;
        $dateErr = $eventend->parse($d);
        if ($dateErr) { print "ERROR: unknown date format \"$d\": $dateErr\n"; exit 101 }
        print "DEBUG: ". $line."\n ". $eventend->printf("event end (DTEND) is %Y-%m-%d %T.\n")  if $debug;
      }
      # DTEND;VALUE=DATE-TIME:20050215T050000Z
      elsif ($line =~ /^DTEND;VALUE=DATE-TIME:(\d+T\d+Z)/)
      {
        my $d = $1;
        $dateErr = $eventend->parse($d);
        if ($dateErr) { print "ERROR: unknown date format \"$d\": $dateErr\n"; exit 102 }

        print "DEBUG: ". $line."\n ". $eventend->printf("event end (DTEND) is %Y-%m-%d %T.\n")  if $debug;
      }
      elsif ($line =~ /^DTEND;/)
      {
        print "ERROR: unknown DTEND format  \"$line\"\n";
        exit 103;
      }

      # last line of a VEVENT
      if ($line eq 'END:VEVENT')
      {
        $insideEvent = 0;
        $num++;
        $vevent = "BEGIN:VEVENT\n";

        foreach $i (sort {
                      # fullname first
                      return -1  if $ical{$a} =~ /^SUMMARY:/;
                      return  1  if $ical{$b} =~ /^SUMMARY:/;

                      # photos last
                      return  1  if $ical{$a} =~ /^DESCRIPTION:/;
                      return -1  if $ical{$b} =~ /^DESCRIPTION:/;

                      # lexical sorting the rest
                      return $ical{$a} cmp $ical{$b}
                    } keys %ical)
        {
          $vevent .= $ical{$i} . "\n";
        }

        $vevent .= $alarm  if $alarm;

        $vevent .= "END:VEVENT\n";

        # find date to be sorted by
        if ($repeatedEvent eq '')
        {
          $sortdate = $eventend->printf('%s');
          if (not $sortdate)
          {
            print "VEVENT without DTEND:\n";
            print $vevent;
            exit 105;
          }
        }
        else
        {
          my ($x, $delta);
          $delta = $eventend->new_delta();

          # multiplying factor
          $interval=1;
          if ($repeatedEvent =~ /;INTERVAL=(\d+)/)
          {
            $interval=$1;
            print "DEBUG: ". $repeatedEvent."\n ". "interval=$interval\n"  if $debug;
          }

          # different repeat types
          if ($repeatedEvent =~ /^RRULE:FREQ=DAILY(;.*)?;UNTIL=(\d+)/)
          {
            $x = $2;
            $dateErr = $eventend->parse($x);
            if ($dateErr) { print "ERROR: unknown date format \"$x\": $dateErr\n"; exit 110 }
          }
          elsif ($repeatedEvent =~ /^RRULE:FREQ=DAILY(;.*)?;COUNT=(\d+)/)
          {
            $x = $2 * $interval;
            $dateErr = $delta->parse("in $x days");
            if ($dateErr) { print "ERROR: unknown date delta format: $dateErr\n"; exit 111 }
            $eventend = $eventend->calc($delta);
          }
          elsif ($repeatedEvent =~ /^RRULE:FREQ=DAILY/ and $repeatedEvent !~ /;(UNTIL|COUNT);/)
          {
            # daily events without "UNTIL" go at the end
            $dateErr = $eventend->parse("22221231T010101Z");
          }

          elsif ($repeatedEvent =~ /^RRULE:FREQ=WEEKLY(;.*)?;UNTIL=(\d+)/)
          {
            $x = $2;
            $dateErr = $eventend->parse($x);
            if ($dateErr) { print "ERROR: unknown date format \"$x\": $dateErr\n"; exit 112 }
          }
          elsif ($repeatedEvent =~ /^RRULE:FREQ=WEEKLY(;.*)?;COUNT=(\d+)/)
          {
            $x = $2 * $interval;
            $dateErr = $delta->parse("in $x weeks");
            if ($dateErr) { print "ERROR: unknown date delta format: $dateErr\n"; exit 113 }
            $eventend = $eventend->calc($delta);
          }
          elsif ($repeatedEvent =~ /^RRULE:FREQ=WEEKLY/ and $repeatedEvent !~ /;(UNTIL|COUNT);/)
          {
            # weekly events without "UNTIL" go at the end
            $dateErr = $eventend->parse("22221231T020202Z");
          }

          elsif ($repeatedEvent =~ /^RRULE:FREQ=MONTHLY(;.*)?;UNTIL=(\d+)/)
          {
            $x = $2;
            $dateErr = $eventend->parse($x);
            if ($dateErr) { print "ERROR: unknown date format \"$x\": $dateErr\n"; exit 114 }
          }
          elsif ($repeatedEvent =~ /^RRULE:FREQ=MONTHLY(;.*)?;COUNT=(\d+)/)
          {
            $x = $2 * $interval;
            $dateErr = $delta->parse("in $x months");
            if ($dateErr) { print "ERROR: unknown date delta format: $dateErr\n"; exit 115 }
            $eventend = $eventend->calc($delta);
          }
          elsif ($repeatedEvent =~ /^RRULE:FREQ=MONTHLY/ and $repeatedEvent !~ /;(UNTIL|COUNT);/)
          {
            # monthly events without "UNTIL" go at the end
            $dateErr = $eventend->parse("22221231T030303Z");
          }

          elsif ($repeatedEvent =~ /^RRULE:FREQ=YEARLY(;.*)?;UNTIL=(\d+)/)
          {
            $x = $2;
            $dateErr = $eventend->parse($x);
            if ($dateErr) { print "ERROR: unknown date format \"$x\": $dateErr\n"; exit 116 }
          }
          elsif ($repeatedEvent =~ /^RRULE:FREQ=YEARLY(;.*)?;COUNT=(\d+)/)
          {
            $x = $2 * $interval;
            $dateErr = $delta->parse("in $x years");
            if ($dateErr) { print "ERROR: unknown date delta format: $dateErr\n"; exit 117 }
            $eventend = $eventend->calc($delta);
          }
          elsif ($repeatedEvent =~ /^RRULE:FREQ=YEARLY/ and $repeatedEvent !~ /;(UNTIL|COUNT);/)
          {
            # yearly events without "UNTIL" go at the end
            $dateErr = $eventend->parse("22221231T040404Z");
          }
          else
          {
            print "ERROR: unknown RRULE format: $repeatedEvent\n";
            exit 120;
          }

          $sortdate = $eventend->printf('%s');
        }

        # make hashkey unique
        while (defined $vevents{$sortdate})
        {
          $sortdate++;
        }
        $vevents{$sortdate} = $vevent;

        $index=0;
        %ical = ();
        $alarm = '';
        $eventend = new Date::Manip::Date;
        $repeatedEvent = '';
      }

      # VALARMs are inside VEVENTs
      elsif ($insideAlarm)
      {
        if ($line eq 'END:VALARM')
        {
          $insideAlarm = 0;
        }
        $alarm .= $line ."\n";
      }

      else
      {
        if ($line =~ /^ /)        # continued lines
        {
          $ical{$index} .= "\n". $line;
        }
        elsif ($line eq 'BEGIN:VALARM')
        {
          $insideAlarm = 1;
          $alarm .= $line ."\n";
        }
        else
        {
          $index++;
          $ical{$index} = $line;
        }
      }
    }
  }

  if ($insideEvent)
  {
    print "ERROR: unexpected end of file\n";
    exit 130;
  }

  foreach $i (sort keys %vevents)
  {
    $calendar .= $vevents{$i} . "\n";
  }

  # repeated vevents without "UNTIL="
  foreach $i (sort keys %vevents2)
  {
    $calendar .= $vevents2{$i} . "\n";
  }

  print "Done sorting $num vevents\n";

  return $header ."\n". $calendar . $footer;
}
