/*
 * @(#)AttributeMap 1.0 03-JUL-04
 *
 * Copyright (c) 2001-2005 Gaudenz Alder
 *
 * See LICENSE file in distribution for licensing details of this source file
 */
package org.jgraph.graph;

import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * A map specifically for the storage of attributes of graph cells. The main
 * advantage of the AttributeMap is that it allows to override cell view
 * behaviour for scaling, translation, diffing, and cloning on a per instance
 * basis without having to change the GraphConstants class
 */
public class AttributeMap extends Hashtable implements Cloneable {

	/**
	 * Shared empty attribute map to return instead of null in applyMap.
	 */
	public static transient AttributeMap emptyAttributeMap = new AttributeMap(0) {
		public Object clone() {
			return this;
		}
	};

	/**
	 * Creates a new attribute map with an initial capacity of 8.
	 */
	public AttributeMap() {
		super(8);
	}

	/**
	 * Creates a new attribute map with the specified initial capacity
	 * 
	 * @param initialCapacity
	 *            the initial capacity of the new map
	 */
	public AttributeMap(int initialCapacity) {
		super(initialCapacity);
	}

	/**
	 * Constructs a new, empty hashtable with the specified initial capacity and
	 * the specified load factor.
	 * 
	 * @param initialCapacity
	 *            the initial capacity of the hashtable.
	 * @param loadCapacity
	 *            the load factor of the hashtable.
	 */
	public AttributeMap(int initialCapacity, float loadCapacity) {
		super(initialCapacity, loadCapacity);
	}

	/**
	 * Constructs a new AttributeMap with the same mappings as the given Map.
	 * 
	 * @param map
	 *            the input map to copy
	 */
	public AttributeMap(Map map) {
		super(map);
	}

	/**
	 * Creates a point of suitable type for this attribute map
	 * 
	 * @return a new point
	 */
	public Point2D createPoint() {
		return new SerializablePoint2D();
	}

	/**
	 * Creates a point of suitable type for this attribute map with the same
	 * values as the point passed in
	 * 
	 * @param p
	 *            the point whose values the new point are to be based on
	 * @return a new copy of the point passed in
	 */
	public Point2D createPoint(Point2D p) {
		if (p != null) {
			return createPoint(p.getX(), p.getY());
		}
		return null;
	}

	/**
	 * Creates a point of suitable type for this attribute map with the same
	 * values as those passed in
	 * 
	 * @param x
	 *            the x-coordinate position of the new point
	 * @param y
	 *            the y-coordinate position of the new point
	 * @return a new point at the coordinates passed in
	 */
	public Point2D createPoint(double x, double y) {
		return new SerializablePoint2D(x, y);
	}

	/**
	 * Creates a rectangle of suitable type for this attribute map
	 * 
	 * @return a new rectangle
	 */
	public Rectangle2D createRect() {
		return new SerializableRectangle2D();
	}

	/**
	 * Creates a rectangle of suitable type for this attribute map with the same
	 * values as those passed in
	 * 
	 * @param x
	 *            the x-coordinate position of the new rectangle
	 * @param y
	 *            the y-coordinate position of the new rectangle
	 * @param w
	 *            the width of the new rectangle
	 * @param h
	 *            the height of the new rectangle
	 * @return a new rectangle at the coordinates and of the dimensions passed
	 *         in
	 */
	public Rectangle2D createRect(double x, double y, double w, double h) {
		return new SerializableRectangle2D(x, y, w, h);
	}

	/**
	 * Creates a rectangle of suitable type for this attribute map at the
	 * position of the point passed in
	 * 
	 * @param pt
	 *            the position of the new rectangle
	 * @return a new rectangle the specified coordinates of zero size
	 */
	public Rectangle2D createRect(Point2D pt) {
		return createRect(pt, 0);
	}

	/**
	 * Creates a rectangle of suitable type for this attribute map at the
	 * position of the point passed in with lengths <code>size</code>
	 * 
	 * @param pt
	 *            the position of the new rectangle
	 * @param size
	 *            the length of both sides of the rectangle
	 * @return a new rectangle the specified position and dimensions
	 */
	public Rectangle2D createRect(Point2D pt, double size) {
		if (pt != null) {
			return createRect(pt.getX(), pt.getY(), size, size);
		}
		return null;
	}

	/**
	 * Clones the rectangle passed in
	 * 
	 * @param rect
	 *            the rectangle to clone
	 * 
	 * @return a copy of the rectangle passed in
	 */
	public Rectangle2D createRect(Rectangle2D rect) {
		if (rect != null) {
			return createRect(rect.getX(), rect.getY(), rect.getWidth(), rect
					.getHeight());
		}
		return null;
	}

