/**
 * License Agreement.
 *
 * Rich Faces - Natural Ajax for Java Server Faces (JSF)
 *
 * 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.ajax4jsf.application;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import javax.faces.FactoryFinder;
import javax.faces.application.StateManager;
import javax.faces.component.UIComponent;
import javax.faces.component.UIViewRoot;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.render.RenderKit;
import javax.faces.render.RenderKitFactory;
import javax.faces.render.ResponseStateManager;

import org.ajax4jsf.context.AjaxContext;
import org.ajax4jsf.event.AjaxPhaseListener;
import org.apache.commons.collections.map.LRUMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * @author shura
 * 
 */
public class AjaxStateManager extends StateManager {

	private static final Class[] STATE_MANAGER_ARGUMENTS = new Class[] { StateManager.class };

	protected static final int DEFAULT_NUMBER_OF_VIEWS = 16;

	private static final String VIEW_STATES_MAP = AjaxStateManager.class
			.getName()
			+ ".VIEW_STATES_MAP";

	private static final Object VIEW_SEQUENCE = AjaxStateManager.class
			.getName()
			+ ".VIEW_SEQUENCE";

	private final StateManager parent;

	private StateManager seamStateManager;

	private final ComponentsLoader componentLoader;

	private volatile int viewSequence = 0;

	private Object viewSequenceMutex = "MUTEX";

	private static final Log _log = LogFactory.getLog(AjaxStateManager.class);

