/**
 * 
 */
package org.ajax4jsf.portlet.application;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import java.util.Map.Entry;

import javax.faces.FacesException;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.PortletConfig;
import javax.portlet.PortletContext;
import javax.portlet.PortletMode;
import javax.portlet.PortletRequest;
import javax.portlet.PortletSession;
import javax.portlet.PortletSessionUtil;
import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;
import javax.portlet.faces.Bridge;
import javax.portlet.faces.BridgeDefaultViewNotSpecifiedException;
import javax.portlet.faces.BridgeException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;

import org.ajax4jsf.portlet.AjaxFacesPortlet;
import org.ajax4jsf.portlet.context.AbstractExternalContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * @author asmirnov
 * 
 */
public class PortletStateHolder {
	/**
	 * 
	 */
	private static final long serialVersionUID = 3040262189850562853L;

	public static final String STATE_HOLDER = PortletStateHolder.class
			.getName();

	private static final String DEFAULT = "default-";

	private static final Log log = LogFactory.getLog(PortletStateHolder.class);

	private PortletMode lastMode = PortletMode.VIEW;

	public static final String WINDOW_ID_RETRIVER = "org.ajax4jsf.portlet.WINDOW_ID_RETRIVER";

	public static final String STATE_ID_PARAMETER = "javax.faces.portlet.STATE_ID";

	private static final int DEFAULT_MAX_MANAGED_SCOPES = 1000;

	/**
	 * View states for a different portlet modes, Views and serial numbers.
	 */
	private Map<String, PortletViewState> states;

	/**
	 * Private constructor - instance must be stored in the session, and created
	 * on nessesary by factory method.
	 * 
	 * @param context
	 */
	private PortletStateHolder(int max) {
		states = new LRUMap<String, PortletViewState>(max);
	}

	public static void init(PortletContext context) {
		synchronized (context) {
			PortletStateHolder stateHolder = (PortletStateHolder) context
					.getAttribute(STATE_HOLDER);
			if (null == stateHolder) {
				int maxScopes = DEFAULT_MAX_MANAGED_SCOPES;
				String maxScopesParameter = context
						.getInitParameter(Bridge.MAX_MANAGED_REQUEST_SCOPES);
				if (null != maxScopesParameter) {
					maxScopes = Integer.parseInt(maxScopesParameter);
				}
				context.setAttribute(STATE_HOLDER, new PortletStateHolder(
						maxScopes));
			}
		}
	}

	/**
	 * Get instance of portlet states, stored in the session. If no previsious
	 * instance in session, create default value.
	 * 
	 * @param context
	 * @return
	 */
	public static PortletStateHolder getInstance(FacesContext context) {
		ExternalContext externalContext = context.getExternalContext();
		PortletStateHolder stateHolder = (PortletStateHolder) externalContext
				.getApplicationMap().get(STATE_HOLDER);
		if (null == stateHolder) {
			throw new BridgeException("Jsf portlet bridge not initialised");
		}
		return stateHolder;
	}

	/**
	 * Calculate viewId for a portlet content.
	 * 
	 * @param context
	 * @return
	 */
	public String getViewId(FacesContext context) {
		String viewId = null;
		// First - check for a existing viewId.
		if (null != context.getViewRoot()) {
			viewId = context.getViewRoot().getViewId();
		}
		// Second - find stored viewId for a current state and namespace.
		if (null == viewId) {
			viewId = getWindowState(context).getViewId();
		}
		// If none of above happen, try to get default viewId for a current
		// mode.
		if (null == viewId) {
			viewId = (String) context.getExternalContext().getRequestMap().get(
					Bridge.DEFAULT_VIEWID);
		}
		if (null == viewId) {
			throw new BridgeDefaultViewNotSpecifiedException(
					"could'n determine portlet view id");
		}
		return viewId;
	}

	/**
	 * Return class contained saved JSF states for a current window.
	 * 
	 * @param context
	 * @return
	 */
	public PortletViewState getWindowState(FacesContext context) {
		String stateId = getStateId(context);
		PortletViewState state = null;
		if (null != stateId) {
			synchronized (states) {
				state = getWindowState(stateId);
				if (null == state) {
					state = new PortletViewState(stateId);
					states.put(stateId, state);
				}
			}
		}
		return state;
	}

	/**
	 * @param stateId
	 * @return
	 */
	public PortletViewState getWindowState(String stateId) {
		PortletViewState state = null;
		if (null != stateId) {
			state = (PortletViewState) states.get(stateId);
		}
		return state;
	}

