package com.mxgraph.io;

import com.mxgraph.io.vdx.PageShapeIDKey;
import com.mxgraph.io.vdx.mxMastersManager;
import com.mxgraph.io.vdx.mxPropertiesManager;
import com.mxgraph.io.vdx.mxStyleSheetManager;
import com.mxgraph.io.vdx.mxVdxConstants;
import com.mxgraph.io.vdx.mxVdxShape;
import com.mxgraph.io.vdx.mxVdxUtils;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import com.mxgraph.model.mxCell;
import com.mxgraph.model.mxGeometry;
import com.mxgraph.util.mxPoint;
import com.mxgraph.view.mxCellState;
import com.mxgraph.view.mxConnectionConstraint;
import com.mxgraph.view.mxGraph;
import java.util.ArrayList;
import org.w3c.dom.Node;

/**
 * Parses a .vdx XML diagram file and imports it in the given graph.<br/>
 * This class depends from the classes contained in
 * com.mxgraph.io.vdx.
 */
public class mxVdxCodec
{
	/**
	 * Stores the vertexes imported.
	 */
	private static HashMap<PageShapeIDKey, mxCell> vertexMap = new HashMap<PageShapeIDKey, mxCell>();

	/**
	 * Stores the shapes that represent Edges.
	 */
	private static HashMap<PageShapeIDKey, mxVdxShape> edgeShapeMap = new HashMap<PageShapeIDKey, mxVdxShape>();

	/**
	 * Stores the shapes that represent Vertexes.
	 */
	private static HashMap<PageShapeIDKey, mxVdxShape> vertexShapeMap = new HashMap<PageShapeIDKey, mxVdxShape>();

	/**
	 * Stores the parents of the shapes imported.
	 */
	private static HashMap<PageShapeIDKey, Object> parentsMap = new HashMap<PageShapeIDKey, Object>();

	/**
	 * Stores the page Elements that represents a background.
	 */
	private static HashMap<String, Element> backgroundsMap = new HashMap<String, Element>();

	private static HashMap<PageShapeIDKey, Object> cellsMap = new HashMap<PageShapeIDKey, Object>();

	private static ArrayList<PageShapeIDKey> shapeIDList = new ArrayList<PageShapeIDKey>();

	/**
	 * Page Height used in the importPage method.<br/>
	 * This value is accumulated to represents multiple pages.
	 */
	private static double pageHeight = 0;

	/**
	 * Height of the actual page.
	 */
	private static double actualPageHeight = 0;

	/**
	 * Number of pages imported until now.<br/>
	 * This number is used in the keys of the maps.
	 */
	private static int pageNumber = 0;

	/**
	 * Its value determines if text label must be formated with html tags or not.<br/>
	 * Don't confuse with the htmlLabels property of the graph. This property is set
	 * in true at beginning of the decode method.
	 */
	private static boolean htmlLabelsEnable = true;

	/**
	 * Checks if html labels are active.
	 * @return Returns <code>true</code> if html labels are enable.
	 */
	public static boolean isHtmlLabelsEnable()
	{
		return htmlLabelsEnable;
	}

	/**
	 * Sets html labels.
	 * @param htmlLabelsEnable New value of the property.
	 */
	public static void setHtmlLabelsEnable(boolean htmlLabelsEnable)
	{
		mxVdxCodec.htmlLabelsEnable = htmlLabelsEnable;
	}

	/**
	 * Remove all the elements from the defined maps.
	 */
	private static void cleanMaps()
	{
		vertexMap.clear();
		edgeShapeMap.clear();
		vertexShapeMap.clear();
		parentsMap.clear();
		backgroundsMap.clear();
		pageHeight = 0;
		actualPageHeight = 0;
	}

	/**
	 * Return the width and height of a Page expressed like a mxPoint.
	 * x = width
	 * y = height
	 * @param page Element that represents a page
	 * @return mxPoint that represents the dimentions of the shape
	 */
	private static mxPoint getPageDimentions(Element page)
	{
		Element pHeight = (Element) page.getElementsByTagName(
				mxVdxConstants.PAGE_HEIGHT).item(0);
		double pageH = Double.valueOf(pHeight.getTextContent())
				* mxVdxUtils.conversionFactor();
		Element pageWidth = (Element) page.getElementsByTagName(
				mxVdxConstants.PAGE_WIDTH).item(0);
		double pageW = Double.valueOf(pageWidth.getTextContent())
				* mxVdxUtils.conversionFactor();
		return new mxPoint(pageW, pageH);
	}

