/*
 * Licensed Materials - Property of Rogue Wave Software, Inc. 
 * © Copyright Rogue Wave Software, Inc. 2014, 2015 
 * © Copyright IBM Corp. 2009, 2014
 * © Copyright ILOG 1996, 2009
 * All Rights Reserved.
 *
 * Note to U.S. Government Users Restricted Rights:
 * The Software and Documentation were developed at private expense and
 * are "Commercial Items" as that term is defined at 48 CFR 2.101,
 * consisting of "Commercial Computer Software" and
 * "Commercial Computer Software Documentation", as such terms are
 * used in 48 CFR 12.212 or 48 CFR 227.7202-1 through 227.7202-4,
 * as applicable.
 */

package demo.runner;

import ilog.views.IlvGrapher;
import ilog.views.IlvGraphic;
import ilog.views.IlvGraphicEnumeration;
import ilog.views.IlvManagerView;
import ilog.views.IlvRect;
import ilog.views.diagrammer.IlvDiagrammer;
import ilog.views.sdm.IlvSDMEngine;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.Timer;
import java.util.TimerTask;

import javax.servlet.http.HttpServletRequest;

/**
 * A base class to <i>run</i> a diagram, which means to simulate real-time 
 * data feed by dynamically changing properties of the nodes and links of 
 * an {@link IlvDiagrammer}.
 */ 
public abstract class DiagramRunner {
  // The diagram component that contains a data model to be modified
  protected IlvDiagrammer _diagrammer;
  
  // Whether the simulation should run constantly, or if it should stop
  // at the end of the diagram
  private boolean _looping;

  // Update interval between each simulation step
  private int _period = 10000;

  // The Timer scheduling the simulation steps on a background thread
  private Timer _timer;
  
  // The TimerTask representing the simulation steps, executed 
  // repeatedly by the Timer
  private TimerTask _task;

  // Property name to store the DiagramRunner inside the IlvDiagrammer
  private static final String runnerProperty = "_IlvDiagrammer_DiagramRunner";


  /**
   * Constructor.
   * 
   * @param diagrammer The diagrammer to be <i>run</i>.
   */ 
  public DiagramRunner(IlvDiagrammer diagrammer) {
    // Keep a reference to the IlvDiagrammer
    _diagrammer = diagrammer;
    // Add this DiagramRunner into the IlvDiagrammer as a property
    _diagrammer.putClientProperty(runnerProperty, this);
    // Apply the StyleSheet (CSS) to this IlvDiagrammer
    diagrammer.getEngine().applyStyle(this, "DiagramRunner", null);
  }
  
  /**
   * Returns the {@link DiagramRunner} attached to the given 
   * {@link IlvDiagrammer} instance.
   * 
   * @param diagrammer The {@link IlvDiagrammer} instance.
   */ 
  public static DiagramRunner getRunner(IlvDiagrammer diagrammer) {
    // Tries to retrieve the DiagramRunner from the IlvDiagrammer (as a property)
    return (DiagramRunner)diagrammer.getClientProperty(runnerProperty);
  }

  /**
   * Returns whether the simulation should repeat continuously or not.
   */ 
  public boolean isLooping() {
    return _looping;
  }

  /**
   * Defines whether the simulation should repeat continuously or not.
   * 
   * @param looping If <code>true</code>, the simulation runs continuously.
   */ 
  public void setLooping(boolean looping) {
    _looping = looping;
  }

  /**
   * Returns the time interval between each simulation step, in milliseconds.
   */
  int getPeriod() {
    return _period;
  }

  /**
   * Defines the time interval between each simulation step, in milliseconds.
   *  
   * @param period The new time interval.
   */
  void setPeriod(int period) {
    _period = period;
  }
  
  /**
   * Abstract method called on all objects when the simulation is started.
   * 
   * @param object  The object being initialized
   */ 
  protected abstract void init(Object object);
 
  /**
   * Abstract method to return the manager bounding box according to the
   * current simulation state.
   * 
   * @param req  The client request
   * @param view The manager view associated with the {@link IlvDiagrammer}
   * 
   * @return The bounding box corresponding to the current simulation state. 
   */
  protected abstract IlvRect getManagerBBox(HttpServletRequest req, IlvManagerView view);
  
  /**
   * Abstratct method that is called at every step of the simulation.
   */ 
  protected abstract void step();
  
  /**
   * Returns a new <code>TimerTask</code>, which invokes
   * {@link #step()} when executed.
   */
  private TimerTask newTask() {
    return new TimerTask() {
      public void run() {
        step();
      }};
  }
  