	private void removeSessionStates(String scoprId) {
		synchronized (states) {
			// Iterate over copy of the keys, so LinkedHashSet do not support concurrent modifications.
			for (Iterator<String> keysIterator = states.keySet().iterator();keysIterator.hasNext();) {
				String key = keysIterator.next();
				if (key.startsWith(scoprId)) {
					keysIterator.remove();
				}
			}

		}
	}

	public String getStateId(FacesContext context) {
		ExternalContext externalContext = context.getExternalContext();
		Object request = externalContext.getRequest();
		String stateId;
		if (request instanceof HttpServletRequest) {
			HttpServletRequest servletRequest = (HttpServletRequest) request;
			stateId = servletRequest.getParameter(STATE_ID_PARAMETER);
//			if (null == stateId) {
//				throw new FacesException("No portlet stateId parameter for AJAX request");
//			}
		} else if (request instanceof PortletRequest) {
			PortletRequest portletRequest = (PortletRequest) request;
			PortletSession portletSession = portletRequest
					.getPortletSession(true);
			PortletConfig portletConfig = (PortletConfig) portletRequest
					.getAttribute(AbstractExternalContext.PORTLET_CONFIG_ATTRIBUTE);
			if (null == portletConfig) {
				throw new FacesException();
			}
			String scopeId = portletConfig.getPortletName()
					+ portletSession.getId();
			String portletModeName = portletRequest.getPortletMode().toString();
			if (request instanceof RenderRequest) {
				RenderRequest renderRequest = (RenderRequest) request;
				stateId = renderRequest.getParameter(STATE_ID_PARAMETER);
				if (null == stateId) {
					RenderResponse response = (RenderResponse) externalContext
							.getResponse();
					stateId = scopeId + portletModeName
							+ response.getNamespace();
				}
			} else if (request instanceof ActionRequest) {
				ActionRequest actionRequest = (ActionRequest) request;
				stateId = (String) actionRequest
						.getAttribute(STATE_ID_PARAMETER);
				if (null == stateId) {

					UUID uuid = UUID.randomUUID();
					stateId = scopeId
							+ portletModeName
							+ uuid.toString();
					actionRequest.setAttribute(STATE_ID_PARAMETER, stateId);
					ActionResponse response = (ActionResponse) externalContext
							.getResponse();
					response.setRenderParameter(STATE_ID_PARAMETER, stateId);
				}
			} else {
				throw new FacesException();
			}
			WindowIDRetriver idRetriver = (WindowIDRetriver) portletSession
					.getAttribute(WINDOW_ID_RETRIVER);
			if (null == idRetriver) {
				idRetriver = new WindowIDRetriver(scopeId);
				portletSession.setAttribute(WINDOW_ID_RETRIVER, idRetriver);
			}
		} else {
			throw new FacesException();
		}
		return stateId;
	}

	/**
	 * @return the lastMode
	 */
	public PortletMode getLastMode() {
		return lastMode;
	}

	/**
	 * @param lastMode
	 *            the lastMode to set
	 */
	public void setLastMode(PortletMode lastMode) {
		this.lastMode = lastMode;
	}

	/**
	 * Based on the Julien Viet idea.
	 * 
	 * @author asmirnov
	 * 
	 */
	public final class WindowIDRetriver implements HttpSessionBindingListener {

		public static final String PORTLET_SCOPE_PREFIX = "javax.portlet.p.";

		private final String scopeId;

		private String windowID;

		/**
		 * @param scopeId
		 */
		private WindowIDRetriver(String scopeId) {
			this.scopeId = scopeId;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see javax.servlet.http.HttpSessionBindingListener#valueBound(javax.servlet.http.HttpSessionBindingEvent)
		 */
		public void valueBound(HttpSessionBindingEvent event) {
			String name = event.getName();
			if (null == name || 0 == name.length()) {
				throw new FacesException(
						"WindowIDRetriver bind to session without name");
			}
			if (PortletSession.PORTLET_SCOPE != PortletSessionUtil
					.decodeScope(name)) {
				throw new FacesException(
						"WindowIDRetriver bind to APPLICATION_SCOPE. PORTLET_SCOPE is required");
			}
			windowID = name.substring(PORTLET_SCOPE_PREFIX.length(), name
					.indexOf('?'));
			if (log.isDebugEnabled()) {
				log
						.debug("WindowIDRetriver have been bind to session for a portlet window "
								+ windowID);
			}

		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see javax.servlet.http.HttpSessionBindingListener#valueUnbound(javax.servlet.http.HttpSessionBindingEvent)
		 */
		public void valueUnbound(HttpSessionBindingEvent event) {
			removeSessionStates(scopeId);
		}

		/**
		 * @return the windowID
		 */
		public String getWindowID() {
			return windowID;
		}

		/**
		 * @return the scopeId
		 */
		public String getScopeId() {
			return scopeId;
		}

	}

}
