#!/usr/bin/ruby1.9.1
# -*- ruby -*-

## today
##
## Author:  Yoshinari Nomura <nom@quickhack.net>
##
## Created: 1999/11/11
## Revised: $Date: 2005/12/04 15:48:20 $
##
## today was originally written in perl.
## This  is a ruby version of it. Original authors were
##
##    Yoshinari Nomura <nom@quickhack.net>
##    OHARA Shigeki <os@iij.ad.jp>
##

$DEBUG2       = false

MailServer    = 'localhost'  ## for --mail option
MyHostName    = 'localhost'  ## for --mail option

require 'mhc-schedule'
require 'mhc-kconv'
require 'net/smtp'

class String
  # digest a string upto max size.
  # when the bound is unsafe for muti-bytes, replace the character with '$'
  def digest (max, tail_adjust = '$', fill_up = nil)
    euc = (Kconv .toeuc self)[0..max-1]

    if fill_up and euc .size < max
      return euc + fill_up * (max - euc .size)
    end

    is2byte = false
    euc .each_byte do |char|
      is2byte = ! is2byte if char & 0x80 == 0x80
    end

    if is2byte
      euc[max-1] = tail_adjust
    end
    return euc
  end
end

def usage(do_exit = true)
  STDERR .print "usage: today [options]
  Show your today's schedules.
  --help               show this message.
  --format=FORMAT      change output format to one of: ps, html, howm
  --category=CATEGORY  pick only in CATEGORY.
                       '!' and space separated multiple values are allowed.
  --date=strig[+n]     set a period of date.
                       string is one of these:
                         today, tomorrow, sun ... sat, yyyymmdd, yyyymm
                       yyyymm lists all days in the month.
                       list n+1 days of schedules if +n is given.
                       default value is 'today+0'
  --mail=ADDRESS       send a e-mail to ADDRESS instead of listing to stdout.\n"
  exit if do_exit
end

def string_to_date(string, range)
  date_from = nil
  date_to   = nil

  case (string .downcase)
  when 'today'
    date_from = MhcDate .new

  when 'tomorrow'
    date_from = MhcDate .new .succ

  when /^(sun|mon|tue|wed|thu|fri|sat)/
    date_from = MhcDate .new .w_this(string .downcase)

  when /^\d{8}$/
    date_from = MhcDate .new(string)

  when /^\d{6}$/
    date_from = MhcDate .new(string + '01')
    if range
      date_to = date_from .succ(range .to_i)
    else
      date_to = MhcDate .new(string + format("%02d", date_from .m_days))
    end
  else
    return nil
  end

  date_to   = date_from .succ((range || '0') .to_i) if !date_to
  return [date_from, date_to]
end

def get_schedule(db, from, to, category, formatter, hook = nil)
  ret = ''

  db .search(from, to, category) .each{|date, items|
    ret += formatter .call(date, items)
  }
  if hook
    return hook .call(db, from, to, category, ret)
  else
    return ret
  end
end

################################################################
## formatter

formatter_normal = Proc .new{|date, items|
  ret = ''
  heading = format("%02d/%02d %s ", date .m, date .d, date .w_s)

  first = true
  items .each{|sch|
    heading = heading .gsub(/./, ' ') if !first
    ret += heading + format("%-11s %s%s\n",
                            sch .time_as_string,
                            MhcKconv::todisp(sch .subject),
                            if sch .location and sch .location != ''
                              ' [' + MhcKconv::todisp(sch .location) + ']'
                            else
                              ''
                            end)
    first = false
  }
  ret
}

formatter_ps = Proc .new{|date, items|
  ret = ''
  heading = format("%d", date .d)

  items .each{|sch|
    if sch .in_category?('Holiday')
      ret += '__HOLIDAY__'
    end
    ret += heading +
      format(" (%-5s %s)__XXX__",
             sch .time_b .to_s,
             sch .subject .digest(15) .gsub(/([()])/, '\\\\\1'))
  }
  MhcKconv::tops(ret)
}