  /**
   * Restarts the simulation.
   */
  public void restart() {
    if (null != _task) {
      try {
        // Tries to cancel the current task
        _task.cancel();
      } catch (Exception e) {
        System.err.println(e.toString());
      }
      
      // Instantiates new Timer and TimerTask
      _timer = new Timer();
      _task = newTask();
      
      // Schedule TimerTask for execution
      try {
        System.err.println("schedule runnable " + getPeriod());
        _timer.schedule(_task, 0, getPeriod());
      } catch (Exception e) {
        System.err.println(e.toString());
      }
    }
  }
  
  /**
   * Starts the simulation
   */
  public void start() {
    // Invoke 'init()' method for every object
    _diagrammer.setAdjusting(true);
    Iterator<?> it = _diagrammer.getAllObjects();
    while (it.hasNext()) {
      init(it.next());
    }
    _diagrammer.setAdjusting(false);

    // Instantiates new Timer and TimerTask 
    _timer = new Timer();
    _task = newTask();

    // Schedule TimerTask for execution
    try {
      System.err.println("schedule runnable " + getPeriod());
      _timer.schedule(_task, 0, getPeriod());
    } catch (Exception e) {
      System.err.println(e.toString());
    }
  }

  /**
   * Stops the simulation
   */
  public void stop() {
    if (null != _timer) {
      // Cancels Timer
      _timer.cancel();
      _task.cancel();
    }
    _task = null;
    _timer = null;
  }

  /**
   * A subclass of {@link DiagramRunner} that modifies some properties of
   * one object after another.
   */ 
  public static abstract class Sequence extends DiagramRunner {
    // The current object, that is, the last object that was modified.
    private Object _current;
    
    /**
     * Constructor
     * 
     * @param diagrammer The {@link IlvDiagrammer} to be <i>run</i>
     */
    public Sequence(IlvDiagrammer diagrammer) {
      super(diagrammer);
    }

    /**
     * Method to return the manager bounding box around the current object
     * being updated.
     * 
     * @param req  The client request
     * @param view The manager view associated with the {@link IlvDiagrammer}
     * 
     * @return The bounding box corresponding to the current simulation state. 
     */
    Override
    protected IlvRect getManagerBBox(HttpServletRequest req, IlvManagerView view) {
      if (null == _current) {
        return null;
      }
      // Get the graphic corresponding to the current object 
      IlvGraphic g = _diagrammer.getEngine().getGraphic(_current, false);
      // Get the bounding box of the graphic
      IlvRect rect = new IlvRect(g.boundingBox());
      // Expand the bounding box to two times the largest dimension 
      rect.expand(2 * Math.max(rect.width+5, rect.height+5));
      
      return rect;
    }
    
    /**
     * <p>Calls the {@link #next(Object)} method to determine what is the next
     * object to be modified.</p>
     * 
     * <p>Then calls {@link #leave(Object)} on the current object.</p>
     * 
     * <p>And finally calls {@link #enter(Object)} on the new object.</p>
     */
    Override
    protected void step() {
      Object next = next(_current);
      if (null != _current) {
        leave(_current);
      }
      if (null != next) {
        _current = next;
        enter(_current);
      } else {
        stop();
      }
    }

    /**
     * Overrides method to clean the current object.
     */
    Override
    public void start() {
      _current = null;
      super.start();
    }

    /**
     * Overrides method to clean the current object.
     */
    Override
    public void stop() {
      super.stop();
      if (null != _current) {
        leave(_current);
        _current = null;
      }
      if (isLooping()) {
        start();
      }
    }
    
    /**
     * Abstract method to compute the next object to be updated in the
     * simulation. The simulation stops if this method returns 
     * <code>null</code>.
     * 
     * @param current The current object.
     * 
     * @return The next object to be updated in the simulation, or 
     *         <code>null</code>.
     */ 
    protected abstract Object next(Object current);
    
    /**
     * Abstract method called at each simulation step to indicate that the
     * updates on the current object is about to be completed.
     * 
     * @param current The current object being finalized.
     */ 
    protected abstract void leave(Object current);
    
    /**
     * Abstract method called at each animation step on the next object
     * to be updated by the simulation. 
     * 
     * @param next The object that will become the new current object
     */ 
    protected abstract void enter(Object next);
  }
  
  /**
   * Implementation of {@link Sequence} that follows a flow diagram in
   * an {@link IlvDiagrammer}.
   */
  public static class Flow extends Sequence {
    /**
     * Constructor.
     * 
     * @param diagrammer The diagrammer to be <i>run</i>.
     */ 
    public Flow(IlvDiagrammer diagrammer) {
      super(diagrammer);
    }

