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

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import javax.faces.FacesException;
import javax.faces.FactoryFinder;
import javax.faces.application.StateManager;
import javax.faces.application.ViewHandler;
import javax.faces.component.NamingContainer;
import javax.faces.component.UIComponent;
import javax.faces.component.UIViewRoot;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.event.AbortProcessingException;
import javax.faces.render.RenderKit;
import javax.faces.render.RenderKitFactory;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.ajax4jsf.Messages;
import org.ajax4jsf.application.AjaxViewHandler;
import org.ajax4jsf.renderkit.AjaxContainerRenderer;
import org.ajax4jsf.renderkit.AjaxRendererUtils;
import org.ajax4jsf.renderkit.RendererUtils;
import org.ajax4jsf.renderkit.RendererUtils.HTML;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * This class incapsulated
 * 
 * @author asmirnov@exadel.com (latest modification by $Author: alexsmirnov $)
 * @version $Revision: 1.1.2.7 $ $Date: 2007/02/08 19:07:16 $
 * 
 */
public class AjaxContextImpl extends AjaxContext {
	
	public static final String SERVLET_ERROR_EXCEPTION_ATTRIBUTE = "javax.servlet.error.exception";

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

	Set<String> ajaxAreasToRender = new HashSet<String>();

	Set<String> ajaxAreasToProcess = null;

	Set<String> ajaxRenderedAreas = new LinkedHashSet<String>();

	boolean ajaxRequest = false;

	boolean selfRender = false;

	Integer viewSequence = new Integer(1);

	String submittedRegionClientId = null;
	
	String ajaxSingleClientId = null;


	ViewIdHolder viewIdHolder = null;

	Map<String, Object> responseDataMap = new HashMap<String, Object> ();

	Map<String, Object>  commonAjaxParameters = new HashMap<String, Object> ();

	Object oncomplete = null;


	
	public void release() {

		ajaxAreasToRender = new HashSet<String>();
		
		ajaxAreasToProcess = null;

		ajaxRenderedAreas = new LinkedHashSet<String>();

		ajaxRequest = false;

		selfRender = false;

		viewSequence = new Integer(1);

		submittedRegionClientId = null;

		viewIdHolder = null;

		responseDataMap = new HashMap<String, Object>();

		commonAjaxParameters = new HashMap<String, Object>();

	}

	/* (non-Javadoc)
	 * @see org.ajax4jsf.context.AjaxContext#decode(javax.faces.context.FacesContext)
	 */
	@Override
	public void decode(FacesContext context) {
		ExternalContext externalContext = context.getExternalContext();
		if (null == externalContext.getRequestMap().get(
				SERVLET_ERROR_EXCEPTION_ATTRIBUTE)) {
			Map<String, String> requestParameterMap = externalContext
					.getRequestParameterMap();
			String ajaxRegionId = requestParameterMap
					.get(AjaxContainerRenderer.AJAX_PARAMETER_NAME);
			setSubmittedRegionClientId(ajaxRegionId);
			setAjaxRequest(null != ajaxRegionId);
			setAjaxSingleClientId(requestParameterMap.get(AjaxRendererUtils.AJAX_SINGLE_PARAMETER_NAME));
		} else {
			// Error page is always serviced as non-ajax.
			setAjaxRequest(false);
			setSubmittedRegionClientId(null);
			setAjaxSingleClientId(null);
		}
	}
	