formatter_html = Proc .new{|date, items|
  buffer_max, buffer = 30, ''

  week = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][date .w]

  items2 = []
  items .each do |schedule|
    time = schedule .time_b
    time &&= "#{time} "
    items2 .push Kconv .toeuc("#{time}#{schedule .subject}")
  end

  if (string = items2 .join(", ")) .size > buffer_max
    string = items2 .collect {|item| item[0,10]} .join(", ")
  end
  buffer += format("%02d/%02d(%s)", date .m, date .d, week) + " "
  buffer += string .digest(buffer_max) + "<BR>\n"
  MhcKconv::tohtml(buffer)
}

formatter_howm = Proc .new{|date, items|
  ret = ''

  items .each{|sch|
    sw = "@"
    ret += "["
    ret += format("%04d-%02d-%02d", date .y, date .m, date .d)
    if (sch .time_b)
      ret += " " + sch .time_b .to_s
    else
      ret += "      "
    end
    ret += "]"
    sw=" "
    case sch .category_as_string .downcase
    when /done/
      sw="."
    when /todo/
      sw="+"
    else
      sw="@"
    end
    ret += sw + " " + MhcKconv::todisp(sch .subject) + "\n"
    if sch .description
      ret += sch .description .gsub(/^/, " ")
    end
  }
  MhcKconv::todisp(ret)
}

################################################################
## hook

hook_ps = Proc .new {|db, from, to, category, ret|
  ps_contents = open(__FILE__) .gets(nil) .split("__END__\n")[1]
  landscape = true # xxx
  holiday  = ''
  schedule = ''

  ret .split('__XXX__') .each{|item|
    if item =~ /^__HOLIDAY__(.*)/
      holiday += $1
    else
      schedule += item
    end
  }
  trans_table = {
    '@MONTH@'     => from .m .to_s,
    '@YEAR@'      => from .y .to_s,
    '@TFONT@'     => 'Times-Bold',
    '@DFONT@'     => 'Helvetica-Bold',
    '@EFONT@'     => 'Times-Roman',
    '@JFONT@'     => 'Ryumin-Light-EUC-H',
    '@HOLIDAYS@'  => holiday,
    '@SCHEDULES@' => schedule,
    '@BANNER@'    => '',
    '@LFOOT@'     => '',
    '@RFOOT@'     => '',
    '@CFOOT@'     => '',
    '@SCALE@'     => landscape ? '1.0 1.0' : '0.75 0.75',
    '@ROTATE@'    => landscape ? '90'      : '0',
    '@TRANSLATE@' => landscape ? '50 -120' : '50 900'
  }
  trans_table .each{|key,val|
    ps_contents .gsub!(key, val)
  }
  ps_contents
}

################################################################
## main

date_from   = date_to = MhcDate .new
formatter   = formatter_normal
hook_proc   = nil

while option = ARGV .shift
  case (option)
  when /^--category=(.+)/
    category = $1

  when /^--format=(.+)/
    case $1
    when 'html'
      formatter = formatter_html
    when 'ps'
      formatter = formatter_ps
      hook_proc = hook_ps
      date_from = date_from .m_first_day
      date_to   = date_from .m_last_day
    when 'howm'
      formatter = formatter_howm
    else
      formatter = formatter_normal
    end

  when /^--date=([^+]+)(\+(-?[\d]+))?/
    date_from, date_to = string_to_date($1, $3) || usage()

  when /^--mail=(.+)/
    mail_address = $1

  else
    usage()
  end
end

user_name = ENV['USERNAME'] || ENV['USER'] || ENV['LOGNAME'] || mail_address

print "date_from: #{date_from .to_s}\n" if $DEBUG2
print "date_to:   #{date_to   .to_s}\n" if $DEBUG2
print "e-mail:    #{mail_address}\n"    if $DEBUG2

db = MhcScheduleDB .new