    /**
     * <p>Returns the next object, following the flow of the diagram:</p>
     * <ul>
     *   <li>The first object is the one that has no incoming links.</li>
     *   <li>The next object of a link is its destination node.</li>
     *   <li>When a node has several outgoing links, one of the links
     *       is chosen randomly, otherwise the next object is the 
     *       outgoing link.</li>
     * </ul>
     * 
     * @param current The current object.
     */ 
    protected Object next(Object current) {
      if (null == current) {
        // There is no 'current' object, tries to start from the
        // currently selected object
        Iterator<?> selection = _diagrammer.getSelectedObjects();
        if (selection.hasNext()) {
          return selection.next();
        }
        // No object is selected, try to find an object with 
        // no incoming links
        Iterator<?> objs = _diagrammer.getAllObjects();
        while (objs.hasNext()) {
          Object o = objs.next();
          if (getLinks(o, false).size() == 0){
            return o;
          }
        }
        // Fallback option, return any object in the diagram
        objs = _diagrammer.getAllObjects();
        if (objs.hasNext()) {
          return objs.next();
        }
        // No objects in the diagram
        return null;
      } else if(_diagrammer.isLink(current)) {
        // The current object is a link, return its 'to' endpoint
        return _diagrammer.getTargetNode(current);
      } else {
        // The current object is a node, try to get a 'from' link
        ArrayList<?> links = getLinks(current, true);
        if (links.size() == 0) {
          // No 'from' links, the simulation is over
          return null;
        } else if (links.size() == 1) {
          // Just one 'from' link, use it
          return links.get(0);
        } else {
          // Multiple 'from' links, see if there is a "preferred" path.
          for (Object link : links) {
            if (_diagrammer.getObjectProperty(link, "sdm:preferred") != null) {
              return link;
            }
          }
          // Otherwise choose randomly.
          return links.get((int)Math.round(Math.random() * (links.size()-1)));
        }
      }
    }

    /**
     * Method invoked on every object in the model when the
     * simulation starts
     */
    /**
     * <p>Method called on all objects when the simulation is started.</p>
     * 
     * <p>Ensures that the node is <b>not</b> selected, and sets the
     * object property <i>status</i> to <i>empty</i>.
     * 
     * @param object  The object being initialized
     */
    Override
    protected void init(Object object) {
      _diagrammer.setSelected(object, false);
      // Simply set the property value to empty
      // This will ensure that the active nor the 
      // done state is represented
      _diagrammer.setObjectProperty(object, "status", "");
    }
    
    /**
     * <p>Method called at each animation step on the next object
     * to be updated by the simulation.</p>
     * 
     * <p>Ensure that the object is selected, then set the object
     * property <i>status</i> to <i>active</i>.</p>
     * 
     * @param next The object that will become the new current object
     */ 
    protected void enter(Object next) {
      _diagrammer.setSelected(next, true);
      _diagrammer.setObjectProperty(next, "status", "active");
    }
    
    /**
     * <p>Method called at each simulation step to indicate that the
     * updates on the current object is about to be completed.</p>
     * 
     * <p>Ensures that the object is not selected, then set the
     * object property <i>status</i> to <i>done</i>.</p>
     * 
     * @param current The current object being finalized.
     */ 
    protected void leave(Object previous) {
      _diagrammer.setSelected(previous, false);
      _diagrammer.setObjectProperty(previous, "status", "done");
    }

    /**
     * Private method to retrieve either the links ending on a node
     * (<i>to</i>) or the links starting on a node (<i>from</i>).
     * 
     * @param node The node connected to links
     * @param from Whether the <i>from</i> or <i>to</i> links should
     *        be returned
     *        
     * @return A list of links either starting (<code>from</code>
     *         parameter is <code>true</code>) or ending in the given
     *         node.
     */
    private ArrayList<?> getLinks(Object node, boolean from) {
      // Get the SDM engine from the IlvDiagrammer
      IlvSDMEngine engine = _diagrammer.getEngine();
      // Get the graphic representing the given node
      IlvGraphic nodeGraphic = engine.getGraphic(node, false);
      // Get the grapher containing all objects
      IlvGrapher grapher = engine.getGrapher();
      // Gets either the 'from' or 'to' links connected to the node
      IlvGraphicEnumeration linksEnum = from ? grapher.getLinksFrom(nodeGraphic) 
                                             : grapher.getLinksTo(nodeGraphic);
      // Converts the enumeration into an ArrayList
      ArrayList<Object> links = new ArrayList<Object>();
      while(linksEnum.hasMoreElements()){
        links.add(engine.getObject(linksEnum.nextElement()));
      }
      return links;
    }
  }
}