	/**
	 * Calculate the absolute coordinates of a cell's point.
	 * @param cellParent Cell that contains the point.
	 * @param graph Graph where the parsed graph is included.
	 * @param point Point to wich coordinates are calculated.
	 * @return The point in absolute coordinates.
	 */
	private static mxPoint calculateAbsolutePoint(mxCell cellParent,
			mxGraph graph, mxPoint point)
	{
		if (cellParent != null)
		{
			mxCellState state = graph.getView().getState(cellParent);
			if (state != null)
			{
				point.setX(point.getX() + state.getX());
				point.setY(point.getY() + state.getY());
			}
		}
		return point;
	}

	/**
	 * Adds a vertex to the graph if 'shape' is a vertex or add the shape to edgeShapeMap if is a edge.
	 * This method doesn't import the subshapes of 'shape'.
	 * @param graph Graph where the parsed graph is included.
	 * @param parent Parent cell of the shape to be imported.
	 * @param shp Shape to be imported.
	 * @param parentHeight Height of the parent cell.
	 * @return the new vertex added. null if 'shape' is not a vertex.
	 */
	private static mxCell addShape(mxGraph graph, Object parent, Element shp,
			double parentHeight)
	{
		//Create a wrapper for shape Element.
		mxVdxShape shape = new mxVdxShape(shp);

		//If is a Shape or a Group add the vertex to the graph.
		if (shp.getAttribute(mxVdxConstants.TYPE).equals(
				mxVdxConstants.TYPE_SHAPE)
				|| shp.getAttribute(mxVdxConstants.TYPE).equals(
						mxVdxConstants.TYPE_GROUP))
		{
			String id = shape.getId();
			shapeIDList.add(new PageShapeIDKey(pageNumber, id));
			//If is a vertex shape
			if (shape.isVertexShape())
			{

				mxCell v1 = null;
				if (shape.isComplexShape())
				{
					v1 = shape.addComplexShapeToGraph(graph, parent,
							parentHeight);
				}
				else
				{

					v1 = shape.addSimpleShapeToGraph(graph, parent,
							parentHeight);
				}
				vertexMap.put(new PageShapeIDKey(pageNumber, id), v1);
				vertexShapeMap.put(new PageShapeIDKey(pageNumber, id), shape);
				cellsMap.put(new PageShapeIDKey(pageNumber, id), v1);
				return v1;

			}
			else
			{
				edgeShapeMap.put(new PageShapeIDKey(pageNumber, id), shape);
			}
		}
		return null;
	}

