/*
 * 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.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.text.DecimalFormat;
import java.util.Collection;
import java.util.Iterator;
import java.util.Properties;
import java.util.Vector;

import javax.swing.Timer;

import ilog.views.IlvGraphic;
import ilog.views.IlvLinkImage;
import ilog.views.IlvPoint;
import ilog.views.IlvRect;
import ilog.views.IlvStroke;
import ilog.views.diagrammer.IlvDiagrammer;
import ilog.views.graphic.IlvArc;
import ilog.views.graphic.IlvMarker;
import ilog.views.sdm.IlvSDMEngine;
import ilog.views.sdm.graphic.IlvGeneralLink;
import ilog.views.sdm.model.IlvDefaultSDMNode;
import ilog.views.sdm.renderer.IlvSDMRenderer;

/**
 * This class implements a simulator that animates objects.
 * 
 */
public final class Simulator implements ActionListener {

  /**
   * Use a custom renderer to simulate CSS
   */
  private static class NoCSSRenderer extends IlvSDMRenderer {
    Interactor _interactor = new Interactor();

    Override
    public IlvGraphic createLinkGraphic(IlvSDMEngine engine, Object link, IlvGraphic from, IlvGraphic to) {
      /*
       * link { class : "ilog.views.sdm.graphic.IlvGeneralLink" ; lineWidth : 15
       * foreground : yellow ; endCap : CAP_ROUND ; }
       */
      IlvGeneralLink g = new IlvGeneralLink();
      g.setFrom(from);
      g.setTo(to);
      g.setLineWidth(15);
      g.setForeground(Color.yellow);
      g.setEndCap(IlvStroke.CAP_ROUND);
      return g;
    }

    Override
    public void addLinkGraphic(IlvSDMEngine engine, Object link, IlvGraphic graphic, boolean redraw) {
      // be sure link layer is below node layer
      engine.getGrapher().addLink((IlvLinkImage) graphic, 9, redraw);
    }

    Override
    public IlvGraphic createNodeGraphic(IlvSDMEngine engine, Object node) {

      /*
       * node.core{ class : "ilog.views.graphic.IlvMarker" ; type : "0" ;
       * LinkConnector : "@#connector"; }
       */
      if ("core".equals(engine.getModel().getTag(node))) {
        Integer X = (Integer) engine.getModel().getObjectProperty(node, "x");
        Integer Y = (Integer) engine.getModel().getObjectProperty(node, "y");
        IlvMarker result = new IlvMarker(new IlvPoint(X.doubleValue(), Y.doubleValue()), 0);

        /*
         * Subobject#connector { class :
         * "ilog.views.linkconnector.IlvCenterLinkConnector"; }
         */
        new ilog.views.linkconnector.IlvCenterLinkConnector().attach(result, false);
        return result;
      }

      /*
       * node{ class : "Graphic" ; color : "@color"; label : "@label"; position
       * : "@position"; LinkConnector : "@#connector"; Interactor : "Interactor"
       * ; }
       */
      Graphic result = new Graphic();
      updateNodeProperties(engine, node, result);
      new ilog.views.linkconnector.IlvCenterLinkConnector().attach(result, false);
      result.setObjectInteractor(_interactor);
      return result;

    }

    Override
    public void addNodeGraphic(IlvSDMEngine engine, Object node, IlvGraphic graphic, boolean redraw) {
      // be sure link layer is below node layer
      engine.getGrapher().addNode(graphic, 10, redraw);
    }

    Override
    public void propertiesChanged(IlvSDMEngine engine, Object object, Collection<String> propertyNames,
        IlvGraphic graphic) {
      if (graphic instanceof Graphic) {
        Graphic g = (Graphic) graphic;
        updateNodeProperties(engine, object, g);
      }
      // no other property changed expected
    }

    // update volatile Graphic properties
    private void updateNodeProperties(IlvSDMEngine engine, Object node, Graphic g) {
      g.setColor((String) engine.getModel().getObjectProperty(node, "color"));
      g.setLabel((String) engine.getModel().getObjectProperty(node, "label"));
      g.setPosition((String) engine.getModel().getObjectProperty(node, "position"));
      // update position
      Integer X = (Integer) engine.getModel().getObjectProperty(node, "x");
      Integer Y = (Integer) engine.getModel().getObjectProperty(node, "y");
      g.move(X.doubleValue(), Y.doubleValue());
    }
  }

