/*
 * DimensionLine.java 17 sept 2007
 *
 * Sweet Home 3D, Copyright (c) 2007 Emmanuel PUYBARET / eTeks <info@eteks.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package com.eteks.sweethome3d.model;

import java.awt.Shape;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.io.ObjectInputStream;

/**
 * A dimension line in plan.
 * @author Emmanuel Puybaret
 */
public class DimensionLine extends HomeObject implements Selectable, Elevatable {
  /**
   * The properties of a dimension line that may change. <code>PropertyChangeListener</code>s added
   * to a dimension line will be notified under a property name equal to the string value of one these properties.
   */
  public enum Property {X_START, Y_START, X_END, Y_END, OFFSET, LENGTH_STYLE, LEVEL}

  private static final long serialVersionUID = 1L;

  private float               xStart;
  private float               yStart;
  private float               xEnd;
  private float               yEnd;
  private float               offset;
  private TextStyle           lengthStyle;
  private Level               level;

  private transient PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
  private transient Shape shapeCache;

  /**
   * Creates a dimension line from (<code>xStart</code>,<code>yStart</code>)
   * to (<code>xEnd</code>, <code>yEnd</code>), with a given offset.
   */
  public DimensionLine(float xStart, float yStart, float xEnd, float yEnd, float offset) {
    this.xStart = xStart;
    this.yStart = yStart;
    this.xEnd = xEnd;
    this.yEnd = yEnd;
    this.offset = offset;
  }

