/*
 * @(#)PolyLineFigure.java
 *
 * Project:		JHotdraw - a GUI framework for technical drawings
 *				http://www.jhotdraw.org
 *				http://jhotdraw.sourceforge.net
 * Copyright:	© by the original author(s) and all contributors
 * License:		Lesser GNU Public License (LGPL)
 *				http://www.opensource.org/licenses/lgpl-license.html
 */

package org.jhotdraw.figures;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.Iterator;
import java.util.List;

import org.jhotdraw.framework.Connector;
import org.jhotdraw.framework.FigureAttributeConstant;
import org.jhotdraw.framework.HandleEnumeration;
import org.jhotdraw.framework.Locator;
import org.jhotdraw.standard.AbstractFigure;
import org.jhotdraw.standard.HandleEnumerator;
import org.jhotdraw.util.CollectionsFactory;
import org.jhotdraw.util.Geom;

/**
 * A poly line figure consists of a list of points.
 * It has an optional line decoration at the start and end.
 *
 * @see LineDecoration
 *
 * @version <$CURRENT_VERSION$>
 *
 * @AJHD refactored - @author Marius Marin
 */
public  class PolyLineFigure extends AbstractFigure {

	public final static int ARROW_TIP_NONE  = 0;
	public final static int ARROW_TIP_START = 1;
	public final static int ARROW_TIP_END   = 2;
	public final static int ARROW_TIP_BOTH  = 3;

	protected List                fPoints;
	protected LineDecoration      fStartDecoration = null;
	protected LineDecoration      fEndDecoration = null;
	protected Color               fFrameColor = Color.black;

	/*
	 * Serialization support.
	 */
	private static final long serialVersionUID = -7951352179906577773L;
	private int polyLineFigureSerializedDataVersion = 1;

	public PolyLineFigure() {
		this(4);
	}

	public PolyLineFigure(int size) {
		fPoints = CollectionsFactory.current().createList(size);
	}

	public PolyLineFigure(int x, int y) {
		fPoints = CollectionsFactory.current().createList();
		fPoints.add(new Point(x, y));
	}

	public Rectangle displayBox() {
		Iterator iter = points();
		if (iter.hasNext()) {
			// avoid starting with origin 0,0 because that would lead to a too large rectangle
			Rectangle r = new Rectangle((Point)iter.next());

			while (iter.hasNext()) {
				r.add((Point)iter.next());
			}

			return r;
		}
		else {
			return new Rectangle();
		}
	}

	public boolean isEmpty() {
		return (size().width < 3) && (size().height < 3);
	}

	public HandleEnumeration handles() {
		List handles = CollectionsFactory.current().createList(fPoints.size());
		for (int i = 0; i < fPoints.size(); i++) {
			handles.add(new PolyLineHandle(this, locator(i), i));
		}
		return new HandleEnumerator(handles);
	}

	public void basicDisplayBox(Point origin, Point corner) {
	}

	/**
	 * Adds a node to the list of points.
	 */
	public void addPoint(int x, int y) {
		fPoints.add(new Point(x, y));
		changed();
	}

	public Iterator points() {
		return fPoints.iterator();
	}

	public int pointCount() {
		return fPoints.size();
	}

	protected void basicMoveBy(int dx, int dy) {
		Iterator iter = points();
		while (iter.hasNext()) {
			((Point)iter.next()).translate(dx, dy);
		}
	}

	/**
	 * Changes the position of a node.
	 */
	public void setPointAt(Point p, int i) {
		willChange();
		fPoints.set(i, p);
		changed();
	}

	/**
	 * Insert a node at the given point.
	 */
	public void insertPointAt(Point p, int i) {
		fPoints.add(i, p);
		changed();
	}

	public void removePointAt(int i) {
		willChange();
		fPoints.remove(i);
		changed();
	}

