#
# Copyright (c) 2006-2013 Ashok P. Nadkarni
# All rights reserved.
#
# See the file LICENSE for license

# Task scheduler API

package require twapi_com

namespace eval twapi {
    variable CLSID_ITaskScheduler {{148BD52A-A2AB-11CE-B11F-00AA00530503}}
    variable CLSID_ITask          {{148BD520-A2AB-11CE-B11F-00AA00530503}}
}

# Return an instance of the task scheduler
proc twapi::itaskscheduler_new {args} {
    array set opts [parseargs args {
        system.arg
    } -maxleftover 0]

    # Get ITaskScheduler interface
    set its [com_create_instance $::twapi::CLSID_ITaskScheduler -model inprocserver -interface ITaskScheduler -raw]
    if {![info exists opts(system)]} {
        return $its
    }
    trap {
        itaskscheduler_set_target_system $its $opts(system)
    } onerror {} {
        IUnknown_Release $its
        rethrow
    }
    return $its
}

interp alias {} ::twapi::itaskscheduler_release {} ::twapi::IUnknown_Release

# Return a new task interface
proc twapi::itaskscheduler_new_itask {its taskname} {
    set iid_itask [name_to_iid ITask]
    set iunk [ITaskScheduler_NewWorkItem $its $taskname $::twapi::CLSID_ITask $iid_itask]
    trap {
        set itask [Twapi_IUnknown_QueryInterface $iunk $iid_itask ITask]
    } finally {
        IUnknown_Release $iunk
    }
    return $itask
}

# Get an existing task
proc twapi::itaskscheduler_get_itask {its taskname} {
    set iid_itask [name_to_iid ITask]
    set iunk [ITaskScheduler_Activate $its $taskname $iid_itask]
    trap {
        set itask [Twapi_IUnknown_QueryInterface $iunk $iid_itask ITask]
    } finally {
        IUnknown_Release $iunk
    }
    return $itask
}

# Check if an itask exists
proc twapi::itaskscheduler_task_exists {its taskname} {
    return [expr {[ITaskScheduler_IsOfType $its $taskname [name_to_iid ITask]] == 0 ? true : false}]
}

# Return list of tasks
proc twapi::itaskscheduler_get_tasks {its} {
    set ienum [ITaskScheduler_Enum $its]
    trap {
        set result [list ]
        set more 1
        while {$more} {
            lassign [IEnumWorkItems_Next $ienum 20] more items
            set result [concat $result $items]
        }
    } finally {
        IUnknown_Release $ienum
    }
    return $result
}

