/**
 * License Agreement.
 *
 *  JBoss RichFaces - Ajax4jsf Component Library
 *
 * Copyright (C) 2007  Exadel, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1 as published by the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
 */

package org.richfaces.renderkit;

import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.faces.FacesException;
import javax.faces.component.NamingContainer;
import javax.faces.component.UIComponent;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.el.MethodBinding;

import org.ajax4jsf.javascript.JSFunction;
import org.ajax4jsf.javascript.JSReference;
import org.ajax4jsf.javascript.ScriptUtils;
import org.ajax4jsf.model.DataVisitor;
import org.ajax4jsf.renderkit.AjaxRendererUtils;
import org.ajax4jsf.renderkit.ComponentsVariableResolver;
import org.richfaces.component.UITree;
import org.richfaces.component.UITreeNode;
import org.richfaces.component.nsutils.NSUtils;
import org.richfaces.component.state.TreeState;
import org.richfaces.component.state.TreeStateAdvisor;
import org.richfaces.model.LastElementAware;
import org.richfaces.model.TreeRange;
import org.richfaces.model.TreeRowKey;

public abstract class TreeRendererBase extends CompositeRenderer {

	protected final static Comparator treeRowKeyComparator = new Comparator() {

		public int compare(Object key1, Object key2) {
			TreeRowKey treeRowKey1 = (TreeRowKey) key1;
			TreeRowKey treeRowKey2 = (TreeRowKey) key2;

			if (treeRowKey1 == null) {
				if (treeRowKey2 == null) {
					return 0;
				} else {
					return -1;
				}
			} else {
				if (treeRowKey2 == null) {
					return 1;
				}
			}

			Iterator iterator1 = treeRowKey1.iterator();
			Iterator iterator2 = treeRowKey2.iterator();

			while (iterator1.hasNext() && iterator2.hasNext()) {
				String id1 = iterator1.next().toString();
				String id2 = iterator2.next().toString();

				int cr = id1.compareTo(id2);
				if (cr != 0) {
					return cr;
				}
			}

			if (iterator1.hasNext()) {
				return 1;
			} else if (iterator2.hasNext()) {
				return -1;
			} else {
				return 0;
			}
		}
	};

	private final class RendererDataModelEventNavigator extends
	TreeDataModelEventNavigator {
		private final FacesContext context;
		private final UITree tree;
		private final Flag droppedDownToLevelFlag;
		private final ResponseWriter writer;

		private String clientId;
		private boolean expanded;
		private boolean showLines;

		private RendererDataModelEventNavigator(UITree tree,
				TreeRowKey floatingKey, FacesContext context, Flag droppedDownToLevelFlag) {
			super(tree, floatingKey);
			this.context = context;
			this.tree = tree;
			this.droppedDownToLevelFlag = droppedDownToLevelFlag;
			this.writer = context.getResponseWriter();

			this.expanded = this.tree.isExpanded();
			this.showLines = this.tree.isShowConnectingLines();
			this.clientId = getClientId();
		}

		public void followRowKey(FacesContext context, TreeRowKey newRowKey) throws IOException {
			super.followRowKey(context, newRowKey);

			this.expanded = this.tree.isExpanded();
			this.clientId = getClientId();
		}

		private String getClientId() {
			Object rowKey = tree.getRowKey();
			String id;
			if (rowKey == null) {
				id = tree.getClientId(context)
				+ NamingContainer.SEPARATOR_CHAR;
			} else {
				id = tree.getNodeFacet().getClientId(context)
				+ NamingContainer.SEPARATOR_CHAR;
			}
			return id;
		}

		public void afterUp(int levels) throws IOException {
			Context c = droppedDownToLevelFlag.getContext();
			if (c != null) {
				c.setHasChildren(false);
				openDiv(c);
				closeDiv();
				droppedDownToLevelFlag.setContext(null);
			}

			//writer.write("** afterUp **");
			for (int i = 0; i < levels; i++) {
				closeDiv();
			}

			//if (!isLastElement) closeDiv();
		}

		public void afterDown() throws IOException {
		}		

		public void beforeDown() throws IOException {
			Context c = droppedDownToLevelFlag.getContext();
			droppedDownToLevelFlag.setContext(null);
			openDiv(c);
			//writer.write("** beforeDown **");

			//if (this.getRowKey()==null ) openDiv();

		}

		public void beforeUp(int levels) throws IOException {
		}

		public void openDiv(Context context) throws IOException {
			writer.startElement("div", tree);

			if (context == null) {
				context = new Context();
				context.setLast(this.actualLast);
				context.setClientId(this.clientId);
				context.setExpanded(this.expanded);
				context.setRowKey(this.getRowKey());
			}

			getUtils().writeAttribute(writer, "id", context.getClientId() + "childs");

			if (!context.isExpanded() || !context.isHasChildren()) {
				getUtils().writeAttribute(writer, "style", "display: none;");
			} else {
				if (tree.isShowConnectingLines()) {
					TreeRowKey floatingKey = getFloatingKey();
					//need the expression only for AJAX update root
					if (floatingKey != null && floatingKey.equals(context.getRowKey())) {
						String expression = "background-image:expression(this.nextSibling ? '' : 'none')";
						getUtils().writeAttribute(writer, "style", expression);
					}
				}
			}

			String styleClasses = "";
			if (context.getRowKey() != null) {
				styleClasses = "dr-tree-layout-on dr-tree-h-ic-div rich-tree-node-children rich-tree-node-cildren";
				if (!context.isLast() && showLines) styleClasses += " dr-tree-h-ic-line";
			}
			if (styleClasses!="") getUtils().writeAttribute(writer, "class", styleClasses);
		}

		public void closeDiv() throws IOException {
			writer.endElement("div");
		}
	}