	/**
	 * @param parent
	 */
	public AjaxStateManager(StateManager parent) {
		super();
		this.parent = parent;
		componentLoader = new ComponentsLoaderImpl();
		// HACK - Seam perform significant operations before save tree state.
		// Try to create it instance by reflection,
		// to call in real state saving operations.
		ClassLoader classLoader = Thread.currentThread()
				.getContextClassLoader();
		if (null == classLoader) {
			classLoader = AjaxStateManager.class.getClassLoader();
		}
		try {
			Class seamStateManagerClass = classLoader
					.loadClass("org.jboss.seam.jsf.SeamStateManager");
			Constructor constructor = seamStateManagerClass
					.getConstructor(STATE_MANAGER_ARGUMENTS);
			seamStateManager = (StateManager) constructor
					.newInstance(new Object[] { new StateManager() {

						protected Object getComponentStateToSave(
								FacesContext arg0) {
							// do nothing
							return null;
						}

						protected Object getTreeStructureToSave(
								FacesContext arg0) {
							// do nothing
							return null;
						}

						protected void restoreComponentState(FacesContext arg0,
								UIViewRoot arg1, String arg2) {
							// do nothing

						}

						protected UIViewRoot restoreTreeStructure(
								FacesContext arg0, String arg1, String arg2) {
							// do nothing
							return null;
						}

						public UIViewRoot restoreView(FacesContext arg0,
								String arg1, String arg2) {
							// do nothing
							return null;
						}

						public SerializedView saveSerializedView(
								FacesContext arg0) {
							// delegate to enclosed class method.
							return buildSerializedView(arg0);
						}

						public void writeState(FacesContext arg0,
								SerializedView arg1) throws IOException {
							// do nothing
						}

					} });
			if (_log.isDebugEnabled()) {
				_log.debug("Create instance of the SeamStateManager");
			}
		} catch (Exception e) {
			seamStateManager = null;
			if (_log.isDebugEnabled()) {
				_log.debug("SeamStateManager is not present");
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.faces.application.StateManager#getComponentStateToSave(javax.faces.context.FacesContext)
	 */
	protected Object getComponentStateToSave(FacesContext context) {
		throw new UnsupportedOperationException();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.faces.application.StateManager#getTreeStructureToSave(javax.faces.context.FacesContext)
	 */
	protected Object getTreeStructureToSave(FacesContext context) {
		throw new UnsupportedOperationException();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.faces.application.StateManager#restoreComponentState(javax.faces.context.FacesContext,
	 *      javax.faces.component.UIViewRoot, java.lang.String)
	 */
	protected void restoreComponentState(FacesContext context,
			UIViewRoot viewRoot, String renderKitId) {
		throw new UnsupportedOperationException();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.faces.application.StateManager#restoreTreeStructure(javax.faces.context.FacesContext,
	 *      java.lang.String, java.lang.String)
	 */
	protected UIViewRoot restoreTreeStructure(FacesContext context,
			String viewId, String renderKitId) {
		throw new UnsupportedOperationException();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.faces.application.StateManager#writeState(javax.faces.context.FacesContext,
	 *      javax.faces.application.StateManager.SerializedView)
	 */
	public void writeState(FacesContext context, SerializedView state)
			throws IOException {
		parent.writeState(context, state);
		if (_log.isDebugEnabled()) {
			_log.debug("Write view state to the response");
		}
		context.getExternalContext().getRequestMap().put(
				AjaxPhaseListener.VIEW_STATE_SAVED_PARAM, Boolean.TRUE);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.faces.application.StateManager#restoreView(javax.faces.context.FacesContext,
	 *      java.lang.String, java.lang.String)
	 */
	public UIViewRoot restoreView(FacesContext context, String viewId,
			String renderKitId) {
		UIViewRoot viewRoot = null;
		ResponseStateManager responseStateManager = getRenderKit(context,
				renderKitId).getResponseStateManager();
		TreeStrutureNode treeStructure = null;
		Object[] state = null;
		if (isSavingStateInClient(context)) {
			treeStructure = (TreeStrutureNode) responseStateManager
					.getTreeStructureToRestore(context, viewId);
			// viewRoot = parent.restoreView(context, viewId, renderKitId);
			state = (Object[]) responseStateManager
					.getComponentStateToRestore(context);
		} else {
			Object[] serializedView = restoreStateFromSession(context, viewId,
					renderKitId);
			if (null != serializedView) {
				treeStructure = (TreeStrutureNode) serializedView[0];
				state = (Object[]) serializedView[1];
			}
		}
		if (null != treeStructure) {
			viewRoot = (UIViewRoot) treeStructure.restore(componentLoader);
			if (null != viewRoot && null != state) {
				viewRoot.processRestoreState(context, state[0]);
				restoreAdditionalState(context, state[1]);
			}
		}
		return viewRoot;

	}

	protected Object[] restoreStateFromSession(FacesContext context,
			String viewId, String renderKitId) {
		Object[] restoredState = null;
		Object id = getRenderKit(context, renderKitId)
				.getResponseStateManager().getTreeStructureToRestore(context,
						viewId);
		ExternalContext externalContext = context.getExternalContext();
		Object session = externalContext.getSession(false);
		if (null == session) {
			if (_log.isDebugEnabled()) {
				_log.debug("Can't restore view state : session expired");
			}
		} else {
			synchronized (session) {
				LRUMap viewStates = (LRUMap) externalContext.getSessionMap()
						.get(VIEW_STATES_MAP);
				if (null != viewStates) {
					LRUMap logicalStates = (LRUMap) viewStates.get(viewId);
					if (null != logicalStates) {
						if (null != id) {
							restoredState = (Object[]) logicalStates.get(id);
							externalContext.getRequestMap().put(VIEW_SEQUENCE,
									id);
							if (null == restoredState) {
								if (_log.isDebugEnabled()) {
									_log
											.debug("No saved view state found for a Id "
													+ id
													+ ". Restore last saved state");
								}
								restoredState = (Object[]) logicalStates
										.get(logicalStates.lastKey());
							}
						} else {
							if (_log.isDebugEnabled()) {
								_log
										.debug("No version Id for a saved view state in request. Restore last saved state");
							}
							restoredState = (Object[]) logicalStates
									.get(logicalStates.lastKey());
						}
					} else if (_log.isDebugEnabled()) {
						_log
								.debug("Can't restore view state : no saved states for a ViewId "
										+ viewId);
					}

				} else if (_log.isDebugEnabled()) {
					_log
							.debug("Can't restore view state : no saved view states in session");
				}

			}
		}

		return restoredState;
	}

	public SerializedView saveSerializedView(FacesContext context) {
		if (null == seamStateManager) {
			return buildSerializedView(context);
		} else {
			// Delegate save method to seam State Manager.
			return seamStateManager.saveSerializedView(context);
		}
	}

	/**
	 * @param context
	 * @return
	 */
	protected SerializedView buildSerializedView(FacesContext context) {
		SerializedView serializedView = null;
		UIViewRoot viewRoot = context.getViewRoot();
		if (null != viewRoot && (!viewRoot.isTransient()) ) {
			TreeStrutureNode treeStructure = new TreeStrutureNode();
			treeStructure.apply(context, viewRoot, new HashSet());
			Object treeState = viewRoot.processSaveState(context);
			Object state[] = { treeState, getAdditionalState(context) };
			if (isSavingStateInClient(context)) {
				serializedView = new SerializedView(treeStructure, state);
			} else {
				serializedView = saveStateInSession(context, treeStructure,
						state);
			}

		}
		return serializedView;
	}

	/**
	 * @param context
	 * @param treeStructure
	 * @param state
	 * @return
	 */
	protected SerializedView saveStateInSession(FacesContext context,
			Object treeStructure, Object state) {
		SerializedView serializedView;
		UIViewRoot viewRoot = context.getViewRoot();
		ExternalContext externalContext = context.getExternalContext();
		Object session = externalContext.getSession(true);
		synchronized (session) {
			LRUMap viewStates = (LRUMap) externalContext.getSessionMap().get(
					VIEW_STATES_MAP);
			if (null == viewStates) {
				viewStates = new LRUMap(getNumberOfViews(externalContext));
				externalContext.getSessionMap()
						.put(VIEW_STATES_MAP, viewStates);
			}
			Object id = getNextViewId(context);
			LRUMap logicalViewsMap = (LRUMap) viewStates.get(viewRoot
					.getViewId());
			if (null == logicalViewsMap) {
				logicalViewsMap = new LRUMap(getNumberOfViews(externalContext));
			}
			// Renew last seen view.
			viewStates.put(viewRoot.getViewId(), logicalViewsMap);
			logicalViewsMap.put(id, new Object[] { treeStructure, state });
			serializedView = new SerializedView(id, null);
		}
		return serializedView;
	}

	protected Object getAdditionalState(FacesContext context) {
		return null;
	}

	protected void restoreAdditionalState(FacesContext context, Object state) {

	}

	protected Object getNextViewId(FacesContext context) {
		AjaxContext ajaxContext = AjaxContext.getCurrentInstance(context);
		if (ajaxContext.isAjaxRequest(context)) {
			Object id = context.getExternalContext().getRequestMap().get(
					VIEW_SEQUENCE);
			if (null != id) {
				return id;
			}
		}
		synchronized (viewSequenceMutex) {
			if (viewSequence++ == Character.MAX_VALUE) {
				viewSequence = 0;
			}
		}
		return UIViewRoot.UNIQUE_ID_PREFIX + ((int) viewSequence);
	}

	protected int getNumberOfViews(ExternalContext externalContext) {
		return DEFAULT_NUMBER_OF_VIEWS;
	}

	protected RenderKit getRenderKit(FacesContext context, String renderKitId) {
		RenderKit renderKit = context.getRenderKit();
		if (null == renderKit) {
			RenderKitFactory factory = (RenderKitFactory) FactoryFinder
					.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
			renderKit = factory.getRenderKit(context, renderKitId);
		}
		return renderKit;
	}

	protected static final class TreeStrutureNode implements Externalizable {
		/**
		 * TODO - implement Externalizable to reduce serialized state.
		 */
		private static final long serialVersionUID = -9038742487716977911L;

		private static final String NULL_ID = "";

		private Map facets = null;

		private List children = null;

		private String type;

		private String id;

		public TreeStrutureNode() {
		}

		public void apply(FacesContext context, UIComponent component,
				Set uniqueIds) {
			type = component.getClass().getName();
			id = component.getId();
			String clientId = component.getClientId(context);
			if (!uniqueIds.add(clientId)) {
				throw new IllegalStateException("duplicate Id for a component "
						+ clientId);
			}
			Map componentFacets = component.getFacets();
			for (Iterator i = componentFacets.entrySet().iterator(); i
					.hasNext();) {
				Entry element = (Entry) i.next();
				UIComponent f = (UIComponent) element.getValue();
				if (!f.isTransient()) {
					TreeStrutureNode facet = new TreeStrutureNode();
					facet.apply(context, f, uniqueIds);
					if (null == facets) {
						facets = new HashMap();
					}
					facets.put(element.getKey(), facet);

				}
			}
			for (Iterator i = component.getChildren().iterator(); i.hasNext();) {
				UIComponent child = (UIComponent) i.next();
				if (!child.isTransient()) {
					TreeStrutureNode t = new TreeStrutureNode();
					t.apply(context, child, uniqueIds);
					if (null == children) {
						children = new ArrayList();
					}
					children.add(t);

				}
			}
		}

		public UIComponent restore(ComponentsLoader loader) {
			UIComponent component;
			component = loader.createComponent(type);
			component.setId(id);
			if (null != facets) {
				for (Iterator i = facets.entrySet().iterator(); i.hasNext();) {
					Entry element = (Entry) i.next();
					UIComponent facet = ((TreeStrutureNode) element.getValue())
							.restore(loader);
					component.getFacets().put(element.getKey(), facet);
				}

			}
			if (null != children) {
				for (Iterator i = children.iterator(); i.hasNext();) {
					TreeStrutureNode node = (TreeStrutureNode) i.next();
					UIComponent child = node.restore(loader);
					component.getChildren().add(child);
				}

			}
			return component;
		}

		/**
		 * @return the facets
		 */
		public Map getFacets() {
			return facets;
		}

		/**
		 * @param facets
		 *            the facets to set
		 */
		public void setFacets(Map facets) {
			this.facets = facets;
		}

		/**
		 * @return the children
		 */
		public List getChildren() {
			return children;
		}

		/**
		 * @param children
		 *            the children to set
		 */
		public void setChildren(List children) {
			this.children = children;
		}

		/**
		 * @return the type
		 */
		public String getType() {
			return type;
		}

		/**
		 * @param type
		 *            the type to set
		 */
		public void setType(String type) {
			this.type = type;
		}

		/**
		 * @return the id
		 */
		public String getId() {
			return id;
		}

		/**
		 * @param id
		 *            the id to set
		 */
		public void setId(String id) {
			this.id = id;
		}

		public void readExternal(ObjectInput in) throws IOException,
				ClassNotFoundException {
			type = in.readUTF();
			id = in.readUTF();
			if (NULL_ID.equals(id)) {
				id = null;
			}
			int facetsSize = in.readInt();
			if (facetsSize > 0) {
				facets = new HashMap(facetsSize);
				for (int i = 0; i < facetsSize; i++) {
					String facetName = in.readUTF();
					TreeStrutureNode facet = new TreeStrutureNode();
					facet.readExternal(in);
					facets.put(facetName, facet);
				}
			}
			int childrenSize = in.readInt();
			if (childrenSize > 0) {
				children = new ArrayList(childrenSize);
				for (int i = 0; i < childrenSize; i++) {
					TreeStrutureNode child = new TreeStrutureNode();
					child.readExternal(in);
					children.add(child);
				}
			}
		}

		public void writeExternal(ObjectOutput out) throws IOException {
			out.writeUTF(type);
			out.writeUTF(null == id ? NULL_ID : id);
			if (null != facets) {
				out.writeInt(facets.size());
				for (Iterator i = facets.entrySet().iterator(); i.hasNext();) {
					Map.Entry entry = (Map.Entry) i.next();
					out.writeUTF((String) entry.getKey());
					TreeStrutureNode node = (TreeStrutureNode) entry.getValue();
					node.writeExternal(out);
				}

			} else {
				out.writeInt(0);
			}
			if (null != children) {
				out.writeInt(children.size());
				for (Iterator i = children.iterator(); i.hasNext();) {
					TreeStrutureNode child = (TreeStrutureNode) i.next();
					child.writeExternal(out);
				}

			} else {
				out.writeInt(0);
			}
		}
	}

}