if mail_address
  header    = "To: #{mail_address}\n"
  header   += "From: secretary-of-#{mail_address}\n"
  header   += "Subject: Today's schedule (#{date_from .to_s})\n"
  header   += "\n"
  header   += "#{user_name}'s schedule: \n\n"
  contents =
    get_schedule(db, date_from, date_to, category, formatter, hook_proc)
  if formatter == formatter_howm
    contents = format("= mhc %s--%s\n", date_from .to_s, date_to .to_s) + contents
  end

  if contents && contents != ''
    message = MhcKconv::tomail(header + contents)
    message .force_encoding("ASCII-8BIT") if RUBY_VERSION .to_f >= 1.9
    Net::SMTPSession .start(MailServer, 25, MyHostName) {|server|
      server .sendmail(message, mail_address, [mail_address])
    }
  end
else
  print format("= mhc %s--%s\n", date_from .to_s, date_to .to_s) if formatter == formatter_howm
  print get_schedule(db, date_from, date_to, category, formatter, hook_proc)
end

################################################################
# postscript code from pscalj.sh
################################################################
__END__
%!
% PostScript program to draw calendar
% Copyright (C) 1987 by Pipeline Associates, Inc.
% Permission is granted to modify and distribute this free of charge.

% The number after /month should be set to a number from 1 to 12.
% The number after /year should be set to the year you want.
% You can change the title and date fonts, if you want.
% We figure out the rest.
% This program won't produce valid calendars before 1800 due to the switch
% from Julian to Gregorian calendars in September of 1752 wherever English
% was spoken.

%% For Japanese. Added by ichimal, 2000/2/6.
%% Original code is generated by k2ps.
/copyfont {     % font-dic extra-entry-count  copyfont  font-dic
        1 index maxlength add dict begin
        {       1 index /FID ne 2 index /UniqueID ne and
                {def}{pop pop} ifelse
        } forall
        currentdict
        end
} bind def

%% For Japanese. Added by ichimal, 2000/2/6.
%% Original code is generated by k2ps.
/narrowfont {   % ASCIIFontName EUCFontName  compositefont  font'
    findfont dup /FontType get 0 eq {
        12 dict begin
            %
            % 7+8 bit EUC font
            %
            12 dict begin
                /EUCFont exch def
                /FontInfo (7+8 bit EUC font) readonly def
                /PaintType 0 def
                /FontType 0 def
                /FontMatrix matrix def
                % /FontName
                /Encoding [
                    16#00 1 16#20 { pop 0 } for
                    16#21 1 16#28 { 16#20 sub } for
                    16#29 1 16#2F { pop 0 } for
                    16#30 1 16#74 { 16#27 sub } for
                    16#75 1 16#FF { pop 0 } for
                ] def
                /FMapType 2 def
                EUCFont /WMode known
                { EUCFont /WMode get /WMode exch def }
                { /WMode 0 def } ifelse
                /FDepVector [
                    EUCFont /FDepVector get 0 get
                    [ 16#21 1 16#28 {} for 16#30 1 16#74 {} for ]
                    {
                        13 dict begin
                            /EUCFont EUCFont def
                            /UpperByte exch 16#80 add def
                            % /FontName
                            /FontInfo (EUC lower byte font) readonly def
                            /PaintType 0 def
                            /FontType 3 def
                            /FontMatrix matrix def
                            /FontBBox {0 0 0 0} def
                            /Encoding [
                                16#00 1 16#A0 { pop /.notdef } for
                                16#A1 1 16#FE { 16#80 sub 16 2 string cvrs
                                                (cXX) dup 1 4 -1 roll
                                                putinterval cvn } for
                                /.notdef
                            ] def
                            % /UniqueID
                            % /WMode
                            /BuildChar {
                                gsave
                                exch dup /EUCFont get setfont
                                /UpperByte get
                                2 string
                                dup 0 4 -1 roll put
                                dup 1 4 -1 roll put
                                dup stringwidth setcharwidth
                                0 0 moveto show
                                grestore
                            } bind def
                            currentdict
                        end
                        /lowerbytefont exch definefont
                    } forall
                ] def
                currentdict
            end
            /eucfont exch definefont
            exch
            findfont 1 copyfont dup begin
                /FontMatrix FontMatrix [.83 0 0 1 0 0.05] matrix concatmatrix def
            end
            /asciifont exch definefont
            exch
            /FDepVector [ 4 2 roll ] def
            /FontType 0 def
            /WMode 0 def
            /FMapType 4 def
            /FontMatrix matrix def
            /Encoding [0 1] def
            /FontBBox {0 0 0 0} def
            currentdict
        end
    }{
        pop findfont 0 copyfont
    } ifelse
} def

