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

import java.io.PrintWriter;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;

import javax.faces.FacesException;
import javax.faces.context.FacesContext;
import javax.faces.lifecycle.Lifecycle;
import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.PortletConfig;
import javax.portlet.PortletContext;
import javax.portlet.PortletException;
import javax.portlet.PortletRequest;
import javax.portlet.PortletResponse;
import javax.portlet.PortletSession;
import javax.portlet.PortletURL;
import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;
import javax.portlet.faces.Bridge;
import javax.portlet.faces.BridgeDefaultViewNotSpecifiedException;
import javax.portlet.faces.BridgeException;

import org.ajax4jsf.context.AjaxContext;
import org.ajax4jsf.portlet.application.PortletStateHolder;
import org.ajax4jsf.portlet.application.PortletViewState;
import org.ajax4jsf.portlet.application.PortletStateHolder.WindowIDRetriver;
import org.ajax4jsf.portlet.context.AbstractExternalContext;
import org.ajax4jsf.portlet.context.PortalActionURL;
import org.ajax4jsf.portlet.context.PortletExternalContextImpl;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * @author asmirnov
 * 
 */
public class AjaxPortletBridge extends AbstractAjaxBridge implements Bridge {

	private static final Log log = LogFactory.getLog(AjaxPortletBridge.class);
	private static final String EXCEPTION_HANDLER_CLASS_PARAMETER = ExceptionHandler.class.getName();
	private PortletConfig portletConfig;
	
