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

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.faces.application.Application;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIViewRoot;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.render.ResponseStateManager;
import javax.portlet.faces.Bridge;
import javax.portlet.faces.BridgeDefaultViewNotSpecifiedException;

import org.ajax4jsf.context.AjaxContext;
import org.ajax4jsf.portlet.context.AbstractExternalContext;
import org.ajax4jsf.portlet.context.PortalActionURL;
import org.ajax4jsf.portlet.seam.ConversationIdRetriver;
import org.ajax4jsf.renderkit.AjaxContainerRenderer;

/**
 * @author asmirnov
 * 
 */
public class PortletViewState implements Serializable {
	
	private static ConversationIdRetriver conversationIdRetriver;
	
	static {
		try {
			conversationIdRetriver = new ConversationIdRetriver();
		} catch(NoClassDefFoundError e){
			// Do nothing, no seam classes available.
			String message = e.getMessage();
			conversationIdRetriver = null;
		}
	}

	/**
	 * 
	 */
	private static final long serialVersionUID = 5630637804542426709L;

	public static final String SAVED_VIEW_ATTRIBUTE = PortletViewState.class
			.getName();

	private static final Class[] excludedClasses = {
			javax.portlet.PortletConfig.class,
			javax.portlet.PortletContext.class,
			javax.portlet.PortletRequest.class,
			javax.portlet.PortletResponse.class,
			javax.portlet.PortletSession.class,
			javax.portlet.PortletPreferences.class,
			javax.portlet.PortalContext.class,
			javax.faces.context.FacesContext.class,
			javax.faces.context.ExternalContext.class,
			javax.servlet.ServletConfig.class,
			javax.servlet.ServletContext.class,
			javax.servlet.ServletRequest.class,
			javax.servlet.ServletResponse.class,
			javax.servlet.http.HttpSession.class, AjaxContext.class };

	private static final String[] excludedRequestAttributes = {
			"javax.servlet.include", ResponseStateManager.VIEW_STATE_PARAM,
			AbstractExternalContext.EXCLUDED_PARAMETERS_ATTRIBUTE };

	public static final String REQUEST_PARAMETERS_ATTRIBUTE = PortletViewState.class
			.getName()
			+ "REQUEST_PARAMS";

	private Map<String, List> messages;

	private transient Map<String, Object> beans;

	private Object treeStructure;

	private Object componentsState;

	private transient UIViewRoot viewRoot;

	private String viewId;

	private Map<String, String[]> requestParameters;
	
	private String namespace;
	
	private PortalActionURL portalActionURL;
	
	private PortalActionURL portalRenderURL;

	private final String stateId;
	
	private String windowId;
	
	private String conversationIdParameter="conversationId";
	
	private String conversationId;


	/**
	 * @param stateId
	 */
	public PortletViewState(String stateId) {
		this.stateId = stateId;
	}

	/**
	 * @return the windowId
	 */
	public String getWindowId() {
		return windowId;
	}

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


	/**
	 * @return the stateId
	 */
	public String getStateId() {
		return stateId;
	}

	/**
	 * @return the viewId
	 */
	public String getViewId() {
		return viewId;
	}

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

	/**
	 * @param facesContext
	 */
	public void saveMessages(FacesContext facesContext) {
		messages = new HashMap<String, List>();
		Iterator idsWithMessages = facesContext.getClientIdsWithMessages();
		while (idsWithMessages.hasNext()) {
			String id = (String) idsWithMessages.next();
			Iterator messages = facesContext.getMessages(id);
			while (messages.hasNext()) {
				FacesMessage message = (FacesMessage) messages.next();
				addMessage(id, message);
			}
		}
	}

	/**
	 * @param facesContext
	 */
	public void restoreMessages(FacesContext facesContext) {
		if (null != messages) {
			Iterator<String> idsWithMessages = getClientIdsWithMessages();
			while (idsWithMessages.hasNext()) {
				String id = idsWithMessages.next();
				Iterator messages = getMessages(id);
				while (messages.hasNext()) {
					FacesMessage message = (FacesMessage) messages.next();
					facesContext.addMessage(id, message);
				}
			}

		}
	}

