/*
 * $Id: JGraphAdapterModel.java,v 1.1.1.1 2005/08/06 05:26:45 gaudenz Exp $
 * 
 * Copyright (c) 2001-2005, Gaudenz Alder
 * 
 * See LICENSE file in distribution for licensing details of this source file
 */
package com.jgraph.example.adapter;

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

import javax.swing.undo.UndoableEdit;

import org.jgraph.graph.AttributeMap;
import org.jgraph.graph.ConnectionSet;
import org.jgraph.graph.DefaultGraphModel;
import org.jgraph.graph.ParentMap;

/**
 * Maps from business objects to cells and manages prototypes to create new cells
 * on the fly.
 * Maps from userobjects to cells and manages cell prototypes.
 */
public class JGraphAdapterModel extends DefaultGraphModel {

	public static final String VERSION = "@NAME@ (v@VERSION@)";
	
	/**
	 * A store of the mapping between model cells and back-end objects
	 */
	protected Map mapping = new Hashtable();

	/**
	 * The current back-end logic object
	 */
	protected JGraphAdapterBackend backend;

	public JGraphAdapterModel() {
		super();
	}

	/**
	 * @param roots
	 * @param attributes
	 */
	public JGraphAdapterModel(List roots, AttributeMap attributes) {
		this(roots, attributes, null);
	}

	/**
	 * @param roots
	 * @param attributes
	 */
	public JGraphAdapterModel(List roots, AttributeMap attributes,
			JGraphAdapterBackend backend) {
		super(roots, attributes);
		this.backend = backend;
	}

	public void addProperty(Object cell, Object key, Object value) {
		if (key != null && value != null) {
			Object userObj = getValue(cell);
			JGraphBusinessObject bo = (JGraphBusinessObject) userObj;
			bo.putProperty(key, value);
			cellsChanged(new Object[] { cell });
		}
	}

	protected Map handleAttributes(Map attributes) {
		Map undo = super.handleAttributes(attributes);
		if (attributes != null) {
			if (undo == null)
				undo = new Hashtable();
			Iterator it = attributes.entrySet().iterator();
			while (it.hasNext()) {
				Map.Entry entry = (Map.Entry) it.next();
				Object cell = entry.getKey();
				Map properties = (Map) entry.getValue();
				if (cell instanceof JGraphBusinessObject) {
					JGraphBusinessObject bo = (JGraphBusinessObject) cell;
					Map deltaOld = new Hashtable();
					Iterator it2 = properties.entrySet().iterator();
					while (it2.hasNext()) {
						Map.Entry property = (Map.Entry) it2.next();
						Object key = property.getKey();
						Object oldValue = bo.putProperty(key, property
								.getValue());
						if (oldValue != null)
							deltaOld.put(key, oldValue);
					}
					undo.put(cell, deltaOld);
				}
			}
		}
		return undo;
	}

	/**
	 * @return Returns the backend.
	 */
	public JGraphAdapterBackend getBackend() {
		return backend;
	}

	/**
	 * @param backend
	 *            The backend to set.
	 */
	public void setBackend(JGraphAdapterBackend backend) {
		this.backend = backend;
	}

	/**
	 * 
	 * @param obj
	 * @return the object that the specified object maps to
	 */
	public Object getMapping(Object obj) {
		return mapping.get(obj);
	}

	/**
	 * 
	 * @param obj
	 * @param cell
	 */
	protected void putMapping(Object obj, Object cell) {
		mapping.put(obj, cell);
	}

	/**
	 * 
	 * @param obj
	 */
	protected void removeMapping(Object obj) {
		mapping.remove(obj);
	}

	/**
	 * 
	 * @param child
	 * @return the user object associated with the parent of the specified child
	 */
	public Object getParentUserObject(Object child) {
		return getValue(getParent(child));
	}

	/**
	 * 
	 * @param edge
	 * @return the user object associated with the vertex connected to the source end of this edge
	 */
	public Object getSourceVertexUserObject(Object edge) {
		Object cell = DefaultGraphModel.getSourceVertex(this, edge);
		return getValue(cell);
	}