	private ExceptionHandler exceptionHandler;

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.portlet.faces.Bridge#destroy()
	 */
	public void destroy() {
		if (log.isDebugEnabled()) {
			log.debug("Destroy portlet " + getPortletConfig().getPortletName());
		}
		super.destroy();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.portlet.faces.Bridge#init(javax.portlet.PortletConfig)
	 */
	public void init(PortletConfig config) throws PortletException {
		this.portletConfig = config;
		PortletContext portletContext = config.getPortletContext();
		try {
			if (log.isDebugEnabled()) {
				log.debug("Start portlet initialisation for "
						+ config.getPortletName());
			}
			synchronized (portletContext) {
				init();
				PortletStateHolder.init(portletContext);
				Lifecycle lifecycle = getLifecycle();
				lifecycle.getPhaseListeners();
			}
		} catch (FacesException e) {
			throw new PortletException("Initialization error", e);
		}
		exceptionHandler = createExceptionHandler(portletContext);
		if (log.isDebugEnabled()) {
			log.debug("Done portlet initialisation for "
					+ config.getPortletName());
		}
		// getPortletContext().setAttribute(PORTLET_CONFIG, config);
	}

	protected ExceptionHandler createExceptionHandler(PortletContext portletContext) {
		String exceptionHandlerClassName = portletContext.getInitParameter(EXCEPTION_HANDLER_CLASS_PARAMETER);
		ExceptionHandler handler = null;
		if(null != exceptionHandlerClassName){
			ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
			if(null == classLoader){
				classLoader = this.getClass().getClassLoader();
			}
			try {
				Class<ExceptionHandler> exceptionHandlerClass = (Class<ExceptionHandler>) classLoader.loadClass(exceptionHandlerClassName);
				handler = exceptionHandlerClass.newInstance();
			} catch (ClassNotFoundException e) {				
				handler = new ExceptionHandlerImpl();
			} catch (InstantiationException e) {
				handler = new ExceptionHandlerImpl();
			} catch (IllegalAccessException e) {
				handler = new ExceptionHandlerImpl();
			}
		} else {
			handler = new ExceptionHandlerImpl();			
		}
		return handler;
	}

	public void doFacesRequest(ActionRequest request, ActionResponse response)
			throws BridgeException {
		initRequest(request, response, Bridge.PortletPhase.ActionPhase);
		FacesContext facesContext = getFacesContext(request, response);
		PortletViewState windowState = PortletStateHolder.getInstance(
				facesContext).getWindowState(facesContext);
		windowState.restoreRequest(facesContext, false);
		// request.setAttribute(Bridge.IS_POSTBACK_ATTRIBUTE,
		// Boolean.valueOf(null != windowState.getViewId()));
		try {
			execute(facesContext);
			// Save view state for a render phases.
			facesContext.getApplication().getStateManager().saveSerializedView(
					facesContext);
			// save request scope variables and Faces Messages.
			if (!facesContext.getResponseComplete()) {
				windowState.saveRequest(facesContext);
			} else {
				String redirectViewId = (String) facesContext.getExternalContext().getRequestMap().get(AbstractExternalContext.REDIRECT_VIEW_ID);
				if(null != redirectViewId){
					windowState.setViewId(redirectViewId);
				}
			}
		} catch (Exception e) {
			// TODO - handle exception.
			log.error("Error processing execute lifecycle", e);
			exceptionHandler.processActionException(facesContext, windowState, e);
		} finally {
			facesContext.release();
		}
	}

	public void doFacesRequest(RenderRequest request, RenderResponse response)
			throws BridgeException {
		initRequest(request, response, Bridge.PortletPhase.RenderPhase);
		FacesContext facesContext = getFacesContext(request, response);
		PortletViewState windowState = PortletStateHolder.getInstance(
				facesContext).getWindowState(facesContext);
		try {
			if (null == response.getContentType()) {
				String contenttype = request.getResponseContentType();
				if (contenttype == null) {
					contenttype = "text/html";
				}
				String encoding = response.getCharacterEncoding();
				if (encoding == null) {
					encoding = "UTF-8";
				}
				response.setContentType(contenttype);

			}
			// set portlet title if its set.
			ResourceBundle bundle = portletConfig.getResourceBundle(request
					.getLocale());
			if (bundle != null) {
				String title = null;
				try {
					title = bundle.getString("javax.portlet.title");
					response.setTitle(title);
				} catch (Exception e) {
					// Ignore MissingResourceException
				}
			}
			String namespace = response.getNamespace();
			windowState.restoreRequest(facesContext, true);
			renderResponse(facesContext, windowState);
			// TODO - detect redirect case. Reset response, clear request variables as far as Seam state.
			// Perform new render phase with a new ViewId.
			String redirectViewId = (String) facesContext.getExternalContext().getRequestMap().get(AbstractExternalContext.REDIRECT_VIEW_ID);
			if(null != redirectViewId){
				windowState.reset();
				windowState.setViewId(redirectViewId);
				// release old FacesContext.
				facesContext.release();
				// Reset attributes to initial state
				ArrayList initialAttributes = (ArrayList) request.getAttribute(AbstractExternalContext.EXCLUDED_PARAMETERS_ATTRIBUTE);
				ArrayList currentAttributes = Collections.list(request.getAttributeNames());
				currentAttributes.removeAll(initialAttributes);
				for (Object newAttribute : currentAttributes) {
					request.removeAttribute((String) newAttribute);
				}
				request.setAttribute(AbstractExternalContext.VIEW_ID_PARAMETER, redirectViewId);
				// Create new FacesContext
				facesContext = getFacesContext(request, response);
				renderResponse(facesContext, windowState);
			}
			// Set important Portal parameters to window state.
			String actionURL = facesContext.getApplication().getViewHandler().getActionURL(facesContext, facesContext.getViewRoot().getViewId());
			actionURL = facesContext.getExternalContext().encodeActionURL(actionURL);
			windowState.setPortalActionURL(new PortalActionURL(actionURL));
			PortletURL portletURL = response.createRenderURL();
			portletURL.setParameter(PortletStateHolder.STATE_ID_PARAMETER, windowState.getStateId());
			String renderUrl = portletURL.toString();
			windowState.setPortalRenderURL(new PortalActionURL(renderUrl));
			windowState.setNamespace(namespace);
			// writer.println("</div>");
			PortletSession portletSession = request.getPortletSession(true);
			WindowIDRetriver idRetriver = (WindowIDRetriver) portletSession.getAttribute(PortletStateHolder.WINDOW_ID_RETRIVER);
			if(null != idRetriver){
				windowState.setWindowId(idRetriver.getWindowID());
			}
			Principal userPrincipal = request.getUserPrincipal();
			// TODO - get user roles, defined in the portlet.xml ( ??? parse it ??? ), and store all values for a "isUserInRole(roleName)" calls
			portletSession.setAttribute(AbstractExternalContext.PORTAL_USER_PRINCIPAL, userPrincipal, PortletSession.APPLICATION_SCOPE);
			if (log.isDebugEnabled()) {
				log
						.debug("Finish rendering portlet for namespace "
								+ namespace);
			}
			// Disable portlet caching.
			// TODO - detect ajax components on page, static views can be
			// cached.
			response.setProperty(RenderResponse.EXPIRATION_CACHE, "0");
		} catch (Exception e) {
			response.reset();
			// TODO - handle exception
			log.error("Error processing execute lifecycle", e);
			exceptionHandler.processRenderException(facesContext, windowState, e);
		} finally {
			facesContext.release();
		}
	}

	/**
	 * @param facesContext
	 * @param windowState
	 * @throws FacesException
	 */
	private void renderResponse(FacesContext facesContext,
			PortletViewState windowState) throws FacesException {
		if (null == facesContext.getViewRoot()) {
			execute(facesContext);
			// TODO - detect redirect case.
		}
		//
		AjaxContext ajaxContext = AjaxContext
				.getCurrentInstance(facesContext);
		Map commonAjaxParameters = ajaxContext.getCommonAjaxParameters();
		commonAjaxParameters.put(
				PortletStateHolder.STATE_ID_PARAMETER, windowState.getStateId());
		render(facesContext);
	}

	/**
	 * @param request
	 * @param response
	 * @param actionPhase
	 * @throws BridgeException
	 */
	protected void initRequest(PortletRequest request,
			PortletResponse response, PortletPhase actionPhase)
			throws BridgeException {
		if (null == request.getAttribute(Bridge.DEFAULT_VIEWID)) {
			throw new BridgeDefaultViewNotSpecifiedException();
		}
		request.setAttribute(Bridge.PORTLET_LIFECYCLE_PHASE, actionPhase);
		request.setAttribute(AbstractExternalContext.PORTLET_CONFIG_ATTRIBUTE,
				getPortletConfig());
	}

	/**
	 * @return the portletConfig
	 */
	protected PortletConfig getPortletConfig() {
		return portletConfig;
	}

	@Override
	protected Object getContext() {
		// TODO Auto-generated method stub
		return portletConfig.getPortletContext();
	}

	@Override
	protected String getInitParameter(String name) {
		String initParameter = portletConfig.getInitParameter(name);
		if (null == initParameter) {
			initParameter = portletConfig.getPortletContext().getInitParameter(
					name);
		}
		return initParameter;
	}

}