	/**
	 * @param context
	 * @throws AbortProcessingException
	 */
	public void renderAjax(FacesContext context) throws FacesException {
		if (log.isDebugEnabled()) {
			log.debug(Messages.getMessage(Messages.RENDER_AJAX_REQUEST,
					getSubmittedRegionClientId()));
		}
		try {
			// Just in case...
			setSelfRender(true);
			setAjaxRequest(true);
			// create response writer.
			ExternalContext extContext = context.getExternalContext();
			RenderKit renderKit = context.getRenderKit();
			String encoding;
			// Depends if we talk about servlets, portlets, ...
			if (extContext.getRequest() instanceof ServletRequest) {
				ServletRequest request = (ServletRequest) extContext
						.getRequest();
				ServletResponse response = (ServletResponse) extContext
						.getResponse();
				// Setup encoding and content type
				String contentType = "text/xml";
				// get the encoding - must be setup by faces context or filter.
				encoding = request.getCharacterEncoding();
				if (encoding == null) {
					encoding = "UTF-8";
				}
				response.setContentType(contentType + ";charset=" + encoding);
			} else
				encoding = "UTF-8";

			PrintWriter servletWriter;
				servletWriter = getWriter(extContext);
			ResponseWriter writer = renderKit.createResponseWriter(
					servletWriter, null, encoding);
			context.setResponseWriter(writer);
			// make response
			writer.startDocument();
			encodeAjaxBegin(context);
			context.getViewRoot().encodeAll(context);
			saveViewState(context);
			encodeAjaxEnd(context);
			writer.endDocument();
			writer.flush();
			writer.close();
			servletWriter.close();
			// Save tree state.
		} catch (IOException e) {
			throw new FacesException(Messages.getMessage(
					Messages.RENDERING_AJAX_REGION_ERROR, getSubmittedRegionClientId()), e);
		} finally {
			context.responseComplete();
			// component.setRendererType(defaultRenderer);
		}
	}

	/**
	 * Encode declaration for AJAX response. Render &lt;html&gt;&lt;body&gt;
	 * 
	 * @param context
	 * @throws IOException
	 */
	public void encodeAjaxBegin(FacesContext context)
			throws IOException {
		UIViewRoot viewRoot = context.getViewRoot();
		// AjaxContainer ajax = (AjaxContainer) component;
		ResponseWriter out = context.getResponseWriter();
		// DebugUtils.traceView("ViewRoot in AJAX Page encode begin");
		out.startElement(HTML.HTML_ELEMENT, viewRoot);
		Locale locale = viewRoot.getLocale();
		out.writeAttribute(HTML.lang_ATTRIBUTE, locale.toString(), "lang");
		out.startElement(HTML.BODY_ELEMENT, viewRoot);
	}

	/**
	 * End encoding of AJAX response. Render tag with included areas and close
	 * &lt;/body&gt;&lt;/html&gt;
	 * 
	 * @param context
	 * @throws IOException
	 */
	public void encodeAjaxEnd(FacesContext context)
			throws IOException {
		// AjaxContainer ajax = (AjaxContainer) component;
		ResponseWriter out = context.getResponseWriter();
		// DebugUtils.traceView("ViewRoot in AJAX Page encode begin");
		out.endElement(HTML.BODY_ELEMENT);
		out.endElement(HTML.HTML_ELEMENT);
	}

	public void saveViewState(FacesContext context) throws IOException {
		ResponseWriter writer = context.getResponseWriter();
		StateManager stateManager = context.getApplication().getStateManager();
		Object serializedView = stateManager
				.saveView(context);
		if (null != serializedView && null != writer) {
			StringWriter bufWriter = new StringWriter();
			ResponseWriter tempWriter;
			tempWriter = writer.cloneWithWriter(bufWriter);
			context.setResponseWriter(tempWriter);
			stateManager.writeState(context, serializedView);
			tempWriter.flush();
			if (bufWriter.getBuffer().length() > 0) {
				context.getExternalContext().getRequestMap().put(
						AjaxViewHandler.SERIALIZED_STATE_KEY,
						bufWriter.toString());
			}
			context.setResponseWriter(writer);
		}
	}

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

	/**
	 * @return Returns the ajaxRequest.
	 */
	public boolean isAjaxRequest() {
		return ajaxRequest;
	}

	/**
	 * @param ajaxRequest
	 *            The ajaxRequest to set.
	 */
	public void setAjaxRequest(boolean ajaxRequest) {
		this.ajaxRequest = ajaxRequest;
	}

	/**
	 * @return Returns the ajaxAreasToRender.
	 */
	public Set<String> getAjaxAreasToRender() {
		return this.ajaxAreasToRender;
	}

	/**
	 * @return the ajaxAreasToProcess
	 */
	@Override
	public Set<String> getAjaxAreasToProcess() {
		return ajaxAreasToProcess;
	}