	/**
	 * Adds a conected edge to the graph.<br/>
	 * These edge are the referenced in one Connect element at least.<br/>
	 * The edge shape imported is taken from edgeShapeMap and is removed from it.
	 * @param graph graph Graph where the parsed graph is included.
	 * @param parent Parent cell of the edge to be imported.
	 * @param connect Connect Element that references an edge shape and the source vertex.
	 * @param sigConnect Connect Element that references the same edge shape that 'connect'
	 * and the target vertex. This parameter may to be null.
	 * @return The new edge. null if not edge is added.
	 */
	private static Object addConectedEdge(mxGraph graph, Element connect,
			Element sigConnect)
	{
		mxCell edge = null;

		//Retrieve edge Shape and Parent
		String shapeConnect = connect.getAttribute(mxVdxConstants.FROM_SHEET);

		mxVdxShape edgeShape = edgeShapeMap.get(new PageShapeIDKey(pageNumber,
				shapeConnect));
		edgeShapeMap.remove(new PageShapeIDKey(pageNumber, shapeConnect));

		if (edgeShape != null)
		{
			Object parent = parentsMap.get(new PageShapeIDKey(pageNumber,
					edgeShape.getId()));

			//Get Parent Height
			double parentHeight = pageHeight;

			mxCell parentCell = (mxCell) parent;

			if (parentCell != null)
			{
				mxGeometry parentGeometry = parentCell.getGeometry();

				if (parentGeometry != null)
				{
					parentHeight = parentGeometry.getHeight();
					parentHeight += pageHeight - actualPageHeight;
				}
			}

			//Get beginXY and endXY coordinates.
			mxPoint beginXY = edgeShape.getBeginXY(parentHeight);
			beginXY = calculateAbsolutePoint((mxCell) parent, graph, beginXY);

			mxPoint endXY = edgeShape.getEndXY(parentHeight);
			endXY = calculateAbsolutePoint((mxCell) parent, graph, endXY);

			//Declare variables.
			mxCell source = null;
			mxCell target = null;
			mxPoint fromConstraint = null;
			mxPoint toConstraint = null;

			//Defines text label
			String textLabel = edgeShape.getTextLabel();

			String from = connect.getAttribute(mxVdxConstants.TO_SHEET);
			mxVdxShape fromShape = vertexShapeMap.get(new PageShapeIDKey(
					pageNumber, from));

			//If the source is not defined.
			if (connect.getAttribute(mxVdxConstants.FROM_CELL).equals(
					mxVdxConstants.END_X)
					|| fromShape == null)
			{
				//Only one side connected.
				source = (mxCell) graph.insertVertex(parent, null, null,
						beginXY.getX(), beginXY.getY(), 0, 0);
				fromConstraint = new mxPoint(0, 0);

				sigConnect = connect;
			}
			else
			{
				//Define Source vertex of the edge.
				source = vertexMap.get(new PageShapeIDKey(pageNumber, from));

				//Get dimentions of vertex
				mxPoint dimentionFrom = fromShape.getDimentions();

				//Get From shape origin and begin/end of edge in absolutes values.
				double height = pageHeight;

				if ((source.getParent() != null)
						&& (source.getParent().getGeometry() != null))
				{
					height = source.getParent().getGeometry().getHeight();
					height += pageHeight - actualPageHeight;
				}

				mxPoint originFrom = fromShape.getOriginPoint(height);
				mxPoint absOriginFrom = calculateAbsolutePoint(
						(mxCell) source.getParent(), graph, originFrom);

				//Determines From Constraints (Connection point) of the edge.
				fromConstraint = new mxPoint(
						(beginXY.getX() - absOriginFrom.getX())
								/ dimentionFrom.getX(),
						(beginXY.getY() - absOriginFrom.getY())
								/ dimentionFrom.getY());

			}

			//If is connected in both sides.
			if (sigConnect != null)
			{
				String to = sigConnect.getAttribute(mxVdxConstants.TO_SHEET);
				mxVdxShape toShape = vertexShapeMap.get(new PageShapeIDKey(
						pageNumber, to));

				if (toShape != null)
				{

					target = vertexMap.get(new PageShapeIDKey(pageNumber, to));

					mxPoint dimentionTo = toShape.getDimentions();

					//Get To shape origin.
					double height = pageHeight;

					if ((target.getParent() != null)
							&& (target.getParent().getGeometry() != null))
					{
						height = target.getParent().getGeometry().getHeight();
						height += pageHeight - actualPageHeight;
					}
					mxPoint originTo = toShape.getOriginPoint(height);

					mxPoint absOriginTo = calculateAbsolutePoint(
							(mxCell) target.getParent(), graph, originTo);

					//Determines To Constraints (Connection point) of the edge.
					toConstraint = new mxPoint(
							(endXY.getX() - absOriginTo.getX())
									/ dimentionTo.getX(),
							(endXY.getY() - absOriginTo.getY())
									/ dimentionTo.getY());

				}
				else
				{

					//Only one side connected.
					target = (mxCell) graph.insertVertex(parent, null, null,
							endXY.getX(), endXY.getY(), 0, 0);
					toConstraint = new mxPoint(0, 0);
				}
			}
			else
			{
				//Only one side connected.
				target = (mxCell) graph.insertVertex(parent, null, null,
						endXY.getX(), endXY.getY(), 0, 0);
				toConstraint = new mxPoint(0, 0);
			}

			//Adjust the constraints.
			fromConstraint = mxVdxUtils.adjustConstraint(fromConstraint);
			toConstraint = mxVdxUtils.adjustConstraint(toConstraint);

			//Defines the style of the edge.
			String style = edgeShape.getStyleFromEdgeShape(parentHeight);

			//Insert new edge and set constraints.
			edge = (mxCell) graph.insertEdge(parent, null, textLabel, source,
					target, style);
			graph.setConnectionConstraint(edge, source, true,
					new mxConnectionConstraint(fromConstraint, false));
			graph.setConnectionConstraint(edge, target, false,
					new mxConnectionConstraint(toConstraint, false));

			//Gets and sets routing points of the edge.
			mxGeometry edgeGeometry = edge.getGeometry();
			List<mxPoint> pointList = edgeShape.getRoutingPoints(parentHeight);
			edgeGeometry.setPoints(pointList);

			//Put cell in the map.
			cellsMap.put(new PageShapeIDKey(pageNumber, shapeConnect), edge);

		}
		return edge;
	}

