/**************************************************************************
* Copyright (c) 2001, 2005 David J. Eck                                   *
*                                                                         *
* Permission is hereby granted, free of charge, to any person obtaining   *
* a copy of this software and associated documentation files (the         *
* "Software"), to deal in the Software without restriction, including     *
* without limitation the rights to use, copy, modify, merge, publish,     *
* distribute, sublicense, and/or sell copies of the Software, and to      *
* permit persons to whom the Software is furnished to do so, subject to   *
* the following conditions:                                               *
*                                                                         *
* The above copyright notice and this permission notice shall be included *
* in all copies or substantial portions of the Software.                  *
*                                                                         *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,         *
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF      *
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  *
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY    *
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,    *
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE       *
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                  *
*                                                                         *
* ----                                                                    *
* (Released under new license, April 2012.)                               *
*                                                                         *
*             David J. Eck                                                *
*             Department of Mathematics and Computer Science              *
*             Hobart and William Smith Colleges                           *
*             300 Pulteney Street                                         *
*             Geneva, NY 14456                                            *
*             eck@hws.edu                                                 *
*             http://math.hws.edu/eck                                     *
**************************************************************************/

package edu.hws.jcm.awt;

import edu.hws.jcm.data.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Vector;

/**
 * Controllers are the focus of all the action in the JCM system.  A Controller can be
 * set to listen for changes (generally changes in user input).  This is done by
 * registering the Controller with (usally) an InputObject.  For example, if a Controller, c,
 * is to respond when the user presses return in a VariableInput, v, then
 * v.setOnUserAction(c) should be called to arrange to have the Controller listen
 * for such actions.   VariableSliders, ExpressionInputs, MouseTrackers, Animators have a similar
 * methods.  It is also possible to set the Controller to listen for events of
 * type AdjustmentEvent, ActionEvent, TextEvent, or ItemEvent (but this feature is
 * left over from an older version of JCM, and I'm not sure whether it's necessary).
 * Whenever a Controller learns of some change, it will process any InputObjects,
 * Ties, and Computables that have been registered with it.
 *   
 * <p>InputObjects and Computables have to be added to a Controller to be processed,
 * using the Controller's add method.  (If you build your inteface out of JCMPanels,
 * then this is done automatically.)  (Note that an InputObject is added to a Controller
 * to have its value checked -- This is separate from registering the Controller to
 * listen for changes in the InputObject.  Often, you have to do both.)  The gatherInputs()
 * method in class JCMPanel can be used to do most of this registration automaticaly.
 *   
 * <p>A Tie that synchronizes two or more Values, to be effective, has to be added to a Controller.
 * See the Tie class for inforamtion about what Ties are and how they are used.
 *   
 * <p>A Controller can have an associated ErrorReporter, which is used to report any
 * errors that occur during the processing.  Currently, an ErrorReporter is either
 * a DisplayCanvas or a MessagePopup.
 *   
 * <p>A Controller can be added to another Controller, which then becomes a sub-controller.
 * Whenever the main Controller hears some action, it also notifies all its sub-controllers
 * about the action.  Furthermore, it can report errors that occur in the sub-controllers,
 * if they don't have their own error reporters.  (Usually, you will just set an error 
 * reporter for the top-level Controller.)
 **/