	/**
	 * @param ajaxAreasToProcess the ajaxAreasToProcess to set
	 */
	@Override
	public void setAjaxAreasToProcess(Set<String> ajaxAreasToProcess) {
		this.ajaxAreasToProcess = ajaxAreasToProcess;
	}

	/**
	 * Add affected regions's ID to ajaxView component.
	 * 
	 * @param component
	 */
	public void addRegionsFromComponent(UIComponent component) {
		// First step - find parent ajax view
		Set<String> ajaxRegions = AjaxRendererUtils.getAjaxAreas(component);
		// if (ajaxRegions == null){
		// FacesContext context = FacesContext.getCurrentInstance();
		// ajaxRegions = AjaxRendererUtils.getAbsoluteId(context,component);
		// }
		if (log.isDebugEnabled()) {
			log.debug(Messages.getMessage(Messages.INVOKE_AJAX_REGION_LISTENER,
					component.getId()));
		}
		if (ajaxRegions != null) {
			for (Iterator<String> iter = ajaxRegions.iterator(); iter.hasNext();) {
				String id = iter.next().toString();
				ajaxAreasToRender.add(convertId(component, id));
			}
		}
		// Is that component limit to list ?
		if(Boolean.TRUE.equals(component.getAttributes().get("limitToList"))){
			setLimitToList(true);
		}
	}

	/** 
	 * Add IDs of regions to process 
	 * 
	 * @see org.ajax4jsf.context.AjaxContext#addAreasToProcessFromComponent(javax.faces.component.UIComponent)
	 */
	@Override
	public void addAreasToProcessFromComponent(FacesContext context, UIComponent component) {
		RendererUtils rendererUtils = RendererUtils.getInstance();
		
		Set<String> areasToProcess = AjaxRendererUtils.getAjaxAreasToProcess(component);
		if (areasToProcess != null) {
			Set<String> convertedAreaIds = new HashSet<String>();

			for (String areaId : areasToProcess) {
				UIComponent areaComponent = rendererUtils.findComponentFor(component, areaId);
				if (areaComponent != null) {
					convertedAreaIds.add(areaComponent.getClientId(context));
				} else {
					convertedAreaIds.add(areaId);
				}
			}
		
			if (this.ajaxAreasToProcess == null) {
				this.ajaxAreasToProcess = convertedAreaIds;
			} else {
				this.ajaxAreasToProcess.addAll(convertedAreaIds);
			}
		}
	}
	
	public void addComponentToAjaxRender(UIComponent component) {
		this.ajaxAreasToRender.add(AjaxRendererUtils.getAbsoluteId(component));
	}

	public void addComponentToAjaxRender(UIComponent base, String id) {
		this.ajaxAreasToRender.add(convertId(base, id));
	}

	/**
	 * Test for relative id of target components. Attempt convert to absolute.
	 * For use as argument for
	 * {@link RendererUtils#findComponentFor(UIComponent, String)}
	 * 
	 * @param component
	 * @param id
	 * @return
	 */
	private String convertId(UIComponent component, String id) {
		if (id.charAt(0) == NamingContainer.SEPARATOR_CHAR) {
			return id;
		}
		if (null == component) {
			throw new NullPointerException(
					"Base component for search re-rendered compnnent is null");
		}
		UIComponent target = RendererUtils.getInstance().findComponentFor(
				component, id);
		if (null != target) {
			return AjaxRendererUtils.getAbsoluteId(target);
		}
		log.warn("Target component for id " + id + " not found");
		return id;
	}

	/**
	 * @return Returns the ajaxRenderedAreas.
	 */
	public Set<String> getAjaxRenderedAreas() {
		return ajaxRenderedAreas;
	}

	public void addRenderedArea(String id) {
		ajaxRenderedAreas.add(id);
	}

	public boolean removeRenderedArea(String id) {
		return ajaxRenderedAreas.remove(id);
	}

	/**
	 * @return Returns the submittedClientId.
	 */
	public String getSubmittedRegionClientId() {
		return this.submittedRegionClientId;
	}

