/*
 * 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.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.opensourcephysics.controls.XML;
import org.opensourcephysics.controls.XMLControl;

public class BoundedShape extends InteractiveShape implements Selectable {
  static int CENTER = 0;
  static int BOTTOM = 1;
  static int LEFT = 2;
  static int TOP = 3;
  static int RIGHT = 4;
  static int CORNER = 5;
  static int NONE = 6;
  int hotspot = NONE;
  int delta = 3;
  int deltaSqr = delta*delta;
  int d2 = 2*delta+1;
  boolean selected = false;
  boolean hideBounds = false;
  Color boundsColor = new Color(128, 128, 255);
  boolean widthDrag = false;
  boolean heightDrag = false;
  boolean xyDrag = true;
  boolean rotateDrag = false;
  Shape pixelBounds = new Rectangle2D.Double(0, 0, 0, 0); // bounding rectangle in pixel coordinates.
  Point2D[] hotSpots = new Point2D[6];
  XYDelegate xyDelegate = new XYDelegate();

  /**
   * Constructs a BoundedShape object for the given shape.
   *
   * @param s Shape
   * @param x double
   * @param y double
   */
  public BoundedShape(Shape s, double x, double y) {
    super(s, x, y);
    for(int i = 0, n = hotSpots.length; i<n; i++) {
      hotSpots[i] = new Point2D.Float(0, 0);
    }
  }

  /**
   * Creates a bounded rectangle.
   * @param x
   * @param y
   * @param w
   * @param h
   * @return the interactive rectangle
   */
  public static BoundedShape createBoundedRectangle(double x, double y, double w, double h) {
    Shape shape = new Rectangle2D.Double(-w/2, -h/2, w, h);
    return new BoundedShape(shape, x, y);
  }

  /**
   * Creates a bounded rectangle.
   * @param x
   * @param y
   * @param b base
   * @param h height
   * @return the rectangle
   */
  public static BoundedShape createBoundedTriangle(double x, double y, double b, double h) {
    GeneralPath path = new GeneralPath();
    path.moveTo((float) (-b/2), (float) (-h/2));
    path.lineTo((float) (+b/2), (float) (-h/2));
    path.lineTo(0, (float) (h/2));
    path.closePath();
    Shape shape = path;
    return new BoundedShape(shape, x, y);
  }

  /**
   * Creates a bounded arrow.
   * @param x
   * @param y
   * @param w base
   * @param h height
   * @return the arrow
   */
  public static BoundedShape createBoundedArrow(double x, double y, double w, double h) {
    InteractiveArrow ia = new InteractiveArrow(x, y, w, h);
    ia.hideBounds = false;
    return ia;
  }

  /**
   * Creates a bounded arrow.
   * @param x
   * @param y
   * @param w base
   * @param h height
   * @return the arrow
   */
  public static BoundedShape createBoundedCenteredArrow(double x, double y, double w, double h) {
    InteractiveCenteredArrow ica = new InteractiveCenteredArrow(x, y, w, h);
    ica.hideBounds = false;
    return ica;
  }

  /**
   * Creates a bounded image.
   * @param x
   * @param y
   * @param image
   * @return the rectangle
   */
  public static BoundedShape createBoundedImage(Image image, double x, double y) {
    return new BoundedImage(image, x, y);
  }

  /**
   * Creates a bounded ellipse.
   *
   * @param x
   * @param y
   * @param w
   * @param h
   * @return BoundedShape
   */
  public static BoundedShape createBoundedEllipse(double x, double y, double w, double h) {
    Shape shape = new Ellipse2D.Double(-w/2, -h/2, w, h);
    return new BoundedShape(shape, x, y);
  }

  /**
   * Creates a bounded circle.
   *
   * @param x
   * @param y
   * @param d the diameter
   * @return the circle
   */
  public static BoundedShape createBoundedCircle(double x, double y, double d) {
    Shape shape = new Ellipse2D.Double(-d/2, -d/2, d, d);
    return new BoundedShape(shape, x, y);
  }

  public void setSelected(boolean selected) {
    this.selected = selected;
  }

  public boolean isSelected() {
    return selected;
  }

  /**
   * Sets the xy drag option.
   *
   * @param enable boolean
   */
  public void setXYDrag(boolean enable) {
    xyDrag = enable;
  }

  /**
   * Gets the xy drag boolean.
   *
   * @return boolean true if center can be dragged
   */
  public boolean isXYDrag() {
    return xyDrag;
  }

  /**
   * Sets the rotate drag option.
   *
   * @param enable boolean
   */
  public void setRotateDrag(boolean enable) {
    rotateDrag = enable;
  }

  /**
   * Gets the rotate drag option.
   * @return boolean
   */
  public boolean isRotateDrag() {
    return rotateDrag;
  }

  /**
   * Sets the width drag option.
   * @param enable boolean
   */
  public void setWidthDrag(boolean enable) {
    widthDrag = enable;
  }

  /**
   * Gets the width width drag option.
   *
   * @return boolean true if center can be dragged
   */
  public boolean isWidthDrag() {
    return widthDrag;
  }

  /**
   * Sets the height drag option.
   * @param enable boolean
   */
  public void setHeightDrag(boolean enable) {
    heightDrag = enable;
  }

  /**
   * Gets the height drag option.
   *
   * @return boolean true if center can be dragged
   */
  public boolean isHeightDrag() {
    return heightDrag;
  }

  public java.awt.Cursor getPreferredCursor() {
    if(xyDrag&&(hotspot==CENTER)) {
      return java.awt.Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
    } else if(rotateDrag&&(hotspot==CORNER)) { // need better cursors!
      return java.awt.Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
    } else if(widthDrag&&(hotspot==LEFT)) {
      return(theta==0) ? java.awt.Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR) : java.awt.Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
    } else if(widthDrag&&(hotspot==RIGHT)) {
      return(theta==0) ? java.awt.Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR) : java.awt.Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
    } else if(heightDrag&&(hotspot==TOP)) {
      return(theta==0) ? java.awt.Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR) : java.awt.Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
    } else if(heightDrag&&(hotspot==BOTTOM)) {
      return(theta==0) ? java.awt.Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR) : 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);
    }
  }

  public void toggleSelected() {
    selected = !selected;
  }

  public Interactive findInteractive(DrawingPanel panel, int xpix, int ypix) {
    if(isInside(panel, xpix, ypix)) {
      return xyDelegate;
    }
    return null;
  }

  /**
   * 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;
  }

  int getHotSpotIndex(int xpix, int ypix, Point2D[] hotSpots) {
    for(int i = 0, n = hotSpots.length; i<n; i++) {
      double dx = xpix-hotSpots[i].getX();
      double dy = ypix-hotSpots[i].getY();
      if(dx*dx+dy*dy<=deltaSqr) {
        return i;
      }
    }
    return NONE;
  }

  void computeScaledHotSpots(Rectangle2D rect, double ar) {
    double sin = Math.sin(theta);
    double cos = Math.cos(theta);
    double centerX = rect.getCenterX()-xoff*toPixels.getScaleX();
    double centerY = rect.getCenterY()-yoff*toPixels.getScaleY();
    double right = rect.getWidth()/2+xoff*toPixels.getScaleX();
    double left = rect.getWidth()/2-xoff*toPixels.getScaleX();
    double bottom = rect.getHeight()/2+yoff*toPixels.getScaleY();
    double top = rect.getHeight()/2-yoff*toPixels.getScaleY();
    hotSpots[CENTER].setLocation(centerX, centerY);                                                                                      // center
    hotSpots[BOTTOM].setLocation(centerX+xoff*toPixels.getScaleX()*cos+bottom*sin/ar, centerY-xoff*toPixels.getScaleX()*sin+bottom*cos); // bottom
    hotSpots[LEFT].setLocation(centerX-left*cos+yoff*toPixels.getScaleY()*sin, centerY+ar*left*sin+yoff*toPixels.getScaleY()*cos); // left
    hotSpots[TOP].setLocation(centerX+xoff*toPixels.getScaleX()*cos-top*sin/ar, centerY-xoff*toPixels.getScaleX()*sin-top*cos); // top
    hotSpots[RIGHT].setLocation(centerX+right*cos+yoff*toPixels.getScaleY()*sin, centerY-ar*right*sin+yoff*toPixels.getScaleY()*cos); // right
    hotSpots[CORNER].setLocation(centerX+right*cos-top*sin/ar, centerY-right*sin*ar-top*cos); // corner
  }

  void computeFixedHotSpots(Rectangle2D rect) {
    double sin = Math.sin(theta);
    double cos = Math.cos(theta);
    double cx = rect.getCenterX()-xoff; // center x
    double cy = rect.getCenterY()+yoff; // center y
    double right = rect.getWidth()/2+xoff;
    double left = xoff-rect.getWidth()/2;
    double bottom = yoff-rect.getHeight()/2;
    double top = rect.getHeight()/2+yoff;
    hotSpots[0].setLocation(cx, cy);                                         // center
    hotSpots[1].setLocation(cx+xoff*cos-bottom*sin, cy-bottom*cos-xoff*sin); // bottom
    hotSpots[2].setLocation(cx+left*cos-yoff*sin, cy-left*sin-yoff*cos);     // left
    hotSpots[3].setLocation(cx+xoff*cos-top*sin, cy-top*cos-xoff*sin);       // top
    hotSpots[4].setLocation(cx+right*cos-yoff*sin, cy-right*sin-yoff*cos);   // right
    hotSpots[5].setLocation(cx+right*cos-top*sin, cy-right*sin-top*cos);     // corner
  }

  /**
   * Sets the x and y coordinates using hotspots.
   *
   * @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==CORNER)) {
      if(pixelSized) {
        double r = -toPixels.getScaleY()/toPixels.getScaleX();
        double dx = x-this.x;
        double dy = y-this.y;
        theta = Math.atan2(r*dy, dx)-Math.atan2(height/2+yoff, (width/2+xoff));
      } else {
        double dx = x-this.x;
        double dy = y-this.y;
        double theta1 = Math.atan2(height/2+yoff, width/2+xoff);
        double theta2 = Math.atan2(dy, dx);
        setTheta(theta2-theta1);
      }
    } else if(widthDrag&&selected&&((hotspot==LEFT)||(hotspot==RIGHT))) {
      if(pixelSized) {
        double dx = toPixels.getScaleX()*(x-this.x)-xoff;
        double dy = toPixels.getScaleY()*(y-this.y)+yoff;
        BoundedShape.this.setWidth(2*Math.sqrt(dx*dx+dy*dy));
      } else {
        double dx = (x-this.x-xoff);
        double dy = (y-this.y-yoff);
        setWidth(2*Math.sqrt(dx*dx+dy*dy));
      }
    } else if(heightDrag&&selected&&((hotspot==TOP)||(hotspot==BOTTOM))) {
      if(pixelSized) {
        double dx = toPixels.getScaleX()*(x-this.x)-xoff;
        double dy = toPixels.getScaleY()*(y-this.y)+yoff;
        BoundedShape.this.setHeight(2*Math.sqrt(dx*dx+dy*dy));
      } else {
        double dx = (x-this.x-xoff);
        double dy = (y-this.y-yoff);
        setHeight(2*Math.sqrt(dx*dx+dy*dy));
      }
    }
  }

  /**
   * Draws the shape.
   *
   * @param panel the drawing panel
   * @param g  the graphics context
   */
  public void draw(DrawingPanel panel, Graphics g) {
    super.draw(panel, g);
    if(pixelSized) {
      drawFixedBounds(panel, g);
    } else {
      drawScaledBounds(panel, g);
    }
  }

  /**
   * Draws the shape.
   *
   * @param panel the drawing panel
   * @param g  the graphics context
   */
  private void drawScaledBounds(DrawingPanel panel, Graphics g) {
    double r = -toPixels.getScaleY()/toPixels.getScaleX();
    if(theta==0) {
      Shape temp = toPixels.createTransformedShape(shape.getBounds2D());
      computeScaledHotSpots(temp.getBounds2D(), r);
      pixelBounds = temp.getBounds2D();
    } else {
      // rotate the shape into standard position to get correct x-y bounds
      Shape temp = AffineTransform.getRotateInstance(-theta, x, y).createTransformedShape(shape);
      // the following alternate should also give the correct bounds in world coordinates
      // Shape temp = new Rectangle2D.Double(x-width/2, y-height/2, width, height);
      temp = toPixels.createTransformedShape(temp);
      computeScaledHotSpots(temp.getBounds2D(), r);
      pixelBounds = temp.getBounds2D();
      if(panel.isSquareAspect()) {
        pixelBounds = AffineTransform.getRotateInstance(-theta, ((Rectangle2D) pixelBounds).getCenterX()-xoff*toPixels.getScaleX(), ((Rectangle2D) pixelBounds).getCenterY()-yoff*toPixels.getScaleY()).createTransformedShape(pixelBounds);
      } else {
        double px = ((Rectangle2D) pixelBounds).getCenterX()-xoff*toPixels.getScaleX();
        double py = ((Rectangle2D) pixelBounds).getCenterY()-yoff*toPixels.getScaleY();
        pixelBounds = AffineTransform.getTranslateInstance(-px, -py).createTransformedShape(pixelBounds);
        pixelBounds = AffineTransform.getScaleInstance(1, 1.0/r).createTransformedShape(pixelBounds);
        pixelBounds = AffineTransform.getRotateInstance(-theta).createTransformedShape(pixelBounds);
        pixelBounds = AffineTransform.getScaleInstance(1, r).createTransformedShape(pixelBounds);
        pixelBounds = AffineTransform.getTranslateInstance(px, py).createTransformedShape(pixelBounds);
      }
    }
    if(!selected||hideBounds) {
      return;
    }
    Graphics2D g2 = ((Graphics2D) g);
    g2.setPaint(boundsColor);
    g2.draw(pixelBounds);
    if(rotateDrag) {
      g2.fillOval((int) hotSpots[CORNER].getX()-delta, (int) hotSpots[CORNER].getY()-delta, d2, d2);
    }
    if(heightDrag) {
      g2.fillRect((int) hotSpots[TOP].getX()-delta, (int) hotSpots[TOP].getY()-delta, d2, d2);
      g2.fillRect((int) hotSpots[BOTTOM].getX()-delta, (int) hotSpots[BOTTOM].getY()-delta, d2, d2);
    }
    if(widthDrag) {
      g2.fillRect((int) hotSpots[LEFT].getX()-delta, (int) hotSpots[LEFT].getY()-delta, d2, d2);
      g2.fillRect((int) hotSpots[RIGHT].getX()-delta, (int) hotSpots[RIGHT].getY()-delta, d2, d2);
    }
    if(xyDrag) {
      g2.fillRect((int) hotSpots[CENTER].getX()-delta, (int) hotSpots[CENTER].getY()-delta, d2, d2);
      g2.setColor(edgeColor);
      g2.fillOval((int) hotSpots[CENTER].getX()-1, (int) hotSpots[CENTER].getY()-1, 3, 3);
      g2.setPaint(boundsColor);
    }
    g.setColor(Color.BLACK);
  }

  /**
   * Draws the shape.
   *
   * @param panel the drawing panel
   * @param g  the graphics context
   */
  private void drawFixedBounds(DrawingPanel panel, Graphics g) {
    if(theta==0) {
      Point2D pt = new Point2D.Double(x, y);
      pt = toPixels.transform(pt, pt);
      Shape temp = AffineTransform.getTranslateInstance(-x+pt.getX()+xoff, -y+pt.getY()-yoff).createTransformedShape(shape.getBounds2D());
      computeFixedHotSpots(temp.getBounds2D());
      pixelBounds = temp.getBounds2D();
    } else {
      // rotate the shape into standard position to get correct x-y bounds
      // Shape temp = AffineTransform.getRotateInstance(-theta, x, y).createTransformedShape(shape);
      Point2D pt = new Point2D.Double(x, y);
      pt = toPixels.transform(pt, pt);
      Shape temp = AffineTransform.getTranslateInstance(-x+pt.getX()+xoff, -y+pt.getY()-yoff).createTransformedShape(shape);
      // temp = AffineTransform.getTranslateInstance(pt.getX(), pt.getY()).createTransformedShape(temp);
      computeFixedHotSpots(temp.getBounds2D());
      pixelBounds = temp.getBounds2D();
      pixelBounds = AffineTransform.getRotateInstance(-theta, pt.getX(), pt.getY()).createTransformedShape(pixelBounds);
    }
    if(!selected||hideBounds) {
      return;
    }
    Graphics2D g2 = ((Graphics2D) g);
    g2.setPaint(boundsColor);
    g2.draw(pixelBounds);
    if(rotateDrag) {
      g2.fillOval((int) hotSpots[CORNER].getX()-delta, (int) hotSpots[CORNER].getY()-delta, d2, d2);
    }
    if(heightDrag) {
      g2.fillRect((int) hotSpots[TOP].getX()-delta, (int) hotSpots[TOP].getY()-delta, d2, d2);
      g2.fillRect((int) hotSpots[BOTTOM].getX()-delta, (int) hotSpots[BOTTOM].getY()-delta, d2, d2);
    }
    if(widthDrag) {
      g2.fillRect((int) hotSpots[LEFT].getX()-delta, (int) hotSpots[LEFT].getY()-delta, d2, d2);
      g2.fillRect((int) hotSpots[RIGHT].getX()-delta, (int) hotSpots[RIGHT].getY()-delta, d2, d2);
    }
    if(xyDrag) {
      g2.fillRect((int) hotSpots[CENTER].getX()-delta, (int) hotSpots[CENTER].getY()-delta, d2, d2);
      g2.setColor(edgeColor);
      g2.fillOval((int) hotSpots[CENTER].getX()-1, (int) hotSpots[CENTER].getY()-1, 3, 3);
      g2.setPaint(boundsColor);
    }
    g.setColor(Color.BLACK);
  }

  /**
   * Gets a description of this object.
   * @return String
   */
  public String toString() {
    return "BoundedShape:"+"\n \t shape="+shapeClass+"\n \t x="+x+"\n \t y="+y+"\n \t width="+width+"\n \t height="+height+"\n \t theta="+theta; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$
  }

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

  /**
   *  A class to save and load BoundedShape in an XMLControl.
   */
  protected static class BoundedShapeLoader extends InteractiveShapeLoader {
    public void saveObject(XMLControl control, Object obj) {
      super.saveObject(control, obj);
      BoundedShape boundedShape = (BoundedShape) obj;
      control.setValue("xy drag", boundedShape.isXYDrag());         //$NON-NLS-1$
      control.setValue("width drag", boundedShape.isWidthDrag());   //$NON-NLS-1$
      control.setValue("height drag", boundedShape.isHeightDrag()); //$NON-NLS-1$
      control.setValue("rotate drag", boundedShape.isRotateDrag()); //$NON-NLS-1$
    }

    public Object createObject(XMLControl control) {
      return new BoundedShape(new Rectangle2D.Double(0, 0, 0, 0), 0, 0); // default shape is a rectangle for now
    }

    public Object loadObject(XMLControl control, Object obj) {
      BoundedShape boundedShape = (BoundedShape) obj;
      boundedShape.setXYDrag(control.getBoolean("xy drag"));         //$NON-NLS-1$
      boundedShape.setWidthDrag(control.getBoolean("width drag"));   //$NON-NLS-1$
      boundedShape.setHeightDrag(control.getBoolean("height drag")); //$NON-NLS-1$
      boundedShape.setRotateDrag(control.getBoolean("rotate drag")); //$NON-NLS-1$
      super.loadObject(control, obj);
      return boundedShape;
    }

  }

  class XYDelegate extends AbstractInteractive implements Selectable {
    public void draw(DrawingPanel panel, Graphics g) {}

    public boolean isInside(DrawingPanel panel, int xpix, int ypix) {
      return BoundedShape.this.isInside(panel, xpix, ypix);
    }

    public void setXY(double x, double y) {
      BoundedShape.this.setHotSpotXY(x, y);
    }

    public void setSelected(boolean selectable) {
      BoundedShape.this.setSelected(selectable);
    }

    public void toggleSelected() {
      BoundedShape.this.toggleSelected();
    }

    public boolean isSelected() {
      return BoundedShape.this.isSelected();
    }

    public Cursor getPreferredCursor() {
      return BoundedShape.this.getPreferredCursor();
    }

  }

}

/*
 * 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
 */
