/*
 * @(#)Map.java	1.6 01/12/03
 *
 * Copyright 2002 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
package javax.swing.text.html;

import java.awt.Polygon;
import java.awt.Rectangle;
import java.util.StringTokenizer;
import java.util.Vector;
import javax.swing.text.AttributeSet;

/**
 * Map is used to represent a map element that is part of an HTML document.
 * Once a Map has been created, and any number of areas have been added,
 * you can test if a point falls inside the map via the contains method.
 *
 * @author  Scott Violet
 * @version 1.6 12/03/01
 */
class Map {
    /** Name of the Map. */
    private String           name;
    /** An array of AttributeSets. */
    private Vector           areaAttributes;
    /** An array of RegionContainments, will slowly grow to match the
     * length of areaAttributes as needed. */
    private Vector           areas;

    public Map() {
    }

    public Map(String name) {
	this.name = name;
    }

    /**
     * Returns the name of the Map.
     */
    public String getName() {
	return name;
    }

    /**
     * Defines a region of the Map, based on the passed in AttributeSet.
     */
    public void addArea(AttributeSet as) {
	if (as == null) {
	    return;
	}
	if (areaAttributes == null) {
	    areaAttributes = new Vector(2);
	}
	areaAttributes.addElement(as.copyAttributes());
    }

    /**
     * Removes the previously created area.
     */
    public void removeArea(AttributeSet as) {
	if (as != null && areaAttributes != null) {
	    int numAreas = (areas != null) ? areas.size() : 0;
	    for (int counter = areaAttributes.size() - 1; counter >= 0;
		 counter--) {
		if (((AttributeSet)areaAttributes.elementAt(counter)).
		    isEqual(as)){
		    areaAttributes.removeElementAt(counter);
		    if (counter < numAreas) {
			areas.removeElementAt(counter);
		    }
		}
	    }
	}
    }

    /**
     * Returns the AttributeSets representing the differet areas of the Map.
     */
    public AttributeSet[] getAreas() {
	int numAttributes = (areaAttributes != null) ? areaAttributes.size() :
	                    0;
	if (numAttributes != 0) {
	    AttributeSet[]    retValue = new AttributeSet[numAttributes];

	    areaAttributes.copyInto(retValue);
	    return retValue;
	}
	return null;
    }

    /**
     * Returns the AttributeSet that contains the passed in location,
     * <code>x</code>, <code>y</code>. <code>width</code>, <code>height</code>
     * gives the size of the region the map is defined over. If a matching
     * area is found, the AttribueSet for it is returned.
     */
    public AttributeSet getArea(int x, int y, int width, int height) {
	int      numAttributes = (areaAttributes != null) ?
	                         areaAttributes.size() : 0;

	if (numAttributes > 0) {
	    int      numAreas = (areas != null) ? areas.size() : 0;

	    if (areas == null) {
		areas = new Vector(numAttributes);
	    }
	    for (int counter = 0; counter < numAttributes; counter++) {
		if (counter >= numAreas) {
		    areas.addElement(createRegionContainment
			    ((AttributeSet)areaAttributes.elementAt(counter)));
		}
		RegionContainment       rc = (RegionContainment)areas.
                                             elementAt(counter);
		if (rc != null && rc.contains(x, y, width, height)) {
		    return (AttributeSet)areaAttributes.elementAt(counter);
		}
	    }
	}
	return null;
    }

    /**
     * Creates and returns an instance of RegionContainment that can be
     * used to test if a particular point lies inside a region.
     */
    protected RegionContainment createRegionContainment
	                          (AttributeSet attributes) {
	Object     shape = attributes.getAttribute(HTML.Attribute.SHAPE);

	if (shape == null) {
	    shape = "rect";
	}
	if (shape instanceof String) {
	    String                shapeString = ((String)shape).toLowerCase();
	    RegionContainment     rc = null;

	    try {
		if (shapeString.equals("rect")) {
		    rc = new RectangleRegionContainment(attributes);
		}
		else if (shapeString.equals("circle")) {
		    rc = new CircleRegionContainment(attributes);
		}
		else if (shapeString.equals("poly")) {
		    rc = new PolygonRegionContainment(attributes);
		}
		else if (shapeString.equals("default")) {
		    rc = DefaultRegionContainment.sharedInstance();
		}
	    } catch (RuntimeException re) {
		// Something wrong with attributes.
		rc = null;
	    }
	    return rc;
	}
	return null;
    }

