package demo.view;

import y.base.NodeCursor;
import y.base.EdgeCursor;
import y.base.YCursor;
import y.base.YList;
import y.geom.YPoint;
import y.layout.BufferedLayouter;
import y.layout.GraphLayout;
import y.layout.Layouter;
import y.layout.LayoutGraph;
import y.view.Graph2D;
import y.view.Graph2DLayoutExecutor;
import y.view.LayoutMorpher;

import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyAdapter;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.Action;
import javax.swing.Timer;

/**
 * Demonstrates how to map key events to view animations.
 *
 * Shift + cursor left/cursor right will rotate the current graph counter-clockwise/
 * clockwise around the center of the view.
 * Shift + cursor up/cursor down will zoom in/out.
 *
 * Note:
 * Key events are inherently platform (and even keyboard) dependent.
 * On linux, holding down a non-modifier key will by default generate keyPressed
 * followed by keyReleased after an initial delay and then
 * keyPressed/keyReleased event pairs in rapid succession as long as the key is
 * held down. This behavior can be disabled though.
 * On windows, multiple keyPressed events are generated but only one keyReleased
 * event.
 */
public class RotateDemo extends DemoBase {

  /**
   * Registers the rotate action and the zoom action and maps them to the
   * appropriate key events.
   */
  protected void registerViewActions() {
    // keep the default action
    super.registerViewActions();


    // NOTE: trapping key events has to be done on the view's canvas!!!
    final JComponent canvas = view.getCanvasComponent();


    // register rotation
    canvas.addKeyListener(
            new ActionTrigger(new RotateAction(false), KeyEvent.VK_LEFT));
    canvas.addKeyListener(
            new ActionTrigger(new RotateAction(true), KeyEvent.VK_RIGHT));

    // register zooming
    canvas.addKeyListener(
            new ActionTrigger(new ZoomAction(true), KeyEvent.VK_UP));
    canvas.addKeyListener(
            new ActionTrigger(new ZoomAction(false), KeyEvent.VK_DOWN));
  }

  /**
   * Rotates the graph in the view be 10 degress clockwise/counter-clockwise.
   */
  final class RotateAction extends AbstractAction {
    boolean clockwise;

    RotateAction( final boolean clockwise ) {
      this.clockwise = clockwise;
    }

    public void actionPerformed( final ActionEvent e ) {
      //Note: for all versions before yFiles for Java 2.7 you have to use the
      //second version of this method which is stated at the end of the article.

      // calculate the geometric info of the rotated graph
      final RotateBy10Degrees rotator = new RotateBy10Degrees(clockwise);
      Graph2DLayoutExecutor executor = new Graph2DLayoutExecutor(Graph2DLayoutExecutor.ANIMATED);

      // apply the geometric info in an animated fashion
      LayoutMorpher morpher = executor.getLayoutMorpher();
      morpher.setPreferredDuration(25);
      morpher.setKeepZoomFactor(true);
      executor.doLayout(view, rotator);
    }

    /**
     * Calculates a layout that is rotated 10 degrees clockwise/
     * counter-clockwise around the center of the view.
     */
    final class RotateBy10Degrees implements Layouter {
      private final double radians;

      RotateBy10Degrees( final boolean clockwise ) {
        this.radians = clockwise ? Math.PI/18.0 : -Math.PI/18.0;
      }

      public boolean canLayout( final LayoutGraph graph ) {
        return true;
      }

      public void doLayout( final LayoutGraph graph ) {
        if (graph == null || graph.isEmpty()) {
          return;
        }

        // set up rotation: 10 degrees around the center of the view
        final Point2D center = view.getCenter();
        final AffineTransform at = new AffineTransform();
        at.rotate(radians, center.getX(), center.getY());

        final Point2D coords = new Point2D.Double();

        // rotate node centers
        for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
          coords.setLocation(
                  graph.getCenterX(nc.node()), graph.getCenterY(nc.node()));
          at.transform(coords, coords);
          graph.setCenter(nc.node(), coords.getX(), coords.getY());
        }

        // rotate edges
        for (EdgeCursor ec = graph.edges(); ec.ok(); ec.next()) {
          final YList newPath = new YList();
          final YList path = graph.getPointList(ec.edge());
          for (YCursor pc = path.cursor(); pc.ok(); pc.next()) {
            final YPoint yp = (YPoint)pc.current();
            coords.setLocation(yp.getX(), yp.getY());
            at.transform(coords, coords);
            newPath.add(new YPoint(coords.getX(), coords.getY()));
          }
          graph.setPoints(ec.edge(), newPath);
        }
      }
    }
  }

  /**
   * Multiplies/divides the current zoom level of the view by 1.25.
   */
  final class ZoomAction extends AbstractAction {
    private final double factor;

    ZoomAction( final boolean zoomIn ) {
      this.factor = zoomIn ? 1.25 : 1.0/1.25;
    }

    public void actionPerformed( final ActionEvent e ) {
      view.setZoom(view.getZoom()*factor);
      view.updateView();
    }
  }

  private static final class ActionTrigger extends KeyAdapter {
    private static final int LATENCY = 35;

    private final int keyCode;
    private final Timer timer;

    ActionTrigger( final Action action, final int keyCode ) {
      this.keyCode = keyCode;
      this.timer = new Timer(LATENCY, action);
      this.timer.setInitialDelay(0);
      this.timer.setRepeats(true);
    }


    public void keyPressed( final KeyEvent e ) {
      if (keyCode == e.getKeyCode() &&
          KeyEvent.SHIFT_DOWN_MASK == e.getModifiersEx()) {
        if (!timer.isRunning()) {
          timer.restart();
        }
      }
    }


    public void keyReleased( final KeyEvent e ) {
      if (keyCode == e.getKeyCode()) {
        timer.stop();
      }
    }
  }

  /**
   * Instantiates and starts this demo.
   */
  public static void main( String[] args ) {
    EventQueue.invokeLater(new Runnable() {
      public void run() {
        initLnF();
        (new RotateDemo()).start();
      }
    });
  }
}
