/* 
 * This file is part of the Echo Point Project.  This project is a collection
 * of Components that have extended the Echo Web Application Framework.
 *
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (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.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 */

package echopointng.ui.syncpeer;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import nextapp.echo2.app.Component;
import nextapp.echo2.app.Style;
import nextapp.echo2.app.update.ServerComponentUpdate;
import nextapp.echo2.webcontainer.ActionProcessor;
import nextapp.echo2.webcontainer.ContainerInstance;
import nextapp.echo2.webcontainer.RenderContext;
import nextapp.echo2.webcontainer.RenderState;
import nextapp.echo2.webrender.Service;
import nextapp.echo2.webrender.WebRenderAction;
import nextapp.echo2.webrender.output.CssStyle;
import nextapp.echo2.webrender.servermessage.DomUpdate;
import nextapp.echo2.webrender.service.JavaScriptService;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

import echopointng.EPNG;
import echopointng.Tree;
import echopointng.tree.TreeModel;
import echopointng.tree.TreePath;
import echopointng.ui.util.CssStyleEx;
import echopointng.ui.util.ImageManager;
import echopointng.ui.util.RenderingContext;
import echopointng.util.TokenizerKit;

/**
 * Synchronization peer for <code>echopointng.Tree</code> components.
 */
public class TreePeer extends AbstractEchoPointContainerPeer implements ActionProcessor {

	/**
	 * Service to provide supporting Tree JavaScript library.
	 */
	public static final Service TREE_SCRIPT_SERVICE = JavaScriptService.forResource("Echopoint.Tree", "/echopointng/ui/resource/js/tree.js");

	static {
		WebRenderAction.getServiceRegistry().add(TREE_SCRIPT_SERVICE);
	}

	/**
	 * <code>TreeRenderState</code> is used to contain render state for
	 * a component.
	 */
	private static class TreeRenderState implements RenderState {

		private Element newTreeDivE;
		private Element oldTreeDivE;
		private ImageManager imageManager = new ImageManager();

		private Element getNewTreeDiv() {
			return newTreeDivE;
		}
		public void setNewTreeDiv(Element newTreeDiv) {
			if (newTreeDiv != null)
				newTreeDiv = (Element) newTreeDiv.cloneNode(true);
			this.newTreeDivE = newTreeDiv;
		}
		private Element getOldTreeDiv() {
			return oldTreeDivE;
		}
		
		private void setOldTreeDiv(Element oldTreeDiv) {
			if (oldTreeDiv != null)
				oldTreeDiv = (Element) oldTreeDiv.cloneNode(true);
			this.oldTreeDivE = oldTreeDiv;
		}
		
		private void prepareForNewRender() {
			if (this.newTreeDivE != null) {
				this.oldTreeDivE = this.newTreeDivE;
			}
			this.newTreeDivE = null;
		}

		public String toString() {
			StringBuffer sb = new StringBuffer();
			
			if (oldTreeDivE != null) {
				NodeList nodeList = oldTreeDivE.getElementsByTagName("table");
				int len = nodeList.getLength();
				sb.append("_____OLD TREE IDS_______\n");
				for (int i = 0; i < len; i++) {
					sb.append(((Element) nodeList.item(i)).getAttribute("id"));
					sb.append('\n');
				}
			}
			if (newTreeDivE != null) {
				NodeList nodeList = newTreeDivE.getElementsByTagName("table");
				int len = nodeList.getLength();
				sb.append("_____NEW TREE IDS_______\n");
				for (int i = 0; i < len; i++) {
					sb.append(((Element) nodeList.item(i)).getAttribute("id"));
					sb.append('\n');
				}
			}
			return sb.toString();
		}
		
	}