	/**
	 * Creates a rectangle of suitable type for this attribute map
	 * 
	 * @param x
	 *            the x-coordinate position of the new rectangle
	 * @param y
	 *            the y-coordinate position of the new rectangle
	 * @param w
	 *            the width of the new rectangle
	 * @param h
	 *            the height of the new rectangle
	 * @param grow1
	 *            the amount both dimensions are to be increased by and the
	 *            position coorindates of the rectangle are to be decreased by
	 * @param grow2
	 *            the additional amount by which both dimensions are to be
	 *            increased by
	 * @return a new rectangle at the coordinates and of the dimensions passed
	 *         in
	 */
	public Rectangle2D createRect(double x, double y, double w, double h,
			double grow1, double grow2) {
		return createRect(x - grow1, y - grow1, w + grow1 + grow2, h + grow1
				+ grow2);
	}

	/**
	 * Creates a clone of the rectangle passed in and manipulates it by
	 * <code>grow1</code> and <code>grow2</code>
	 * 
	 * @param grow1
	 *            the amount both dimensions are to be increased by and the
	 *            position coorindates of the rectangle are to be decreased by
	 * @param grow2
	 *            the additional amount by which both dimensions are to be
	 *            increased by
	 * @return a new rectangle at the coordinates and of the dimensions passed
	 *         in
	 */
	public Rectangle2D createRect(Rectangle2D rect, double grow1, double grow2) {
		if (rect != null) {
			return createRect(rect.getX(), rect.getY(), rect.getWidth(), rect
					.getHeight(), grow1, grow2);
		}
		return null;
	}

	/**
	 * Apply the <code>change</code> to this views attributes.
	 * <code>change</code> must be a <code>Map</code> previously obtained
	 * from this object.
	 * 
	 * @param change
	 *            the change to apply
	 * @return a map that may be used to undo the change to target.
	 */
	public AttributeMap applyMap(Map change) {
		AttributeMap undo = new AttributeMap();
		if (change != null) {
			// Handle Remove All
			if (GraphConstants.isRemoveAll(change)) {
				undo.putAll(this);
				clear();
			}
			// Handle Remove Individual
			Object[] remove = GraphConstants.getRemoveAttributes(change);
			if (remove != null) {
				// don't store command
				for (int i = 0; i < remove.length; i++) {
					Object oldValue = remove(remove[i]);
					if (oldValue != null)
						undo.put(remove[i], oldValue);
				}
			}
			// Attributes that were empty are added to removeattibutes.
			// Performance and transient memory peak are reduced by lazily
			// instantiating the set.
			Set removeAttributes = null;
			Iterator it = change.entrySet().iterator();
			while (it.hasNext()) {
				Map.Entry entry = (Map.Entry) it.next();
				Object key = entry.getKey();
				if (!key.equals(GraphConstants.REMOVEALL)
						&& !key.equals(GraphConstants.REMOVEATTRIBUTES)
						&& !key.equals(GraphConstants.VALUE)) {
					Object oldValue = applyValue(key, entry.getValue());
					if (oldValue == null) {
						if (removeAttributes == null) {
							removeAttributes = new HashSet();
						}
						removeAttributes.add(key);
					}
					else {
						undo.put(key, oldValue);
					}
				}
			}
			if (removeAttributes != null && !removeAttributes.isEmpty()) {
				GraphConstants.setRemoveAttributes(undo, removeAttributes
						.toArray());
			}
		}
		return undo;
	}

	/**
	 * Apply the <code>key</code> to <code>value</code>
	 * 
	 * @param key
	 *            the map key whose value is to be altered
	 * @param value
	 *            the new value to be applied to the specified key
	 * @return the old value.
	 */
	public Object applyValue(Object key, Object value) {
		// In all other cases we put the new value into the
		// map. If we encounter a list (of points) or rectangle
		// these will be cloned before insertion. Cloning includes
		// replacing the rectangle/points with serializable objects.
		if (value instanceof Rectangle2D)
			value = createRect((Rectangle2D) value);
		if (value instanceof Point2D)
			value = createPoint((Point2D) value);
		if (value instanceof Point2D[])
			value = clonePoints((Point2D[]) value);
		if (value instanceof List) // FIXME: PointList interface?
			value = clonePoints((List) value);
		return put(key, value);
	}

