/*
 * 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.
 */
package interactor;

import java.awt.AWTEvent;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.*;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.*;
import java.util.logging.Logger;
import java.util.logging.Level;

import ilog.views.chart.IlvAxis;
import ilog.views.chart.IlvChart;
import ilog.views.chart.IlvChartDecoration;
import ilog.views.chart.IlvChartInteractor;
import ilog.views.chart.IlvCoordinateSystem;
import ilog.views.chart.IlvDataInterval;
import ilog.views.chart.IlvDataWindow;
import ilog.views.chart.IlvDoublePoint;
import ilog.views.chart.IlvStyle;
import ilog.views.chart.graphic.IlvDataIndicator;
import ilog.views.util.collections.IlvCollections;
import ilog.views.util.IlvResourceUtil;

/**
 * A class to interact on an <code>IlvDataIndicator</code>.
 * The <code>IlvChartDataIndicatorInteractor</code> class provides the
 * following facilities to graphically modify the definition of a data
 * indicator:
 * <ul><li>Drag a border of the indicator to modify the value of the
 * corresponding bound.</li>
 * <li>Drag the indicator itself to modify the corresponding range
 * definition.</li>
 * </ul>.
 * <p>The <code>IlvChartDataIndicatorInteractor</code> defines two modes of
 * interaction whether its <code>opaqueEdit</code> property is enabled or not.
 * If the property is enabled, the indicator modification is "real time": each
 * <code>mouseMotion</code> event immediately modifies the indicator values
 * and hence cannot be canceled. If the mode is disabled, the indicator
 * modification will only be performed when the interaction ends (on a
 * <code>MOUSE_RELEASED</code> event).
 * <p>The registered name of this interactor is "DataIndicator".
 */
public class IlvChartDataIndicatorInteractor extends IlvChartInteractor
{
  // ============================ Metainformation ============================

  static {
    IlvChartInteractor.register("DataIndicator",
                                IlvChartDataIndicatorInteractor.class);
  }

  /**
   * Returns a localized name for this interactor class.
   */
  public static String getLocalizedName(Locale locale)
  {
    return IlvResourceUtil.getBundle("messages",
                                     IlvChartDataIndicatorInteractor.class,
                                     locale)
           .getString("IlvChartDataIndicatorInteractor");
  }


  // ============================ Logging Support ============================

  static Logger logger;
  static Logger getLogger() {
    if (logger == null)
      logger =
        Logger.getLogger(IlvChartDataIndicatorInteractor.class.getName());
    return logger;
  }


  // ===================== Customization, Bean Properties =====================

  private transient IlvDataIndicator _indicator;

  /**
   * Returns the indicator that is being dragged.
   */
  protected final IlvDataIndicator getIndicator()
  {
    return _indicator;
  }

  //--------------------------------------------------------------------------

  private boolean _opaqueEdit;

  /**
   * Returns whether the <code>opaqueEdit</code> mode is enabled.
   * @see #setOpaqueEdit
   */
  public final boolean isOpaqueEdit()
  {
    return _opaqueEdit;
  }

  /**
   * Sets whether the <code>opaqueEdit</code> mode is enabled. This property
   * defines how the modifications are propagated to the indicator. If
   * <code>true</code>, each mouse event corresponds to an immediate indicator
   * modification. If <code>false</code>, the modification occurs when the
   * interaction ends. The default value is <code>false</code>.
   */
  public void setOpaqueEdit(boolean opaque)
  {
    _opaqueEdit = opaque;
  }


  // ============================ State Variables ============================

  private transient Object _currentValue;

  //--------------------------------------------------------------------------

  /**
   * This interface implements the detail behavior, as a reaction to
   * mouse events.
   */
  private interface InteractionHandler extends Serializable
  {
    public Object computeValue(double dx, double dy, boolean apply);
    public Cursor getCursor();
  }