/month @MONTH@ def
/year @YEAR@ def
/titlefont /@TFONT@ def
/dayfont /@DFONT@ def
%% For Japanese. Changed by ichimal, 2000/2/6.
%% Original code is generated by k2ps.
% /eventfont /@EFONT@ def
/Courier-Ryumin
    /Courier /@JFONT@ narrowfont definefont pop
/eventfont /Courier-Ryumin def

/holidays [ @HOLIDAYS@ ] def
/schedules [ @SCHEDULES@ ] def
/Bannerstring (@BANNER@) def
/Lfootstring (@LFOOT@) def
/Rfootstring (@RFOOT@) def
/Cfootstring (@CFOOT@) def

% calendar names - change these if you don't speak english
% "August", "April" and "February" could stand to be kerned even if you do

/month_names
[ (January) (February) (March) (April) (May) (June) (July)
(August) (September) (October) (November) (December) ]
def

/day_names
[ (Sunday) (Monday) (Tuesday) (Wednesday) (Thursday) (Friday) (Saturday) ]
def

% layout parameters - you can change these, but things may not look nice

/daywidth 100 def
/dayheight 95 def

/titlefontsize 48 def
/weekdayfontsize 10 def
/datefontsize 30 def
/footfontsize 20 def

/topgridmarg 35 def
/leftmarg 35 def
/daytopmarg 10 def
/dayleftmarg 5 def

% layout constants - don't change these, things probably won't work

/rows 5 def
/subrows 6 def

% calendar constants - change these if you want a French revolutionary calendar

/days_week 7 def

/days_month [ 31 28 31 30 31 30 31 31 30 31 30 31 ] def

/isleap {                               % is this a leap year?
        year 4 mod 0 eq                 % multiple of 4
        year 100 mod 0 ne               % not century
        year 1000 mod 0 eq or and       % unless it's a millenia
} def

/ndays {                                % number of days in this month
        days_month month 1 sub get
        month 2 eq                      % February
        isleap and
        {
                1 add
        } if
} def

/weekday {                              % weekday (range 0-6) for integer date
        days_week mod
} def

/startday {                             % starting day-of-week for this month
        /off year 2032 sub def          % offset from start of "epoch"
        off
        off 4 idiv add                  % number of leap years
        off 100 idiv sub                % number of centuries
        off 1000 idiv add               % number of millenia
        4 add weekday days_week add     % offset from Jan 1 2032
        /off exch def
        1 1 month 1 sub {
                /idx exch def
                days_month idx 1 sub get
                idx 2 eq
                isleap and
                {
                        1 add
                } if
                /off exch off add def
        } for
        off weekday                     % 0--Sunday, 1--monday, etc.
} def

/prtevent {             % event-string day prtevent
                        %  print out an event
        /start startday def
        /day 2 1 roll def
        day start add 1 sub 7 mod daywidth mul
        day start add 1 sub 7 div truncate dayheight neg mul
        -5
        numevents day start add get -10 mul add
        numevents
        day start add
        numevents day start add get 1 add
        put
        add moveto
        show
} def

/drawevents {           % read in a file full of events; print
                        %  the events for this month
        /numevents
        [0 0 0 0 0 0 0
         0 0 0 0 0 0 0
         0 0 0 0 0 0 0
         0 0 0 0 0 0 0
         0 0 0 0 0 0 0
         0 0 0 0 0 0 0] def
         eventfont findfont 9 scalefont setfont
         0 2 holidays length 2 sub { % for the "Holidays"
                dup
                1 add holidays 2 1 roll get
                2 1 roll holidays 2 1 roll get
                prtevent
        } for
         0 2 schedules length 2 sub { % for the "Schedules"
                dup
                1 add schedules 2 1 roll get
                2 1 roll schedules 2 1 roll get
                prtevent
        } for

} def