	/**
	 * Returns a list where all instances of PortView are replaced by their
	 * correspnding Point instance.
	 * 
	 * @param points
	 *            the points to be cloned
	 * @return the cloned points
	 */
	public Point2D[] clonePoints(Point2D[] points) {
		List pts = clonePoints(points, true);
		Point2D[] newPoints = new Point2D[pts.size()];
		pts.toArray(newPoints);
		return newPoints;
	}

	/**
	 * Returns a list where all instances of PortView are replaced by their
	 * correspnding Point instance.
	 * 
	 * @param points
	 *            the points to be cloned
	 * @return the cloned points
	 */
	public List clonePoints(List points) {
		return clonePoints(points.toArray(), true);
	}

	/**
	 * Returns a list where all instances of PortView are replaced by their
	 * correspnding Point instance.
	 */
	public List clonePoints(Object[] points, boolean convertPortViews) {
		// TODO: Change the list in-place?
		ArrayList newList = new ArrayList(points.length);
		for (int i = 0; i < points.length; i++) {
			// Clone Point
			Object point = points[i];
			if (point instanceof PortView && convertPortViews)
				point = createPoint(((PortView) point).getLocation());
			else if (point instanceof Point2D)
				point = createPoint((Point2D) point);
			newList.add(point);
		}
		return newList;
	}

	/**
	 * Translates the maps in <code>c</code> using
	 * <code>translate(Map, int, int)</code>.
	 */
	public static void translate(Collection c, double dx, double dy) {
		Iterator it = c.iterator();
		while (it.hasNext()) {
			Object map = it.next();
			if (map instanceof AttributeMap)
				((AttributeMap) map).translate(dx, dy);
		}
	}

	/**
	 * Translates <code>map</code> by the given amount.
	 */
	public void translate(double dx, double dy) {
		// Translate Bounds
		if (GraphConstants.isMoveable(this)) {
			Rectangle2D bounds = GraphConstants.getBounds(this);
			if (bounds != null) {
				int moveableAxis = GraphConstants.getMoveableAxis(this);
				if (moveableAxis == GraphConstants.X_AXIS)
					dy = 0;
				else if (moveableAxis == GraphConstants.Y_AXIS)
					dx = 0;
				bounds.setFrame(bounds.getX() + dx, bounds.getY() + dy, bounds
						.getWidth(), bounds.getHeight());
			}
			// Translate Points
			List points = GraphConstants.getPoints(this);
			if (points != null) {
				for (int i = 0; i < points.size(); i++) {
					Object obj = points.get(i);
					if (obj instanceof Point2D) {
						Point2D pt = (Point2D) obj;
						pt.setLocation(pt.getX() + dx, pt.getY() + dy);
					}
				}
			}
		}
	}

	/**
	 * Scales <code>map</code> by the given amount.
	 */
	public void scale(double sx, double sy, Point2D origin) {
		// Scale Bounds
		Rectangle2D bounds = GraphConstants.getBounds(this);
		if (bounds != null) {
			Point2D p = createPoint(bounds.getX(), bounds.getY());
			Point2D loc = (Point2D) p.clone();
			p.setLocation(origin.getX()
					+ Math.round((p.getX() - origin.getX()) * sx), origin
					.getY()
					+ Math.round((p.getY() - origin.getY()) * sy));
			if (!p.equals(loc)) // Scale Location
				translate(p.getX() - loc.getX(), p.getY() - loc.getY());
			int sizeableAxis = GraphConstants.getSizeableAxis(this);
			if (sizeableAxis == GraphConstants.X_AXIS)
				sy = 1;
			else if (sizeableAxis == GraphConstants.Y_AXIS)
				sx = 1;
			double w = Math.max(1, Math.round(bounds.getWidth() * sx));
			double h = Math.max(1, Math.round(bounds.getHeight() * sy));
			// Scale Bounds
			bounds.setFrame(bounds.getX(), bounds.getY(), w, h);
		}
		// Scale Points
		List points = GraphConstants.getPoints(this);
		if (points != null) {
			Iterator it = points.iterator();
			while (it.hasNext()) {
				Object obj = it.next();
				if (obj instanceof Point2D) {
					// Scale Point
					Point2D loc = (Point2D) obj;
					Point2D p = (Point2D) loc.clone();
					p.setLocation(origin.getX()
							+ Math.round((p.getX() - origin.getX()) * sx),
							origin.getY()
									+ Math.round((p.getY() - origin.getY())
											* sy));
					// Move Point
					loc.setLocation(p);
				}
			}
		}
	}

