#!/usr/bin/ruby
#
# Generate a timeline from iCal format to stdout
#
# call this programm with --help to get usage info
#
# I've used http://blog.funkensturm.de/2008/02/10/finally-railsicalendar-ical-ics-publish-with-ruby-on-rails/
# as a jumpstart, so thanks to Marius Maximus
#
# This programm requires the vpim gem:
#
# sudo gems install vpim
#
# If you want to hack on this script then you'll find the vpim docu here:
#
#	http://vpim.rubyforge.org/
#
#
# Copyrighted 2008 by Tomas Pospisek, tpo_deb at sourcepole.ch,
# under the BSD license: http://www.opensource.org/licenses/bsd-license.php

require 'rubygems'
require 'vpim/icalendar'

DEBUG=false

class Range
  def each_day
    current = first
    while current < last do
        yield current
      current += 3600 * 24
    end
  end
end

class Timeline

  def initialize( path_ics, from, to)
    @start    = Time.parse( from ) # outputs in local time !
    @end      = Time.parse( to )   # => we need to use local time everywhere!
    @interval = 3 # months
    @days     = {}
    @events   = []
    @rank     = {} # which position is an event at?
    @max_event_rank = 0
    @path_ics=path_ics
    @field_size = 3 # must be > mday.length
  end

  def print_field( content = (" " * @field_size) )
      printf( "%#{@field_size}s", content.to_s[0,@field_size] ) # printf bug - doesn't shorten string!
  end

  def print_offset( offset )
    print " " * offset
  end

  def parse_ics
    # parse each icalendar *.ics file in path_ics and check for event occurences
    Dir.glob(File.join(@path_ics, '*.ics')).each do |file|
      Vpim::Icalendar.decode(File.open(file)).each do |calendar|
        calendar.components(Vpim::Icalendar::Vevent) do |event|
          (@start..@end).each_day do |day|
            if event.occurs_in?(day, day + 3600 * 24 )
              if ! @rank.has_key? event
                @rank[ event ] = @max_event_rank
                @max_event_rank += 1
                @events << event
              end
              @days[ [day, @rank[ event ] ] ] = event
            end
          end
        end
      end
    end
  end

  def print_header
    puts 'From: ' << @start.to_s
    puts 'To  : ' << @end.to_s
  end

  def print_months
    position_offset = 0
    (@start..@end).each_day do |day|
      if day.mday == 1
        print_field day.strftime("%B") # month name
      else
        print_field
      end
    end
    puts
  end

  def print_days
    position_offset = 0
    (@start..@end).each_day do |day|
      print_field( day.mday )
    end
    puts
  end

  def print_weekdays
    (@start..@end).each_day do |day|
      if (1..5).include?( day.wday )# Sunday or Saturday
        print_field( day.strftime("%a")[0,1] )
      else
        print_field
      end
    end
    puts
  end

  def print_allocation( max_summary_len )
    @events.each_with_index do |event, rank| 
      printf "%#{max_summary_len}s", event.summary
      (@start..@end).each_day do |day|
        if @days.has_key? [ day, rank ]
          event = @days[ [ day, rank ] ]

          # Exclude recurrence rule hack
          exme = false
          event.propvaluearray('EXDATE').each do |exdate|
            exdate = exdate.to_time
            exme = true if day.year == exdate.year && day.mon == exdate.mon && day.day == exdate.day
          end
          next if exme # Skip this event

          print_field( 'x' )
        else
          print_field
        end
      end
      puts
    end
  end

  def longest_summary( events )
    events.inject(0) do |maxlen, event|
      len = event.summary.length
      len > maxlen ? len : maxlen
    end
  end

  def print_timeline
    print_header
    parse_ics
    max_summary_len = longest_summary( @events )

    original_end = @end
    while @start < original_end do
      @in_3_months = @start + 3 * 31 * 24 * 3600
      @end = Time.local( @in_3_months.year, @in_3_months.month )
      [ :print_months,
        :print_days,
        :print_weekdays,
      ].each do |proc|
        print_offset( max_summary_len )
        self.send proc
      end
      print_allocation( max_summary_len )
      @start = @end
    end
  end
end

def help
  puts "usage:"
  puts "  print_cal path_to_ical_file[s] from-date to-date"
  puts
  puts "will generate a timeline from iCal format to stdout"
  puts "format for date is '2008-04'"
  exit -1
end

help if ARGV.length < 3 

Timeline.new( ARGV[0], ARGV[1], ARGV[2] ).print_timeline