	/**
	 * Splits the segment at the given point if a segment was hit.
	 * @return the index of the segment or -1 if no segment was hit.
	 */
	public int splitSegment(int x, int y) {
		int i = findSegment(x, y);
		if (i != -1) {
			insertPointAt(new Point(x, y), i+1);
		}
		return i+1;
	}

	public Point pointAt(int i) {
		return (Point)fPoints.get(i);
	}

	/**
	 * Joins to segments into one if the given point hits a node
	 * of the polyline.
	 * @return true if the two segments were joined.
	 */
	public boolean joinSegments(int x, int y) {
		for (int i= 1; i < fPoints.size()-1; i++) {
			Point p = pointAt(i);
			if (Geom.length(x, y, p.x, p.y) < 3) {
				removePointAt(i);
				return true;
			}
		}
		return false;
	}

	public Connector connectorAt(int x, int y) {
		return new PolyLineConnector(this);
	}

	/**
	 * Sets the start decoration.
	 */
	public void setStartDecoration(LineDecoration l) {
		fStartDecoration = l;
	}

	/**
	 * Returns the start decoration.
	 */
	public LineDecoration getStartDecoration() {
		return fStartDecoration;
	}

	/**
	 * Sets the end decoration.
	 */
	public void setEndDecoration(LineDecoration l) {
		fEndDecoration = l;
	}

	/**
	 * Returns the end decoration.
	 */
	public LineDecoration getEndDecoration() {
		return fEndDecoration;
	}

	public void draw(Graphics g) {
		g.setColor(getFrameColor());
		Point p1, p2;
		for (int i = 0; i < fPoints.size()-1; i++) {
			p1 = pointAt(i);
			p2 = pointAt(i+1);
			drawLine(g, p1.x, p1.y, p2.x, p2.y);
		}
		decorate(g);
	}

	/**
	 * Can be overriden in subclasses to draw different types of lines
	 * (e.g. dotted lines)
	 */
	protected void drawLine(Graphics g, int x1, int y1, int x2, int y2) {
		g.drawLine(x1, y1, x2, y2);
	}