	private class DataVisitorWithLastElement implements DataVisitor,
	LastElementAware {

		private boolean isLastElement = false;

		private final Flag flag;

		private final UITree tree;

		private final RendererDataModelEventNavigator navigator;

		private TreeStateAdvisor methodBindingAdvisor = null;

		private Object floatingKey;

		private DataVisitorWithLastElement(Flag flag, UITree tree,
				RendererDataModelEventNavigator navigator, Object rowKey) {
			this.flag = flag;
			this.tree = tree;
			this.navigator = navigator;
			this.floatingKey = rowKey;
		}

		public void process(FacesContext context, Object rowKey, Object argument)
		throws IOException {
			TreeRowKey<?> treeRowKey = (TreeRowKey<?>) rowKey;

			processAdvisors(context, treeRowKey);

			navigator.followRowKey(context, treeRowKey);

			Context c = flag.getContext();
			if (c != null) {
				c.setHasChildren(false);
				navigator.openDiv(c);
				navigator.closeDiv();
			}

			UITreeNode nodeFacet = tree.getNodeFacet();
			Object oldAttrValue = nodeFacet.getAttributes().get("isLastElement");
			Object oldAjaxRootAttrValue = nodeFacet.getAttributes().get("isAjaxUpdateRoot");
			try {
				nodeFacet.getAttributes().put("isLastElement", new Boolean(isLastElement));
				nodeFacet.getAttributes().put("isAjaxUpdateRoot", new Boolean(floatingKey != null && floatingKey.equals(rowKey)));
				ResponseWriter writer = context.getResponseWriter();
				if (isLastElement && this.navigator.showLines) {
					writer.startElement("p", tree);
					writer.writeAttribute("class", "dr-tree-last-node-marker", null);
					writer.endElement("p");
				}

				renderChild(context, nodeFacet);


				c = new Context();
				c.setClientId(nodeFacet.getClientId(context) + NamingContainer.SEPARATOR_CHAR);
				c.setLast(this.isLastElement);
				c.setExpanded(tree.isExpanded());
				c.setRowKey(tree.getRowKey());
				flag.setContext(c);

				//writer.write("** after renderChild **");
				//navigator.openDiv();
			} finally {
				if (oldAttrValue != null) {
					nodeFacet.getAttributes().put("isLastElement", oldAttrValue);
				} else {
					nodeFacet.getAttributes().remove("isLastElement");
				}

				if (oldAjaxRootAttrValue != null) {
					nodeFacet.getAttributes().put("isAjaxUpdateRoot", oldAjaxRootAttrValue);
				} else {
					nodeFacet.getAttributes().remove("isAjaxUpdateRoot");
				}
			}
		}

		public void setLastElement() {
			isLastElement = true;
			navigator.setLastElement();
		}

		public void resetLastElement() {
			isLastElement = false;
			navigator.resetLastElement();
		}

		public void processAdvisors(FacesContext context, TreeRowKey rowKey) throws IOException {
			TreeState state = (TreeState) tree.getComponentState();
			TreeStateAdvisor stateAdvisor = (TreeStateAdvisor)tree.getStateAdvisor();

			if (null == stateAdvisor) {
				if (null == methodBindingAdvisor) {
					methodBindingAdvisor = new TreeStateAdvisor() {
						public Boolean adviseNodeOpened(UITree tree) {
							MethodBinding adviseNodeOpened = tree.getAdviseNodeOpened();
							if (null != adviseNodeOpened) {
								return (Boolean) adviseNodeOpened.invoke(FacesContext.getCurrentInstance(), new Object[] {tree});
							}
							return null;
						}

						public Boolean adviseNodeSelected(UITree tree) {
							MethodBinding adviseNodeSelected = tree.getAdviseNodeSelected();
							if (null != adviseNodeSelected) {
								return (Boolean) adviseNodeSelected.invoke(FacesContext.getCurrentInstance(), new Object [] {tree});
							}
							return null;
						}
					};
				}
				stateAdvisor = methodBindingAdvisor;
			}

			Boolean adviseOpened = stateAdvisor.adviseNodeOpened(tree); 
			if (null != adviseOpened) {
				if (adviseOpened.booleanValue()) {
					state.makeExpanded(rowKey);
				} else {
					state.makeCollapsed(rowKey);
				}
			}
			
			Boolean adviseSelected = stateAdvisor.adviseNodeSelected(tree); 
			if (null != adviseSelected) {
				if (adviseSelected.booleanValue()) {
					if (!state.isSelected(rowKey)) {
						state.setSelected(rowKey);
					}
				}
				else {
					if (state.isSelected(rowKey)) {
						state.setSelected(null);
					}
				}
			}
		}
	}