# Sets the specified properties of the ITask
proc twapi::itask_configure {itask args} {

    array set opts [parseargs args {
        application.arg
        maxruntime.int
        params.arg
        priority.arg
        workingdir.arg
        account.arg
        password.arg
        comment.arg
        creator.arg
        data.arg
        idlewait.int
        idlewaitdeadline.int
        interactive.bool
        deletewhendone.bool
        disabled.bool
        hidden.bool
        runonlyifloggedon.bool
        startonlyifidle.bool
        resumesystem.bool
        killonidleend.bool
        restartonidleresume.bool
        dontstartonbatteries.bool
        killifonbatteries.bool
    } -maxleftover 0]

    if {[info exists opts(priority)]} {
        switch -exact -- $opts(priority) {
            normal      {set opts(priority) 0x00000020}
            abovenormal {set opts(priority) 0x00008000}
            belownormal {set opts(priority) 0x00004000}
            high        {set opts(priority) 0x00000080}
            realtime    {set opts(priority) 0x00000100}
            idle        {set opts(priority) 0x00000040}
            default     {error "Unknown priority '$opts(priority)'. Must be one of 'normal', 'high', 'idle' or 'realtime'"}
        }
    }

    foreach {opt fn} {
        application ITask_SetApplicationName
        maxruntime  ITask_SetMaxRunTime
        params      ITask_SetParameters
        workingdir  ITask_SetWorkingDirectory
        priority    ITask_SetPriority
        comment            IScheduledWorkItem_SetComment
        creator            IScheduledWorkItem_SetCreator
        data               IScheduledWorkItem_SetWorkItemData
        errorretrycount    IScheduledWorkItem_SetErrorRetryCount
        errorretryinterval IScheduledWorkItem_SetErrorRetryInterval
    } {
        if {[info exists opts($opt)]} {
            $fn  $itask $opts($opt)
        }
    }

    if {[info exists opts(account)]} {
        if {$opts(account) ne ""} {
            if {![info exists opts(password)]} {
                error "Option -password must be specified if -account is specified"
            }
        } else {
            # System account. Set password to NULL pointer indicated
            # by magic null pointer
            set opts(password) $::twapi::nullptr
        }
        IScheduledWorkItem_SetAccountInformation $itask $opts(account) $opts(password)
    }

    if {[info exists opts(idlewait)] || [info exists opts(idlewaitdeadline)]} {
        # If either one is not specified, get the current settings
        if {! ([info exists opts(idlewait)] &&
               [info exists opts(idlewaitdeadline)]) } {
            lassign [IScheduledWorkItem_GetIdleWait $itask] idle dead
            if {![info exists opts(idlewait)]} {
                set opts(idlewait) $idle
            }
            if {![info exists opts(idlewaitdeadline)]} {
                set opts(idlewaitdeadline) $dead
            }
        }
        IScheduledWorkItem_SetIdleWait $itask $opts(idlewait) $opts(idlewaitdeadline)
    }

    # Finally figure out and set the flags if needed
    if {[info exists opts(interactive)] ||
        [info exists opts(deletewhendone)] ||
        [info exists opts(disabled)] ||
        [info exists opts(hidden)] ||
        [info exists opts(runonlyifloggedon)] ||
        [info exists opts(startonlyifidle)] ||
        [info exists opts(resumesystem)] ||
        [info exists opts(killonidleend)] ||
        [info exists opts(restartonidleresume)] ||
        [info exists opts(dontstartonbatteries)] ||
        [info exists opts(killifonbatteries)]} {

        # First, get the current flags
        set flags [IScheduledWorkItem_GetFlags $itask]
        foreach {opt val} {
            interactive         0x1
            deletewhendone      0x2
            disabled            0x4
            startonlyifidle     0x10
            hidden              0x200
            runonlyifloggedon   0x2000
            resumesystem        0x1000
            killonidleend       0x20
            restartonidleresume 0x800
            dontstartonbatteries 0x40
            killifonbatteries   0x80
        } {
            # Set / reset the bit if specified
            if {[info exists opts($opt)]} {
                if {$opts($opt)} {
                    setbits flags $val
                } else {
                    resetbits flags $val
                }
            }
        }

        # Now set the new value of flags
        IScheduledWorkItem_SetFlags $itask $flags
    }


    return
}