  /**
   * Initializes new dimension line transient fields
   * and reads its properties from <code>in</code> stream with default reading method.
   */
  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    this.propertyChangeSupport = new PropertyChangeSupport(this);
    in.defaultReadObject();
  }

  /**
   * Adds the property change <code>listener</code> in parameter to this dimension line.
   */
  public void addPropertyChangeListener(PropertyChangeListener listener) {
    this.propertyChangeSupport.addPropertyChangeListener(listener);
  }

  /**
   * Removes the property change <code>listener</code> in parameter from this dimension line.
   */
  public void removePropertyChangeListener(PropertyChangeListener listener) {
    this.propertyChangeSupport.removePropertyChangeListener(listener);
  }

  /**
   * Returns the start point abscissa of this dimension line.
   */
  public float getXStart() {
    return this.xStart;
  }

  /**
   * Sets the start point abscissa of this dimension line. Once this dimension line
   * is updated, listeners added to this dimension line will receive a change notification.
   */
  public void setXStart(float xStart) {
    if (xStart != this.xStart) {
      float oldXStart = this.xStart;
      this.xStart = xStart;
      this.shapeCache = null;
      this.propertyChangeSupport.firePropertyChange(Property.X_START.name(), oldXStart, xStart);
    }
  }

  /**
   * Returns the start point ordinate of this dimension line.
   */
  public float getYStart() {
    return this.yStart;
  }

  /**
   * Sets the start point ordinate of this dimension line. Once this dimension line
   * is updated, listeners added to this dimension line will receive a change notification.
   */
  public void setYStart(float yStart) {
    if (yStart != this.yStart) {
      float oldYStart = this.yStart;
      this.yStart = yStart;
      this.shapeCache = null;
      this.propertyChangeSupport.firePropertyChange(Property.Y_START.name(), oldYStart, yStart);
    }
  }

  /**
   * Returns the end point abscissa of this dimension line.
   */
  public float getXEnd() {
    return this.xEnd;
  }

  /**
   * Sets the end point abscissa of this dimension line. Once this dimension line
   * is updated, listeners added to this dimension line will receive a change notification.
   */
  public void setXEnd(float xEnd) {
    if (xEnd != this.xEnd) {
      float oldXEnd = this.xEnd;
      this.xEnd = xEnd;
      this.shapeCache = null;
      this.propertyChangeSupport.firePropertyChange(Property.X_END.name(), oldXEnd, xEnd);
    }
  }

  /**
   * Returns the end point ordinate of this dimension line.
   */
  public float getYEnd() {
    return this.yEnd;
  }

  /**
   * Sets the end point ordinate of this dimension line. Once this dimension line
   * is updated, listeners added to this dimension line will receive a change notification.
   */
  public void setYEnd(float yEnd) {
    if (yEnd != this.yEnd) {
      float oldYEnd = this.yEnd;
      this.yEnd = yEnd;
      this.shapeCache = null;
      this.propertyChangeSupport.firePropertyChange(Property.Y_END.name(), oldYEnd, yEnd);
    }
  }

  /**
   * Returns the offset of this dimension line.
   */
  public float getOffset() {
    return this.offset;
  }

  /**
   * Sets the offset of this dimension line.  Once this dimension line
   * is updated, listeners added to this dimension line will receive a change notification.
   */
  public void setOffset(float offset) {
    if (offset != this.offset) {
      float oldOffset = this.offset;
      this.offset = offset;
      this.shapeCache = null;
      this.propertyChangeSupport.firePropertyChange(Property.OFFSET.name(), oldOffset, offset);
    }
  }

  /**
   * Returns the length of this dimension line.
   */
  public float getLength() {
    return (float)Point2D.distance(getXStart(), getYStart(), getXEnd(), getYEnd());
  }

  /**
   * Returns the text style used to display dimension line length.
   */
  public TextStyle getLengthStyle() {
    return this.lengthStyle;
  }

  /**
   * Sets the text style used to display dimension line length.
   * Once this dimension line is updated, listeners added to it will receive a change notification.
   */
  public void setLengthStyle(TextStyle lengthStyle) {
    if (lengthStyle != this.lengthStyle) {
      TextStyle oldLengthStyle = this.lengthStyle;
      this.lengthStyle = lengthStyle;
      this.propertyChangeSupport.firePropertyChange(Property.LENGTH_STYLE.name(), oldLengthStyle, lengthStyle);
    }
  }

  /**
   * Returns the level which this dimension line belongs to.
   * @since 3.4
   */
  public Level getLevel() {
    return this.level;
  }

  /**
   * Sets the level of this dimension line. Once this dimension line is updated,
   * listeners added to this dimension line will receive a change notification.
   * @since 3.4
   */
  public void setLevel(Level level) {
    if (level != this.level) {
      Level oldLevel = this.level;
      this.level = level;
      this.propertyChangeSupport.firePropertyChange(Property.LEVEL.name(), oldLevel, level);
    }
  }

  /**
   * Returns <code>true</code> if this dimension line is at the given <code>level</code>
   * or at a level with the same elevation and a smaller elevation index.
   * @since 3.4
   */
  public boolean isAtLevel(Level level) {
    return this.level == level
        || this.level != null && level != null
           && this.level.getElevation() == level.getElevation()
           && this.level.getElevationIndex() < level.getElevationIndex();

  }

  /**
   * Returns the points of the rectangle surrounding
   * this dimension line and its extension lines.
   * @return an array of the 4 (x,y) coordinates of the rectangle.
   */
  public float [][] getPoints() {
    double angle = Math.atan2(this.yEnd - this.yStart, this.xEnd - this.xStart);
    float dx = (float)-Math.sin(angle) * this.offset;
    float dy = (float)Math.cos(angle) * this.offset;

    return new float [] [] {{this.xStart, this.yStart},
                            {this.xStart + dx, this.yStart + dy},
                            {this.xEnd + dx, this.yEnd + dy},
                            {this.xEnd, this.yEnd}};
  }

  /**
   * Returns <code>true</code> if this dimension line intersects
   * with the horizontal rectangle which opposite corners are at points
   * (<code>x0</code>, <code>y0</code>) and (<code>x1</code>, <code>y1</code>).
   */
  public boolean intersectsRectangle(float x0, float y0, float x1, float y1) {
    Rectangle2D rectangle = new Rectangle2D.Float(x0, y0, 0, 0);
    rectangle.add(x1, y1);
    return getShape().intersects(rectangle);
  }

  /**
   * Returns <code>true</code> if this dimension line contains
   * the point at (<code>x</code>, <code>y</code>)
   * with a given <code>margin</code>.
   */
  public boolean containsPoint(float x, float y, float margin) {
    return containsShapeAtWithMargin(getShape(), x, y, margin);
  }

  /**
   * Returns <code>true</code> if the middle point of this dimension line
   * is the point at (<code>x</code>, <code>y</code>)
   * with a given <code>margin</code>.
   */
  public boolean isMiddlePointAt(float x, float y, float margin) {
    double angle = Math.atan2(this.yEnd - this.yStart, this.xEnd - this.xStart);
    float dx = (float)-Math.sin(angle) * this.offset;
    float dy = (float)Math.cos(angle) * this.offset;
    float xMiddle = (this.xStart + this.xEnd) / 2 + dx;
    float yMiddle = (this.yStart + this.yEnd) / 2 + dy;
    return Math.abs(x - xMiddle) <= margin && Math.abs(y - yMiddle) <= margin;
  }

  /**
   * Returns <code>true</code> if the extension line at the start of this dimension line
   * contains the point at (<code>x</code>, <code>y</code>)
   * with a given <code>margin</code> around the extension line.
   */
  public boolean containsStartExtensionLinetAt(float x, float y, float margin) {
    double angle = Math.atan2(this.yEnd - this.yStart, this.xEnd - this.xStart);
    Line2D startExtensionLine = new Line2D.Float(this.xStart, this.yStart,
        this.xStart + (float)-Math.sin(angle) * this.offset,
        this.yStart + (float)Math.cos(angle) * this.offset);
    return containsShapeAtWithMargin(startExtensionLine, x, y, margin);
  }

  /**
   * Returns <code>true</code> if the extension line at the end of this dimension line
   * contains the point at (<code>x</code>, <code>y</code>)
   * with a given <code>margin</code> around the extension line.
   */
  public boolean containsEndExtensionLineAt(float x, float y, float margin) {
    double angle = Math.atan2(this.yEnd - this.yStart, this.xEnd - this.xStart);
    Line2D endExtensionLine = new Line2D.Float(this.xEnd, this.yEnd,
        this.xEnd + (float)-Math.sin(angle) * this.offset,
        this.yEnd + (float)Math.cos(angle) * this.offset);
    return containsShapeAtWithMargin(endExtensionLine, x, y, margin);
  }

  /**
   * Returns <code>true</code> if <code>shape</code> contains
   * the point at (<code>x</code>, <code>y</code>)
   * with a given <code>margin</code>.
   */
  private boolean containsShapeAtWithMargin(Shape shape, float x, float y, float margin) {
    if (margin == 0) {
      return shape.contains(x, y);
    } else {
      return shape.intersects(x - margin, y - margin, 2 * margin, 2 * margin);
    }
  }

  /**
   * Returns the shape matching this dimension line.
   */
  private Shape getShape() {
    if (this.shapeCache == null) {
      // Create the rectangle that matches piece bounds
      double angle = Math.atan2(this.yEnd - this.yStart, this.xEnd - this.xStart);
      float dx = (float)-Math.sin(angle) * this.offset;
      float dy = (float)Math.cos(angle) * this.offset;

      GeneralPath dimensionLineShape = new GeneralPath();
      // Append dimension line
      dimensionLineShape.append(new Line2D.Float(this.xStart + dx, this.yStart + dy, this.xEnd + dx, this.yEnd + dy), false);
      // Append extension lines
      dimensionLineShape.append(new Line2D.Float(this.xStart, this.yStart, this.xStart + dx, this.yStart + dy), false);
      dimensionLineShape.append(new Line2D.Float(this.xEnd, this.yEnd, this.xEnd + dx, this.yEnd + dy), false);
      // Cache shape
      this.shapeCache = dimensionLineShape;
    }
    return this.shapeCache;
  }

  /**
   * Moves this dimension line of (<code>dx</code>, <code>dy</code>) units.
   */
  public void move(float dx, float dy) {
    setXStart(getXStart() + dx);
    setYStart(getYStart() + dy);
    setXEnd(getXEnd() + dx);
    setYEnd(getYEnd() + dy);
  }

  /**
   * Returns a clone of this dimension line.
   */
  @Override
  public DimensionLine clone() {
    DimensionLine clone = (DimensionLine)super.clone();
    clone.propertyChangeSupport = new PropertyChangeSupport(clone);
    clone.level = null;
    return clone;
  }
}