	/**
	 * Adds a new edge not conected to any vertex to the graph.
	 * @param graph Graph where the parsed graph is included.
	 * @param parent Parent cell of the edge to be imported.
	 * @param edgeShape Shape Element that represents an edge.
	 * @return The new edge added.
	 */
	private static Object addNotConnectedEdge(mxGraph graph, Object parent,
			mxVdxShape edgeShape)
	{
		mxCell edge = null;

		//Defines the label of the edge.
		String textLabel = edgeShape.getTextLabel();

		//Get begin and end of edge.
		double height = pageHeight;
		mxCell parentCell = (mxCell) parent;

		if (parentCell != null)
		{
			mxGeometry parentGeometry = parentCell.getGeometry();

			if (parentGeometry != null)
			{
				height = parentGeometry.getHeight();
			}
		}

		mxPoint beginXY = edgeShape.getBeginXY(height);
		mxPoint endXY = edgeShape.getEndXY(height);

		//Create the source and target cell of the edge.
		mxCell target = (mxCell) graph.insertVertex(parent, null, null,
				endXY.getX(), endXY.getY(), 0, 0);

		mxCell source = (mxCell) graph.insertVertex(parent, null, null,
				beginXY.getX(), beginXY.getY(), 0, 0);

		//Define style of the edge
		String style = edgeShape.getStyleFromEdgeShape(height);

		//Determines Constraints (Connection points) of the edge.
		mxPoint fromConstraint = new mxPoint(0, 0);
		mxPoint toConstraint = new mxPoint(0, 0);

		//Insert new edge and set constraints.
		edge = (mxCell) graph.insertEdge(source.getParent(), null, textLabel,
				source, target, style);
		graph.setConnectionConstraint(edge, source, true,
				new mxConnectionConstraint(fromConstraint, false));
		graph.setConnectionConstraint(edge, target, false,
				new mxConnectionConstraint(toConstraint, false));

		//Gets and sets routing points of the edge.
		mxGeometry edgeGeometry = edge.getGeometry();
		List<mxPoint> pointList = edgeShape.getRoutingPoints(height);
		edgeGeometry.setPoints(pointList);

		cellsMap.put(new PageShapeIDKey(pageNumber, edgeShape.getId()), edge);

		return edge;
	}

	/**
	 * Finds the connect element that corresponds with the connect param.
	 * @param connectList List that contains the connect elements
	 * @param connect Connect Element that references an edge shape.
	 * @param index Index where starts the search.
	 * @return The connect element that corresponds with the connect param. It is,
	 * both references to the same edge shape.
	 */
	private static Element findSigConnect(List<Node> connectList,
			Element connect, int index)
	{
		int length = connectList.size();
		String shapeConn1 = connect.getAttribute(mxVdxConstants.FROM_SHEET);
		Element sigConnect = null;
		boolean end = false;

		for (int i = index + 1; (i < length) && (!end); i++)
		{
			sigConnect = (Element) connectList.get(i);
			String shapeConn2 = sigConnect
					.getAttribute(mxVdxConstants.FROM_SHEET);

			if (shapeConn1.equals(shapeConn2))
			{
				end = true;
			}
			else
			{
				sigConnect = null;
			}
		}
		return sigConnect;
	}

	/**
	 * Adds to the graph all the subshapes included in a shape and recursively.
	 * @param shape Shape element from wich its subshapes will be imported.
	 * @param graph Graph where the parsed graph is included.
	 * @param parent Parent cell of the subShapes to be imported.
	 */
	private static void decodeShape(Element shape, mxGraph graph, Object parent)
	{

		mxVdxShape shp = new mxVdxShape(shape);

		//If a shape is complex(formed by several shapes) its subshapes are not considered.
		//its subshapes have been considered already.
		if (!shp.isComplexShape())
		{
			NodeList childs = shape.getChildNodes();

			if (mxVdxUtils.nodeListHasTag(childs, mxVdxConstants.SHAPES))
			{
				Element shapes = mxVdxUtils.nodeListTag(childs,
						mxVdxConstants.SHAPES);
				NodeList shapeList = shapes.getChildNodes();

				List<Element> shpList = mxVdxUtils.nodeListTags(shapeList,
						mxVdxConstants.SHAPE);

				int shapeLength = shpList.size();

				//Get the masterShapes of shape
				double parentHeight = shp.getDimentions().getY();

				//Process the sub-shapes
				for (int j = 0; j < shapeLength; j++)
				{
					Element shapeInside = shpList.get(j);
					//Get the master of the sub-shape
					String Id = shapeInside.getAttribute(mxVdxConstants.ID);
					parentsMap.put(new PageShapeIDKey(pageNumber, Id), parent);
					Object vertex = addShape(graph, parent, shapeInside,
							parentHeight);

					if (vertex != null)
					{
						decodeShape(shapeInside, graph, vertex);
					}
				}
			}
		}
	}

