/*
 * Licensed Materials - Property of Perforce Software, Inc. 
 * © Copyright Perforce Software, Inc. 2014, 2021 
 * © 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.
 */

import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.SimpleBeanInfo;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ResourceBundle;

import javax.swing.Action;
import javax.swing.ImageIcon;
import javax.swing.Timer;

import ilog.views.IlvGrapher;
import ilog.views.IlvGraphic;
import ilog.views.IlvGraphicEnumeration;
import ilog.views.diagrammer.IlvDiagrammer;
import ilog.views.diagrammer.application.IlvDiagrammerAction;
import ilog.views.sdm.IlvSDMEngine;
import ilog.views.util.IlvImageUtil;

/**
 * A base class for "running" a diagram, that is, dynamically changing
 * properties of a diagram's nodes and/or links to simulate a real-time data
 * feed.
 */
public abstract class DiagramRunner extends Timer implements ActionListener {
  // The diagram component that we want to animate.
  //
  IlvDiagrammer diagrammer;

  private boolean looping;

  private static final String runnerProperty = "_IlvDiagrammer_DiagramRunner";

  private static final String loopProperty = "_IlvDiagrammer_DiagramRunner_Loop";

  /**
   * Creates a new diagram runner.
   * 
   * @param delay
   *          The delay between two animation steps.
   * @param diagrammer
   *          The diagrammer to be animated.
   */
  public DiagramRunner(IlvDiagrammer diagrammer, int delay) {
    super(delay, null);
    setInitialDelay(0);
    addActionListener(this);
    this.diagrammer = diagrammer;
    diagrammer.putClientProperty(runnerProperty, this);
    diagrammer.getEngine().applyStyle(this, "DiagramRunner", null);
    if (Boolean.TRUE.equals(diagrammer.getClientProperty(loopProperty))) {
      setLooping(true);
    }
    diagrammer.putClientProperty(loopProperty, null);
  }

  /**
   * Returns the diagram runner attached to an IlvDiagrammer object.
   * 
   * @param diagrammer
   *          The diagram component.
   */
  public static DiagramRunner getRunner(IlvDiagrammer diagrammer) {
    return (DiagramRunner) diagrammer.getClientProperty(runnerProperty);
  }

  /**
   * Returns true if the animation loops continuously.
   */
  public boolean isLooping() {
    return looping;
  }

  /**
   * If looping is true, the animation loops continuously.
   */
  public void setLooping(boolean looping) {
    this.looping = looping;
  }

  /**
   * Calls the "step" method inside an adjustment sequence.
   */
  Override
  public void actionPerformed(ActionEvent e) {
    diagrammer.setAdjusting(true);
    step();
    diagrammer.setAdjusting(false);
  }

  /**
   * Called on all objects when the animation is started.
   */
  protected abstract void init(Object object);

  /**
   * This method is called at every step of the animation.
   */
  protected abstract void step();

  Override
  public void start() {
    super.start();
    diagrammer.setAdjusting(true);
    Iterator<?> it = diagrammer.getAllObjects();
    while (it.hasNext()) {
      init(it.next());
    }
    diagrammer.setAdjusting(false);
  }

  Override
  public void stop() {
    super.stop();
    if (looping) {
      setInitialDelay(getDelay());
      start();
    } else {
      IlvDiagrammerAction.updateActions(diagrammer.getRootPane(), diagrammer);
    }
  }

  /**
   * A subclass of 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;

    public Sequence(IlvDiagrammer diagrammer, int delay) {
      super(diagrammer, delay);
    }

    /**
     * Calls the "next" method to determine which object to modify next. Then
     * calls "leave" on the previous object, and "enter" on the new object.
     */
    Override
    protected void step() {
      Object next = next(current);
      if (current != null) {
        leave(current);
      }
      if (next != null) {
        current = next;
        enter(current);
      } else {
        stop();
      }
    }

    Override
    public void start() {
      current = null;
      super.start();
    }

    Override
    public void stop() {
      super.stop();
      if (current != null) {
        leave(current);
        current = null;
      }
    }

    /**
     * Returns the next object to animate. If this method returns null, the
     * animation stops.
     * 
     * @param previous
     *          The current object.
     */
    protected abstract Object next(Object previous);

    /**
     * Called at each animation step on the previous current object.
     * 
     * @param previous
     *          The previous current object.
     */
    protected abstract void leave(Object previous);