	public TreeRendererBase() {
		super();
		addContributor(DraggableRendererContributor.getInstance());
		addContributor(DropzoneRendererContributor.getInstance());

		addParameterEncoder(DnDParametersEncoder.getInstance());
	}

	public void writeNamespace(FacesContext context, UIComponent component) throws IOException {
		NSUtils.writeNameSpace(context, component);
	}

	public void encodeAjaxChildren(FacesContext context, UIComponent component,
			String path, Set ids, Set renderedAreas) throws IOException {
		super.encodeAjaxChildren(context, component, path, ids, renderedAreas);

		try {
			if (component instanceof UITree) {
				UITree tree = (UITree) component;

				String id = path + tree.getId();

				tree.captureOrigValue();
				//Object rowKey = tree.getRowKey();

				boolean encodeScripts = false;

				tree.setRowKey(context, null);

				//we should add xmlns to AJAX response
				//we'll write neutral inner element and add xmlns there
				ResponseWriter responseWriter = context.getResponseWriter();
				responseWriter.startElement("div", tree);
				writeNamespace(context, component);

				List encodedAreaIds = new ArrayList();

				try {
					Set ajaxKeys = tree.getAllAjaxKeys(); 
					if (ajaxKeys != null) {
						List sortedKeys = new ArrayList(ajaxKeys.size());
						sortedKeys.addAll(ajaxKeys);
						Collections.sort(sortedKeys, treeRowKeyComparator);
						Iterator ajaxKeysItr = sortedKeys.iterator();
						TreeRowKey lastKey = null;
						boolean nullRoot = false;

						while (!nullRoot && ajaxKeysItr.hasNext()) {
							TreeRowKey key = (TreeRowKey) ajaxKeysItr.next();

							if (lastKey == null) {
								lastKey = key;
							} else {
								if (!lastKey.isSubKey(key)) {
									lastKey = key;
								} else {
									//skip nodes that's parent nodes have been rendered
									continue;
								}
							}

							if (key == null || key.depth() == 0) {
								nullRoot = true;
								key = null;
							}

							tree.setRowKey(context, key);

							if (key == null || tree.isRowAvailable()) {
								String treeClientId;
								if (key == null) {
									treeClientId = tree.getClientId(context);
								} else {
									treeClientId = tree.getNodeFacet().getClientId(context);
								}

								String treeChildrenId = treeClientId + NamingContainer.SEPARATOR_CHAR + "childs";
								
								writeContent(context, tree, key);
								encodeScripts = true;
								renderedAreas.add(treeClientId);
								encodedAreaIds.add(treeClientId);

								renderedAreas.add(treeChildrenId);
								//encodedAreaIds.add(id+":childs");
							} else {
								String cid = tree.getClientId(context);
								String message = MessageFormat.format(
										"Failed to re-render tree node: {0} due to model data unavailability! " +
										"Maybe parent node should be re-rendered instead?", 
										new Object[] { cid });
								
								ExternalContext externalContext = context.getExternalContext();
								externalContext.log(message);
							}
						}
						//ajaxKeys.clear();
					}
				} catch (Exception e) {
					throw new FacesException(e);
				} finally {
					try {
						tree.setRowKey(context, null);
						tree.restoreOrigValue();
					} catch (Exception e) {
						context.getExternalContext().log(e.getMessage(), e);
					}
				}
				if (encodeScripts) {
					writeScript(context, tree, encodedAreaIds, renderedAreas);

					String inputId = encodeSelectionStateInput(context, tree);
					if (inputId != null) {
						renderedAreas.add(inputId);
					}
				}

				responseWriter.endElement("div");
				tree.clearRequestKeysSet();
			}
		} finally {
			try {
				ComponentsVariableResolver.removeVariables(this, component);
			} catch (Exception e) {
				context.getExternalContext().log(e.getMessage(), e);
			}
		}
	}