	/**
	 * @return the _messages
	 */
	Map<String, List> getMessages() {
		return messages;
	}

	/**
	 * @param _messages
	 *            the _messages to set
	 */
	private void setMessages(Map<String, List> _messages) {
		this.messages = _messages;
	}

	/**
	 * @return the _viewRoot
	 */
	public UIViewRoot getViewRoot() {
		return viewRoot;
	}

	/**
	 * @param root
	 *            the _viewRoot to set
	 */
	public void setViewRoot(UIViewRoot root) {
		viewRoot = root;
	}

	private void addMessage(String clientId, FacesMessage message) {
		List<FacesMessage> list = messages.get(clientId);
		if (list == null) {
			list = new ArrayList<FacesMessage>();
			messages.put(clientId, list);
		}
		list.add(message);
	}

	private Iterator<String> getClientIdsWithMessages() {
		return (messages.keySet().iterator());
	}

	private Iterator getMessages(String clientId) {
		List list = messages.get(clientId);
		if (list != null) {
			return (list.iterator());
		} else {
			return (Collections.EMPTY_LIST.iterator());
		}
	}

	/**
	 * @return the componentsState
	 */
	public Object getComponentsState() {
		return componentsState;
	}

	/**
	 * @param componentsState
	 *            the componentsState to set
	 */
	public void setComponentsState(Object componentsState) {
		this.componentsState = componentsState;
	}

	/**
	 * @return the treeStructure
	 */
	public Object getTreeStructure() {
		return treeStructure;
	}

	/**
	 * @param treeStructure
	 *            the treeStructure to set
	 */
	public void setTreeStructure(Object treeStructure) {
		this.treeStructure = treeStructure;
	}

	/**
	 * Save request-scope beans, as described in the JSR-301 5.1
	 * 
	 * @param facesContext
	 */
	public void saveBeans(FacesContext facesContext) {
		beans = null;
		ExternalContext externalContext = facesContext.getExternalContext();
		Map<String, Object> requestMap = externalContext
				.getRequestMap();
		List existingAttributes = (List) requestMap
				.get(AbstractExternalContext.EXCLUDED_PARAMETERS_ATTRIBUTE);
		if (null == existingAttributes) {
			existingAttributes = Collections.EMPTY_LIST;
		}
		for (Iterator<Entry<String, Object>> iterator = requestMap.entrySet()
				.iterator(); iterator.hasNext();) {
			Entry<String, Object> entry = iterator.next();
			boolean include = true;
			String attributeName = entry.getKey();
			if (existingAttributes.contains(attributeName)) {
				include = false;
			}
			for (int i = 0; (i < excludedClasses.length) && include; i++) {
				if (excludedClasses[i].isInstance(entry.getValue())) {
					include = false;
				}
			}
			for (int i = 0; (i < excludedRequestAttributes.length) && include; i++) {
				if (attributeName.startsWith(excludedRequestAttributes[i])) {
					include = false;
				}
			}
			if (include) {
				if (null == beans) {
					beans = new HashMap<String, Object>();
				}
				beans.put(attributeName, entry.getValue());
			}
		}
		// Save request parameters ( all or a View state only ) restored as requered
		// in the JSR 301 PLT 5.1
		String preserveActionParamAttr = externalContext.getInitParameter(Bridge.BRIDGE_PACKAGE_PREFIX+Bridge.PRESERVE_ACTION_PARAMS);
		if(null != preserveActionParamAttr && "true".equalsIgnoreCase(preserveActionParamAttr)){
			requestParameters = new HashMap<String, String[]>(externalContext.getRequestParameterValuesMap());
			requestParameters.remove(AjaxContainerRenderer.AJAX_PARAMETER_NAME);
		} else {
			requestParameters = new HashMap<String, String[]>();
			String[] viewStateParam = externalContext
					.getRequestParameterValuesMap().get(
							ResponseStateManager.VIEW_STATE_PARAM);
			if (null != viewStateParam && viewStateParam.length > 0) {
				requestParameters.put(ResponseStateManager.VIEW_STATE_PARAM,
						viewStateParam);
			}
		}
		saveSeamConversationId(facesContext);
	}