	private TreeRenderState setupTreeImageRenderState(Component component) {
		//
		// Tree uses its own special image render state
		//
		TreeRenderState treeImageRenderState;
		RenderState renderState = retreiveRenderState(component);
		if (renderState == null || !(renderState instanceof TreeRenderState)) {
			treeImageRenderState = new TreeRenderState();
			storeRenderState(component, treeImageRenderState);
		} else {
			treeImageRenderState = (TreeRenderState) renderState;
		}
		return treeImageRenderState;
	}

	/**
	 * This sets up the EchoPoint RenderingContext helper and then calls the
	 * abstract renderHtml method.
	 * 
	 * @see nextapp.echo2.webcontainer.DomUpdateSupport#renderHtml(nextapp.echo2.webcontainer.RenderContext,
	 *      nextapp.echo2.app.update.ServerComponentUpdate, org.w3c.dom.Node,
	 *      nextapp.echo2.app.Component)
	 */
	public void renderHtml(RenderContext rc, ServerComponentUpdate update, Node parentNode, Component component) {
		if (!component.isRenderVisible()) {
			// we really should never be called if we are invisble but to be
			// really sure
			Element blank = rc.getServerMessage().getDocument().createElement("bdo");
			blank.setAttribute("id", ContainerInstance.getElementId(component));
			parentNode.appendChild(blank);
			return;
		}
		RenderingContext renderingContext = new RenderingContext(rc, update, component);
		renderHtml(renderingContext, parentNode, component);
	}

	/**
	 * @see echopointng.ui.syncpeer.AbstractEchoPointPeer#renderHtml(echopointng.ui.util.RenderingContext,
	 *      Node, nextapp.echo2.app.Component)
	 */
	public void renderHtml(RenderingContext rc, Node parent, Component component) {
		Style fallbackStyle = EPNG.getFallBackStyle(component);

		rc.addLibrary(TREE_SCRIPT_SERVICE);

		TreeRenderState treeRenderState = setupTreeImageRenderState(component);

		CssStyleEx style = new CssStyleEx(component,fallbackStyle);
		
		final Element outerTreeDiv = rc.createE("div");
		parent.appendChild(outerTreeDiv);
		rc.addStandardWebSupport(outerTreeDiv);

		outerTreeDiv.setAttribute("id", rc.getElementId());
		outerTreeDiv.setAttribute("style", style.renderInline());
		
		TreeRenderer.EventSupportCallBack callback = new TreeRenderer.EventSupportCallBack() {
			/**
			 * @see echopointng.ui.syncpeer.TreeRenderer.EventSupportCallBack#onExpandoCell(nextapp.echo2.webrender.output.CssStyle,
			 *      org.w3c.dom.Element, echopointng.Tree, java.lang.Object)
			 */
			public String onExpandoCell(RenderingContext rc, CssStyle cssStyle, Element cellElement, Tree tree, Object treeNode, TreePath treeNodePath) {
				return addToggleEvents(rc, cssStyle, tree, treeNode, treeNodePath);
			}

			/**
			 * @see echopointng.ui.syncpeer.TreeRenderer.EventSupportCallBack#onNodeComponentCell(nextapp.echo2.webrender.output.CssStyle,
			 *      org.w3c.dom.Element, echopointng.Tree, java.lang.Object)
			 */
			public String onNodeComponentCell(RenderingContext rc, CssStyle cssStyle, Element cellElement, Tree tree, Object treeNode,
					TreePath treeNodePath) {
				return null;
			}

			/**
			 * @see echopointng.ui.syncpeer.TreeRenderer.EventSupportCallBack#onNodeIconCell(nextapp.echo2.webrender.output.CssStyle,
			 *      org.w3c.dom.Element, echopointng.Tree, java.lang.Object)
			 */
			public String onNodeIconCell(RenderingContext rc, CssStyle cssStyle, Element cellElement, Tree tree, Object treeNode,
					TreePath treeNodePath) {
				return addSelectEvents(rc, cssStyle, tree, treeNode, treeNodePath, "icon");
			}

			/**
			 * @see echopointng.ui.syncpeer.TreeRenderer.EventSupportCallBack#onNodeTextCell(nextapp.echo2.webrender.output.CssStyle,
			 *      org.w3c.dom.Element, echopointng.Tree, java.lang.Object)
			 */
			public String onNodeTextCell(RenderingContext rc, CssStyle cssStyle, Element cellElement, Tree tree, Object treeNode,
					TreePath treeNodePath) {
				return addSelectEvents(rc, cssStyle, tree, treeNode, treeNodePath, "text");
			}

			/**
			 * @see echopointng.ui.syncpeer.TreeRenderer.EventSupportCallBack#onTreeRow(nextapp.echo2.webrender.output.CssStyle,
			 *      org.w3c.dom.Element, echopointng.Tree)
			 */
			public void onTreeRow(RenderingContext rc, CssStyle cssStyle, Element treeElement, Tree tree, TreePath treeNodePath) {
				outerTreeDiv.appendChild(treeElement);
				String eventElementId = createEventId(tree, "treerow", "", null, treeNodePath);
				treeElement.setAttribute("id", eventElementId);
			}
		};
		TreeRenderer treeRenderer = new TreeRenderer(rc, callback, (Tree) component, fallbackStyle, treeRenderState.imageManager);
		treeRenderer.renderTree();

		// save a copy of the newly rendered tree div node
		if (rc.getRP(Tree.PROPERTY_PARTIAL_UPDATE_SUPPORT,fallbackStyle, true)) {
			treeRenderState.setNewTreeDiv(outerTreeDiv);
		}
	}

