/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.wicket.markup.html.link;

import java.awt.Shape;
import java.awt.geom.PathIterator;

import org.apache.wicket.AttributeModifier;
import org.apache.wicket.Component;
import org.apache.wicket.behavior.Behavior;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.html.image.Image;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.markup.repeater.RepeatingView;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.PropertyModel;

/**
 * A client-side image map implementation which allows you to "attach" the map to any existing
 * {@link Image} component.
 * 
 * @since 1.5
 */
public class ClientSideImageMap extends Panel
{
	private static final long serialVersionUID = 1L;
	private static final String CIRCLE = "circle";
	private static final String POLYGON = "poly";
	private static final String RECTANGLE = "rect";

	private RepeatingView areas;

	/**
	 * Constructs a client-side image map which is "attached" to the given {@link Image} component.
	 * 
	 * @param id
	 *            the component id
	 * @param image
	 *            the image component
	 */
	public ClientSideImageMap(String id, Image image)
	{
		super(id);
		setOutputMarkupId(true);
		add(AttributeModifier.replace("name", new PropertyModel<String>(this, "markupId")));
		image.add(AttributeModifier.replace("usemap", new UsemapModel()));

		areas = new RepeatingView("area");
		add(areas);
	}

	private String circleCoordinates(int x, int y, int radius)
	{
		return x + "," + y + "," + radius;
	}

	private String polygonCoordinates(int... coordinates)
	{
		final StringBuilder buffer = new StringBuilder();
		for (int i = 0; i < coordinates.length; i++)
		{
			buffer.append(coordinates[i]);

			if (i < (coordinates.length - 1))
			{
				buffer.append(',');
			}
		}
		return buffer.toString();
	}

	private String rectangleCoordinates(int x1, int y1, int x2, int y2)
	{
		return x1 + "," + y1 + "," + x2 + "," + y2;
	}

	private String shapeCoordinates(Shape shape)
	{
		final StringBuilder sb = new StringBuilder();
		final PathIterator pi = shape.getPathIterator(null, 1.0);
		final float[] coords = new float[6];
		final float[] lastMove = new float[2];
		while (!pi.isDone())
		{
			switch (pi.currentSegment(coords))
			{
				case PathIterator.SEG_MOVETO :
					if (sb.length() != 0)
					{
						sb.append(',');
					}
					sb.append(Math.round(coords[0]));
					sb.append(',');
					sb.append(Math.round(coords[1]));
					lastMove[0] = coords[0];
					lastMove[1] = coords[1];
					break;
				case PathIterator.SEG_LINETO :
					if (sb.length() != 0)
					{
						sb.append(',');
					}
					sb.append(Math.round(coords[0]));
					sb.append(',');
					sb.append(Math.round(coords[1]));
					break;
				case PathIterator.SEG_CLOSE :
					if (sb.length() != 0)
					{
						sb.append(',');
					}
					sb.append(Math.round(lastMove[0]));
					sb.append(',');
					sb.append(Math.round(lastMove[1]));
					break;
			}
			pi.next();
		}
		return sb.toString();
	}

	@Override
	protected void onComponentTag(ComponentTag tag)
	{
		// Must be attached to an map tag
		checkComponentTag(tag, "map");

		super.onComponentTag(tag);
	}

	/**
	 * Generates a unique id string. This makes it easy to add items to be rendered w/out having to
	 * worry about generating unique id strings in your code.
	 * 
	 * @return unique child id
	 */
	public String newChildId()
	{
		return areas.newChildId();
	}

	/**
	 * Adds a circle-shaped area centered at (x,y) with radius r.
	 * 
	 * @param link
	 *            the link
	 * @param x
	 *            x coordinate of the center of the circle
	 * @param y
	 *            y coordinate of center
	 * @param radius
	 *            the radius
	 * @return this
	 */
	public ClientSideImageMap addCircleArea(AbstractLink link, int x, int y, int radius)
	{
		areas.add(link);

		link.add(new Area(circleCoordinates(x, y, radius), CIRCLE));

		return this;
	}

	/**
	 * Adds a polygon-shaped area defined by coordinates.
	 * 
	 * @param link
	 *            the link
	 * @param coordinates
	 *            the coordinates for the polygon
	 * @return This
	 */
	public ClientSideImageMap addPolygonArea(AbstractLink link, int... coordinates)
	{
		areas.add(link);

		link.add(new Area(polygonCoordinates(coordinates), POLYGON));

		return this;
	}

	/**
	 * Adds a rectangular-shaped area.
	 * 
	 * @param link
	 *            the link
	 * @param x1
	 *            top left x
	 * @param y1
	 *            top left y
	 * @param x2
	 *            bottom right x
	 * @param y2
	 *            bottom right y
	 * @return this
	 */
	public ClientSideImageMap addRectangleArea(AbstractLink link, int x1, int y1, int x2, int y2)
	{
		areas.add(link);
		link.add(new Area(rectangleCoordinates(x1, y1, x2, y2), RECTANGLE));
		return this;
	}

	/**
	 * Adds an area defined by a shape object.
	 * 
	 * @param link
	 *            the link
	 * @param shape
	 *            the shape
	 * @return this
	 */
	public ClientSideImageMap addShapeArea(AbstractLink link, Shape shape)
	{
		areas.add(link);
		link.add(new Area(shapeCoordinates(shape), POLYGON));
		return this;
	}

	/**
	 * Encapsulates the concept of an <area> within a <map>.
	 */
	private static class Area extends Behavior
	{
		private static final long serialVersionUID = 1L;

		private final String coordinates;
		private final String type;

		protected Area(final String coordinates, final String type)
		{
			this.coordinates = coordinates;
			this.type = type;
		}

		@Override
		public void onComponentTag(Component component, ComponentTag tag)
		{
			tag.put("shape", type);
			tag.put("coords", coordinates);
		}
	}

	private class UsemapModel extends Model<String>
	{
		private static final long serialVersionUID = 1L;

		@Override
		public String getObject()
		{
			return "#" + getMarkupId();
		}
	}
}
