/*
  daemon.t version 1.00 (7 May 2001)
  Daemon priority sorting for TADS 2
  Dan Schmidt <[email protected]>

TADS 2 does not allow authors to specify exactly when daemons should run
relative to each other.  The ill effects of this can range from
infelicitous (you lose control over the order in which some messages are
printed) to quite annoying (you don't know for sure when the turncount()
daemon will run, incrementing global.turnsofar).  daemon.t allows you to
specify a priority for each daemon, so that you can set exactly the
order in which they should run.

To use it, simply start up the timeline object at the beginning of time
by inserting the following line into init():

 notify (timeline, &run, 0);

and replace all your calls to notify, unnotify, set/remdaemon, and
set/remfuse with the following calls:

- add_daemon (obj, func [, priority]) starts a daemon that will execute
  at the end of every turn, starting this one.  If you want the daemon
  to be a global function, rather than the method of an object, set obj
  to nil.  The priority argument is optional and defaults to
  timeline.default_priority (1000 by default) if not given.

- add_fuse (obj, func, timeout [, priority]) starts a fuse; the specified
  function or method will be called timeout turns from now.  As with
  add_daemon, the priority argument is optional, and obj may be nil to
  specify a global function.

- rem_daemon (obj, func) removes the specified daemon.

- rem_fuse (obj, func) removes the specified fuse.

If any of these functions are called from within a daemon/fuse, they
will not take effect until the following turn.

The larger the priority number of a daemon or fuse, the later it runs.
This may seem kind of unintuitive, but think of it in the 'take a number
at the deli' sense; low numbers are served first.

Daemons and fuses share the same priority queue, so a daemon with
priority 10 executes before a fuse with priority 20 which executes
before a daemon with priority 30.  Daemons/fuses with the same priority
happen to execute most-recently-added first, but don't count on that.

TADS' setdaemon and setfuse builtins take an extra parameter, which is
passed to the daemon or fuse function.  daemon.t's add_daemon and
add_fuse functions support this behavior as well; if the func
parameter is a two-element list consisting of a symbol and a
parameter, rather than just a symbol, then the first element of the
list is taken to be the function to be called, and the second element
is taken to be the parameter to pass to the function.

If you use the two-element func option for add_daemon or add_fuse, you
must also use it when removing, as with setdaemon/setfuse.

Some examples:

 add_daemon (elephant, &stomp_around);

   causes elephant.stomp_around to be called every turn.

 add_daemon (elephant, &stomp_around, 100);

   has the same behavior, but elephant.stomp_around now executes with a
   priority of 100, rather than the default.

 rem_daemon (elephant, &stomp_around);

   stops elephant.stomp_around from being called any more.

 add_daemon (elephant. [&trumpet 3]);

   causes elephant.trumpet(3) to be called every turn.

 add_fuse (elephant, &go_berserk, 5, 50);

   causes elephant.go_berserk to be called 5 turns from now.  It will
   execute with a priority of 50 (so, for example, it would execute
   before the stomp_around daemon).

daemon.t will happily coexist with 'real' daemons, fuses, and
notifies, but you won't have control over when the other ones execute.

*/

#pragma C+

// A sysdaemon is a method to be called on an object every turn.
// They are kept as an explicit linked list since sorting actual
// TADS lists is a pain in the neck.

class sysdaemon: object
 pri = 0                               // priority: 0 max, 32767 min
 obj = nil                             // object to have method called (or nil for function)
 func = nil                            // method/function to call
 timeout = 0                           // when to call, or -1 for every turn
 next = nil                            // next daemon on priority-ordered list

 // Initialize
 ini (o, f, t, p) = {
   self.obj = o;
   self.func = f;
   self.timeout = t;
   self.pri = p;
 }
;

null_daemon: sysdaemon pri=32767;               // End of list sentinel
head_daemon: sysdaemon pri=-1 next=null_daemon; // Beginning of list marker

