/**
 * 
 */
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.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;

/**
 * @author asmirnov
 * 
 */
public class PortletViewState implements Serializable {

	/**
	 * 
	 */
	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;

	/**
	 * @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;
		Map<String, Object> requestMap = facesContext.getExternalContext()
				.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());
			}
		}
		_requestParameters = new HashMap<String, String[]>(facesContext
				.getExternalContext().getRequestParameterValuesMap());
		// Seam hack - save conversation Id for a request parameter.
		try {
			Object conversationId = facesContext.getApplication()
					.evaluateExpressionGet(facesContext, "#{conversation.id}",
							Object.class);
			if(null != conversationId){
				_requestParameters.put("conversationId", new String[]{conversationId.toString()});
			}
		} catch (Exception e) {
			// Do nothing - no seam conversation found !
		}
	}

	public void restoreBeans(FacesContext facesContext) {
		Map<String, Object> requestMap = facesContext.getExternalContext()
				.getRequestMap();
		if (null != beans) {
			requestMap.putAll(beans);
		}
		requestMap.put(REQUEST_PARAMETERS_ATTRIBUTE, _requestParameters);
	}

	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);
	}

}