  /**
   * This class implements the detail behavior when an indicator is
   * translated.
   */
  private final class Translater implements InteractionHandler
  {
    Override
    public Object computeValue(double dx, double dy, boolean apply)
    {
      Object value;
      int type = getIndicator().getType();
      switch (type) {
        case IlvDataIndicator.X_VALUE:
        case IlvDataIndicator.Y_VALUE:
          double v = (_currentValue != null
                      ? ((Double)_currentValue).doubleValue()
                      : getIndicator().getValue());
          getLogger().log(Level.FINE, "v:" + v +"   dx:" + dx + "   dy:" + dy);
          v += type == IlvDataIndicator.X_VALUE ? dx : dy;
          if (apply)
            getIndicator().setValue(v);
          value = Double.valueOf(v);
          getLogger().log(Level.FINE, "value:" + value);
          break;
        default:
          IlvDataWindow window = (_currentValue != null
                                  ? (IlvDataWindow)_currentValue
                                  : getIndicator().getDataWindow());
          if (type != IlvDataIndicator.Y_RANGE) {
            window.xRange.translate(dx);
          }
          if (type != IlvDataIndicator.X_RANGE) {
            window.yRange.translate(dy);
          }

          if (apply) {
            if (type == IlvDataIndicator.X_RANGE)
              getIndicator().setRange(window.xRange);
            else if (type == IlvDataIndicator.Y_RANGE)
              getIndicator().setRange(window.yRange);
            else
              getIndicator().setDataWindow(window);
          }
          value = window;
      }
      return value;
    }
    Override
    public Cursor getCursor()
    {
      return Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
    }
  }
  private InteractionHandler _translater = new Translater();

  /**
   * This class implements the detail behavior when an indicator is
   * resized.
   */
  private final class Reshaper implements InteractionHandler
  {
    static final int NONE = 0;
    static final int XMIN = 1;
    static final int XMAX = 2;
    static final int YMIN = 3;
    static final int YMAX = 4;

    private int bound;

    public Reshaper(int bound)
    {
      this.bound = bound;
    }
    Override
    public Cursor getCursor()
    {
      return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
    }
    Override
    public Object computeValue(double dx, double dy, boolean apply)
    {
      IlvDataWindow window = (_currentValue != null
                              ? (IlvDataWindow)_currentValue
                              : getIndicator().getDataWindow());
      if(window == null)
        return null;
      IlvDataInterval range = null;
      int type = getIndicator().getType();
      switch (bound) {
        case XMIN:
          window.xRange.min += dx;
          range = window.xRange;
          break;
        case XMAX:
          window.xRange.max += dx;
          range = window.xRange;
          break;
        case YMIN:
          window.yRange.min += dy;
          range = window.yRange;
          break;
        case YMAX:
          window.yRange.max += dy;
          range = window.yRange;
          break;
      }
      if (apply) {
        if (type != IlvDataIndicator.WINDOW)
          getIndicator().setRange(range);
        else
          getIndicator().setDataWindow(window);
      }
      return window;
    }
  }

  // The detail interaction delegate.
  private InteractionHandler _handler;

  //--------------------------------------------------------------------------

  private transient IlvDoublePoint _dragPoint;


  // ============================ Ghost handling ============================

  private IlvStyle getGhostStyle()
  {
    IlvStyle style = _indicator.getStyle();
    if (style == null)
      style = new IlvStyle(1, getGhostColor());
    return style;
  }

  /**
   * Draws the ghost.
   */
  Override
  protected void drawGhost(Graphics g)
  {
    IlvChart chart = getChart();
    int type = _indicator.getType();
    IlvStyle style = getGhostStyle();
    IlvCoordinateSystem sys = getChart().getCoordinateSystem(getYAxisIndex());
    Shape shp;
    switch (type) {
      case IlvDataIndicator.X_VALUE:
      case IlvDataIndicator.Y_VALUE:
        double value = ((Double)_currentValue).doubleValue();
        int axisType =
          (_indicator.getAxisIndex() == -1 ? IlvAxis.X_AXIS : IlvAxis.Y_AXIS);
        shp = chart.getProjector().getShape(value,
                                            axisType,
                                            chart.getChartArea().getPlotRect(),
                                            sys);
        break;
      default:
        IlvDataWindow w = new IlvDataWindow((IlvDataWindow)_currentValue);
        w.intersection(sys.getVisibleWindow());
        if (w.isEmpty())
          return;
        shp = chart.getProjector().getShape(w,
                                            chart.getChartArea().getPlotRect(),
                                            sys);
        break;
    }
    style.renderShape(g, shp);
  }