    /**
     * Creates and returns an array of integers from the String
     * <code>stringCoords</code>. If one of the values represents a
     * % the returned value with be negative. If a parse error results
     * from trying to parse one of the numbers null is returned.
     */
    static protected int[] extractCoords(Object stringCoords) {
	if (stringCoords == null || !(stringCoords instanceof String)) {
	    return null;
	}

	StringTokenizer    st = new StringTokenizer((String)stringCoords,
						    ", \t\n\r");
	int[]              retValue = null;
	int                numCoords = 0;

	while(st.hasMoreElements()) {
	    String         token = st.nextToken();
	    int            scale;

	    if (token.endsWith("%")) {
		scale = -1;
		token = token.substring(0, token.length() - 1);
	    }
	    else {
		scale = 1;
	    }
	    try {
		int       intValue = Integer.parseInt(token);

		if (retValue == null) {
		    retValue = new int[4];
		}
		else if(numCoords == retValue.length) {
		    int[]    temp = new int[retValue.length * 2];

		    System.arraycopy(retValue, 0, temp, 0, retValue.length);
		    retValue = temp;
		}
		retValue[numCoords++] = intValue * scale;
	    } catch (NumberFormatException nfe) {
		return null;
	    }
	}
	if (numCoords > 0 && numCoords != retValue.length) {
	    int[]    temp = new int[numCoords];

	    System.arraycopy(retValue, 0, temp, 0, numCoords);
	    retValue = temp;
	}
	return retValue;
    }


    /**
     * Defines the interface used for to check if a point is inside a
     * region.
     */
    interface RegionContainment {
	/**
	 * Returns true if the location <code>x</code>, <code>y</code>
	 * falls inside the region defined in the receiver.
	 * <code>width</code>, <code>height</code> is the size of
	 * the enclosing region.
	 */
	public boolean contains(int x, int y, int width, int height);
    }


    /**
     * Used to test for containment in a rectangular region.
     */
    static class RectangleRegionContainment implements RegionContainment {
	/** Will be non-null if one of the values is a percent, and any value
	 * that is non null indicates it is a percent
	 * (order is x, y, width, height). */
	float[]       percents;
	/** Last value of width passed in. */
	int           lastWidth;
	/** Last value of height passed in. */
	int           lastHeight;
        /** Top left. */
        int           x0;
        int           y0;
        /** Bottom right. */
        int           x1;
        int           y1;

	public RectangleRegionContainment(AttributeSet as) {
	    int[]    coords = Map.extractCoords(as.getAttribute(HTML.
							   Attribute.COORDS));

	    percents = null;
	    if (coords == null || coords.length != 4) {
		throw new RuntimeException("Unable to parse rectangular area");
	    }
	    else {
		x0 = coords[0];
		y0 = coords[1];
		x1 = coords[2];
		y1 = coords[3];
		if (x0 < 0 || y0 < 0 || x1 < 0 || y1 < 0) {
		    percents = new float[4];
		    lastWidth = lastHeight = -1;
		    for (int counter = 0; counter < 4; counter++) {
			if (coords[counter] < 0) {
			    percents[counter] = Math.abs
				        (coords[counter]) / 100.0f;
			}
			else {
			    percents[counter] = -1.0f;
			}
		    }
		}
	    }
	}

	public boolean contains(int x, int y, int width, int height) {
	    if (percents == null) {
		return contains(x, y);
	    }
	    if (lastWidth != width || lastHeight != height) {
		lastWidth = width;
		lastHeight = height;
		if (percents[0] != -1.0f) {
		    x0 = (int)(percents[0] * width);
		}
		if (percents[1] != -1.0f) {
		    y0 = (int)(percents[1] * height);
		}
		if (percents[2] != -1.0f) {
		    x1 = (int)(percents[2] * width);
		}
		if (percents[3] != -1.0f) {
		    y1 = (int)(percents[3] * height);
		}
	    }
	    return contains(x, y);
	}

        public boolean contains(int x, int y) {
            return ((x >= x0 && x <= x1) &&
                    (y >= y0 && y <= y1));
        }
    }