proc twapi::itask_get_info {itask args} {
    # Note options errorretrycount and errorretryinterval are not implemented
    # by the OS so left out
    array set opts [parseargs args {
        all
        application
        maxruntime
        params
        priority
        workingdir
        account
        comment
        creator
        data
        idlewait
        idlewaitdeadline
        interactive
        deletewhendone
        disabled
        hidden
        runonlyifloggedon
        startonlyifidle
        resumesystem
        killonidleend
        restartonidleresume
        dontstartonbatteries
        killifonbatteries
        lastruntime
        nextruntime
        status
    } -maxleftover 0]

    set result [list ]
    if {$opts(all) || $opts(priority)} {
        switch -exact -- [twapi::ITask_GetPriority $itask] {
            32    { set priority normal }
            64    { set priority idle }
            128   { set priority high }
            256   { set priority realtime }
            16384 { set priority belownormal }
            32768 { set priority abovenormal }
            default { set priority unknown }
        }
        lappend result -priority $priority
    }

    foreach {opt fn} {
        application ITask_GetApplicationName
        maxruntime  ITask_GetMaxRunTime
        params      ITask_GetParameters
        workingdir  ITask_GetWorkingDirectory
        account            IScheduledWorkItem_GetAccountInformation
        comment            IScheduledWorkItem_GetComment
        creator            IScheduledWorkItem_GetCreator
        data               IScheduledWorkItem_GetWorkItemData
    } {
        if {$opts(all) || $opts($opt)} {
            trap {
                lappend result -$opt [$fn  $itask]
            } onerror {TWAPI_WIN32 -2147216625} {
                # THe information is empty in the scheduler database
                lappend result -$opt {}
            }
        }
    }

    if {$opts(all) || $opts(lastruntime)} {
        trap {
            lappend result -lastruntime [_timelist_to_timestring [IScheduledWorkItem_GetMostRecentRunTime $itask]]
        } onerror {TWAPI_WIN32 267011} {
            # Not run yet at all
            lappend result -lastruntime {}
        }
    }

    if {$opts(all) || $opts(nextruntime)} {
        trap {
            lappend result -nextruntime [_timelist_to_timestring [IScheduledWorkItem_GetNextRunTime $itask]]
        } onerror {TWAPI_WIN32 267010} {
            # Task is disabled
            lappend result -nextruntime disabled
        } onerror {TWAPI_WIN32 267015} {
            # No triggers set
            lappend result -nextruntime notriggers
        } onerror {TWAPI_WIN32 267016} {
            # No triggers set
            lappend result -nextruntime oneventonly
        }
    }

    if {$opts(all) || $opts(status)} {
        set status [IScheduledWorkItem_GetStatus $itask]
        if {$status == 0x41300} {
            set status ready
        } elseif {$status == 0x41301} {
            set status running
        } elseif {$status == 0x41302} {
            set status disabled
        } elseif {$status == 0x41305} {
            set status partiallydefined
        } else {
            set status unknown
        }
        lappend result -status $status
    }


    if {$opts(all) || $opts(idlewait) || $opts(idlewaitdeadline)} {
        lassign [IScheduledWorkItem_GetIdleWait $itask] idle dead
        if {$opts(all) || $opts(idlewait)} {
            lappend result -idlewait $idle
        }
        if {$opts(all) || $opts(idlewaitdeadline)} {
            lappend result -idlewaitdeadline $dead
        }
    }

    # Finally figure out and set the flags if needed
    if {$opts(all) ||
        $opts(interactive) ||
        $opts(deletewhendone) ||
        $opts(disabled) ||
        $opts(hidden) ||
        $opts(runonlyifloggedon) ||
        $opts(startonlyifidle) ||
        $opts(resumesystem) ||
        $opts(killonidleend) ||
        $opts(restartonidleresume) ||
        $opts(dontstartonbatteries) ||
        $opts(killifonbatteries)} {

        # First, get the current flags
        set flags [IScheduledWorkItem_GetFlags $itask]
        foreach {opt val} {
            interactive         0x1
            deletewhendone      0x2
            disabled            0x4
            startonlyifidle     0x10
            hidden              0x200
            runonlyifloggedon   0x2000
            resumesystem        0x1000
            killonidleend       0x20
            restartonidleresume 0x800
            dontstartonbatteries 0x40
            killifonbatteries   0x80
        } {
            if {$opts(all) || $opts($opt)} {
                lappend result -$opt [expr {($flags & $val) ? true : false}]
            }
        }
    }


    return $result
}

# Get the runtimes for a task within an interval
proc twapi::itask_get_runtimes_within_interval {itask args} {
    array set opts [parseargs args {
        start.arg
        end.arg
        {count.int 1}
        statusvar.arg
    } -maxleftover 0]

    if {[info exists opts(start)]} {
        set start [_timestring_to_timelist $opts(start)]
    } else {
        set start [_seconds_to_timelist [clock seconds]]
    }
    if {[info exists opts(end)]} {
        set end [_timestring_to_timelist $opts(end)]
    } else {
        set end {2038 1 1 0 0 0 0}
    }

    set result [list ]
    if {[info exists opts(statusvar)]} {
        upvar $opts(statusvar) status
    }
    lassign [IScheduledWorkItem_GetRunTimes $itask $start $end $opts(count)] status timelist

    foreach time $timelist {
        lappend result [_timelist_to_timestring $time]
    }


    return $result
}

# Saves the specified ITask
proc twapi::itask_save {itask} {
    set ipersist [Twapi_IUnknown_QueryInterface $itask [name_to_iid IPersistFile] IPersistFile]
    trap {
        IPersistFile_Save $ipersist "" 1
    } finally {
        IUnknown_Release $ipersist
    }
    return
}

# Show property editor for a task
proc twapi::itask_edit_dialog {itask args} {
    array set opts [parseargs args {
        {hwin.arg 0}
    } -maxleftover 0]

    return [twapi::IScheduledWorkItem_EditWorkItem $itask $opts(hwin) 0]
}