    /**
     * Called at each animation step on the new current object.
     * 
     * @param next
     *          The new current object.
     */
    protected abstract void enter(Object next);
  }

  /**
   * Bean info class
   */
  public class SequenceBeanInfo extends SimpleBeanInfo {

    /**
     * Constructor
     */
    public SequenceBeanInfo() {
    }
  }

  public static class Flow extends Sequence {
    public Flow(IlvDiagrammer diagrammer, int delay) {
      super(diagrammer, delay);
    }

    /**
     * Returns the next object, following the flow of the diagram: - The first
     * object is the one that has no incoming links. - The next object of a link
     * is its destination node. - When a node has several outgoing links, one of
     * the links is chosen randomly, otherwise the next object is the outgoing
     * link.
     * 
     * @param previous
     *          The current object.
     */
    Override
    protected Object next(Object previous) {
      if (previous == null) {
        Iterator<?> selection = diagrammer.getSelectedObjects();
        if (selection.hasNext()) {
          return selection.next();
        } else {
          Iterator<?> objs = diagrammer.getAllObjects();
          while (objs.hasNext()) {
            Object o = objs.next();
            if (getLinks(o, false).size() == 0) {
              return o;
            }
          }
          objs = diagrammer.getAllObjects();
          if (objs.hasNext())
            return objs.next();
          else
            return null;
        }
      } else if (diagrammer.isLink(previous)) {
        return diagrammer.getTargetNode(previous);
      } else {
        List<Object> links = getLinks(previous, true);
        if (links.size() == 0) {
          return null;
        } else if (links.size() == 1) {
          return links.get(0);
        } else {
          // See if there is a "preferred" path.
          //
          for (int i = 0; i < links.size(); i++) {
            Object link = links.get(i);
            if (diagrammer.getObjectProperty(link, "sdm:preferred") != null) {
              return link;
            }
          }
          // Otherwise choose randomly.
          //
          return links.get((int) Math.round(Math.random() * (links.size() - 1)));
        }
      }
    }

    private List<Object> getLinks(Object node, boolean from) {
      IlvSDMEngine engine = diagrammer.getEngine();
      IlvGrapher grapher = engine.getGrapher();
      IlvGraphic nodeGraphic = engine.getGraphic(node, false);
      IlvGraphicEnumeration linksEnum = from ? grapher.getLinksFrom(nodeGraphic) : grapher.getLinksTo(nodeGraphic);
      List<Object> links = new ArrayList<Object>();
      while (linksEnum.hasMoreElements()) {
        links.add(engine.getObject(linksEnum.nextElement()));
      }
      return links;
    }

    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", "");
    }

    /**
     * Selects the next object.
     */
    Override
    protected void enter(Object next) {
      diagrammer.setSelected(next, true);
      diagrammer.setObjectProperty(next, "status", "active");
      diagrammer.scrollToObject(next);
    }

    /**
     * Deselects the previous object.
     */
    Override
    protected void leave(Object previous) {
      diagrammer.setSelected(previous, false);
      diagrammer.setObjectProperty(previous, "status", "done");
    }
  }

  /**
   * Bean info class
   */
  public class FlowBeanInfo extends SimpleBeanInfo {

    /**
     * Constructor
     */
    public FlowBeanInfo() {
    }
  }

  /**
   * An action that starts/stops a diagram runner.
   */
  public static abstract class Run extends IlvDiagrammerAction.ToggleAction {
    public Run() {
      super("Run", ResourceBundle.getBundle("runner"));
    }

    Override
    protected boolean isSelected(IlvDiagrammer diagrammer) throws Exception {
      return diagrammer != null && DiagramRunner.getRunner(diagrammer) != null
          && DiagramRunner.getRunner(diagrammer).isRunning();
    }

    Override
    protected void setSelected(IlvDiagrammer diagrammer, boolean selected) throws Exception {
      if (diagrammer != null) {
        DiagramRunner runner = DiagramRunner.getRunner(diagrammer);
        if (selected) {
          if (runner == null) {
            runner = createRunner(diagrammer);
          }
          runner.start();
        } else if (runner != null) {
          boolean oldLooping = runner.isLooping();
          runner.setLooping(false);
          runner.stop();
          runner.setLooping(oldLooping);
          setIcon(Action.SMALL_ICON);
        }
      }
    }

    Override
    protected void setSelected(boolean selected) {
      super.setSelected(selected);
      if (selected) {
        setIcon("StopIcon");
      } else {
        setIcon(Action.SMALL_ICON);
      }
    }

    private void setIcon(String key) {
      try {
        String s = getResourceBundle().getString(getPrefix() + "." + key);
        Image image = null;
        image = IlvImageUtil.getImageFromFile(getClass(), s);
        putValue(Action.SMALL_ICON, new ImageIcon(image));
      } catch (IOException e) {
        e.printStackTrace();
      }
    }

    Override
    protected boolean isEnabled(IlvDiagrammer diagrammer) throws Exception {
      return true;
    }

    protected abstract DiagramRunner createRunner(IlvDiagrammer diagrammer);
  }

  /**
   * An action that toggle continuous looping of the animation.
   */
  public static class Loop extends IlvDiagrammerAction.ToggleAction {
    public Loop() {
      super("Loop", ResourceBundle.getBundle("runner"));
    }

    Override
    protected boolean isSelected(IlvDiagrammer diagrammer) throws Exception {
      return diagrammer != null
          && ((DiagramRunner.getRunner(diagrammer) != null && DiagramRunner.getRunner(diagrammer).isLooping())
              || Boolean.TRUE.equals(diagrammer.getClientProperty(loopProperty)));
    }

    Override
    protected void setSelected(IlvDiagrammer diagrammer, boolean selected) throws Exception {
      if (diagrammer != null) {
        DiagramRunner runner = DiagramRunner.getRunner(diagrammer);
        if (runner != null) {
          runner.setLooping(selected);
        } else {
          diagrammer.putClientProperty(loopProperty, Boolean.valueOf(selected));
        }
      }
    }

    Override
    protected boolean isEnabled(IlvDiagrammer diagrammer) throws Exception {
      return true;
    }
  }
}