import edu.mit.ai.psg.traveler.*;
import edu.mit.ai.psg.jexa.*;
import edu.mit.ai.psg.ui.outliner.*;
import edu.mit.ai.psg.ui.patches.CloseableFrame;
import java.lang.reflect.*;
import java.util.*;
import java.awt.*;
import java.awt.event.*;

/** A simulation of this scenario, for illustrating trace biography:
    <blockquote>
       Billee earns 300/week and rents a place at 1000/month.<br>
       The landlord, Lordy, directly debits Billee's account monthly.<br>
       If the rent doesn't come through, Lordy gets anxious,<br>
       and calls on bill collectors who retry weekly.<br>
    </blockquote>

    This version has calls to Trace added to record calls on the Account,
    and activities which start new threads.

    @author CarlManning, caroma@ai.mit.edu<br>
    Copyright (c) 1999 Massachusetts Institute of Technology
**/
class SharedAccountBioTraced { 
  public static void run(int msPerYear) {
    final CallRecord runRecord =
      Trace.receivedStatic(runMethod, new Object[]{});
    final AccountTraced account = new AccountTraced("MyChecking", 0);
    final int yearLength = msPerYear; // e.g., 3000ms
    final int monthLength = yearLength/12;
    final int weekLength = yearLength/52;
    final long yearStart = System.currentTimeMillis();
    final long yearEnd = yearStart + yearLength;

    Thread billee = new Thread("Billee") {
      public void run() {
	Method billeeMeth= Trace.getMethod(this.getClass(),"run",null);
	CallRecord billeeRecord= Trace.received(this, billeeMeth,null);
	int salary = 300; 
	for (int week = 1; week <= 52; week++) {
	  // add a note in this thread's call record
	  Trace.doing("...Sleeping to work...");
	  sleepTo(yearStart + week * weekLength);
	  account.deposit(salary);
	}
	Trace.returningVoid(billeeRecord);
      }};

    Thread lordy = new Thread("Lordy") {
      public void run() {
	Method lordyMeth = Trace.getMethod(this.getClass(),"run",null);
	final CallRecord lordyRecord =
	  Trace.received(this, lordyMeth, null);
	final int rent = 1000; final int penalty = 10; 
	final Collection outstandingCollectors =
	  Collections.synchronizedCollection(new LinkedList());
	int collectorCount = 0;
	for (int month = 1; month <= 12; month++) {
	  try { account.withdraw(rent); }
	  catch (IllegalArgumentException overdraft) {

	    // if not paid, create and start bill collector to retry weekly
	    String collectorName = "Collector"+(++collectorCount);
	    ActivityRecord startCollectorActivity = 
	      Trace.beginning("starting "+collectorName);
	    Thread collector = new Thread(collectorName) {
	      int due = rent + penalty;
	      long startDate = System.currentTimeMillis();
	      int week = 0;
	      public void run() { 
		Method collectorMeth =
		  Trace.getMethod(this.getClass(),"run",null);
		CallRecord collectorRecord =
		  Trace.received(this, collectorMeth, null);

		while (System.currentTimeMillis() <= yearEnd) { 
		  try {
		    account.withdraw(due);
		    break;
		  } catch (IllegalArgumentException overdraft2) { 
		    week++;     due += penalty; 
		    // add a note in this thread's call record
		    Trace.doing("...snoring...");
		    sleepTo(startDate + week * weekLength);
		  }
		}
		outstandingCollectors.remove(this);
		Trace.returningVoid(collectorRecord);
		return;
	      }}; // end collector

	    outstandingCollectors.add(collector);
	    collector.start();
	    Trace.exitting(startCollectorActivity);
	    Thread.yield();
	  }
	  // if unpaid lordy can't sleep 
	  Trace.doing(outstandingCollectors.isEmpty()?
		      "...sweet dreams..." : "...fitful naps ...");
	  sleepTo(yearStart + month * monthLength);
	}
	Trace.returningVoid(lordyRecord);
	return;
      }}; // end lordy

    billee.start();
    lordy.start();
    try { billee.join(); lordy.join(); } catch (InterruptedException e) {}
    Trace.returningVoid(runRecord);
    return;
  }
  static void sleepTo(long wakeupTime) {
    long now = System.currentTimeMillis();
    try { Thread.sleep(wakeupTime - now); }
    catch (InterruptedException e) {}
    catch (IllegalArgumentException e) { } // time has past
  }
  static Method runMethod =
    Trace.getMethod(SharedAccountBioTraced.class,
		    "run", new Class[]{int.class});
}