  /**
   * Returns the ghost bounds.
   */
  Override
  protected Rectangle getGhostBounds()
  {
    IlvChart chart = getChart();
    int type = _indicator.getType();
    IlvStyle style = getGhostStyle();
    IlvCoordinateSystem sys = getChart().getCoordinateSystem(getYAxisIndex());
    Shape shp;
    switch (type) {
      case IlvDataIndicator.X_VALUE:
      case IlvDataIndicator.Y_VALUE:
        double value = ((Double)_currentValue).doubleValue();
        int axisType =
          (_indicator.getAxisIndex() == -1 ? IlvAxis.X_AXIS : IlvAxis.Y_AXIS);
        shp = chart.getProjector().getShape(value,
                                            axisType,
                                            chart.getChartArea().getPlotRect(),
                                            sys);
        break;
      default:
        IlvDataWindow w = new IlvDataWindow((IlvDataWindow)_currentValue);
        w.intersection(sys.getVisibleWindow());
        if (w.isEmpty())
          return new Rectangle();
        shp = chart.getProjector().getShape(w,
                                            chart.getChartArea().getPlotRect(),
                                            sys);
        break;
    }
    return style.getShapeBounds(shp).getBounds();
  }


  // ========================= Mouse event handling =========================

  /**
   * Determines which indicator the user meant by clicking at (x,y).
   * Sets _indicator and returns true if one was found; returns false otherwise.
   */
  private boolean findIndicator(int x, int y)
  {
    List<IlvChartDecoration> decos = getChart().getDecorations();
    Iterator<IlvChartDecoration> ite = IlvCollections.reversedIterator(decos);
    while (ite.hasNext()) {
      IlvChartDecoration deco = ite.next();
      if (deco instanceof IlvDataIndicator) {
        if (((IlvDataIndicator)deco).contains(x, y)) {
          _indicator = (IlvDataIndicator)deco;
          return true;
        }
      }
    }
    return false;
  }