timeline: object
 head = head_daemon                    // head of our linked list of sysdaemons
 default_priority = 1000               // priority if not specified

 // If we're traversing the daemon list, we set self.running to true
 // and save off any pending add_daemons and rem_daemons.
 running = nil
 pending_adds = []
 pending_rems = []

 add (o, f, t, p) = {
   if (p <= head_daemon.pri || p >= null_daemon.pri) {
     "[BUG] priority out of range while adding daemon or fuse\n";
     return;
   }

   if (self.running) {
     self.pending_adds += [[o, f, t, p]]; // save it off
   } else {
     local d = self.head;
     local newd = new sysdaemon;       // create a new sysdaemon representing this call
     if (t == 0) t = -1;               // to us, -1 means permanent
     newd.ini (o, f, t, p);

     // Insert into the sorted list
     while (1) {
       if (newd.pri <= d.next.pri) {
         newd.next = d.next;           // splice it in
         d.next = newd;
         break;
       }
       d = d.next;                     // move down the list
     }
   }
 }

 rem (o, f) = {
   if (self.running) {
     self.pending_rems += [[o, f]];    // save it off
   } else {
     // Delete from the sorted list
     local flist = (datatype(f) == DTY_LIST); // f is list?
     local d = self.head;
     while (1) {
       if (d.next == null_daemon) {
         "[BUG] tried to remove nonexistent daemon\n";
         return;
       }
       // '==' doesn't work for lists, thus the following annoyance
       if (d.next.obj == o && flist ? (d.next.func[1] == f[1] && d.next.func[2] == f[2])
                                    : (d.next.func == f)) {
         local destroyed = d.next;     // save a handle to the upcoming guy
         d.next = d.next.next;         // splice it out
         delete destroyed;             // and kill it
         break;
       }
       d = d.next;                     // move down the list
     }
   }
 }

 run = {
   // Traverse the sorted list, calling each daemon
   local prevd = self.head;            // previous daemon called
   local d = prevd.next;               // daemon to call

   self.running = true;                // mark ourselves as traversing the list
   while (d != null_daemon) {
     local nextd = d.next;             // daemon to call next round
     if (d.timeout <= 0) {             // fuse expired, or it's permanent
       if (d.obj) {
         if (proptype(d, &func) == DTY_LIST) {
           (d.obj).(d.func[1])(d.func[2]); // method call w/ arg
         } else {
           (d.obj).(d.func);           // method call
         }
       } else {
         if (proptype(d, &func) == DTY_LIST) {
           (d.func[1])(d.func[2]);     // function call w/ arg
         } else {
           (d.func)();                 // function call
         }
       }
     }
     if (d.timeout == 0) {             // fuse expired
       prevd.next = d.next;            // splice it out
       delete d;                       // and kill it
     } else if (d.timeout > 0) {
       --d.timeout;                    // fuse runs down
     }
     prevd = d;                        // move down the list
     d = nextd;
   }
   self.running = nil;                 // done with the list

   // Now handle any adds and rems that took place while we were traversing.
   {
     local i;
     for (i = 1; i <= length(self.pending_adds); ++i) {
       local p = self.pending_adds[i];
       self.add (p[1], p[2], p[3], p[4]);
     }
     for (i = 1; i <= length(self.pending_rems); ++i) {
       local p = self.pending_rems[i];
       self.rem (p[1], p[2]);
     }
     self.pending_adds = [];
     self.pending_rems = [];
   }
 }
;

////////////////////////////////////////////////////////////
//
// Now follows the global function interface

add_daemon: function (obj, func, ...)
{
 local pri = timeline.default_priority;
 if (argcount >= 3) {
   pri = getarg(3);
 }
 timeline.add (obj, func, 0, pri);
}

add_fuse: function (obj, func, t, ...)
{
 local pri = timeline.default_priority;
 if (argcount >= 4) {
   pri = getarg(4);
 }
 timeline.add (obj, func, t, pri);
}

rem_daemon: function (obj, func)
{
 timeline.rem (obj, func);
}

rem_fuse: function (obj, func)
{
 timeline.rem (obj, func);
}