# "Reminder" plugin for Tkabber. 2010-09-16 v. 0.9.4.2
# Written by Serge Yudin xmpp:bigote@jabber.ru
# Its purpose is to remind about your contact birthdays automatically
# and about some tasks that you assign by means of "/remind" command.
#
# USAGE
# Type "/remind help" when being in Tkabber
# either take a look at reminder::help procedure here.
#####################################
#
# TODO: Birthday reminding isn't ready yet, at all.
#

namespace eval reminder {
    ::msgcat::mcload [file join [file dirname [info script]] msgs]
    if {![::plugins::is_registered reminder]} {
        ::plugins::register reminder \
            -namespace [namespace current] \
            -source [info script] \
            -description [::msgcat::mc "Whether the Reminder plugin is loaded."] \
            -loadcommand [namespace code load] \
            -unloadcommand [namespace code unload]
        return
    }
    custom::defgroup Plugins [::msgcat::mc "Plugins options."] -group Tkabber

    custom::defgroup Reminder [::msgcat::mc "Reminder options."] -group Plugins

    custom::defvar options(remind_birthdays) 1 \
        [::msgcat::mc "Remind about your contact birthdays."] \
        -type boolean -group Reminder

    custom::defvar options(days_before_birthday) "1" \
        [::msgcat::mc "How many days before a birthday\
        you would like to be reminded."] \
        -type string -group Reminder

    custom::defvar options(play_sound) 1 \
        [::msgcat::mc "Play sound when a remind pops up."] \
        -type boolean -group Reminder

    custom::defvar options(silent_cmd_execution) 0 \
        [::msgcat::mc "Do not show a message dialog if the message is a command."] \
        -type boolean -group Reminder

    custom::defvar stored_rem_list {} \
        "List of reminders to store" \
        -type string -group Hidden

}

namespace eval ::sound {
# Taken from Alarm plugin.
    custom::defvar options(plugin_reminder_sound) \
        [file join [file dirname [info script]] sounds reminder.wav] \
        [::msgcat::mc "Play this sound file."] \
        -command [list [namespace current]::load_sound_file plugin_reminder] \
        -type file -group Reminder
}

proc reminder::load {} {
    variable rem_list
    variable rem_list2save
    variable winid 0
    trace variable [namespace current]::rem_list2save w \
    [namespace current]::save_reminders
    hook::add finload_hook \
    [namespace current]::restore_reminders 71
    hook::add generate_completions_hook \
    [namespace current]::reminder_compls
    hook::add chat_send_message_hook \
    [namespace current]::handle_commands 18
    hook::add finload_hook \
    [list ::sound::load_sound_file plugin_reminder] 100
}

proc reminder::unload {} {
    variable rem_list
    variable rem_list2save
    variable winid
    hook::remove finload_hook \
    [namespace current]::restore_reminders 71
    hook::remove generate_completions_hook \
    [namespace current]::reminder_compls
    hook::remove chat_send_message_hook \
    [namespace current]::handle_commands 18
    hook::remove finload_hook \
    [list ::sound::load_sound_file plugin_reminder] 100
    catch {unset winid}
    catch {unset rem_list}
    catch {unset rem_list2save}
}

proc reminder::reminder_compls {chatid compsvar wordstart line} {
    upvar 0 $compsvar comps

    if {!$wordstart} {
        lappend comps {/remind } {/listrems } {/delrem }
    }
}

proc reminder::handle_commands {chatid user body type} {
    variable rem_list
# Detect the command.
    if {[string match {/remind*} $body]} {
        set cmd /remind
    } elseif {[string match {/listrems*} $body]} {
        set cmd /listrems
    } elseif {[string match {/delrem*} $body]} {
        set cmd /delrem
    } else return

    if {[string equal [string range $body 0 7] "/remind "]} {
        lassign [::plugins::muc::parse_nick_reason $body 8] timeout message
    } elseif {[string equal [string range $body 0 8] "/listrems"]} {
        [namespace current]::list_reminders $chatid
    } elseif {[string equal [string range $body 0 7] "/delrem "]} {
        set num [::plugins::muc::parse_nick $body 8]
    } else {
        [namespace current]::show_error $chatid
        return stop
    }
    switch -- $cmd {
        /remind {
            set timeout [[namespace current]::parse_timeout $timeout $chatid]
            set group [chat::get_jid $chatid]
            if {$timeout == -1} {
                chat::add_message $chatid $group error "Wrong timeout format.\
                Should be H\[H\]:MM:SS or M\[M\]:SS or S\[S\]" {}
                return stop
            } elseif {$timeout == -2} {
                chat::add_message $chatid $group error "Wrong timeout keyword.\
                Must be one of: year, month, fortnight, week, day, hour,\
                min (minute), tomorrow." {}
                return stop
            } elseif {$timeout == -3} {
                chat::add_message $chatid $group error "You set something really odd ;-)" {}
                return stop
            } elseif {[string equal -nocase $timeout help]} {
                return stop
            } elseif {$timeout < -3} {
                chat::add_message $chatid $group error \
                "It seems that you have set your reminder to some date in the past." {}
                return stop
            }
            [namespace current]::set_reminder $timeout $message
            [namespace current]::list_reminders $chatid
        }
        /delrem {
            if {[info exists rem_list] && [llength $rem_list] > 0} {
                if {$num > 0 && $num <= [llength $rem_list]} {
                    set rem [lindex $rem_list [expr $num - 1]]
                } else {
                    set group [chat::get_jid $chatid]
                    chat::add_message $chatid $group error "Wrong reminder index.\
                    It should be between 1 and [llength $rem_list]." {}
                    return stop
                }
                after cancel [lindex $rem 0]
                set rem_list [lreplace $rem_list $num-1 $num-1]
                [namespace current]::list_reminders $chatid
            } else {
                return stop
            }
        }
    }
    return stop
}