  /**
   * The name of the resource that contains the CSS.
   */
  private static final String CSS = "node.css";

  /**
   * The name of the properties file that controls the simulation.
   */
  private static final String PROPERTIES = "simulator.properties";

  /**
   * The external properties that control the simulation.
   */
  private static Properties properties;

  /**
   * The instance of the IlvDiagrammer.
   */
  private IlvDiagrammer diagrammer;

  /**
   * The Swing timer used to animate the objects in the model.
   */
  private Timer timer;

  // Properties that control the simulation.

  /**
   * The minimum number of objects to create in the random model.
   */
  private int minObjects = getSimulatorInt("min.objects");

  /**
   * The maximum number of objects to create in the random model.
   */
  private int maxObjects = getSimulatorInt("max.objects");

  /**
   * The maximum speed for the objects in the model.
   */
  private int maxSpeed = getSimulatorInt("max.speed");

  /**
   * The delay between clock ticks of the simulator.
   */
  private int timerDelay = getSimulatorInt("simulator.interval");

  private int speedAccelerator = 1;

  /**
   * the rotating speed of the "radar" in degrees per timer delay
   */
  private int radarSpeed = getSimulatorInt("radar.speed");

  /**
   * center and size of the ellipse, used as trajectory for moving the objects
   */
  private int cx = 382;
  private int cy = 280;
  private int rx = 108;
  private int ry = 50;

  /**
   * A node used as the center of the ellipse. Its display properties are
   * defined in the CSS
   */
  private IlvDefaultSDMNode core;

  /**
   * A graphic object representing the area of the radar
   */
  private IlvArc radar = null;

  /**
   * Poistion of the current node that is being moved
   */
  private final IlvPoint nodePosition = new IlvPoint();

  /**
   * graphics objects representing areas on the drawing.
   */
  private IlvGraphic redZone;
  private IlvGraphic blueZone;
  private IlvGraphic greenZone;

  /**
   * counters
   */
//  private int inRedZone = 0;
//  private int inBlueZone = 0;
//  private int inGreenZone = 0;

  /**
   * position of the next object to park
   */
  int parkLimitX = CustomGraphics.VIEW_WIDTH;

  /**
   * an array to store object which are already parked
   */
  Vector<Node> parkedNodes = new Vector<Node>();

  /**
   * A formatter to display decimal values
   */
  private DecimalFormat formatter = new DecimalFormat("#.#");

  /**
   * The node choosen as target
   */
  private Node targetNode = null;

  /**
   * The link that will link the core and the target
   */
  private Object link = null;

  /**
   * The primary speed of the target
   */
  int primaryNodeSpeed = 0;

  /**
   * Create an instance of the simulator.
   *
   * @param diagram
   *          the instance of the diagrammer.
   * @param useCSS
   *          if false use a custom renderer (without CSS) as well.
   *
   * @throws Exception
   *           if an error occurs for any reason.
   */
  public Simulator(final IlvDiagrammer diagram, boolean useCSS) throws Exception {

    diagrammer = diagram;

    timer = new Timer(timerDelay, this);
    if (useCSS) {
      // this is the style-sheet for our symbols
      URL css = Simulator.class.getResource(CSS);
      if (css != null) {
        diagrammer.getEngine().setBaseURL(css.toString());
        diagrammer.setStyleSheet(css);
      } else {
        throw new Exception("Unable to load CSS " + CSS);
      }
    } else {
      // no css: use a custom renderer to simulate the css.
      diagram.getEngine().setRenderer(new NoCSSRenderer());
    }
  }

  /**
   * Start the simulator.
   */
  public void start() {
    if (!timer.isRunning()) {
      timer.start();
    }
  }

  /**
   * Stop the simulator.
   */
  public void stop() {
    if (timer.isRunning()) {
      timer.stop();
    }
  }