	/**
	 * @param facesContext
	 */
	public void saveSeamConversationId(FacesContext facesContext) {
		// Seam hack - save conversation Id for a request parameter.
		if(null != conversationIdRetriver) {
			conversationIdRetriver.setConversationIdParameter(this);
		}
	}

	public void restoreBeans(FacesContext facesContext) {
		Map<String, Object> requestMap = facesContext.getExternalContext()
				.getRequestMap();
		if (null != beans) {
			requestMap.putAll(beans);
		}
		HashMap<String, String[]> parameters;
		if (null != requestParameters) {
			parameters = new HashMap<String, String[]>(requestParameters);
		} else {
			parameters = new HashMap<String, String[]>();
		}
		if(null != conversationId){
			parameters.put(conversationIdParameter, new String[]{conversationId});
		}
		requestMap.put(REQUEST_PARAMETERS_ATTRIBUTE, parameters);
	}

	public void restoreRequest(FacesContext facesContext, boolean b) {
		ExternalContext externalContext = facesContext.getExternalContext();
		Map<String, Object> requestMap = externalContext.getRequestMap();
		if (b) {
			facesContext.setViewRoot(getViewRoot());
			setViewRoot(null);
			restoreMessages(facesContext);
			restoreBeans(facesContext);
		}
		String viewId = getViewId();
		if (null == viewId
				&& Bridge.PortletPhase.ActionPhase.equals(requestMap
						.get(Bridge.PORTLET_LIFECYCLE_PHASE))) {
			viewId = (String) externalContext.getRequestParameterMap().get(
					AbstractExternalContext.VIEW_ID_PARAMETER);
		}
		if (null == viewId) {
			viewId = (String) requestMap.get(Bridge.DEFAULT_VIEWID);
			if (null == viewId) {
				throw new BridgeDefaultViewNotSpecifiedException(
						"could'n determine portlet view id");
			}
		} else {
			requestMap.put(Bridge.IS_POSTBACK_ATTRIBUTE, Boolean.TRUE);
		}
		setViewId(viewId);
		requestMap.put(AbstractExternalContext.VIEW_ID_PARAMETER, viewId);

	}

	public void saveRequest(FacesContext facesContext) {
		UIViewRoot root = facesContext.getViewRoot();
		setViewRoot(root);
		if (null != root) {
			setViewId(root.getViewId());
		}
		saveBeans(facesContext);
		saveMessages(facesContext);
	}

	/**
	 * @return the namespace
	 */
	public String getNamespace() {
		return namespace;
	}

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

	/**
	 * @return the portalActionURL
	 */
	public PortalActionURL getPortalActionURL() {
		return portalActionURL;
	}

	/**
	 * @param portalActionURL the portalActionURL to set
	 */
	public void setPortalActionURL(PortalActionURL actionURL) {
		this.portalActionURL = actionURL;
	}

	/**
	 * @return the portalRenderURL
	 */
	public PortalActionURL getPortalRenderURL() {
		return portalRenderURL;
	}

	/**
	 * @param portalRenderURL the portalRenderURL to set
	 */
	public void setPortalRenderURL(PortalActionURL portalURL) {
		this.portalRenderURL = portalURL;
	}

	public void reset() {
		this.requestParameters=null;
		this.beans = null;
		this.componentsState = null;
		this.messages = null;
		this.portalActionURL=null;
		this.portalRenderURL=null;
		this.treeStructure=null;
		this.viewRoot=null;
		this.viewId=null;
		this.conversationId=null;
	}
	
	/**
	 * @return the conversationIdParameter
	 */
	public String getConversationIdParameter() {
		return conversationIdParameter;
	}

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

	/**
	 * @return the conversationId
	 */
	public String getConversationId() {
		return conversationId;
	}

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

}