	/**
	 * Returns a new map that contains all (key, value)-pairs of
	 * <code>newState</code> where either key is not used or value is
	 * different for key in <code>oldState</code>. In other words, this
	 * method removes the common entries from oldState and newState, and returns
	 * the "difference" between the two.
	 * 
	 * This method never returns null.
	 */
	public Map diff(Map newState) {
		Map diff = new Hashtable();
		Iterator it = newState.entrySet().iterator();
		while (it.hasNext()) {
			Map.Entry entry = (Map.Entry) it.next();
			Object key = entry.getKey();
			Object newValue = entry.getValue();
			Object oldValue = get(key);
			if (oldValue == null || !oldValue.equals(newValue))
				diff.put(key, newValue);
		}
		return diff;
	}

	/**
	 * Returns a clone of <code>map</code>, from keys to values. If the map
	 * contains bounds or points, these are cloned as well. References to
	 * <code>PortViews</code> are replaces by points. <br>
	 * <b>Note: </b> Extend this method to clone custom user objects.
	 */
	public Object clone() {
		// TODO, is cloning the hash table excessive?
		return cloneEntries((AttributeMap) super.clone());
	}

	/**
	 * Clones special object entried in the given map.
	 */
	public AttributeMap cloneEntries(AttributeMap newMap) {
		// Clone Bounds
		Rectangle2D bounds = GraphConstants.getBounds(newMap);
		if (bounds != null)
			GraphConstants.setBounds(newMap, (Rectangle2D) (bounds.clone()));
		// Clone List Of Points
		List points = GraphConstants.getPoints(newMap);
		if (points != null)
			GraphConstants.setPoints(newMap, clonePoints(points));
		// Clone extra label positions
		Point2D[] positions = GraphConstants.getExtraLabelPositions(newMap);
		if (positions != null)
			GraphConstants.setExtraLabelPositions(newMap,
					clonePoints(positions));
		// Clone Edge Label
		Point2D label = GraphConstants.getLabelPosition(newMap);
		if (label != null)
			GraphConstants.setLabelPosition(newMap, (Point2D) label.clone());
		return newMap;
	}

	public static class SerializablePoint2D extends Point2D.Double implements
			Serializable {

		public SerializablePoint2D() {
			super();
		}

		public SerializablePoint2D(double x, double y) {
			super(x, y);
		}

		public void setX(double x) {
			setLocation(x, getY());
		}

		public void setY(double y) {
			setLocation(getX(), y);
		}

		private void writeObject(ObjectOutputStream out) throws IOException {
			out.defaultWriteObject();
			out.writeObject(new java.lang.Double(getX()));
			out.writeObject(new java.lang.Double(getY()));
		}

		private void readObject(ObjectInputStream in) throws IOException,
				ClassNotFoundException {
			in.defaultReadObject();
			java.lang.Double x = (java.lang.Double) in.readObject();
			java.lang.Double y = (java.lang.Double) in.readObject();
			setLocation(x.doubleValue(), y.doubleValue());
		}

	}

	public static class SerializableRectangle2D extends Rectangle2D.Double
			implements Serializable {

		public SerializableRectangle2D() {
			super();
		}

		public SerializableRectangle2D(double x, double y, double width,
				double height) {
			super(x, y, width, height);
		}

		public void setX(double x) {
			setFrame(x, getY(), getWidth(), getHeight());
		}

		public void setY(double y) {
			setFrame(getX(), y, getWidth(), getHeight());
		}

		public void setWidth(double width) {
			setFrame(getX(), getY(), width, getHeight());
		}

		public void setHeight(double height) {
			setFrame(getX(), getY(), getWidth(), height);
		}

		private void writeObject(ObjectOutputStream out) throws IOException {
			out.defaultWriteObject();
			out.writeObject(new java.lang.Double(getX()));
			out.writeObject(new java.lang.Double(getY()));
			out.writeObject(new java.lang.Double(getWidth()));
			out.writeObject(new java.lang.Double(getHeight()));
		}

		private void readObject(ObjectInputStream in) throws IOException,
				ClassNotFoundException {
			in.defaultReadObject();
			java.lang.Double x = (java.lang.Double) in.readObject();
			java.lang.Double y = (java.lang.Double) in.readObject();
			java.lang.Double width = (java.lang.Double) in.readObject();
			java.lang.Double height = (java.lang.Double) in.readObject();
			setFrame(x.doubleValue(), y.doubleValue(), width.doubleValue(),
					height.doubleValue());
		}
	}
}