    /**
     * Used to test for containment in a polygon region.
     */
    static class PolygonRegionContainment extends Polygon implements
	         RegionContainment {
	/** If any value is a percent there will be an entry here for the
	 * percent value. Use percentIndex to find out the index for it. */
	float[]           percentValues;
	int[]             percentIndexs;
	/** Last value of width passed in. */
	int               lastWidth;
	/** Last value of height passed in. */
	int               lastHeight;

	public PolygonRegionContainment(AttributeSet as) {
	    int[]    coords = Map.extractCoords(as.getAttribute(HTML.Attribute.
								COORDS));

	    if (coords == null || coords.length == 0 ||
		coords.length % 2 != 0) {
		throw new RuntimeException("Unable to parse polygon area");
	    }
	    else {
		int        numPercents = 0;

		lastWidth = lastHeight = -1;
		for (int counter = coords.length - 1; counter >= 0;
		     counter--) {
		    if (coords[counter] < 0) {
			numPercents++;
		    }
		}

		if (numPercents > 0) {
		    percentIndexs = new int[numPercents];
		    percentValues = new float[numPercents];
		    for (int counter = coords.length - 1, pCounter = 0;
			 counter >= 0; counter--) {
			if (coords[counter] < 0) {
			    percentValues[pCounter] = coords[counter] /
				                      -100.0f;
			    percentIndexs[pCounter] = counter;
			    pCounter++;
			}
		    }
		}
		else {
		    percentIndexs = null;
		    percentValues = null;
		}
		npoints = coords.length / 2;
		xpoints = new int[npoints];
		ypoints = new int[npoints];
		
		for (int counter = 0; counter < npoints; counter++) {
		    xpoints[counter] = coords[counter + counter];
		    ypoints[counter] = coords[counter + counter + 1];
		}
	    }
	}

	public boolean contains(int x, int y, int width, int height) {
	    if (percentValues == null || (lastWidth == width &&
					  lastHeight == height)) {
		return contains(x, y);
	    }
	    // Force the bounding box to be recalced.
	    bounds = null;
	    lastWidth = width;
	    lastHeight = height;
	    float fWidth = (float)width;
	    float fHeight = (float)height;
	    for (int counter = percentValues.length - 1; counter >= 0;
		 counter--) {
		if (percentIndexs[counter] % 2 == 0) {
		    // x
		    xpoints[percentIndexs[counter] / 2] = 
			    (int)(percentValues[counter] * fWidth);
		}
		else {
		    // y
		    ypoints[percentIndexs[counter] / 2] = 
			    (int)(percentValues[counter] * fHeight);
		}
	    }
	    return contains(x, y);
	}
    }


    /**
     * Used to test for containment in a circular region.
     */
    static class CircleRegionContainment implements RegionContainment {
	/** X origin of the circle. */
	int           x;
	/** Y origin of the circle. */
	int           y;
	/** Radius of the circle. */
	int           radiusSquared;
	/** Non-null indicates one of the values represents a percent. */
	float[]       percentValues;
	/** Last value of width passed in. */
	int           lastWidth;
	/** Last value of height passed in. */
	int           lastHeight;

	public CircleRegionContainment(AttributeSet as) {
	    int[]    coords = Map.extractCoords(as.getAttribute(HTML.Attribute.
								COORDS));

	    if (coords == null || coords.length != 3) {
		throw new RuntimeException("Unable to parse circular area");
	    }
	    x = coords[0];
	    y = coords[1];
	    radiusSquared = coords[2] * coords[2];
	    if (coords[0] < 0 || coords[1] < 0 || coords[2] < 0) {
		lastWidth = lastHeight = -1;
		percentValues = new float[3];
		for (int counter = 0; counter < 3; counter++) {
		    if (coords[counter] < 0) {
			percentValues[counter] = coords[counter] /
			                         -100.0f;
		    }
		    else {
			percentValues[counter] = -1.0f;
		    }
		}
	    }
	    else {
		percentValues = null;
	    }
	}

	public boolean contains(int x, int y, int width, int height) {
	    if (percentValues != null && (lastWidth != width ||
					  lastHeight != height)) {
		int      newRad = Math.min(width, height) / 2;

		lastWidth = width;
		lastHeight = height;
		if (percentValues[0] != -1.0f) {
		    this.x = (int)(percentValues[0] * width);
		}
		if (percentValues[1] != -1.0f) {
		    this.y = (int)(percentValues[1] * height);
		}
		if (percentValues[2] != -1.0f) {
		    radiusSquared = (int)(percentValues[2] * 
				   Math.min(width, height));
		    radiusSquared *= radiusSquared;
		}
	    }
	    return (((x - this.x) * (x - this.x) +
		     (y - this.y) * (y - this.y)) <= radiusSquared);
	}
    }


    /**
     * An implementation that will return true if the x, y location is
     * inside a rectangle defined by origin 0, 0, and width equal to
     * width passed in, and height equal to height passed in.
     */
    static class DefaultRegionContainment implements RegionContainment {
	/** A global shared instance. */
	static DefaultRegionContainment  si = null;

	public static DefaultRegionContainment sharedInstance() {
	    if (si == null) {
		si = new DefaultRegionContainment();
	    }
	    return si;
	}

	public boolean contains(int x, int y, int width, int height) {
	    return (x <= width && x >= 0 && y >= 0 && y <= width);
	}
    }
}
