/* Activity is a formal class to group common behavior among
   tasks and milestones for projects.  Activities know how to
   determine if they are on the critical path and keep track
   of their earlyStart, lateFinish and slack time.
   The user can set an earlyStart or lateFinish to override
   what is calculated.

   Since Activity descends from Node, it inherits all of the
   methods and instance variables from Node, e.g. name, desc,
   inputs, outputs, etc.
*/!!

inherit(Node, #Activity, #(earlyStart     /* calculated */ 
lateFinish     /* calculated */
userEarlyStart /* user entered */
userLateFinish /* user entered */
slack          /* slack time */
critical       /* boolean */), 2, nil)!!

now(ActivityClass)!!

now(Activity)!!

/* Return the class name.  Generic version.
   Ancestors will redefine this to use resources. */
Def className(self)
{
  ^asString(class(self));
}!!

/* Return true if the activity is "complete", e.g.
   connected.  Descendants may redefine this.
   This is for experimental use only.   */
Def  complete(self)
{ 
  ^size(inputs) + size(outputs) > 0;
}!!

/* In general, activities place no limit on the number
   of connections.  Descendants, such as Task, will
   redefine this to place limits. */
Def  maxConnectionNames(self, names)
{ 
  ^names;
}!!

/* Delete an activity.  First check to see if it is
   connected to anything and check with the user. */
Def  delete(self)
{ 
  if size(inputs) + size(outputs) = 0
    cor yesNoBox(loadString(PW_WARNING),
                 name + loadString(PW_ACTUSE1) +
                 loadString(PW_ACTUSE2) + CR_LF +
                 loadString(PW_DELETE)) == IDYES
     do(inputs,
       {using(input)
        disconnect(input, self);
     });
     do(outputs,
       {using(output)
        disconnect(self, output);
     });
     removeNode(network, self);
     remove(network.display, pos(self));
  endif;      
}!!

/* Parse a string of nodeNames and return a set of
   nodes.  Checks to see if they exist.  */
Def  parseNodeNames(self, nodeNames | names, nodes, aNode)
{
  names := words(nodeNames);
  names := maxConnectionNames(self, names);
  nodes := new(OrderedCollection, 5);
  do(names,
    {using(name) 
     if aNode := checkNodeExists(network, name)
        add(nodes, aNode);
     endif;
  });
  ^nodes;
}!!

/* Checks the connections and reconnects if necessary. 
   If the connections change, we may have to recalc
   the entire project. */
Def  checkConnection(self, inputsString, outputsString
                     | newInputs, newOutputs, calcReqd)
{  
  if inputsString <> getInputNames(self)
    newInputs := parseNodeNames(self, inputsString);
    do(inputs,
      {using(input)
       if not(input in newInputs)
          disconnect(network, input.name, self.name); 
          calcReqd := true;
       endif;
     });
    do(newInputs,
      {using(input)
       if not(input in inputs)
        connect(network, input.name, self.name); 
        calcReqd := true;
       endif;
     });
   endif;

  if outputsString <> getOutputNames(self)
    newOutputs := parseNodeNames(self, outputsString);   
    do(outputs,
      {using(output)
       if not(output in newOutputs)
         disconnect(network, self.name, output.name); 
         calcReqd := true;
       endif
     });
    do(newOutputs,
      {using(output)
       if not(output in outputs)
         connect(network, self.name, output.name); 
         calcReqd := true;
       endif;
     });
  endif;            
  
  if autoCalc(network) cand calcReqd
    recalc(network)
  endif;
}!!

/* Return a string of output names. 
   This is useful when editing. */
Def  getOutputNames(self | str)
{
  str := "";
  do(outputs,
    {using(output) str := str + getName(output) + " ";
  });
  ^str;
}!!

/* Return a string of input names. 
   This is useful when editing. */
Def  getInputNames(self | str)
{
  str := "";
  do(inputs,
    {using(input) str := str + getName(input) + " ";
  });
  ^str;
}!!

/* Display and edit the activity information. 
   Descendants that use this should have a dialogClass()
   method that returns the dialog class to be used. The
   dialog class should define methods run() and setEditItem().
*/
Def  editInfo(self | dlg, retValue)
{
  showWaitCurs();              /* this can take a while */
  dlg := new(dialogClass(self));
  setEditItem(dlg, self);
  retValue := run(dlg, ThePort);
  showOldCurs();               /* all done */
  ^retValue;
}!!

/* In the simplest case an activity is critical if slack = 0.
   However, if the user has set the lateFinish, it could
   introduce excess slack to the project.  Therefore, a node
   is  critical if its slack is equal to the excess slack. */
Def  calcCritical(self)
{ 
  critical := (slack = getSlack(network));
}!!

/* Set the values of an activity. 
   Values is an array of name, desc,
   userEarlyStart, userLateFinish. */
Def  setValues(self, values | oldUES, oldULF)
{
  oldUES := userEarlyStart;
  oldULF := userLateFinish;
  name := values[0];
  desc := values[1];
  userEarlyStart := values[2];
  userLateFinish := values[3];
  if autoCalc(network) cand 
     (oldUES <> userEarlyStart cor
      oldULF <> userLateFinish)
    recalc(self);
  endif;
}!!

/* Get the user set earlyStart time. */
Def  getUserEarlyStart(self)
{ 
 ^userEarlyStart;
}!!

/* Get the user set lateFinish time. */
Def  getUserLateFinish(self)
{ 
 ^userLateFinish;
}!!

