object $scheduler: $utilities;

var $root created_on = 810294797;
var $root inited = 1;
var $root flags = ['methods, 'code, 'variables, 'core];
var $root manager = $scheduler;
var $root managed = [$scheduler];
var $scheduler task_index = 1;
var $scheduler blocked_tasks = #[];
var $scheduler task_queue = [];
var $scheduler sub_schedulers = [$heart];
var $scheduler expected_lag = 0;
var $scheduler server_lag = 0;
var $scheduler suspended_tasks = #[[3, [$dns, 0]], [85195, [$dns, 0]], [93363, [$dns, 0]], [17443, [$dns, $brad]], [40745, [$dns, 0]], [45603, [$dns, $brandon]], [1496, [$dns, 0]], [3783, [$dns, 0]], [14561, [$dns, 0]], [30980, [$dns, 0]], [34228, [$dns, 0]], [34575, [$dns, 0]], [43786, [$dns, 0]], [43829, [$dns, 0]], [49895, [$dns, $user_mattc]], [141161, [$dns, $shkoo]], [141197, [$dns, $shkoo]], [291237, [$dns, $brandon]], [945648, [$miro]]];


public method $scheduler.sleep() {
    arg howlong;
    
    .add_task(howlong, 'resume_job, task_id());
    .suspend(this());
};

private method $scheduler.remove_first_task() {
    // sender must be an agent, or admin.
    catch any {
        .del_from_task_queue(1);
    } with {
        // $sys.log(traceback());
    }
    if (!task_queue)
        task_index = 1;
};

public method $scheduler.task_queue() {
    // sender must be system, for now
    (> .perms(sender(), 'system) <);
    return task_queue;
};

public method $scheduler.del_task() {
    arg tid;
    var task, n;
    
    // sender must be system, for now.
    if (type(tid) != 'integer)
        throw(~type, "Task Identification must be an integer");
    for task in (task_queue) {
        n++;
        if (task[1] == tid) {
            sender() == task[4] && caller() == task[5] || (> .perms(sender(), 'system) <);
            .del_from_task_queue(n);
            return 1;
        }
    }
    throw(~tasknf, "No task found by that TID");
};

public method $scheduler.add_task() {
    arg time, method, @args;
    var task, i, j, tid, flags, x, y, task_time;
    
    if (type(time) != 'integer || type(method) != 'symbol || type(args) != 'list)
        throw(~type, "Arguments are not an integer, symbol, and list.");
    if (time < 0)
        throw(~time, "Time is negative.");
    if (time > 31536000)
        throw(~time, "Try to schedule a task LESS than a year from now?");
    
    // get a new db task id
    ++task_index;
    tid = task_index;
    
    // flags can be set in a system only add_task, for now set them as 'system
    flags = ['system];
    task_time = time();
    task = [tid, time + task_time, task_time, sender(), caller(), method, flags, args];
    .add_to_task_queue(task);
    return tid;
};

public method $scheduler.pulse() {
    var task, sub, t;
    
    // called by $sys.heartbeat
    if (caller() != $sys)
        throw(~perm, "Sender is not system");
    t = time();
    while (task_queue && t > task_queue[1][2]) {
        task = task_queue[1];
        .remove_first_task();
        catch any
            (> task[4].as_this_run(task[4], task[6], task[8]) <);
        with
            (| task[4].manager().tell_traceback(traceback()) |);
    }
    
    // call sub schedulers 
    for sub in (sub_schedulers)
        (| sub.pulse() |);
    
    // $#Edited: 30 Apr 97 17:50 $scheduler
    // $#Edited: 08 Jan 98 21:24 $brian
};

public method $scheduler.sys_add_task() {
    arg time, method, sender, caller, flags, @args;
    var task, i, j, tid, x, y, tmpq, task_time;
    
    // use `@info $scheduler' for more information. 
    // [tid, time, time(), sender(), caller(), method, flags, args]
    //
    if (!$sys.is_agent(sender()))
        throw(~perm, "Sender is not an agent or admin, use .add_task()");
    if (type(time) != 'integer || type(method) != 'symbol || type(args) != 'list)
        throw(~type, "Arguments are not an integer, symbol, and list.");
    if (time < 1)
        throw(~time, "Time is negative.");
    if (time > 31536000)
        throw(~time, "Try to schedule a task LESS than a year from now?");
    if (!valid(sender))
        throw(~type, "The argument for sender is not a valid object");
    if (!valid(caller))
        throw(~type, "The argument for caller is not a valid object");
    if (type(flags) != 'list)
        throw(~type, "Send flags as a list of symbols");
    
    //
    ++task_index;
    tid = task_index;
    task_time = time();
    
    // flags can be set in a system only add_task, for now set them as 'system
    task = [tid, task_time + time, task_time, sender, caller, method, flags, args];
    .add_to_task_queue(task);
    return tid;
};

public method $scheduler.add_sub_scheduler() {
    arg object;
    
    if (!$sys.is_admin(sender()))
        throw(~perm, "Only admins may add sub schedulers.");
    if (type(object) != 'objnum)
        throw(~type, "Object must be a dbref.");
    sub_schedulers += [object];
};

public method $scheduler.del_sub_scheduler() {
    arg object;
    var pos, s;
    
    if (!$sys.is_admin(sender()))
        throw(~perm, "Only admins may delete sub schedulers.");
    if (type(object) != 'objnum)
        throw(~type, "Object must be a dbref.");
    pos = object in sub_schedulers;
    if (!pos)
        throw(~objnf, "Object not a sub scheduler.");
    s = [];
    if (pos > 1)
        s += [sub_schedulers.subrange(1, pos - 1)];
    if (s < sub_schedulers.length())
        s += [sub_schedulers.subrange(pos + 1)];
    sub_schedulers = s;
};

public method $scheduler.has_blocked_tasks() {
    arg ident;
    
    return blocked_tasks.contains(ident);
};

public method $scheduler.block_task() {
    arg ident;
    var tasks;
    
    // I want to be atomic!
    // Add the task_id to the queue of blocked tasks for this identifier.
    if (blocked_tasks.contains(ident))
        tasks = blocked_tasks[ident] + [task_id()];
    else
        tasks = [task_id()];
    blocked_tasks = blocked_tasks.add(ident, tasks);
    
    // And go to sleep until we are woken.
    $sys.suspend();
};

public method $scheduler.unblock_task() {
    arg ident;
    var tasks;
    
    // I want to be atomic!
    // The caller should have checked first, but we will fail silently.
    if (!.has_blocked_tasks(ident))
        return;
    
    // Get the blocked tasks queue for this identifier.
    tasks = blocked_tasks[ident];
    
    // If this is the last blocked task, then clear the queue, otherwise
    // just delete the task_id that we are resuming.
    if (tasks.length() == 1)
        blocked_tasks = blocked_tasks.del(ident);
    else
        blocked_tasks = blocked_tasks.add(ident, tasks.delete(1));
    
    // Wake it up and go.
    $sys.resume(tasks[1]);
};

protected method $scheduler.resume_job() {
    arg tid;
    
    (> .resume(tid) <);
};

public method $scheduler.suspend() {
    arg @objs;
    
    objs += [user()];
    suspended_tasks = dict_add(suspended_tasks, task_id(), objs);
    return (> suspend() <);
};

public method $scheduler.resume() {
    arg task_id, @return_value;
    var objs;
    
    if ((objs = (| suspended_tasks[task_id] |))) {
        if (!(sender() in objs) && !$sys.is_system(sender()))
            throw(~perm, sender() + " may not resume task " + task_id);
        suspended_tasks = dict_del(suspended_tasks, task_id);
    }
    return (> resume(task_id, @return_value) <);
};

public method $scheduler.cancel() {
    arg task_id;
    var objs;
    
    if ((objs = (| suspended_tasks[task_id] |))) {
        if (!(sender() in objs) && !$sys.is_system(sender()))
            throw(~perm, sender() + " may not cancel task " + task_id);
        suspended_tasks = dict_del(suspended_tasks, task_id);
    }
    return (> cancel(task_id) <);
};

public method $scheduler.task_info() {
    arg @args;
    
    (> .perms(sender(), 'system) <);
    return (> task_info(@args) <);
};

public method $scheduler.suspended_task() {
    arg task;
    
    return (> suspended_tasks[task] <);
};

private method $scheduler.del_from_task_queue() {
    arg i;
    
    refresh();
    
    // this must *not* throw
    task_queue = $heap.del(task_queue, i, 2);
};

private method $scheduler.add_to_task_queue() {
    arg task;
    
    refresh();
    
    // We don't want to run out of tics
    task_queue = $heap.push(task_queue, task, 2);
};

root method $scheduler.core_scheduler() {
    suspended_tasks = #[];
    task_queue = [];
};
new object $scheduler: $has_commands;