  /**
   * Perform the simulator action. This method is called by the timer, according
   * to specified delay. Following tasks are done<br>
   * - it moves the radar area - it moves all the nodes, excepts the link and
   * the core, and get a random target - creates a link between the core and the
   * target node (if exists) - if there's a target, bring it closer to the core
   *
   * @param e
   *          the {@link ActionEvent ActionEvent} associated with this action.
   */
  Override
  public void actionPerformed(final ActionEvent e) {
    try {
      setAdjusting(true);

      // move radar. The radar area is an arc. To give the impression
      // that it is turning, we change the start angle
      radar.setStartAngle(radar.getStartAngle() - radarSpeed);
      radar.reDraw();

      Iterator<?> it = diagrammer.getAllObjects();

      // reinit counters
//      inRedZone = 0;
//      inGreenZone = 0;
//      inBlueZone = 0;

      // move each node (except core and link)
      while (it.hasNext()) {
        Object obj = it.next();
        if (obj != null && (obj instanceof Node) && obj != core && !diagrammer.isLink(obj)) {
          Node node = (Node) obj;
          moveNode(node);

          // get a random target, among nodes with orbit >1
          if (targetNode == null && node.getOrbit() > 1 && getRandom(0, 100) == 3) {
            nodePosition.setLocation(node.getX(), node.getY());
            if (radar.contains(nodePosition, nodePosition, null) && node.getProperty("action").equals("turning")) {
              targetNode = node;
            }
          }

        }
      }

      // all nodes have been moved and a target has been selected
      // then creates a link if it does not yet exists
      if (link == null && targetNode != null) {
        // get the node speed before accelerating it to the radar speed
        primaryNodeSpeed = targetNode.getSpeed();
        targetNode.setSpeed(radarSpeed);
        // clear printed information
        diagrammer.setObjectProperty(targetNode, "label", "");
        diagrammer.setObjectProperty(targetNode, "position", "");
        // creates a link and adds it to the diagrammer
        link = diagrammer.createLink("link", core, targetNode);
        diagrammer.addObject(link, null);

      }

      // if a link is created then get the node closer and closer to the core
      // until its orbit is <=1
      // when done (orbit=1), reset node primary speed and printed data, remove
      // link and
      // prepare to get next target
      if (link != null) {
        double orbit = targetNode.getOrbit();
        if (orbit > 1) {
          // get node closer by changing its orbit
          orbit -= 0.02;
          targetNode.setOrbit(orbit);
        } else {
          // reset primary speed and printed data
          targetNode.setSpeed(primaryNodeSpeed);
          diagrammer.setObjectProperty(targetNode, "color", "black");
          diagrammer.setObjectProperty(targetNode, "label", "");
          diagrammer.setObjectProperty(targetNode, "position", "");
          // remove link and reset data in order to prepare next target
          diagrammer.removeObject(link);
          targetNode = null;
          link = null;
        }
      }
    } finally {
      setAdjusting(false);
    }
  }

  /**
   * Move the Node according to its "action" property:<br>
   * - make it turning around the core, according to speed, angle and orbit -
   * park it in the right bottom corner of the view - unpark it
   *
   * @param node
   *          the Node to move.
   */