interp alias {} ::twapi::itask_release {} ::twapi::IUnknown_Release

# Get information about a trigger
proc twapi::itasktrigger_get_info {itt} {
    array set data [ITaskTrigger_GetTrigger $itt]

    set result(-begindate) [format %04d-%02d-%02d $data(wBeginYear) $data(wBeginMonth) $data(wBeginDay)]

    set result(-starttime) [format %02d:%02d $data(wStartHour) $data(wStartMinute)]

    if {$data(rgFlags) & 1} {
        set result(-enddate) [format %04d-%02d-%02d $data(wEndYear) $data(wEndMonth) $data(wEndDay)]
    } else {
        set result(-enddate) ""
    }

    set result(-duration) $data(MinutesDuration)
    set result(-interval) $data(MinutesInterval)
    if {$data(rgFlags) & 2} {
        set result(-killatdurationend) true
    } else {
        set result(-killatdurationend) false
    }

    if {$data(rgFlags) & 4} {
        set result(-disabled) true
    } else {
        set result(-disabled) false
    }

    switch -exact -- [lindex $data(type) 0] {
        0 {
            set result(-type) once
        }
        1 {
            set result(-type) daily
            set result(-period) [lindex $data(type) 1]
        }
        2 {
            set result(-type) weekly
            set result(-period) [lindex $data(type) 1]
            set result(-weekdays) [format 0x%x [lindex $data(type) 2]]
        }
        3 {
            set result(-type) monthlydate
            set result(-daysofmonth) [format 0x%x [lindex $data(type) 1]]
            set result(-months) [format 0x%x [lindex $data(type) 2]]
        }
        4 {
            set result(-type) monthlydow
            set result(-weekofmonth) [lindex {first second third fourth last} [lindex $data(type) 2]]
            set result(-weekdays) [format 0x%x [lindex $data(type) 2]]
            set result(-months) [format 0x%x [lindex $data(type) 3]]
        }
        5 {
            set result(-type) onidle
        }
        6 {
            set result(-type) atsystemstart
        }
        7 {
            set result(-type) atlogon
        }
    }
    return [array get result]
}


# Configure a task trigger
proc twapi::itasktrigger_configure {itt args} {
    array set opts [parseargs args {
        begindate.arg
        enddate.arg
        starttime.arg
        interval.int
        duration.int
        killatdurationend.bool
        disabled.bool
        type.arg
        weekofmonth.int
        {period.int 1}
        {weekdays.int 0x7f}
        {daysofmonth.int 0x7fffffff}
        {months.int 0xfff}
    } -maxleftover 0]


    array set data [ITaskTrigger_GetTrigger $itt]

    if {[info exists opts(begindate)]} {
        lassign  [split $opts(begindate) -]  year month day
        # Note we trim leading zeroes else Tcl thinks its octal
        set data(wBeginYear) [scan $year %d]
        set data(wBeginMonth) [scan $month %d]
        set data(wBeginDay) [scan $day %d]
    }

    if {[info exists opts(starttime)]} {
        lassign [split $opts(starttime) :] hour minute
        # Note we trim leading zeroes else Tcl thinks its octal
        set data(wStartHour) [scan $hour %d]
        set data(wStartMinute) [scan $minute %d]
    }

    if {[info exists opts(enddate)]} {
        if {$opts(enddate) ne ""} {
            setbits data(rgFlags) 1;        # Indicate end date is present
            lassign  [split $opts(enddate) -] year month day
            # Note we trim leading zeroes else Tcl thinks its octal
            set data(wEndYear) [scan $year %d]
            set data(wEndMonth) [scan $month %d]
            set data(wEndDay) [scan $day %d]
        } else {
            resetbits data(rgFlags) 1;  # Indicate no end date
        }
    }


    if {[info exists opts(duration)]} {
        set data(MinutesDuration) $opts(duration)
    }

    if {[info exists opts(interval)]} {
        set data(MinutesInterval) $opts(interval)
    }

    if {[info exists opts(killatdurationend)]} {
        if {$opts(killatdurationend)} {
            setbits data(rgFlags) 2
        } else {
            resetbits data(rgFlags) 2
        }
    }

    if {[info exists opts(disabled)]} {
        if {$opts(disabled)} {
            setbits data(rgFlags) 4
        } else {
            resetbits data(rgFlags) 4
        }
    }

    # Note the type specific options are only used if -type is specified
    if {[info exists opts(type)]} {
        switch -exact -- $opts(type) {
            once {
                set data(type) [list 0]
            }
            daily {
                set data(type) [list 1 $opts(period)]
            }
            weekly {
                set data(type) [list 2 $opts(period) $opts(weekdays)]
            }
            monthlydate {
                set data(type) [list 3 $opts(daysofmonth) $opts(months)]
            }
            monthlydow {
                set data(type) [list 4 $opts(weekofmonth) $opts(weekdays) $opts(months)]
            }
            onidle {
                set data(type) [list 5]
            }
            atsystemstart {
                set data(type) [list 6]
            }
            atlogon {
                set data(type) [list 7]
            }
        }
    }

    ITaskTrigger_SetTrigger $itt [array get data]
    return
}