  /**
   * After findIndicator returned true, determine which kind of interaction
   * to use with _indicator.
   */
  private InteractionHandler getInteractionHandler(MouseEvent event)
  {
    IlvDoublePoint start = getData(event);
    InteractionHandler handler;
    IlvDataWindow window = _indicator.getDataWindow();
    int type = _indicator.getType();
    switch (type) {
      case IlvDataIndicator.X_VALUE:
      case IlvDataIndicator.Y_VALUE:
        // All the user can do with such an indicator is to move it.
        // No need to test the event's coordinates - findIndicator already did
        // it.
        handler = _translater;
        break;
      case IlvDataIndicator.X_RANGE:
      case IlvDataIndicator.Y_RANGE:
        // When clicking near one of the two borders, the user wants to move
        // this border; otherwise he wants to move the entire indicator.
        {
          boolean onX = type == IlvDataIndicator.X_RANGE;
          IlvDataInterval range;
          double value;
          if (onX) {
            range = window.xRange;
            value = start.x;
          } else {
            range = window.yRange;
            value = start.y;
          }
          double l = range.getLength();
          double delta = l*0.10;
          if (value < range.min + delta)
            handler = new Reshaper(onX ? Reshaper.XMIN : Reshaper.YMIN);
          else if (value > range.max - delta)
            handler = new Reshaper(onX ? Reshaper.XMAX : Reshaper.YMAX);
          else // (value >= range.min + delta && value <= range.max - delta)
            handler = _translater;
        }
        break;
      case IlvDataIndicator.WINDOW:
        // When clicking near one of the four borders, the user wants to move
        // this border; otherwise he wants to move the entire indicator.
        {
          int nearXBorder;
          double distToXBorder;
          {
            double l = window.xRange.getLength();
            double delta = l*0.10;
            if (start.x < window.xRange.min + delta) {
              nearXBorder = Reshaper.XMIN;
              IlvDoublePoint pt =
                new IlvDoublePoint(window.xRange.min,
                                   start.y > window.yRange.max ? window.yRange.max :
                                   start.y < window.yRange.min ? window.yRange.min :
                                   start.y);
              pt = toDisplay(pt);
              distToXBorder = Math.hypot(event.getX()-pt.x, event.getY()-pt.y);
            } else if (start.x > window.xRange.max - delta) {
              nearXBorder = Reshaper.XMAX;
              IlvDoublePoint pt =
                new IlvDoublePoint(window.xRange.max,
                                   start.y > window.yRange.max ? window.yRange.max :
                                   start.y < window.yRange.min ? window.yRange.min :
                                   start.y);
              pt = toDisplay(pt);
              distToXBorder = Math.hypot(event.getX()-pt.x, event.getY()-pt.y);
            } else {
              nearXBorder = Reshaper.NONE;
              distToXBorder = 0;
            }
          }
          int nearYBorder;
          double distToYBorder;
          {
            double l = window.yRange.getLength();
            double delta = l*0.10;
            if (start.y < window.yRange.min + delta) {
              nearYBorder = Reshaper.YMIN;
              IlvDoublePoint pt =
                new IlvDoublePoint(start.x > window.xRange.max ? window.xRange.max :
                                   start.x < window.xRange.min ? window.xRange.min :
                                   start.x,
                                   window.yRange.min);
              pt = toDisplay(pt);
              distToYBorder = Math.hypot(event.getX()-pt.x, event.getY()-pt.y);
            } else if (start.y > window.yRange.max - delta) {
              nearYBorder = Reshaper.YMAX;
              IlvDoublePoint pt =
                new IlvDoublePoint(start.x > window.xRange.max ? window.xRange.max :
                                   start.x < window.xRange.min ? window.xRange.min :
                                   start.x,
                                   window.yRange.max);
              pt = toDisplay(pt);
              distToYBorder = Math.hypot(event.getX()-pt.x, event.getY()-pt.y);
            } else {
              nearYBorder = Reshaper.NONE;
              distToYBorder = 0;
            }
          }
          if (nearXBorder != Reshaper.NONE && nearYBorder != Reshaper.NONE) {
            if (distToXBorder < distToYBorder) {
              nearYBorder = Reshaper.NONE;
            } else {
              nearXBorder = Reshaper.NONE;
            }
          }
          if (nearXBorder != Reshaper.NONE)
            handler = new Reshaper(nearXBorder);
          else if (nearYBorder != Reshaper.NONE)
            handler = new Reshaper(nearYBorder);
          else
            handler = _translater;
        }
        break;
      default:
        throw new AssertionError();
    }
    return handler;
  }

  /**
   * Called to validate the new position. You can override this
   * method to perform additional checks. The default implementation clip the
   * point to the coordinate system data window.
   * @param pt The new position to validate (in data coordinate system).
   */
  protected void validate(IlvDoublePoint pt)
  {
    IlvDataInterval range = getChart().getXAxis().getDataRange();
    pt.x = Math.max(range.min, Math.min(range.max, pt.x));
    range = getChart().getYAxis(_indicator.getAxisIndex()).getDataRange();
    pt.y = Math.max(range.min, Math.min(range.max, pt.y));
  }

  /**
   * Handles the mouse events.
   */
  Override
  public void processMouseEvent(MouseEvent event)
  {
    switch (event.getID()) {
      case MouseEvent.MOUSE_PRESSED:
        if (((event.getModifiersEx() & getEventMaskEx()) == getEventMaskEx())
            && ((event.getModifiersEx() & ~getEventMaskEx()) == 0)) {
          if (findIndicator(event.getX(), event.getY())) {
            _dragPoint = getData(event);
            _handler = getInteractionHandler(event);
            startOperation(event);
            _currentValue = _handler.computeValue(0, 0, _opaqueEdit);
            if (!_opaqueEdit)
              drawGhost();
            if (isConsumeEvents())
              event.consume();
          }
        }
        break;

      case MouseEvent.MOUSE_RELEASED:
        if ((event.getModifiersEx() & getEventMaskEx()) != getEventMaskEx()) {
          if (isInOperation()) {
            IlvDoublePoint pt = getData(event);
            validate(pt);
            if (!_opaqueEdit)
              drawGhost();
            double dx = pt.x - _dragPoint.x;
            double dy = pt.y - _dragPoint.y;
            _handler.computeValue(dx, dy, true);
            endOperation(event);
            if (isConsumeEvents())
              event.consume();
          }
        }
        break;
    }
  }