/* Return a string to be used as a caption in a window. */
Def  makeCaption(self)
{
  ^className(self) + ": "+getName(self)+
   if critical(self)
      loadString(PW_CRITICAL)
   else
      loadString(PW_NONCRITICAL)
   endif;
}!!

/* Summarize useful information on a single line. */
Def  getInfoLine(self | str)
{
  if critical(self)
    str := "*";
  else
    str := " ";
  endif;
  ^str + field(name, 8) 
   + field(className(self), 4) + " "
   + field(asString(getCost(self)), 3) + " "
   + field(asString(getTime(self)), 2) + " " 
   + field(asString(getSlack(self)), 3) + " " 
   + field(asString(getEarlyStart(self)), 8) + " "
   + field(asString(getLateFinish(self)), 8);
}!!

/* Get the lateFinish time.  At End, lateFinish = earlyStart,
   unless the user set a lateFinish. */
Def  getLateFinish(self)
{ 
  if (size(outputs) == 0 and not(userLateFinish))
    lateFinish := earlyStart
  endif;
 ^lateFinish;
}!!

/* Calculate slack for an Activity.  This is done
   on the backward recalc pass when ES, LF are
   up to date.  Slack is never < 0. */
Def  calcSlack(self)
{  
  ^slack := max(0, getLateStart(self) - getEarlyStart(self));
}!!

/* Get the slack value.  */
Def getSlack(self)
{
  ^slack;
}!!

/* Return the status of whether the node is critical or not. */
Def  critical(self)
{ 
  ^critical;
}!!

/* Invalidate a node and recursively invalidate all
   nodes it's connected to.  This is used to force
   an entire recalc of the network. */
Def invalidate(self, visited)
{
 reset(self);                      /* reset ivars */
 do(outputs,                       /* pass along */
    {using(elem)
     if not(elem in visited)       /* avoid looping */
        add(visited, elem);
        invalidate(elem, visited); /* recurse */
     endif;
  });
}!!

/* Recalculate the network from this node onwards.
   This requires forcing a forward recalc1 and a
   backwards recalc2 from the end of the net. */
Def  recalc(self)
{
  recalc1(self,true);      /* force forward recalc1 */
  recalc2(network);        /* do backwards recalc2 */
}!!

/* Backward recalc pass:  (not optimized)
   Recalculate an activity's lateFinish then its slack and
   determine if it's critical.
   If the user has set a lateFinish, use it instead of
   recalculating.  Always propogate recalc2 since a
   change to the time of a node will not change lateFinish,
   but it can change slack and critical, which are only
   known on the backwards pass.
   
   formula:  LF = min(LF(i) - time(i)) for all outputs i
   
   Note: This could be optimized to only propogate recalc2
   as far back as the next Milestone.  */
Def  recalc2(self)
{  
  if (userLateFinish)
    lateFinish := userLateFinish;   /* user override */
  else
    lateFinish := asLong(date(12,31,1999));
    do(outputs,
      {using(output)
      lateFinish := min(lateFinish, 
                         getLateFinish(output) - getTime(output));
    });
    lateFinish := asDate(lateFinish);
  endif;
  
  calcSlack(self);
  calcCritical(self);

  /* Continue sending the recalc2 message. */

  do(inputs,
    {using(input)
    recalc2(input);
  });
}!!

/* Forward recalc pass: (optimized minimal recalc)
   Recalculate an activity's earlyStart.  If the user has
   set an earlyStart, use it instead of recalculating.
   Send a message to the node's outputs to recalculate only
   if a forced recalc is required, or if earlyStart changes.
   
   formula:  ES = max(ES(i) + time(i)) for all inputs i

   arguments:
     timeChanged: force a recalc1 if the time has changed
*/
Def  recalc1(self, timeChanged | oldEarlyStart)
{   
  oldEarlyStart := earlyStart;

  if (userEarlyStart)
    earlyStart := userEarlyStart;     /* user override */
  else
    earlyStart := asLong(new(Date));    
    do(inputs,
      {using(input) 
      earlyStart := max(earlyStart,
                     getEarlyStart(input) + getTime(input));                 
    });
    earlyStart := asDate(earlyStart);
  endif;
  
  /* Recalculate outputs only if earlyStart changed OR if time has
     changed.  Don't force it to continue beyond the next level.  */

  if timeChanged cor (earlyStart <> oldEarlyStart)   
    do(outputs,
      {using(output) recalc1(output, nil);
    });
  endif;
}!!

/* Initialize a new Activity. */
Def  init(self)
{ init(self:Node);        /* use ancestor init */
  reset(self);            /* reset ivars */
} !!

/* Reset the instance variables. This is called by
   init and when an entire recalc is forced. */
Def reset(self)
{
  slack := 0;
  lateFinish := earlyStart := new(Date);
  critical := nil;
}!!

/* The user can enter a late finish date to be used
   instead of the calculated value. */
Def  setLateFinish(self, aDate)
{ 
  userLateFinish := aDate;
  if autoCalc(network)        /* just do backwards calc */
     recalc2(network);
  endif;
}!!

/* The user can enter an early start date to be used
   instead of the calculated value. */
Def  setEarlyStart(self, aDate)
{ 
  userEarlyStart := aDate;
  if autoCalc(network)
    recalc(self);
  endif;
} !!

/* Get the earlyStart time. */
Def  getEarlyStart(self)
{ 
 ^earlyStart;
} !!

/* Calculcate the earlyFinish time. */
Def  getEarlyFinish(self)
{ 
 ^asDate(getEarlyStart(self) + getTime(self));
}!!

/* Calcualate the lateStart time. */
Def  getLateStart(self)
{ 
 ^asDate(getLateFinish(self) - getTime(self));
}!!