	/**
	 * 
	 * @param edge
	 * @return the user object associated with the vertex connected to the target end of this edge
	 */
	public Object getTargetVertexUserObject(Object edge) {
		Object cell = DefaultGraphModel.getTargetVertex(this, edge);
		return getValue(cell);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jgraph.graph.GraphModel#edit(java.util.Map,
	 *      org.jgraph.graph.ConnectionSet, org.jgraph.graph.ParentMap,
	 *      javax.swing.undo.UndoableEdit[])
	 */
	public void edit(Map attributes, ConnectionSet cs, ParentMap pm,
			UndoableEdit[] edits) {
		if ((attributes != null && !attributes.isEmpty())
				|| (cs != null && !cs.isEmpty())) {
			try {
				// Call source / targetChanged
				processConnectionSet(cs, true);
				processParentMap(pm, true);
				processNestedAttributes(attributes, true);
				super.edit(attributes, cs, pm, edits);
			} catch (Exception e) {
				e.printStackTrace();
			}
		} else {
			super.edit(attributes, cs, pm, edits);
		}
	}

	protected Object cloneUserObject(Object userObject) {
		if (userObject instanceof JGraphBusinessObject)
			return ((JGraphBusinessObject) userObject).clone();
		return super.cloneUserObject(userObject);
	}