public class Controller implements java.io.Serializable, Computable, InputObject,
                      AdjustmentListener, ActionListener, TextListener, ItemListener {
                      
   /**
    * Computable objects controlled by this controller.  Note that Controllers
    * are Computables, so this list can include sub-controllers.
    */
   protected Vector computables;

   /**
    * InputObjects controlled by this controller.  Note that Controllers
    * are InputObjects, so this list can include sub-controllers.
    */
   protected Vector inputs;
                                            
   /**
    * Ties that have been added to this controller.
    */
   protected Vector ties;
   
   /**
    * Used for reporting errors that occur in the
    * compute() method of this controller.  If the errorReporter
    * is null and if this controller has a parent,
    * then the parent will report the error.  If
    * no ancestor has an errorReporter, the error
    * message is written to standard output.
    */
   protected ErrorReporter errorReporter;
                                            
   /**
    * The parent of this controller, if any.
    * This is set automatically when one
    * controller is added to another.
    */
   protected Controller parent;

   /**
    * If non-null, this is an error message
    * that has been reported and not yet cleared.
    */
   protected String errorMessage;
   
   /**
    * Create a Controller.
    */
   public Controller() {
   }

   /**   
    * Set the ErrorReporter used to report errors that occur when the
    * compute() method of this Controller is executed.
    */
   public void setErrorReporter(ErrorReporter r) {
      errorReporter = r;
   }
   
   /**
    * Get the ErrorReporter for this Controller.  Return null if there is none.
    */
   public ErrorReporter getErrorReporter() {
      return errorReporter;
   }

   /**   
    * Add an object to be controlled by this controller.  It should be of
    * one or more of the types InputObject, Computable, Tie.  If it is
    * a Controller, then this Controller becomes its parent.
    */
   public void add(Object obj) {
      if (obj == null)
         return;
      if (obj instanceof Controller) {
          Controller c = (Controller)obj;
          if (c.parent != null)
             c.parent.remove(this);
          c.parent = this;
      }
      if (obj instanceof Computable) {
         if (computables == null)
            computables = new Vector();
         computables.addElement(obj);
      }
      if (obj instanceof InputObject) {
         if (inputs == null)
            inputs = new Vector();
         inputs.addElement(obj);
      }
      if (obj instanceof Tie) {
         if (ties == null)
            ties = new Vector();
         ties.addElement(obj);
      }
   }
   
   /**
    * Remove the object from the controller (if present).
    */
   public void remove(Object obj) {
      if (obj == null)
         return;
      if (computables != null) {
         computables.removeElement(obj);
         if (computables.size() == 0)
            computables = null;
      }
      if (inputs != null) {
         inputs.removeElement(obj);
         if (inputs.size() == 0)
            inputs = null;
      }
      if (ties != null) {
         ties.removeElement(obj);
         if (ties.size() == 0)
            ties = null;
      }
      if (obj instanceof Controller && ((Controller)obj).parent == this)
         ((Controller)obj).parent = null;
   }
   
   /**
    * If this controller has a parent, remove it from its parent.  (Then, a call to the
    * former parent's compute() method will not call this controller's compute().)
    */
   public void removeFromParent() {
      if (parent != null)
         parent.remove(this);
   }
   
   // ----------------- Listening for events ----------------------
   
   /**
    *  Simply calls compute when the Controller hears an ActionEvent.
    *  This is not meant to be called directly.
    */
   public void actionPerformed(ActionEvent evt) {
      compute();
   }
   
   /**
    *  Simply calls compute when the Controller hears a TextEvent.
    *  This is not meant to be called directly.
    */
   public void textValueChanged(TextEvent evt) {
      compute();
   }
   
   /**
    *  Simply calls compute when the Controller hears an AdjustmantEvent.
    *  This is not meant to be called directly.
    */
   public void adjustmentValueChanged(AdjustmentEvent evt) {
      compute();
   }
   
   /**
    *  Simply calls compute when the Controller hears an ItemEvent.
    *  This is not meant to be called directly.
    */
   public void itemStateChanged(ItemEvent evt) {
      compute();
   }

   // -------------- Implementation and error-handling ----------------------

   /**   
    * When an contoller computes, it first calls checkInput() for any
    * InputOjects that it controls (including those in sub-controllers).
    * It then handles any Ties.  Finally,
    * it calls the compute() method of any Computables.  If an error
    * occurs, it reports it.  JCMErrors (which should represent errors
    * on the part of the user) will generally only occur during the
    * checkInput() phase.  Internal, programmer errors can occur at
    * any time and might leave the sytem in an unhappy state.  They are
    * reported as debugging aids for the programmer.  When one occurs,
    * a stack trace is printed to standard output.
    */
   synchronized public void compute() {
      try {
         checkInput();
         doTies();
         clearErrorMessage();
         doCompute();
      }
      catch (JCMError e) { 
         if (errorMessage == null || !errorMessage.equals(e.getMessage()))
            reportError(e.getMessage());
      }
      catch (RuntimeException e) {
         reportError("Internal programmer's error detected?  " + e);
         e.printStackTrace();
      }
   }

   /**   
    * Call checkInput() of each InputObject.  Can throw a JCMError.
    * This is mostly meant to be called by Controller.compute().
    * Note that this will recurse though any sub-controllers of
    * this controller, so that when comput() is called,
    * all the InputObjects in the sub-controllers
    * are processed before ANY Tie or Computable is processed.
    * Similarly, the Ties and Computables in the sub-controllers
    * are processed in separate passes.
    */
   public void checkInput() {
      if (inputs != null) {
         int top = inputs.size();
         for (int i = 0; i < top; i++)
            ((InputObject)inputs.elementAt(i)).checkInput();
      }
   }
   
   /**
    * Check the Ties in this controller and its sub-controllers.
    */
   protected void doTies() {
      if (inputs != null) {
         int top = inputs.size();
         for (int i = 0; i < top; i++)
            if (inputs.elementAt(i) instanceof Controller)
               ((Controller)inputs.elementAt(i)).doTies();
      }
      if (ties != null) {
         int top = ties.size();
         for (int i = 0; i < top; i++)
            ((Tie)ties.elementAt(i)).check();
      }
   }
   
   /**
    * Compute the Computables in this controller and its sub-controllers.
    */
   protected void doCompute() {
      if (computables != null) {
         int top = computables.size();
         for (int i = 0; i < top; i++) {
            Object obj = computables.elementAt(i);
            if (obj instanceof Controller)
               ((Controller)obj).doCompute();
            else
               ((Computable)obj).compute();
         } 
      }
   }
   
   /**
    * Report the specified error message.
    */
   public void reportError(String message) {
      if (message == null)
         clearErrorMessage();
      if (errorReporter != null) {
         errorReporter.setErrorMessage(this,message);
         errorMessage = message;
      }
      else if (parent != null)
         parent.reportError(errorMessage);
      else {
         errorMessage = message;
         System.out.println("***** Error:  " + errorMessage);
      }
   }
   
   /**
    * Clear the error message.
    */
   protected void clearErrorMessage() {
      if (errorReporter != null)
         errorReporter.clearErrorMessage();
      else if (parent != null)
            parent.clearErrorMessage();
      errorMessage = null;
   }
   
   /**
    * Should be called by the ErrorReporter if the ErrorReporter clears the error itself.
    * (This is only used to avoid repeatedly setting the same error message, during an
    * animation for example.)
    */
   public void errorCleared() {
      errorMessage = null;
   }
   
   /**
    * Method required by InputObject interface; in this class, calls the same method
    * recursively on any input objects controlled by this controller.  This is meant to 
    * be called by JCMPanel.gatherInputs().
    */
    public void notifyControllerOnChange(Controller c) {
      if (inputs != null) {
         int top = inputs.size();
         for (int i = 0; i < top; i++)
            ((InputObject)inputs.elementAt(i)).notifyControllerOnChange(c);
      }
    }
    
    /**
     * Calles notifyControllerOnChange(this).  That is, it sets all the InputObjects in
     * this Controller, and in subcontrollers, to notify this Controller when they change.
     */
    public void gatherInputs() {
       notifyControllerOnChange(this);
    }

} // end class Controller