proc reminder::show_error {chatid} {
    set group [chat::get_jid $chatid]
    chat::add_message $chatid $group error "Missing argument(s). Usage:\n\
    /remind <time>\n\[message\]\n\n\
    /delrem <selected remind>\n\n\
    Type \'/remind help\' to see some examples." {}
}

proc reminder::set_reminder {timeout message} {
    variable rem_list
    variable rem_list2save

    set timeout [string trimleft $timeout 0]
    set rid [after [expr {1000 * $timeout}] \
    [namespace current]::show_reminder [list $message]]
    set exp_date [expr {$timeout + [clock seconds]}]
    lappend lri $rid $exp_date "$message"
    lappend rem_list $lri
    set rem_list2save $rem_list
}

proc reminder::show_reminder {msg} {
    variable winid
    variable options
    set w .reminder$winid
    incr winid
    if {[winfo exists $w]} {
        destroy $w
    }
    if {$options(play_sound)} {
        ::sound::play $::sound::options(plugin_reminder_sound)
    }
    if {[string match cmd\ * $msg]} {
        set cmd [string trimleft $msg cmd\ ]
        if {$options(silent_cmd_execution)} {
            [namespace current]::clean_rem_list
            if {[catch {eval $cmd}]} {
                append msg "\nFAILED!"
            } else {
                return
            }
        } else {
            if {[catch {eval $cmd}]} {
                append msg "\nFAILED!"
            }
        }
    }
    set msg "Now is: [clock format [clock seconds]]\n$msg"
    Dialog $w -title [::msgcat::mc "Reminder"] \
        -modal none -separator 1 -anchor e -default 0 -parent .
        set f [$w getframe]
        $w add -text [::msgcat::mc "OK"] -command "destroy $w; \
        [namespace current]::clean_rem_list"
        label $w.lsure -padx 10 -pady 5 -text [::msgcat::mc $msg]
        pack $w.lsure -expand yes -fill both
        $w draw
}

proc reminder::list_reminders {chatid} {
    variable rem_list
    variable r_number
    set group [chat::get_jid $chatid]
    chat::add_message $chatid $group info "List of current reminders:" {}
    if {![info exists rem_list]} {
        chat::add_message $chatid $group info "No reminders were set yet." {}
        return stop
    }
    [namespace current]::clean_rem_list
    set r_number 1
    foreach lri $rem_list {
        set msg [list $r_number\tExpires:\ [clock format [lindex $lri 1]] [lindex $lri 2]]
        chat::add_message $chatid $group info "[join $msg]" {}
        incr r_number
    }
}

proc reminder::clean_rem_list {} {
    variable rem_list2save
    variable rem_list
    set r_number 1
    set rl_1 $rem_list
# Checking for expired reminders and removing them, if any.
    foreach lri $rem_list {
        if {[clock seconds]<[lindex $lri 1]} {
        incr r_number
        } else {
            set rl_1 [lreplace $rem_list $r_number-1 $r_number-1]
        }
    }
    set rem_list $rl_1
    set rem_list2save $rem_list
}

