/*
 * Open Source Physics software is free software as described near the bottom of this code file.
 *
 * For additional information and documentation on Open Source Physics please see:
 * <http://www.opensourcephysics.org/>
 */

package org.opensourcephysics.display;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.opensourcephysics.controls.XML;
import org.opensourcephysics.controls.XMLControl;
import org.opensourcephysics.controls.XMLLoader;

public class InteractiveArrow extends BoundedShape {
  Point2D[] hotSpots = new Point2D[2]; // shadows superclass field
  BasicStroke stroke = new BasicStroke(2);
  Shape head;
  static int HEAD = 1;

  /**
   * Constructor InteractiveArrow
   * @param x
   * @param y
   * @param w
   * @param h
   */
  public InteractiveArrow(double x, double y, double w, double h) {
    super(new Line2D.Double(0, 0, w, h), x, y);
    theta = (w==0) ? 0 : Math.atan2(h, w);
    head = getHead(theta);
    setRotateDrag(true);
    hideBounds = true;
    width = w;
    height = h;
    for(int i = 0, n = hotSpots.length; i<n; i++) {
      hotSpots[i] = new Point2D.Float(0, 0);
    }
  }

  /**
   * Sets the stroke for rendering fat arrows.
   * @param width double
   */
  public void setStrokeWidth(double width) {
    stroke = new BasicStroke((float) width);
  }

  /**
   * Drawing offset not supported.
   *
   * @param xoffset double
   * @param yoffset double
   */
  public void setOffset(double xoffset, double yoffset) {
    return;
  }

  /**
   * Determines if the shape is enabled and if the given pixel coordinates are within the shape.
   *
   * @param panel DrawingPanel
   * @param xpix int
   * @param ypix int
   * @return boolean
   */
  public boolean isInside(DrawingPanel panel, int xpix, int ypix) {
    hotspot = NONE;
    if(!enabled) {
      return false;
    }
    if(pixelBounds.contains(xpix, ypix)&&!selected) {
      return true;
    }
    if(selected) {
      hotspot = getHotSpotIndex(xpix, ypix, hotSpots);
      return true;
    }
    return false;
  }

  /**
   * Rotates the arrow without changing its length.
   *
   * @param theta
   */
  public void setTheta(double theta) {
    double len = Math.sqrt(width*width+height*height);
    double dx = len*Math.cos(theta);
    double dy = len*Math.sin(theta);
    shape = new Line2D.Double(this.x, this.y, this.x+dx, this.y+dy);
    shape = AffineTransform.getTranslateInstance(x, y).createTransformedShape(shape);
    width = dx;
    height = dy;
    this.theta = theta;
    head = getHead(theta);
  }

  /**
   * Sets the x and y coordinates using hot spots.
   *
   * @param x
   * @param y
   */
  void setHotSpotXY(double x, double y) {
    if(hideBounds) {
      setXY(x, y);
      return;
    }
    if(xyDrag&&selected&&(hotspot==CENTER)) {
      setXY(x, y);
    } else if(rotateDrag&&selected&&(hotspot==HEAD)) {
      double r = -toPixels.getScaleY()/toPixels.getScaleX();
      double dx = x-this.x;
      double dy = y-this.y;
      shape = new Line2D.Double(this.x, this.y, this.x+dx, this.y+dy);
      width = dx;
      height = dy;
      theta = (width==0) ? theta : Math.atan2(r*height, width);
      head = getHead(theta);
    }
  }

  /**
   * Sets the origin, width (horizontal) and height (vertical) components of this arrow.
   *
   * @param width double
   * @param height double
   */
  public void setWidthHeight(double width, double height) {
    shape = new Line2D.Double(x, y, x+width, y+height);
    this.width = width;
    this.height = height;
    theta = (width==0) ? theta : Math.atan2(height, width);
    head = getHead(theta);
  }