	/**
	 * The TablePeer now has the ability to update only the tree node rows that
	 * have changed. Therefore to do this is must unwind some of the automatic
	 * rendering support provided by the base classes and rather render some if
	 * it itself.
	 * 
	 * @see echopointng.ui.syncpeer.AbstractEchoPointPeer#renderAdd(nextapp.echo2.webcontainer.RenderContext,
	 *      nextapp.echo2.app.update.ServerComponentUpdate, java.lang.String,
	 *      nextapp.echo2.app.Component)
	 */
	public void renderAdd(RenderContext rc, ServerComponentUpdate update, String targetId, Component component) {
		Element domAddElementDirective = DomUpdate.renderElementAdd(rc.getServerMessage());
		DocumentFragment newDocFragment = rc.getServerMessage().getDocument().createDocumentFragment();
		renderHtml(rc, update, newDocFragment, component);
		DomUpdate.renderElementAddContent(rc.getServerMessage(), domAddElementDirective, targetId, newDocFragment);
	}

	/**
	 * The TablePeer now has the ability to update only the tree node rows that
	 * have changed. Therefore to do this is must unwind some of the automatic
	 * rendering support provided by the base classes and rather render some if
	 * it itself.
	 * 
	 * @see echopointng.ui.syncpeer.AbstractEchoPointContainerPeer#renderUpdate(nextapp.echo2.webcontainer.RenderContext,
	 *      nextapp.echo2.app.update.ServerComponentUpdate, java.lang.String)
	 */
	public boolean renderUpdate(RenderContext rc, ServerComponentUpdate update, String targetId) {
		Component tree = (Tree) update.getParent();
		boolean partialUpdateSupport = Tree.getRenderProperty(tree,Tree.PROPERTY_PARTIAL_UPDATE_SUPPORT,true);

		TreeRenderState treeImageRenderState = setupTreeImageRenderState(tree);
		treeImageRenderState.prepareForNewRender();

		DocumentFragment newDocFragment = rc.getServerMessage().getDocument().createDocumentFragment();
		renderHtml(rc, update, newDocFragment, tree);

		//
		// perform tree row difference analysis
		Element oldTreeDivE = treeImageRenderState.getOldTreeDiv();
		Element newTreeDivE = treeImageRenderState.getNewTreeDiv();
		if (partialUpdateSupport && oldTreeDivE != null) {
			sendTreeRowDiffs(rc, oldTreeDivE, newTreeDivE);
		} else {
			// do a normal remove of the old DOM and add the new one
			String removeTagetId = ContainerInstance.getElementId(update.getParent());
			DomUpdate.renderElementRemove(rc.getServerMessage(), removeTagetId);
			
			Element domAddElementDirective = DomUpdate.renderElementAdd(rc.getServerMessage());
			DomUpdate.renderElementAddContent(rc.getServerMessage(), domAddElementDirective, targetId, newDocFragment);
		}
		//
		// keep only one copy of the lastest tree rendered div
		if (partialUpdateSupport) {
			treeImageRenderState.setOldTreeDiv(newTreeDivE);
			treeImageRenderState.setNewTreeDiv(null);
		}
		return true;
	}