proc reminder::parse_timeout {to chatid} {
# Absolute date (and time). We don't check ONLY time here.
    if {[string match */* $to]} {
# I had to set -gmt 0 to avoid 1 hour shift.
# I have to avoid using -format to be compatible with tcl8.4
        set tmp [clock scan [join "$to"] -base 0 -gmt 0]
# So, when scanning date, it gives an absolute value,
# so I have to calculate a timeout to pass it along
# (which will be recalculated to an absolute value again :D)
        set to [expr $tmp - [clock seconds]]
# Relative timeout such as 32:20:00 (32 hours 20 min) or 15:30 (15 min 30 sec).
# We don't use clock scan here, as it doesn't accept hours value more than 24.
    } elseif {[string match *:* $to] && ![string match *\ * $to]} {
        foreach t [split $to :] {
            set t [string trimleft $t]
            if {$t == {}} {
                set t 0
            }
            lappend time $t
        }
        set to [lindex $time 0]
        for {set i 1} {$i < [llength $time]} {incr i} {
            if {[string trimleft [lindex $time $i] 0] >= 60} {
                return -1
            }
            set to [expr {$to * 60 + [lindex $time $i]}]
        }
# Relative timeouts like "tomorrow 1:00:00" (in 25 hours)
# or "2 years" (in 2 years), "fortnight 00:00:30" (in 2 weeks 30 sec), etc.
# -gmt 1 should be used to avoid 1 hour shift.
    } elseif {[string match *\ * $to]} {
        set res [catch {clock scan "$to" -base 0 -gmt 1} to]
        if {$res} {
            set to -2
        }
# A single word, either digital timeout or a word like "tomorrow" (in 24 hours).
    } elseif {![string match *\ * $to]} {
# Allowed timeout keywords.
        if {[string match *year* $to] || [string match *month* $to]
         || [string match *week* $to] || [string match *day* $to]
         || [string match *hour* $to] || [string match *min* $to]
         || [string match *fortnight* $to] || [string match *tomorrow* $to]} {
            set res [catch {clock scan "$to" -base 0 -gmt 1} to]
            if {$res} {
                set to -2
            }
# If timeout is an integer, then it's in seconds.
        } elseif {[string is integer $to]} {
            set to [string trimleft $to 0]
        } elseif {[string match *help* $to]} {
            [namespace current]::help $chatid
        } else {
            set to -3
        }
    } else {
        set to -3
    }
    return $to
}

proc reminder::help {chatid} {
    set group [chat::get_jid $chatid]
    lappend help_msg "\n/remind 180\
        \nTurn off boiling eggs.\
        \n\tThis will remind you in three minutes that your eggs are probably ready :)\
        \n\n/remind 30:15\
        \nGo to the work.\
        \n\tThis will remind you in 30 min 15 sec that you should go to your work.\
        \n\tPlease note that seconds value should not exceed 60.\
        \n\n/remind 1:2:05\
        \nCall my Dad.\
        \n\tWill remind you in an hour, 2 min and 5 sec to call to your Dad.\
        \n\tPlease note that minutes and/or seconds value should not exceed 60.\
        \n\n/remind tomorrow 10:30:00\
        \nCall to my office.\
        \n\tIt is a _relative_ timeout, not absolute time.\
        \n\tIn this case the reminder will shoot in 34 hours 30 minutes.\
        \n\tKeywords: \'year\', \'month\', \'fortnight\', \'week\', \'day\', \'hour\', \'min (minute)\', \'tomorrow\'.\
        \n\tShould be used with caution.\
        \n\n/remind hour\
        \nGo shopping.\
        \n\tA short form to say 1:00:00.\
        \n\n/remind 12/25/2009 10:00:00\
        \nMerry Christmas!\
        \n\tor\
        \n/remind 12/25/2009\
        \nMerry Christmas!\
        \n\tSets an absolute date and time for a reminder.\
        \n\tPlease note the date must be in the format MM/DD/YYYY HH:MM:SS\
        \n\tOtherwise, there could be errors or wrong timing calculation.\
        \n/remind 10 min\
        \ncmd if {\[expr rand\(\)\] < 0.5} {\
        \nputs \"Hello world\"\
        \n} else {\
        \nputs \"Goodbye world\"\
        \n}\
        \n\
        \n/remind 3 hours\
        \ncmd ::muc::unban ::xmpp::1 foo@conference.jabber.ru pupkin@jabber.ru\
        \n\
        \n/remind 01/31/2011 12:00:00\
        \ncmd source /home/myself/bin/test.tcl\
        \n\tIf your message starts with a \"cmd\" keyword, the text that follows it\
        \n\twill be considered as a tcl script and executed when its clock strikes.\
        \n\n/listrems\
        \n\tThis will show you a list of all running reminders you have set.\
        \n\n/delrem 1\
        \n\tThis will delete reminder number one (from a list obtained by /listrems).\
        \n\nAll reminders are stored in custom.tcl automatically\
        \nafter having set a new reminder and after deleting one.\
        \nThe list is restored from the file after Tkabber's start."
    chat::add_message $chatid $group error "Some examples of using /remind command:" {}
    chat::add_message $chatid $group info "[join $help_msg]" {}
    return stop
}

proc reminder::save_reminders args {
    variable rem_list2save
    variable stored_rem_list

    set stored_rem_list $rem_list2save
}


proc reminder::restore_reminders args {
    variable rem_list
    variable stored_rem_list

    set list $stored_rem_list
    foreach i $list {
        if {$i != ""} {
# Counting new timeout, for the current time has changed.
            set old_time [lindex $i 1]
            set to [expr $old_time - [clock seconds]]
            set msg [lindex $i 2]
# Check all saved reminders for expired ones.
            if {$to > 0} {
                [namespace current]::set_reminder $to $msg
            } else {
# If there are expired reminders, they are to be shown at once.
                lappend rem_list $i
                set msg "Old reminder. Expired: [clock format $old_time]\n$msg"
                after idle [namespace current]::show_reminder [list $msg]
            }
        }
    }
}