  /**
   * Draws the arrow.
   *
   * @param panel  the world in which the arrow is viewed
   * @param g  the graphics context upon which to draw
   */
  public void draw(DrawingPanel panel, Graphics g) {
    Graphics2D g2 = (Graphics2D) g;
    toPixels = panel.getPixelTransform();
    Shape temp;
    Point2D pt = new Point2D.Double(x, y);
    pt = toPixels.transform(pt, pt);
    computePixelBounds(pt);
    if(pixelSized) {
      // translate the shape to correct pixel coordinates
      temp = new AffineTransform(1, 0, 0, -1, -x+pt.getX(), y+pt.getY()).createTransformedShape(shape);
    } else {
      temp = toPixels.createTransformedShape(shape);
    }
    g2.setPaint(edgeColor);
    Stroke oldStroke = g2.getStroke();
    g2.setStroke(stroke);
    g2.draw(temp);
    hotSpots[0].setLocation(pt);
    pt = new Point2D.Double(x+width, y+height);
    pt = toPixels.transform(pt, pt);
    hotSpots[1].setLocation(pt);
    temp = AffineTransform.getTranslateInstance(pt.getX(), pt.getY()).createTransformedShape(head);
    g2.fill(temp);
    g2.draw(temp);
    g2.setStroke(oldStroke);
    if(!selected||hideBounds) {
      return;
    }
    g2.setPaint(boundsColor);
    if(xyDrag) {
      g2.fillRect((int) hotSpots[CENTER].getX()-delta, (int) hotSpots[CENTER].getY()-delta, d2, d2);
    }
    if(rotateDrag) {
      g2.fillOval((int) hotSpots[HEAD].getX()-delta, (int) hotSpots[HEAD].getY()-delta, d2, d2);
    }
    g2.setPaint(Color.BLACK);
  }

  private void computePixelBounds(Point2D pt) {
    double dx = toPixels.getScaleX()*width;
    double dy = toPixels.getScaleY()*height;
    double len = Math.sqrt(dx*dx+dy*dy)+delta;
    Rectangle2D rect = new Rectangle2D.Double(pt.getX(), pt.getY()-delta, len, d2);
    pixelBounds = AffineTransform.getRotateInstance(-theta, pt.getX(), pt.getY()).createTransformedShape(rect);
  }

  /**
   * Gets the cursor depending on the current hot spot.
   *
   * @return Cursor
   */
  public java.awt.Cursor getPreferredCursor() {
    if(xyDrag&&(hotspot==CENTER)) {
      return java.awt.Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
    } else if(rotateDrag&&(hotspot==HEAD)) {
      return java.awt.Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
    } else if(selected) {
      return java.awt.Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
    } else {
      return java.awt.Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
    }
  }

  private Shape getHead(double theta) {
    float headSize = 4+2*stroke.getLineWidth();
    GeneralPath path = new GeneralPath();
    path.moveTo(0, 0);
    path.lineTo((-headSize), (-headSize/2));
    path.lineTo((-headSize), (+headSize/2));
    path.closePath();
    AffineTransform rot = AffineTransform.getRotateInstance(-theta);
    Shape head = rot.createTransformedShape(path);
    return head;
  }

  /**
   * Gets the XML object loader for this class.
   * @return ObjectLoader
   */
  public static XML.ObjectLoader getLoader() {
    return new InteractiveArrowLoader();
  }

  /**
   * A class to save and load InteractiveArrow in an XMLControl.
   */
  protected static class InteractiveArrowLoader extends XMLLoader {
    public void saveObject(XMLControl control, Object obj) {
      InteractiveArrow arrow = (InteractiveArrow) obj;
      control.setValue("x", arrow.x);                      //$NON-NLS-1$
      control.setValue("y", arrow.y);                      //$NON-NLS-1$
      control.setValue("width", arrow.width);              //$NON-NLS-1$
      control.setValue("height", arrow.height);            //$NON-NLS-1$
      control.setValue("is enabled", arrow.isEnabled());   //$NON-NLS-1$
      control.setValue("is measured", arrow.isMeasured()); //$NON-NLS-1$
      control.setValue("color", arrow.color);              //$NON-NLS-1$
    }

    public Object createObject(XMLControl control) {
      return new InteractiveArrow(0, 0, 0, 0);
    }

    public Object loadObject(XMLControl control, Object obj) {
      InteractiveArrow arrow = (InteractiveArrow) obj;
      double x = control.getDouble("x");      //$NON-NLS-1$
      double y = control.getDouble("y");      //$NON-NLS-1$
      double w = control.getDouble("width");  //$NON-NLS-1$
      double h = control.getDouble("height"); //$NON-NLS-1$
      arrow.enabled = control.getBoolean("is enabled");        //$NON-NLS-1$
      arrow.enableMeasure = control.getBoolean("is measured"); //$NON-NLS-1$
      arrow.color = (Color) control.getObject("color");        //$NON-NLS-1$
      arrow.setXY(x, y);
      arrow.setWidthHeight(w, h);
      return obj;
    }

  }

}

/*
 * Open Source Physics software is free software; you can redistribute
 * it and/or modify it under the terms of the GNU General Public License (GPL) as
 * published by the Free Software Foundation; either version 2 of the License,
 * or(at your option) any later version.

 * Code that uses any portion of the code in the org.opensourcephysics package
 * or any subpackage (subdirectory) of this package must must also be be released
 * under the GNU GPL license.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
 * or view the license online at http://www.gnu.org/copyleft/gpl.html
 *
 * Copyright (c) 2007  The Open Source Physics project
 *                     http://www.opensourcephysics.org
 */