	public boolean containsPoint(int x, int y) {
		Rectangle bounds = displayBox();
		bounds.grow(4,4);
		if (!bounds.contains(x, y)) {
			return false;
		}

		for (int i = 0; i < fPoints.size()-1; i++) {
			Point p1 = pointAt(i);
			Point p2 = pointAt(i+1);
			if (Geom.lineContainsPoint(p1.x, p1.y, p2.x, p2.y, x, y)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Gets the segment of the polyline that is hit by
	 * the given point.
	 * @return the index of the segment or -1 if no segment was hit.
	 */
	public int findSegment(int x, int y) {
		for (int i = 0; i < fPoints.size()-1; i++) {
			Point p1 = pointAt(i);
			Point p2 = pointAt(i+1);
			if (Geom.lineContainsPoint(p1.x, p1.y, p2.x, p2.y, x, y)) {
				return i;
			}
		}
		return -1;
	}

	private void decorate(Graphics g) {
		if (getStartDecoration() != null) {
			Point p1 = pointAt(0);
			Point p2 = pointAt(1);
			getStartDecoration().draw(g, p1.x, p1.y, p2.x, p2.y);
		}
		if (getEndDecoration() != null) {
			Point p3 = pointAt(fPoints.size()-2);
			Point p4 = pointAt(fPoints.size()-1);
			getEndDecoration().draw(g, p4.x, p4.y, p3.x, p3.y);
		}
	}

	/**
	 * Gets the attribute with the given name.
	 * PolyLineFigure maps "ArrowMode"to a
	 * line decoration.
	 *
	 * @deprecated use getAttribute(FigureAttributeConstant) instead
	 */
	public Object getAttribute(String name) {
		return getAttribute(FigureAttributeConstant.getConstant(name));
	}

	/**
	 * Gets the attribute with the given name.
	 * PolyLineFigure maps "ArrowMode"to a
	 * line decoration.
	 */
	public Object getAttribute(FigureAttributeConstant attributeConstant) {
		if (attributeConstant.equals(FigureAttributeConstant.FRAME_COLOR)) {
			return getFrameColor();
		}
		else if (attributeConstant.equals(FigureAttributeConstant.ARROW_MODE)) {
			int value = 0;
			if (getStartDecoration() != null) {
				value |= ARROW_TIP_START;
			}
			if (getEndDecoration() != null) {
				value |= ARROW_TIP_END;
			}
			return new Integer(value);
		}
		return super.getAttribute(attributeConstant);
	}

	/**
	 * Sets the attribute with the given name.
	 * PolyLineFigure interprets "ArrowMode"to set
	 * the line decoration.
	 *
	 * @deprecated use setAttribute(FigureAttributeConstant, Object) instead
	 */
	public void setAttribute(String name, Object value) {
		setAttribute(FigureAttributeConstant.getConstant(name), value);
	}

	/**
	 * Sets the attribute with the given name.
	 * PolyLineFigure interprets "ArrowMode"to set
	 * the line decoration.
	 */
	public void setAttribute(FigureAttributeConstant attributeConstant, Object value) {
		if (attributeConstant.equals(FigureAttributeConstant.FRAME_COLOR)) {
			setFrameColor((Color)value);
			changed();
		}
		else if (attributeConstant.equals(FigureAttributeConstant.ARROW_MODE)) {
			Integer intObj = (Integer)value;
			if (intObj != null) {
				int decoration = intObj.intValue();
				if ((decoration & ARROW_TIP_START) != 0) {
					setStartDecoration(new ArrowTip());
				}
				else {
					setStartDecoration(null);
				}
				if ((decoration & ARROW_TIP_END) != 0) {
					setEndDecoration(new ArrowTip());
				}
				else {
					setEndDecoration(null);
				}
			}
			changed();
		}
		else {
			super.setAttribute(attributeConstant, value);
		}
	}

//	AJHD: refactored persistence - the persistence-specific methods,
//	write/read, are introduced via inter-type declarations from the
//	persistence.PersistentAttributeFigure aspect.
//	public void write(StorableOutput dw) {
//		super.write(dw);
//		dw.writeInt(fPoints.size());
//		Iterator iter = points();
//		while (iter.hasNext()) {
//			Point p = (Point)iter.next();
//			dw.writeInt(p.x);
//			dw.writeInt(p.y);
//		}
//		dw.writeStorable(fStartDecoration);
//		dw.writeStorable(fEndDecoration);
//		dw.writeColor(fFrameColor);
//	}
//
//	public void read(StorableInput dr) /*@AJHD refactored throws IOException*/ {
//		super.read(dr);
//		int size = dr.readInt();
//		fPoints = CollectionsFactory.current().createList(size);
//		for (int i=0; i<size; i++) {
//			int x = dr.readInt();
//			int y = dr.readInt();
//			fPoints.add(new Point(x,y));
//		}
//		setStartDecoration((LineDecoration)dr.readStorable());
//		setEndDecoration((LineDecoration)dr.readStorable());
//		fFrameColor = dr.readColor();
//	}

	/**
	 * Creates a locator for the point with the given index.
	 */
	public static Locator locator(int pointIndex) {
		return new PolyLineLocator(pointIndex);
	}

	protected Color getFrameColor() {
		return fFrameColor;
	}

	protected void setFrameColor(Color c) {
		fFrameColor = c;
	}

	/**
	 * Hook method to change the rectangle that will be invalidated
	 */
	protected Rectangle invalidateRectangle(Rectangle r) {
		// SF-bug id: 533953: provide this method to customize invalidated rectangle
		Rectangle parentR = super.invalidateRectangle(r);
		if (getStartDecoration() != null) {
			parentR.add(getStartDecoration().displayBox());
		}
		if (getEndDecoration() != null) {
			parentR.add(getEndDecoration().displayBox());
		}
		return parentR;
	}
}