  private void moveNode(final Node node) {

    // If the node is parked then do nothing
    String action = (String) node.getProperty("action");
    if (action.equals("parked")) {
      return;
    }

    // gets position of the node for future computation
    int newX = node.getX();
    int newY = node.getY();

    // gets node data
    double orbit = node.getOrbit();
    int angle = node.getAngle();
    int speed = node.getSpeed();

    // --- the node is turning ---
    if (action.startsWith("turning")) {

      // computes new position
      angle = angle + speed;
      node.setAngle(angle);
      newX = cx + (int) (Math.cos(Math.toRadians(angle)) * (orbit * rx));
      newY = cy + (int) (Math.sin(Math.toRadians(angle)) * (orbit * ry));
      nodePosition.setLocation(newX, newY);

      // changes "position" property of the node
      if (node != targetNode) {
        diagrammer.setObjectProperty(node, "position",
            "L" + formatter.format(orbit) + " / " + angle + "\u00B0 / " + speed);
      }
      // moves the node
      diagrammer.setObjectProperty(node, "x", Integer.valueOf(newX));
      diagrammer.setObjectProperty(node, "y", Integer.valueOf(newY));

      // Do not do following actions if orbit <=1
      if (orbit <= 1) {
        return;
      }

      // get IlvGraphic objects corresponding to areas
      if (redZone == null)
        redZone = diagrammer.getView().getManager().getObject("REDZONE");
      if (blueZone == null)
        blueZone = diagrammer.getView().getManager().getObject("BLUEZONE");
      if (greenZone == null)
        greenZone = diagrammer.getView().getManager().getObject("GREENZONE");

      // test if the node enters/leaves a predefined area, and changes its
      // "color" property
      if (redZone.contains(nodePosition, nodePosition, null)) {
        diagrammer.setObjectProperty(node, "color", "red");
//        inRedZone++;
      } else if (blueZone.contains(nodePosition, nodePosition, null)) {
        diagrammer.setObjectProperty(node, "color", "blue");
//        inBlueZone++;
      } else if (greenZone.contains(nodePosition, nodePosition, null)) {
        diagrammer.setObjectProperty(node, "color", "green");
//        inGreenZone++;
      }
    }
    // --- the node must be parked ---
    else if (action.startsWith("parking")) {
      // compute new Y until reaching the bottom line
      if (!action.endsWith("_x")) {
        newY += 12;
        if (newY >= 550) {
          newY = 550;
          node.setProperty("action", "parking_x"); // this indicates that in
                                                   // next timer animations, the
                                                   // node should only moves on
                                                   // X
          diagrammer.setObjectProperty(node, "label", "");
          diagrammer.setObjectProperty(node, "position", "");
        }
      }
      // compute new X when bottom has been reached
      else {
        newX += 12;
        if (newX + 9 >= parkLimitX) { // when nodes reach the parking position
                                      // stop it
          newX = parkLimitX - 12;
          parkLimitX = parkLimitX - 18;
          node.setProperty("action", "parked"); // this "parked" property value
                                                // prevents the node from moving
                                                // next times
          parkedNodes.add(node); // adds the node in the list of the parked
                                 // nodes
        }
      }
      diagrammer.setObjectProperty(node, "x", Integer.valueOf(newX));
      diagrammer.setObjectProperty(node, "y", Integer.valueOf(newY));
    }
    // --- the node must be unparked ---
    else if (action.startsWith("unparking")) {

      // compute new Y until to reach the top line
      if (!action.endsWith("_x")) {

        // all the nodes which are already parked should be reparked before
        // that the node leaves its parking position for the first time
        if (newY == 550) {
          int idx = parkedNodes.indexOf(node);
          parkLimitX = parkLimitX + 18; // new limit for next park
          for (int i = idx + 1; i < parkedNodes.size(); i++) {
            Node n = parkedNodes.get(i);

            // translate nodes by 18 pixels
            int newx = ((Integer) n.getProperty("x")) + 18;
            diagrammer.setObjectProperty(n, "x", Integer.valueOf(newx));
          }
          // current not is not anymmore part od the parked nodes
          parkedNodes.remove(node);
        }

        // compute new Y
        newY -= 12;
        int topline = cy + (int) (Math.sin(Math.toRadians(angle)) * (orbit * ry));
        if (newY < topline) {
          newY = topline;
          node.setProperty("action", "unparking_x"); // this indicates that in
                                                     // next timer aniation, the
                                                     // node should only moves
                                                     // on X
        }
      }
      // compute X when top line has been reached
      else {
        newX -= 12;
        int positionX = cx + (int) (Math.cos(Math.toRadians(angle)) * (orbit * rx));
        if (newX - 9 < positionX) { // 9 = width of node's icone/2
          newX = positionX;
          node.setProperty("action", "turning"); // node is unparked, it can
                                                 // continue to turn around the
                                                 // core
        }
      }

      // move the node to its new position
      diagrammer.setObjectProperty(node, "x", Integer.valueOf(newX));
      diagrammer.setObjectProperty(node, "y", Integer.valueOf(newY));
    }

  }

  /**
   * Add a random node to the model.
   */
  private void addRandomNode() {

    // we create our own node we do not ask the diagrammer to do it
    double orbit = getRandom(2, 4);
    int angle = getRandom(0, 360);
    int speed = getRandom(1, maxSpeed);

    int id2 = getRandom(0, 10);
    int id1 = getRandom(0, 10);
    String tag = "S." + id1 + "." + id2;
    Node node = new Node(tag, orbit, angle, speed);

    // set custom defined properties
    diagrammer.setObjectProperty(node, "action", "turning");
    diagrammer.setObjectProperty(node, "color", "green");
    diagrammer.setObjectProperty(node, "label", tag);
    diagrammer.setObjectProperty(node, "position", "L" + orbit + " / " + angle + "\u00B0 / " + speed);

    // now set the initial location
    int x = cx + (int) (Math.cos(Math.toRadians(angle)) * (orbit * rx));
    int y = cy + (int) (Math.sin(Math.toRadians(angle)) * (orbit * ry));

    node.setProperty("x", Integer.valueOf(x));
    node.setProperty("y", Integer.valueOf(y));
    diagrammer.addObject(node, null);
  }