	/**
	 * Imports a page of the document with the actual pageHeight.<br/>
	 * In .vdx, the Y-coordinate grows upward from the bottom of the page.<br/>
	 * The page height is used for calculate the correct position in JGraph using
	 * this formula: JGraph_Y_Coord = PageHeight - VDX_Y_Coord.
	 * @param page Actual page Element to be imported
	 * @param graph Graph where the parsed graph is included.
	 * @param parent The parent of the elements to be imported. This should be the default parent.
	 */
	private static void importPage(Element page, mxGraph graph, Object parent)
	{

		NodeList shapesList = page.getElementsByTagName(mxVdxConstants.SHAPES);

		//Updates the page number.
		pageNumber++;

		if (shapesList.getLength() > 0)
		{
			Element shapes = (Element) shapesList.item(0);
			NodeList shapeList = shapes.getChildNodes();

			List<Element> shpList = mxVdxUtils.nodeListTags(shapeList,
					mxVdxConstants.SHAPE);

			int shapeLength = shpList.size();

			for (int j = 0; j < shapeLength; j++)
			{
				Element shape = shpList.get(j);

				Object vertex = addShape(graph, parent, shape, pageHeight);
				decodeShape(shape, graph, vertex);
			}
			//Process the Connects and add edges.
			NodeList connectsList = page
					.getElementsByTagName(mxVdxConstants.CONNECTS);

			if (connectsList.getLength() > 0)
			{
				Element connects = (Element) connectsList.item(0);
				NodeList connectList = connects
						.getElementsByTagName(mxVdxConstants.CONNECT);
				List<Node> list = mxVdxUtils.copyNodeList(connectList);

				for (int j = 0; j < list.size(); j++)
				{
					Element connect = (Element) list.get(j);
					Element sigConnect = findSigConnect(list, connect, j);
					list.remove(sigConnect);
					addConectedEdge(graph, connect, sigConnect);
				}
			}

			//Process not conected edges.
			Iterator<mxVdxShape> it = edgeShapeMap.values().iterator();

			while (it.hasNext())
			{
				mxVdxShape edgeShape = it.next();
				addNotConnectedEdge(graph, parentsMap.get(new PageShapeIDKey(
						pageNumber, edgeShape.getId())), edgeShape);

			}
		}
	}

	/**
	 * Recieves a xml document and parses it generating a new graph that is inserted in graph.
	 * @param document XML to be parsed
	 * @param graph Graph where the parsed graph is included.
	 */
	public static void decode(Document document, mxGraph graph)
	{

		Object parent = graph.getDefaultParent();

		graph.getModel().beginUpdate();
		graph.setHtmlLabels(true);
		Document doc = document;

		//Inicialize the Style Sheet Manager
		mxStyleSheetManager.getInstance().initialise(doc);

		//Inicialize the Master Manager
		mxMastersManager.getInstance().initialise(doc);

		//Inicialize the Properties Manager
		mxPropertiesManager.getInstance().initialise(doc);

		//Imports each page of the document.
		NodeList vdxPages = doc.getElementsByTagName(mxVdxConstants.PAGES);

		if (vdxPages.getLength() > 0)
		{
			Element pages = (Element) vdxPages.item(0);
			NodeList pageList = pages.getElementsByTagName(mxVdxConstants.PAGE);

			if (pageList.getLength() > 0)
			{
				//Retrieves the backgrounds pages
				for (int p = 0; p < pageList.getLength(); p++)
				{
					Element page = (Element) pageList.item(p);
					String back = page.getAttribute(mxVdxConstants.BACKGROUND);
					if ((back != null && back.equals(mxVdxConstants.TRUE)))
					{
						String id = page.getAttribute(mxVdxConstants.ID);
						backgroundsMap.put(id, page);
					}
				}
				//Import the pages that are not background.
				//If a page references a background page, the background is imported previously
				//to the actual page.
				for (int p = 0; p < pageList.getLength(); p++)
				{
					Element page = (Element) pageList.item(p);
					String back = page.getAttribute(mxVdxConstants.BACKGROUND);

					if (!(back != null && back.equals(mxVdxConstants.TRUE)))
					{
						actualPageHeight = getPageDimentions(page).getY();
						pageHeight += actualPageHeight;
						String backId = page
								.getAttribute(mxVdxConstants.BACK_PAGE);
						if (backId != null && !backId.equals(""))
						{
							//Import the background.
							Element background = backgroundsMap.get(backId);
							importPage(background, graph, parent);
						}
						//Import the actual page.
						importPage(page, graph, parent);
					}
				}
			}
		}
		Object[] order = mxVdxUtils.getOrderArray(shapeIDList, cellsMap);
		graph.orderCells(false, order);
		graph.getModel().endUpdate();
		cleanMaps();

	}
}