interp alias {} ::twapi::itasktrigger_release {} ::twapi::IUnknown_Release

# Create a new task from scratch. Basically a wrapper around the
# corresponding itaskscheduler, itask and itasktrigger calls
proc twapi::mstask_create {taskname args} {

    # The options are a combination of itask_configure and
    # itasktrigger_configure.
    # Note the disabled option default to false explicitly. This is because
    # the task trigger will default to disabled unless specifically set.
    array set opts [parseargs args {
        system.arg
        application.arg
        maxruntime.int
        params.arg
        priority.arg
        workingdir.arg
        account.arg
        password.arg
        comment.arg
        creator.arg
        data.arg
        idlewait.int
        idlewaitdeadline.int
        interactive.bool
        deletewhendone.bool
        {disabled.bool false}
        hidden.bool
        runonlyifloggedon.bool
        startonlyifidle.bool
        resumesystem.bool
        killonidleend.bool
        restartonidleresume.bool
        dontstartonbatteries.bool
        killifonbatteries.bool
        begindate.arg
        enddate.arg
        starttime.arg
        interval.int
        duration.int
        killatdurationend.bool
        type.arg
        period.int
        weekdays.int
        daysofmonth.int
        months.int
    } -maxleftover 0]

    set its [itaskscheduler_new]
    trap {
        if {[info exists opts(system)]} {
            itaskscheduler_set_target_system $opts(system)
        }

        set itask [itaskscheduler_new_itask $its $taskname]
        # Construct the command line for configuring the task
        set cmd [list itask_configure $itask]
        foreach opt {
            application
            maxruntime
            params
            priority
            workingdir
            account
            password
            comment
            creator
            data
            idlewait
            idlewaitdeadline
            interactive
            deletewhendone
            disabled
            hidden
            runonlyifloggedon
            startonlyifidle
            resumesystem
            killonidleend
            restartonidleresume
            dontstartonbatteries
            killifonbatteries
        } {
            if {[info exists opts($opt)]} {
                lappend cmd -$opt $opts($opt)
            }
        }
        eval $cmd

        # Now get a trigger and configure it
        set itt [lindex [itask_new_itasktrigger $itask] 1]
        set cmd [list itasktrigger_configure $itt]
        foreach opt {
            begindate
            enddate
            interval
            starttime
            duration
            killatdurationend
            type
            period
            weekdays
            daysofmonth
            months
            disabled
        } {
            if {[info exists opts($opt)]} {
                lappend cmd -$opt $opts($opt)
            }
        }
        eval $cmd

        # Save the task
        itask_save $itask

    } finally {
        IUnknown_Release $its
        if {[info exists itask]} {
            IUnknown_Release $itask
        }
        if {[info exists itt]} {
            IUnknown_Release $itt
        }
    }
    return
}

# Delete a task
proc twapi::mstask_delete {taskname args} {
    # The options are a combination of itask_configure and
    # itasktrigger_configure
    array set opts [parseargs args {
        system.arg
    } -maxleftover 0]
    set its [itaskscheduler_new]
    trap {
        if {[info exists opts(system)]} {
            itaskscheduler_set_target_system $opts(system)
        }
        itaskscheduler_delete_task $its $taskname
    } finally {
        IUnknown_Release $its
    }
    return
}