  /**
   * Create a random model.
   */
  public void createRandomModel() {
    try {
      setAdjusting(true);

      // delete any old nodes
      Iterator<?> it = diagrammer.getAllObjects();
      while (it.hasNext()) {
        diagrammer.removeObject(it.next());
        it = diagrammer.getAllObjects();
      }

      // how many nodes should we add?
      int nNodes = getRandom(minObjects, maxObjects);

      // create the nodes
      for (int i = 0; i < nNodes; i++) {
        addRandomNode();
      }

      // create the core
      core = new IlvDefaultSDMNode("core");
      core.setProperty("x", Integer.valueOf(cx));
      core.setProperty("y", Integer.valueOf(cy));
      diagrammer.addObject(core, null);

      // creates the radar
      if (radar == null) {
        double rrx = 5 * rx;
        double rry = 4 * ry;
        radar = new IlvArc(new IlvRect(cx - rrx, cy - rry, 2 * rrx, 2 * rry), 0, 45, false, true);
        radar.setBackground(new Color(255, 255, 0, 30));
        diagrammer.getView().getManager().addObject(radar, 1, false);
      }

    } finally {
      setAdjusting(false);
    }
  }

  /**
   * Get a value for the simulator from the properties files as a string.
   *
   * @param name
   *          the name of the property to get from the properties file.
   *
   * @return the value of the property.
   *
   * @throws IOException
   *           if the property can not be found for any reason.
   */
  private static String getSimulatorString(final String name) throws IOException {
    if (properties == null) {
      properties = new Properties();
      InputStream props = Simulator.class.getResourceAsStream(PROPERTIES);
      if (props == null) {
        throw new IOException("Not able to open resource " + PROPERTIES);
      }
      try {
        properties.load(props);
      } finally {
        try {
          props.close();
        } catch (IOException e) {
          // ignore
        }
      }
    }

    if (properties == null) {
      throw new IOException("Not able to read properties " + PROPERTIES);
    }

    String res = properties.getProperty(name);
    if (res != null) {
      return res;
    }

    // if we get here then a problem occurred
    throw new IOException("No property " + name + " in " + PROPERTIES);
  }

  /**
   * Get a value for the simulator from the properties files as an integer.
   *
   * @param name
   *          the name of the property to get from the properties file.
   *
   * @return the value of the property.
   *
   * @throws IOException
   *           if the property can not be found for any reason.
   */
  private static int getSimulatorInt(final String name) throws IOException {
    return Integer.parseInt(getSimulatorString(name));
  }

  /**
   * Create a random number within a given range.
   *
   * @param min
   *          the minimum value for the random number.
   * @param max
   *          the maximum value for the random number.
   *
   * @return the random number.
   */
  private int getRandom(final int min, final int max) {
    double d = Math.random() * (max - min);
    return (min + (int) (d + 0.5));
  }

  /**
   * Convenience method to set the adjusting flag on the Diagrammer engine.
   *
   * @param adjusting
   *          <code>true</code> if we are making changes to the model,
   *          <code>false</code> if we have finished the changes to the model.
   */
  private void setAdjusting(final boolean adjusting) {
    diagrammer.getEngine().setAdjusting(adjusting);
  }

  /**
   * Accelerate the timer delay
   * 
   * @param speedAccelerator
   *          the factor of acceleration
   */
  public void setSpeedAccelerator(int speedAccelerator) {
    // do nothing if zero
    if (speedAccelerator < 1) {
      this.speedAccelerator = 1;
      return;
    }
    this.speedAccelerator = speedAccelerator;
    int newdelay = timerDelay / speedAccelerator;
    timer.setDelay(newdelay);
  }

  /**
   * Reset the simulator and recreate the model
   */
  public void reset() {
    stop();
    timer = new Timer(timerDelay, this);
    setSpeedAccelerator(speedAccelerator);
    parkedNodes.clear();
    parkLimitX = CustomGraphics.VIEW_WIDTH;
    createRandomModel();
  }
}