% ------------------------------------------------------------------------

/prtnum { 3 string cvs show } def

/center {                               % center string in given width
        /width exch def
        /str exch def width str
        stringwidth pop sub 2 div 0 rmoveto str show
} def

/centernum { exch 3 string cvs exch center } def

/drawgrid {                             % draw calendar boxes
        titlefont findfont weekdayfontsize scalefont setfont
        currentpoint /y0 exch def /x0 exch def
        0 1 days_week 1 sub {
                submonth 0 eq
                {
                        x0 y0 moveto
                        dup dup daywidth mul 40 rmoveto
                        day_names exch get
                        daywidth center
                } if
                x0 y0 moveto
                daywidth mul topgridmarg rmoveto
                1.0 setlinewidth
                submonth 0 eq
                {
                        /rowsused rows 1 sub def
                }
                {
                        /rowsused rows def
                }
                ifelse
                0 1 rowsused {
                        gsave
                        daywidth 0 rlineto
                        0 dayheight neg rlineto
                        daywidth neg 0 rlineto
                        closepath stroke
                        grestore
                        0 dayheight neg rmoveto
                } for
        } for
} def

/drawnums {                             % place day numbers on calendar
        dayfont findfont datefontsize
        submonth 0 ne
        {
                2.5 mul
        } if scalefont setfont
        /start startday def
        /days ndays def
        start daywidth mul dayleftmarg add daytopmarg rmoveto
        submonth 0 ne
        {
                dayleftmarg neg dayheight -2 div rmoveto
        } if
        1 1 days {
                /day exch def
                gsave
                day start add weekday 0 eq
                {
                        submonth 0 eq
                        {
                                .7 setgray
                        } if
                } if
                day start add weekday 1 eq
                {
                        submonth 0 eq
                        {
                                .7 setgray
                        } if
                } if
                %% Added by ichimal, 2000.2
        submonth 0 eq {
            0 2 holidays length 2 sub {
                holidays 2 1 roll get day eq {
                    .7 setgray
                    exit
                } if
            } for
        } if
                submonth 0 eq
                {
                        isdouble
                        {
                                day prtdouble
                        }
                        {
                                day prtnum
                        } ifelse
                }
                {
                        day daywidth centernum
                } ifelse
                grestore
                day start add weekday 0 eq
                {
                        currentpoint exch pop dayheight sub 0 exch moveto
                        submonth 0 eq
                        {
                                dayleftmarg 0 rmoveto
                        } if
                }
                {
                        daywidth 0 rmoveto
                } ifelse
        } for
} def
/isdouble {                             % overlay today with next/last week?
        days start add rows days_week mul gt
        {
                day start add rows days_week mul gt
                {
                        true true
                }
                {
                        day start add rows 1 sub days_week mul gt
                        day days_week add days le and
                        {
                                false true
                        }
                        {
                                false
                        } ifelse
                } ifelse
        }
        {
                false
        } ifelse
} def

/prtdouble {
        gsave
          dayfont findfont datefontsize 2 mul 3 div scalefont setfont
          exch
          {
                (23/) stringwidth pop dayheight rmoveto
                prtnum
          }
          {
                0 datefontsize 5 div rmoveto
                prtnum
                0 datefontsize -5 div rmoveto
                gsave
                  dayfont findfont datefontsize scalefont setfont
                  (/) show
                grestore
          } ifelse
        grestore
} def