	/**
	 * Called to work out the differences between the old set of tree rows and the
	 * new one set.
	 * 
	 * This will only send down DOM changes for the differences
	 *  
	 */
	private void sendTreeRowDiffs(RenderContext rc, Element oldTreeDivE, Element newTreeDivE) {

		Map visitedMap = new HashMap();
		Map deleteMap = new HashMap();
		Map addMap = new HashMap();
		List addList = new ArrayList();
		List deleteList = new ArrayList();
		
		//---------------------------------
		// TREE ROW ANALYSIS
		//---------------------------------
		
		// OLD TREE ROWS
		Element[] lastTableRows = getElementsByTagNameOneLevel(oldTreeDivE,"table");
		
		// NEW TREE ROWS
		Element[] newTableRows = getElementsByTagNameOneLevel(newTreeDivE,"table");

		//
		// look for added and changed element
		for (int i = 0; i < newTableRows.length; i++) {
			Element newTreeRow = newTableRows[i];
			String newTreeRowId = newTreeRow.getAttribute("id");
			visitedMap.put(newTreeRowId, newTreeRow);

			Element lastTreeRow = getElementById(newTreeRowId, lastTableRows);
			if (lastTreeRow == null) {
				//
				// ADDS - must be a a new table row
				addList.add(newTreeRow);
				addMap.put(newTreeRowId,newTreeRow);
			} else if (!areNodesEqual(newTreeRow, lastTreeRow)) {
				//
				// CHANGES - an exisitng row that has changed. 
				//
				// Since Echo2 needs to do a dom-remove and then a dom-add, it 
				// really is both an add and a delete
				//
				addList.add(newTreeRow);
				addMap.put(newTreeRowId,newTreeRow);
				deleteList.add(newTreeRow);
				deleteMap.put(newTreeRowId,newTreeRow);
			}
		}
		// look for deleted elements
		for (int i = 0; i < lastTableRows.length; i++) {
			Element lastTreeRow = lastTableRows[i];
			String lastTreeRowId = lastTreeRow.getAttribute("id");
			if (visitedMap.get(lastTreeRowId) == null) {
				//
				// DELETES - present on the old but not in the new
				deleteList.add(lastTreeRow);
				deleteMap.put(lastTreeRowId,lastTreeRow);
			}
		}
		//
		//---------------------------------
		// DOM UPDATES BASED ON DIFFERENCES
		//---------------------------------
		//	
		String parentDivId = newTreeDivE.getAttribute("id");
		Document doc = rc.getServerMessage().getDocument();

		// DELETES
		for (Iterator iter = deleteList.iterator(); iter.hasNext();) {
			Element delTreeRowE = (Element) iter.next();
			String treeRowId = delTreeRowE.getAttribute("id");
			DomUpdate.renderElementRemove(rc.getServerMessage(),treeRowId);
		}
		//
		// ADDS
		for (Iterator iter = addList.iterator(); iter.hasNext();) {
			Element addTreeRowE = (Element) iter.next();
			
			//
			// if we are to append the new additions and send them down
			// we must clone them otherwise they are removed from
			// out "state of play" Elements which we use to track old
			// state.
			//
			DocumentFragment docFrag = doc.createDocumentFragment();
			Node clonedAddTreeRowE = addTreeRowE.cloneNode(true);
			docFrag.appendChild(clonedAddTreeRowE);
			
				
			/*
			 * Echo2 uses the standard DOM insertBefore() semantics. However this does not quite
			 * suit as as we would rather and insertAfter() mechanism.  None the less we need
			 * to move forward to find a "solid" table row singling to use as the sibling for the
			 * insertBefore() operation.  If we can find one going forwards, then it must require a
			 * parent appendChild() operation instead.
			 */
			Element siblingTreeRowE = (Element) addTreeRowE.getNextSibling();
			while (siblingTreeRowE != null) {
				String siblingId = siblingTreeRowE.getAttribute("id");
				//
				// if the row has been neither deleted nor newly added then its a spot for the
				// new row to be inserted before
				if (deleteMap.get(siblingId) == null && addMap.get(siblingId) == null) {
					Element domAddElementDirective = DomUpdate.renderElementAdd(rc.getServerMessage());
					DomUpdate.renderElementAddContent(rc.getServerMessage(),domAddElementDirective, parentDivId,siblingId,docFrag);
					break;
				}
				siblingTreeRowE = (Element) siblingTreeRowE.getNextSibling();
			}
			if (siblingTreeRowE == null) {
				// we have navigated forward without finding a spot to put the new row
				// so it must be a parent child append.
				Element domAddElementDirective = DomUpdate.renderElementAdd(rc.getServerMessage());
				DomUpdate.renderElementAddContent(rc.getServerMessage(),domAddElementDirective, parentDivId,docFrag);
			}
		}
	}
	
