package com.mxgraph.layout;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.mxgraph.model.mxCellPath;
import com.mxgraph.model.mxGeometry;
import com.mxgraph.model.mxICell;
import com.mxgraph.model.mxIGraphModel;
import com.mxgraph.util.mxPoint;
import com.mxgraph.view.mxGraph;
import com.mxgraph.view.mxGraphView;

public class mxParallelEdgeLayout extends mxGraphLayout
{

	/**
	 * Specifies the spacing between the edges. Default is 20.
	 */
	protected int spacing;

	/**
	 * Constructs a new stack layout layout for the specified graph,
	 * spacing, orientation and offset.
	 */
	public mxParallelEdgeLayout(mxGraph graph)
	{
		this(graph, 20);
	}

	/**
	 * Constructs a new stack layout layout for the specified graph,
	 * spacing, orientation and offset.
	 */
	public mxParallelEdgeLayout(mxGraph graph, int spacing)
	{
		super(graph);
		this.spacing = spacing;
	}

	/*
	 * (non-Javadoc)
	 * @see com.mxgraph.layout.mxIGraphLayout#execute(java.lang.Object)
	 */
	public void execute(Object parent)
	{
		Map<String, List<Object>> lookup = findParallels(parent);

		graph.getModel().beginUpdate();
		try
		{
			Iterator<List<Object>> it = lookup.values().iterator();

			while (it.hasNext())
			{
				List<Object> parallels = it.next();

				if (parallels.size() > 1)
				{
					layout(parallels);
				}
			}
		}
		finally
		{
			graph.getModel().endUpdate();
		}
	}

	/**
	 * 
	 */
	protected Map<String, List<Object>> findParallels(Object parent)
	{
		Map<String, List<Object>> lookup = new Hashtable<String, List<Object>>();
		mxIGraphModel model = graph.getModel();
		int childCount = model.getChildCount(parent);

		for (int i = 0; i < childCount; i++)
		{
			Object child = model.getChildAt(parent, i);

			if (!isEdgeIgnored(child))
			{
				String id = getEdgeId(child);

				if (id != null)
				{
					if (!lookup.containsKey(id))
					{
						lookup.put(id, new ArrayList<Object>());
					}

					lookup.get(id).add(child);
				}
			}
		}

		return lookup;
	}

	/**
	 * 
	 */
	protected String getEdgeId(Object edge)
	{
		mxGraphView view = graph.getView();
		Object src = view.getVisibleTerminal(edge, true);
		Object trg = view.getVisibleTerminal(edge, false);

		if (src instanceof mxICell && trg instanceof mxICell)
		{
			String srcId = mxCellPath.create((mxICell) src);
			String trgId = mxCellPath.create((mxICell) trg);

			return (srcId.compareTo(trgId) > 0) ? trgId + "-" + srcId : srcId
					+ "-" + trgId;
		}

		return null;
	}

	/**
	 * 
	 */
	protected void layout(List<Object> parallels)
	{
		Object edge = parallels.get(0);
		mxIGraphModel model = graph.getModel();
		mxGeometry src = model.getGeometry(model.getTerminal(edge, true));
		mxGeometry trg = model.getGeometry(model.getTerminal(edge, false));

		// Routes multiple loops
		if (src == trg)
		{
			double x0 = src.getX() + src.getWidth() + this.spacing;
			double y0 = src.getY() + src.getHeight() / 2;

			for (int i = 0; i < parallels.size(); i++)
			{
				route(parallels.get(i), x0, y0);
				x0 += spacing;
			}
		}
		else if (src != null && trg != null)
		{
			// Routes parallel edges
			double scx = src.getX() + src.getWidth() / 2;
			double scy = src.getY() + src.getHeight() / 2;

			double tcx = trg.getX() + trg.getWidth() / 2;
			double tcy = trg.getY() + trg.getHeight() / 2;

			double dx = tcx - scx;
			double dy = tcy - scy;

			double len = Math.sqrt(dx * dx + dy * dy);

			double x0 = scx + dx / 2;
			double y0 = scy + dy / 2;

			double nx = dy * spacing / len;
			double ny = dx * spacing / len;

			x0 += nx * (parallels.size() - 1) / 2;
			y0 -= ny * (parallels.size() - 1) / 2;

			for (int i = 0; i < parallels.size(); i++)
			{
				route(parallels.get(i), x0, y0);
				x0 -= nx;
				y0 += ny;
			}
		}
	}

	/**
	 * 
	 */
	protected void route(Object edge, double x, double y)
	{
		if (graph.isCellMovable(edge))
		{
			setEdgePoints(edge, Arrays
					.asList(new mxPoint[] { new mxPoint(x, y) }));
		}
	}

}