/** A class for a shared account for deposits and withdrawals **/
class AccountTraced
implements Cloneable, SharedAccountBioApplet.CloneablePublic 
  // (added SharedAccountBioApplet.CloneablePublic interface
  //  to make clone method public on public interface for applet)
{
  /** name **/
  private String myName;
  /** balance should be >= 0 **/
  private int myBalance;
  /** Constructor:  initBalance should be >= 0. **/
  public AccountTraced(String name, int initBalance) { 
    CallRecord record =
      Trace.received(this, accountConstructor,
		     new Object[]{name, new Integer(initBalance)});
    myName = name; 
    myBalance = initBalance;
    synchronized(this) { // capture initial state in bio
      ActivityRecord r = Trace.beganSynchronization(this, accountConstructor);
      Trace.exitting(r);
    }
    Trace.returning(record, this);
  }
  static final Constructor accountConstructor =
    Trace.getConstructor(AccountTraced.class,
			 new Class[]{String.class, int.class});
  /** clone method made public so Account can be cloned in Applet without
      making it a public class (which would either require that it
      be in its own file [harder to browse], or be a nested class of a
      public class [name becomes long]) **/
  public Object clone() throws CloneNotSupportedException {
    return super.clone(); }
  /** return current balance of account **/
  public synchronized int getBalance() {
    CallRecord record =
      Trace.received(this, balanceMethod, null);
    int result = myBalance;
    return Trace.returning(record, result);
  }
  static final Method balanceMethod =
    Trace.getMethod(AccountTraced.class, "getBalance", null);
  /** deposit amount >= 0 **/
  public synchronized void deposit(int amount) {
    CallRecord record =
      Trace.received(this, depositMethod, new Object[]{new Integer(amount)});
    myBalance += amount;
    Trace.returningVoid(record);
  }
  static final Method depositMethod =
    Trace.getMethod(AccountTraced.class,"deposit", new Class[]{int.class});
  /** withdraw amount >= 0 **/
  public synchronized void withdraw(int amount) {
    CallRecord record =
      Trace.received(this, withdrawMethod, new Object[]{new Integer(amount)});
    if (amount <= myBalance)
      myBalance -= amount;
    else 
      throw ((IllegalArgumentException)
	     Trace.throwing
	       (record, new IllegalArgumentException("Overdraft")));

    Trace.returningVoid(record);
  }
  static final Method withdrawMethod =
    Trace.getMethod(AccountTraced.class,"withdraw",new Class[]{int.class});
  public String toString() {
    return "Account(name="+myName+", balance="+myBalance+")"; }
}

/** This applet can be run as an application as well **/
public class SharedAccountBioApplet extends JexaApplet { 
  /** application startup: invokes initFrameApplet **/
  public static void main(String[] args) {
    if (args.length > 0) initSecsPerSimYear = args[0];
    // run applet as frame
    Frame frame = new SharedAccountBioApplet().initFrameApplet();
    // provide clean exit -- exit when original frame is closed.
    frame.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) { System.exit(0); }});
  }
  // invoke simulation
  static String initSecsPerSimYear = "12";
  public static ActivityRecord run(final String parameter) {
    ActivityRecord record =
      Trace.entering(new StringRecordObservable
		     ("Tracing SharedAccountBio and calls to Account"));
    Thread thread = new Thread(new Runnable() {
      public void run() { 
	SharedAccountBioTraced.run(1000*Integer.parseInt(parameter));
      }});
    thread.start();
    // ensure at least one first level child is available when first displayed
    while(record.getChildren().isEmpty()) Thread.yield();
    return record;
  }    
  // Applet methods 
  public String getAppletTitle() { return "Traveler: SharedAccountBio"; }
  public Object getObjectToExamine() {
    return null;
  }
  public void initPanelInPlaceApplet() {
    Trace.traceRecorder = new TraceRecorderObservable();
    TravelerOutliner.ensureInitialized();
    super.initPanelInPlaceApplet();
    Panel headPanel = (Panel) getComponent(0);
    Panel jexaPanel = (Panel) getComponent(1);
    headPanel.add(makeParamPanel(jexaPanel), BorderLayout.SOUTH);
  }    
  public Frame initFrameApplet() {
    Trace.traceRecorder = new TraceRecorderObservable();
    TravelerOutliner.ensureInitialized();
    Frame frame = new CloseableFrame(getAppletTitle());
    Panel jexaPanel = new Panel();
    OutlineNode node =
      Outliner.outlineMaker.makeOutlineNode(getObjectToExamine());
    Outliner.outlineMaker.initializePane(jexaPanel, node);
    frame.add(makeParamPanel(jexaPanel), BorderLayout.NORTH);
    frame.add(jexaPanel, BorderLayout.CENTER);
    frame.setBounds(60, 28, 580, 300);
    frame.show();
    return frame;
  }    
  public Panel makeParamPanel(final Container jexaPane) {
    Panel paramPanel = new Panel(new FlowLayout(FlowLayout.CENTER, 0, 0));
    paramPanel.setBackground(SystemColor.control); 
    paramPanel.add(new Label("1 simulated year = "));
    final TextField paramField =
      new TextField(initSecsPerSimYear, 2);
    paramPanel.add(paramField);
    paramPanel.add(new Label("seconds"));
    final Button paramButton = new Button("Go");
    paramButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
	String param = paramField.getText().trim();
	try {
	  int secs = Integer.parseInt(param);
	  if (secs <= 0 || secs > 120) throw new NumberFormatException();
	} catch (NumberFormatException err) {
	  paramField.setText(param = initSecsPerSimYear);
	}
	ActivityRecord record = run(param); 
	// display record
	OutlineNode node = Outliner.outlineMaker.makeOutlineNode(record);
	jexaPane.invalidate();
	Outliner.outlineMaker.initializePane(jexaPane, node);
	jexaPane.validate();
      }});
    paramPanel.add(paramButton);
    return paramPanel;
  }
  // public interface to make AccountTraced cloneable in applet
  public interface CloneablePublic {
    public Object clone() throws CloneNotSupportedException;
  }
}