/drawfill {                             % place fill squares on calendar
        /start startday def
        /days ndays def
        currentpoint /y0 exch def /x0 exch def
        submonth 0 eq
        {
                usefirst
                {
                        /fillstart 2 def
                }
                {
                        /fillstart 0 def
                }
                ifelse
        }
        {
                /fillstart 0 def
        }
        ifelse
        fillstart daywidth mul topgridmarg rmoveto
        1.0 setlinewidth
        fillstart 1 start 1 sub {
                gsave
                .9 setgray
                daywidth 0 rlineto
                0 dayheight neg rlineto
                daywidth neg 0 rlineto
                closepath fill
                grestore
                daywidth 0 rmoveto
        } for
        x0 y0 moveto
        submonth 0 ne
        {
                /lastday rows 1 add days_week mul def
                days_week 1 sub daywidth mul -440 rmoveto
        }
        {
                /lastday rows days_week mul 2 sub fillstart add def
                days_week 3 sub fillstart add daywidth mul
                -440 dayheight add rmoveto
        } ifelse
        lastday -1 ndays start 1 add add
        {
                /day exch def
                gsave
                .9 setgray
                daywidth 0 rlineto
                0 dayheight neg rlineto
                daywidth neg 0 rlineto
                closepath fill
                grestore
                day weekday 1 eq
                {
                        x0 y0 moveto
                        days_week 1 sub daywidth mul -440 dayheight add rmoveto
                }
                {
                        daywidth neg 0 rmoveto
                } ifelse
        } for
} def

/usefirst {                             % are last two boxes used by days?
        start ndays add rows days_week mul 3 sub gt
        start 2 ge and

} def

/calendar
{
        titlefont findfont titlefontsize scalefont setfont
        0 60 moveto
        /month_name month_names month 1 sub get def
        month_name show
        /yearstring year 10 string cvs def
        daywidth days_week mul yearstring stringwidth pop sub 60 moveto
        yearstring show

        eventflag {
                % Show a centered Banner if any at the Top
                daywidth days_week mul 2 div
                Bannerstring stringwidth pop 2 div sub
                60 moveto
                Bannerstring show
                % Show footnotes left-center-right
                eventfont findfont footfontsize scalefont setfont
                /bottomrow { dayheight rows mul 5 sub neg } def
                0 bottomrow moveto
                Lfootstring show
                daywidth days_week mul Rfootstring stringwidth pop sub
                bottomrow moveto
                Rfootstring show
                daywidth days_week mul Cfootstring stringwidth pop sub 2 div
                bottomrow moveto
                Cfootstring show

        } if

        0 -5 moveto
        drawnums

        0 -5 moveto
        drawfill

        eventflag {
                0 0 moveto
                drawevents
        } if

        0 -5 moveto
        drawgrid
} def

/eventflag true def

@SCALE@ scale
@ROTATE@ rotate
@TRANSLATE@ translate
/submonth 0 def
calendar
/eventflag false def
month 1 sub 0 eq
{
        /lmonth 12 def
        /lyear year 1 sub def
}
{
        /lmonth month 1 sub def
        /lyear year def
} ifelse
month 1 add 13 eq
{
        /nmonth 1 def
        /nyear year 1 add def
}
{
        /nmonth month 1 add def
        /nyear year def
} ifelse
usefirst
{
        0 30 translate
}
{
        days_week 2 sub daywidth mul -350 translate
}
ifelse
/submonth 1 def
/year lyear def
/month lmonth def
gsave
.138 .138 scale
12 -120 translate
calendar
grestore
/submonth 1 def
/year nyear def
/month nmonth def
daywidth 0 translate
gsave
.138 .138 scale
12 -120 translate
calendar
grestore

showpage
__END__

### Copyright Notice:

## Copyright (C) 1999, 2000 Yoshinari Nomura. All rights reserved.
## Copyright (C) 2000 MHC developing team. All rights reserved.

## Redistribution and use in source and binary forms, with or without
## modification, are permitted provided that the following conditions
## are met:
##
## 1. Redistributions of source code must retain the above copyright
##    notice, this list of conditions and the following disclaimer.
## 2. Redistributions in binary form must reproduce the above copyright
##    notice, this list of conditions and the following disclaimer in the
##    documentation and/or other materials provided with the distribution.
## 3. Neither the name of the team nor the names of its contributors
##    may be used to endorse or promote products derived from this software
##    without specific prior written permission.
##
## THIS SOFTWARE IS PROVIDED BY THE TEAM AND CONTRIBUTORS ``AS IS''
## AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
## FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
## THE TEAM OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
## INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
## (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
## SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
## HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
## STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
## ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
## OF THE POSSIBILITY OF SUCH DAMAGE.

### today ends here