	/**
	 * The events can be expansion or contraction events as well as selection events
	 * 
	 * @see nextapp.echo2.webcontainer.ActionProcessor#processAction(nextapp.echo2.webcontainer.ContainerInstance,
	 *      nextapp.echo2.app.Component, org.w3c.dom.Element)
	 */
	public void processAction(ContainerInstance ci, Component component, Element actionElement) {
		Tree tree = (Tree) component;
		TreeModel model = (TreeModel) tree.getRenderProperty(Tree.PROPERTY_MODEL);
		if (model == null)
			return;
		String actionName = actionElement.getAttribute(ActionProcessor.ACTION_NAME);
		String actionValue = actionElement.getAttribute(ActionProcessor.ACTION_VALUE);
		// its a tree path
		String[] paths = TokenizerKit.tokenize(actionValue, "[]");
		Object[] nodes = new Object[paths.length];
		Object parent = null;
		for (int i = 0; i < paths.length; i++) {
			if (i == 0) {
				nodes[i] = model.getRoot();
			} else {
				int childIndex = Integer.parseInt(paths[i]);
				Object node = model.getChild(parent, childIndex);
				nodes[i] = node;
			}
			parent = nodes[i];
		}
		TreePath nodePath = new TreePath(nodes);
		ci.getUpdateManager().getClientUpdateManager().setComponentAction(component, actionName, nodePath);
	}

	/**
	 * Adds the event handling for toggling the nodes expanded or contracted.
	 */
	private String addToggleEvents(RenderingContext rc, CssStyle cssStyle, Tree tree, Object treeNode, TreePath treeNodePath) {
		String eventElementId = createEventId(tree, "expando", "toggle", treeNode, treeNodePath);
		if (tree.isRenderEnabled()) {
			cssStyle.setAttribute("cursor", "pointer");
			rc.renderEventAdd("click", eventElementId, "EchoPointTree.doClickAction");
		}
		return eventElementId;
	}

	/**
	 * Adds the event handling for selecting nodes
	 */
	private String addSelectEvents(RenderingContext rc, CssStyle cssStyle, Tree tree, Object treeNode, TreePath treeNodePath, String cellType) {
		String eventElementId = createEventId(tree, cellType, "select", treeNode, treeNodePath);
		if (tree.isRenderEnabled()) {
			cssStyle.setAttribute("cursor", "pointer");
			rc.renderEventAdd("click", eventElementId, "EchoPointTree.doClickAction");
		}
		return eventElementId;
	}