	public String encodeSelectionStateInput(FacesContext context, UITree tree) throws IOException {
		String result = "";
		TreeState treeState = (TreeState) tree.getComponentState();
		TreeRowKey selectedNodeKey = treeState.getSelectedNode();
		if (selectedNodeKey != null) {
			Object rowKey = tree.getRowKey();
			try {
				tree.setRowKey(selectedNodeKey);
				if (tree.isRowAvailable()) {
					result = tree.getNodeFacet().getClientId(context);
				}
			} finally {
				try {
					tree.setRowKey(rowKey);
				} catch (Exception e) {
					context.getExternalContext().log(e.getMessage(), e);
				}
			}
		}

		ResponseWriter writer = context.getResponseWriter();
		writer.startElement("input", tree);
		writer.writeAttribute("type", "hidden", null);
		String selectionHolderInputId = tree.getSelectionStateInputName(context);
		writer.writeAttribute("id", selectionHolderInputId, null);
		writer.writeAttribute("name", selectionHolderInputId, null);

		writer.writeAttribute("value", result, null);
		writer.endElement("input");

		return selectionHolderInputId;
	}

	protected String getAjaxScript(FacesContext context, UITree tree) {
		String id = tree.getBaseClientId(context);
		JSFunction function = AjaxRendererUtils
		.buildAjaxFunction(tree, context);
		Map eventOptions = AjaxRendererUtils.buildEventOptions(context, tree);
		Map parameters = (Map) eventOptions.get("parameters");
		parameters.remove(id);
		parameters.put(id + UITree.SELECTED_NODE_PARAMETER_NAME,
				new JSReference("event.selectedNode"));
		function.addParameter(eventOptions);
		StringBuffer buffer = new StringBuffer();
		function.appendScript(buffer);
		buffer.append("; return false;");
		return buffer.toString();
	}

	protected String getScriptContributions(FacesContext context, UITree tree) {
		return super.getScriptContributions(getJavaScriptVarName(context, tree), context, tree);
	}