	/**
	 * @param submittedClientId
	 *            The submittedClientId to set.
	 */
	@Override
	public void setSubmittedRegionClientId(String submittedClientId) {
		this.submittedRegionClientId = submittedClientId;
	}

	/**
	 * @return the ajaxSingleClientId
	 */
	@Override
	public String getAjaxSingleClientId() {
		return ajaxSingleClientId;
	}

	/**
	 * @param ajaxSingleClientId the ajaxSingleClientId to set
	 */
	@Override
	public void setAjaxSingleClientId(String ajaxSingleClientId) {
		this.ajaxSingleClientId = ajaxSingleClientId;
	}

	/**
	 * @return Returns the selfRender.
	 */
	public boolean isSelfRender() {
		return selfRender;
	}

	/**
	 * @param selfRender
	 *            The selfRender to set.
	 */
	public void setSelfRender(boolean selfRender) {
		this.selfRender = selfRender;
	}

	/**
	 * @return the vievIdHolder
	 */
	public ViewIdHolder getViewIdHolder() {
		return viewIdHolder;
	}

	/**
	 * @param viewIdHolder
	 *            the vievIdHolder to set
	 */
	public void setViewIdHolder(ViewIdHolder viewIdHolder) {
		this.viewIdHolder = viewIdHolder;
	}

	/**
	 * @return the responseData
	 */
	public Object getResponseData() {
		return responseDataMap.get(RESPONSE_DATA_KEY);
	}

	/**
	 * @param responseData
	 *            the responseData to set
	 */
	public void setResponseData(Object responseData) {
		this.responseDataMap.put(RESPONSE_DATA_KEY, responseData);
	}

	/**
	 * @return the responseDataMap
	 */
	public Map<String, Object> getResponseDataMap() {
		return responseDataMap;
	}

	/**
	 * Gives back the writer of a Response object.
	 * 
	 * @param extContext
	 *            The external context.
	 * @return The writer of the response.
	 * @throws FacesException
	 *             If the response object has no getWriter() method.
	 */
	protected PrintWriter getWriter(ExternalContext extContext)
			throws FacesException {
		PrintWriter writer = null;
		Object response = extContext.getResponse();
		try {
			Method gW = response.getClass()
					.getMethod("getWriter", new Class[0]);
			writer = (PrintWriter) gW.invoke(response, new Object[0]);
		} catch (Exception e) {
			throw new FacesException(e);
		}
		return writer;
	}


	public String getAjaxActionURL(FacesContext context) {
		// Check arguments
		if (null == context) {
			throw new NullPointerException(
					"Faces context for build AJAX Action URL is null");
		}
		UIViewRoot viewRoot = context.getViewRoot();
		if (null == viewRoot) {
			throw new NullPointerException(
					"Faces view tree for build AJAX Action URL is null");
		}
		String viewId = viewRoot.getViewId();
		if (null == viewId) {
			throw new NullPointerException(
					"View id for build AJAX Action URL is null");
		}
		if (!viewId.startsWith("/")) {
			throw new IllegalArgumentException(
					"Illegal view Id for build AJAX Action URL: " + viewId);
		}
		ViewHandler viewHandler = context.getApplication().getViewHandler();
		String actionURL = viewHandler.getActionURL(context, viewId);
		// HACK - check for a Jboss PortletBridge implementation. If present, append DirectLink attribute to url.
		// TODO - how to detect portlet application ?
		if (null != context.getExternalContext().getApplicationMap().get(
				"org.jboss.portletbridge.application.PortletStateHolder")) {
			// Mark Ajax action url as transparent with jsf-portlet bridge.
			actionURL = actionURL
					+ ((actionURL.lastIndexOf('?') > 0) ? "&" : "?")
					+ "javax.portlet.faces.DirectLink=true";

		}
		return context.getExternalContext().encodeActionURL(actionURL);
	}

	/**
	 * @return the commonAjaxParameters
	 */
	public Map<String, Object> getCommonAjaxParameters() {
		return commonAjaxParameters;
	}

	/**
	 * @return the oncomplete
	 */
	public Object getOncomplete() {
		return oncomplete;
	}

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

}