	/**
	 * Creates a identifier to unique identify a node and its containing Tree.
	 */
	private String createEventId(Tree tree, String cellType, String baseAction, Object treeNode, TreePath treeNodePath) {
		TreeModel model = (TreeModel) tree.getRenderProperty(Tree.PROPERTY_MODEL);

		String pathId = TreeRenderer.createPathId(treeNodePath, model);
		String treeId = "tree|" + ContainerInstance.getElementId(tree) + '|' + cellType;
		String eventElementId = treeId + '|' + baseAction + '|' + pathId;
		return eventElementId;
	}

	/**
	 * Called to work out whether 2 DOM Nodes are equal in the sense they have
	 * the same name, attributes and that all their children are equal in the
	 * same sense as well.
	 */
	private boolean areNodesEqual(Node n1, Node n2) {
		if (n1 == n2)
			return true;
		if (n1.equals(n2))
			return true;

		boolean areEq = true;

		areEq = areEq && n1.getClass().equals(n2.getClass());
		areEq = areEq && n1.getNodeType() == n2.getNodeType();

		if (areEq && (n1 instanceof Text || n1 instanceof Attr)) {
			areEq = areEq && n1.getNodeValue().equals(n2.getNodeValue());
			// we dont decend any lower for Attr and Text nodes
			// as this is a short cut
			return areEq;
		}
		areEq = areEq && n1.getNodeName().equals(n2.getNodeName());
		areEq = areEq && areNamedNodeMapsEqual(n1.getAttributes(), n2.getAttributes());
		if (areEq) {
			// child nodes next
			NodeList nodeList1 = n1.getChildNodes();
			NodeList nodeList2 = n2.getChildNodes();
			int len1 = nodeList1.getLength();
			int len2 = nodeList2.getLength();
			if (len1 != len2) {
				return false;
			}
			for (int i = 0; i < len1; i++) {
				Node childNode1 = nodeList1.item(i);
				Node childNode2 = nodeList2.item(i);
				if (!areNodesEqual(childNode1, childNode2)) {
					return false;
				}
			}
		}
		return areEq;
	}

	/**
	 * Called to work out if two NamedNodeMaps contain Nodes that are equal
	 * in the same sense as areNodesEqual().
	 */
	private boolean areNamedNodeMapsEqual(NamedNodeMap attMap1, NamedNodeMap attMap2) {
		if (attMap1 == attMap2)
			return true;
		if (attMap1 == null && attMap2 != null)
			return false;
		if (attMap1 != null && attMap2 == null)
			return false;
		int len1 = attMap1.getLength();
		int len2 = attMap2.getLength();
		if (len1 != len2) {
			return false;
		}
		for (int i = 0; i < len1; i++) {
			Node val1 = attMap1.item(i);
			Node val2 = attMap2.item(i);
			if (!areNodesEqual(val1, val2)) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Looks inside an array of Elements and returns the first one with the
	 * matching "id" attribute
	 */
	private Element getElementById(String id, Element[] elems) {
		if (id == null)
			return null;
		for (int i = 0; i < elems.length; i++) {
			if (id.equals(elems[i].getAttribute("id"))) {
				return elems[i];
			}
		}
		return null;
	}
	
	/**
	 * Gets and array of Elements from a parent element via tag name but decends only one level
	 */
	private Element[] getElementsByTagNameOneLevel(Element parentE, String tagName) {
		List tagList = new LinkedList();
		for (Node node = parentE.getFirstChild(); node != null; node = node.getNextSibling()) {
			if (node instanceof Element) {
				String testTagName = ((Element) node).getTagName();
				if (tagName.equals(testTagName)) {
					tagList.add(node);
				}
			}
		}
		return (Element[]) tagList.toArray(new Element[tagList.size()]);
	}

}