	public Object valueForCellChanged(Object cell, Object newValue) {
		Object userObject = getValue(cell);
		if (userObject instanceof JGraphBusinessObject
				&& newValue instanceof String) {
			JGraphBusinessObject user = (JGraphBusinessObject) userObject;
			Object oldLabel = user.getValue();
			user.setValue(newValue);
			return oldLabel;
		} else
			return super.valueForCellChanged(cell, newValue);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jgraph.graph.GraphModel#insert(java.lang.Object[],
	 *      java.util.Map, org.jgraph.graph.ConnectionSet,
	 *      org.jgraph.graph.ParentMap, javax.swing.undo.UndoableEdit[])
	 */
	public void insert(Object[] cells, Map attributes, ConnectionSet cs,
			ParentMap pm, UndoableEdit[] edits) {
		if (cells != null || attributes != null || cs != null) {
			try {
				processInsert(cells, attributes, cs, pm, true);
				super.insert(cells, attributes, cs, pm, edits);
			} catch (Exception e) {
				e.printStackTrace();

			}
		} else {
			super.insert(cells, attributes, cs, pm, edits);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jgraph.graph.GraphModel#remove(java.lang.Object[])
	 */
	public void remove(Object[] cells) {
		if (cells != null) {
			try {
				processRemove(cells, true);
				super.remove(cells);
			} catch (Exception e) {
				e.printStackTrace();
			}
		} else {
			super.remove(cells);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jgraph.graph.GraphModel#insert(java.lang.Object[],
	 *      java.util.Map, org.jgraph.graph.ConnectionSet,
	 *      org.jgraph.graph.ParentMap, javax.swing.undo.UndoableEdit[])
	 */
	public void processInsert(Object[] cells, Map attributes, ConnectionSet cs,
			ParentMap pm, boolean validate) throws Exception {
		if (cells != null || attributes != null || cs != null) {
			if (cells != null) {
				// Vertices in the first run (so they have an ID)
				for (int i = 0; i < cells.length; i++) {
					if (!isPort(cells[i])) {
						if (!isEdge(cells[i])) {
							fireVertexAdded(cells[i], validate);
							if (!validate)
								putMapping(getValue(cells[i]), cells[i]);
						}
					}
				}
				// Edges in the second run
				for (int i = 0; i < cells.length; i++) {
					if (!isPort(cells[i])) {
						if (isEdge(cells[i]) && cs != null) {
							fireEdgeAdded(cells[i],
									cs.getPort(cells[i], false), cs.getPort(
											cells[i], true), validate);
							if (!validate)
								putMapping(getValue(cells[i]), cells[i]);
						}
					}
				}
			}
			processConnectionSet(cs, validate);
			processParentMap(pm, validate);
			processNestedAttributes(attributes, validate);
		}
	}

	protected void processRemove(Object[] cells, boolean validate)
			throws Exception {
		if (cells != null) {
			for (int i = 0; i < cells.length; i++) {
				if (!isPort(cells[i])) {
					fireCellRemoved(cells[i], validate);
					if (!validate)
						removeMapping(getValue(cells[i]));
				}
			}
		}
	}

	/**
	 * 
	 * @param cs
	 * @param validate
	 * @throws Exception
	 */
	protected void processConnectionSet(ConnectionSet cs, boolean validate)
			throws Exception {
		if (cs != null) {
			Iterator it = cs.connections();
			while (it.hasNext()) {
				ConnectionSet.Connection conn = (ConnectionSet.Connection) it
						.next();
				Object edge = conn.getEdge();
				if (contains(edge)) {
					if (conn.isSource()) {
						fireSourceChanged(edge, conn.getPort(), validate);
					} else {
						fireTargetChanged(edge, conn.getPort(), validate);
					}
				}
			}
		}
	}

	/**
	 * 
	 * @param pm
	 * @param validate
	 * @throws Exception
	 */
	protected void processParentMap(ParentMap pm, boolean validate)
			throws Exception {
		if (pm != null) {
			Iterator it = pm.entries();
			while (it.hasNext()) {
				ParentMap.Entry entry = (ParentMap.Entry) it.next();
				Object parent = entry.getParent();
				Object child = entry.getChild();
				if (contains(child)) {
					fireParentChanged(child, parent, validate);
				}
			}
		}
	}

	/**
	 * 
	 * @param nested
	 * @param validate
	 * @throws Exception
	 */
	protected void processNestedAttributes(Map nested, boolean validate)
			throws Exception {
		if (nested != null) {
			Iterator it = nested.entrySet().iterator();
			while (it.hasNext()) {
				Map.Entry entry = (Map.Entry) it.next();
				Object cell = entry.getKey();
				if (!isPort(cell)) { // TODO: Remove isPort check
					Map change = (Map) entry.getValue();
					fireAttributesChanged(cell, change, validate);
				}
			}
		}
	}

	// Override parent method to hook the backend into the execute method.
	protected GraphModelEdit createEdit(Object[] inserted, Object[] removed,
			Map attributes, ConnectionSet cs, ParentMap pm) {
		return new BusinessModelEdit(inserted, removed, attributes, cs, pm);
	}

	public class BusinessModelEdit extends GraphModelEdit {

		protected boolean inProgress = false;

		/**
		 * @param inserted
		 * @param removed
		 * @param attributes
		 * @param connectionSet
		 * @param parentMap
		 */
		public BusinessModelEdit(Object[] inserted, Object[] removed,
				Map attributes, ConnectionSet connectionSet, ParentMap parentMap) {
			super(inserted, removed, attributes, connectionSet, parentMap);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.jgraph.event.GraphModelEvent.ExecutableGraphChange#execute()
		 */
		public void execute() {
			super.execute();
			try {
				if (!inProgress) {
					inProgress = true;
					processRemove(insert, false);
					processInsert(remove, previousAttributes,
							previousConnectionSet, previousParentMap, false);
					fireCommit();
				}
			} catch (Exception e) {
				e.printStackTrace();
				try {
					fireRollback();
					undo();
				} catch (Exception e1) {
					e1.printStackTrace();
				}
			} finally {
				inProgress = false;
			}
		}

	}

	/**
	 * 
	 * @throws Exception
	 */
	public void fireCommit() throws Exception {
		if (backend != null)
			backend.commit();
	}

	/**
	 * 
	 * @throws Exception
	 */
	public void fireRollback() throws Exception {
		if (backend != null)
			backend.rollback();
	}

	/**
	 * 
	 * @param vertex
	 * @param validate
	 * @throws Exception
	 */
	public void fireVertexAdded(Object vertex, boolean validate)
			throws Exception {
		if (backend != null)
			backend.vertexAdded(this, vertex, validate);
	}

	/**
	 * 
	 * @param edge
	 * @param source
	 * @param target
	 * @param validate
	 * @throws Exception
	 */
	public void fireEdgeAdded(Object edge, Object source, Object target,
			boolean validate) throws Exception {
		if (backend != null)
			backend.edgeAdded(this, edge, source, target, validate);
	}

	/**
	 * 
	 * @param object
	 * @param validate
	 * @throws Exception
	 */
	public void fireCellRemoved(Object object, boolean validate)
			throws Exception {
		if (backend != null)
			backend.cellRemoved(this, object, validate);
	}

	/**
	 * 
	 * @param child
	 * @param parent
	 * @param validate
	 * @throws Exception
	 */
	public void fireParentChanged(Object child, Object parent, boolean validate)
			throws Exception {
		if (backend != null)
			backend.parentChanged(this, child, parent, validate);
	}

	/**
	 * 
	 * @param edge
	 * @param source
	 * @param validate
	 * @throws Exception
	 */
	public void fireSourceChanged(Object edge, Object source, boolean validate)
			throws Exception {
		if (backend != null)
			backend.sourceChanged(this, edge, source, validate);
	}

	/**
	 * 
	 * @param edge
	 * @param target
	 * @param validate
	 * @throws Exception
	 */
	public void fireTargetChanged(Object edge, Object target, boolean validate)
			throws Exception {
		if (backend != null)
			backend.targetChanged(this, edge, target, validate);
	}

	/**
	 * 
	 * @param cell
	 * @param change
	 * @param validate
	 * @throws Exception
	 */
	public void fireAttributesChanged(Object cell, Map change, boolean validate)
			throws Exception {
		if (backend != null)
			backend.attributesChanged(this, cell, change, validate);
	}

}