  /**
   * Handles the mouse motion events.
   */
  Override
  public void processMouseMotionEvent(MouseEvent event)
  {
    switch (event.getID()) {
      case MouseEvent.MOUSE_DRAGGED:
        if (isInOperation()) {
          IlvDoublePoint pt = getData(event);
          validate(pt);
          double dx = pt.x - _dragPoint.x;
          double dy = pt.y - _dragPoint.y;
          if (_opaqueEdit) {
            _currentValue = _handler.computeValue(dx, dy, true);
          } else {
            drawGhost();
            _currentValue = _handler.computeValue(dx, dy, false);
            drawGhost();
          }
          _dragPoint = pt;
          if (isConsumeEvents())
            event.consume();
        }
        break;
    }
  }

  /**
   * Handles the key event.
   */
  Override
  public void processKeyEvent(KeyEvent event)
  {
    if (event.getID() == KeyEvent.KEY_PRESSED
        && event.getKeyCode() == KeyEvent.VK_ESCAPE) {
      if (!_opaqueEdit && _dragPoint != null)
        drawGhost();
      abort();
      if (isConsumeEvents())
        event.consume();
    }
  }


  // =================== Overrides from IlvChartInteractor ===================

  /**
   * Indicates whether the interactor can be used with a 3D chart.
   * @return <code>false</code>.
   */
  Override
  public boolean has3DSupport()
  {
    return false;
  }

  /**
   * Called when the interaction starts.
   * You may override this method to add your own initialization
   * code.
   * @param event The event that starts the interaction.
   */
  Override
  protected void startOperation(MouseEvent event)
  {
    super.startOperation(event);
    setAllowDrawGhost(!_opaqueEdit);
    if (_handler != null)
      setCursor(_handler.getCursor());
  }

  /**
   * Called when the interaction ends.
   * You may override this method to add your own initialization
   * code.
   * @param event The event that ends the interaction.
   */
  Override
  protected void endOperation(MouseEvent event)
  {
    super.endOperation(event);
    setAllowDrawGhost(false);
    setCursor(null);
    _indicator = null;
    _dragPoint = null;
    _currentValue = null;
  }

  /**
   * Called when the interaction has been aborted.
   * You may override this method to add your own initialization
   * code.
   */
  Override
  protected void abort()
  {
    super.abort();
    setAllowDrawGhost(false);
    setCursor(null);
    _indicator = null;
    _dragPoint = null;
    _currentValue = null;
  }


  // ============================= Constructors =============================

  private void initTransientFields()
  {
    _dragPoint = null;
    _indicator = null;
    _currentValue = null;
  }

  /**
   * Creates a new <code>IlvChartDataIndicatorInteractor</code> object
   * associated to the BUTTON1_DOWN_MASK mouse button with the
   * <code>opaqueEdit</code> mode disabled.
   */
  public IlvChartDataIndicatorInteractor()
  {
    this(0, MouseEvent.BUTTON1_DOWN_MASK, false);
  }

  /**
   * Creates a new <code>IlvChartEditPointInteractor</code> object associated
   * with the given mouse button linked to the given Y-axis.
   */
  public IlvChartDataIndicatorInteractor(int yAxisIdx,
                                         int buttonOrKeyEventMask,
                                         boolean opaqueEdit)
  {
    super(yAxisIdx, buttonOrKeyEventMask);
    enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK |
                 AWTEvent.MOUSE_EVENT_MASK |
                 AWTEvent.KEY_EVENT_MASK);
    _opaqueEdit = opaqueEdit;
    initTransientFields();
  }


  // ============================= Serialization =============================

  private void readObject(ObjectInputStream in)
    throws IOException, ClassNotFoundException
  {
    in.defaultReadObject();
    initTransientFields();
  }

}