	protected String getJavaScriptVarName(FacesContext context, UITree tree) {
		String id = tree.getBaseClientId(context);
		return "Richfaces_Tree_" + id.replaceAll("[^A-Za-z0-9_]", "_");
	}

	private void writeScript(FacesContext context, UITree tree, List encodedAreaIds,
			Set renderedAreas) throws IOException {
		final ResponseWriter writer = context.getResponseWriter();
		final String clientId = tree.getBaseClientId(context);

		String scriptId = clientId + NamingContainer.SEPARATOR_CHAR + "script";
		writer.startElement("div", tree);
		getUtils().writeAttribute(writer, "id", scriptId);
		writer.startElement("script", tree);
		getUtils().writeAttribute(writer, "type", "text/javascript");

		String varName = getJavaScriptVarName(context, tree);

		writer.writeText(varName + ".getNodeElements(" + 
				ScriptUtils.toScript(encodedAreaIds) + ");", null);


		writer.endElement("script");
		writer.endElement("div");

		renderedAreas.add(tree.getClientId(context)
				+ NamingContainer.SEPARATOR_CHAR + "input");

		renderedAreas.add(scriptId);

	}

	public void encodeChildren(FacesContext context, UIComponent component)
	throws IOException {

		writeContent(context, (UITree) component, null);
	}

	public void writeContent(final FacesContext context, final UITree input)
	throws IOException {
		writeContent(context, input, null);
	}

	public void writeContent(final FacesContext context, final UITree input,
			TreeRowKey key) throws IOException {
		// simple flag can be used here because
		// we cannot jump more than one level down until next node
		// when rendering
		Flag droppedDownToLevelFlag = new Flag();

		TreeRowKey rowKey = (TreeRowKey) key;

		//Object savedRowKey = input.getRowKey();
		try {
			input.captureOrigValue();

			input.setRowKey(context, key);

			RendererDataModelEventNavigator levelNavigator = new RendererDataModelEventNavigator(input, rowKey, context,
					droppedDownToLevelFlag);

			final TreeRange stateRange = (TreeRange) input.getComponentState().getRange();
			TreeRange treeRange = new TreeRange() {

				public boolean processChildren(TreeRowKey rowKey) {
					return stateRange.processChildren(rowKey);
				}

				public boolean processNode(TreeRowKey rowKey) {
					Object currentKey = input.getRowKey();

					if (currentKey == null ? rowKey != null : !currentKey.equals(rowKey)) {
						//currentKey NE rowKey
						input.setRowKey(context, rowKey);
					}

					UITreeNode nodeFacet = input.getNodeFacet();
					if (!nodeFacet.isRendered()) {
						return false;
					}

					return stateRange.processNode(rowKey);
				}

			};

			input.transferQueuedNode();
			
			//TODO should render if current node not in range?
			input.walk(context, new DataVisitorWithLastElement(droppedDownToLevelFlag, input,
					levelNavigator, key), treeRange, key, null);

			levelNavigator.followRowKey(context, null);
		} finally {
			input.setRowKey(context, null);
			input.restoreOrigValue();
		}
	}
}

class Flag {
	private Context context;

	public Context getContext() {
		return context;
	}

	public void setContext(Context context) {
		this.context = context;
	}
}

class Context {
	private String clientId;
	private Object rowKey;
	private boolean expanded;
	private boolean last;
	private boolean hasChildren = true;
	public String getClientId() {
		return clientId;
	}
	public void setClientId(String clientId) {
		this.clientId = clientId;
	}
	public Object getRowKey() {
		return rowKey;
	}
	public void setRowKey(Object rowKey) {
		this.rowKey = rowKey;
	}
	public boolean isExpanded() {
		return expanded;
	}
	public void setExpanded(boolean expanded) {
		this.expanded = expanded;
	}
	public boolean isLast() {
		return last;
	}
	public void setLast(boolean last) {
		this.last = last;
	}
	public boolean isHasChildren() {
		return hasChildren;
	}
	public void setHasChildren(boolean hasChildren) {
		this.hasChildren = hasChildren;
	}
}