/**
 * 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.application.Application;
import javax.faces.application.StateManager;
import javax.faces.application.ViewHandler;
import javax.faces.application.StateManager.SerializedView;
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.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.ajax4jsf.Messages;
import org.ajax4jsf.application.AjaxViewHandler;
import org.ajax4jsf.component.AjaxContainer;
import org.ajax4jsf.component.AjaxViewRoot;
import org.ajax4jsf.renderkit.AjaxContainerRenderer;
import org.ajax4jsf.renderkit.AjaxRendererUtils;
import org.ajax4jsf.renderkit.RendererUtils;
import org.ajax4jsf.renderkit.UserResourceRenderer;
import org.ajax4jsf.renderkit.RendererUtils.HTML;
import org.ajax4jsf.resource.InternetResourceBuilder;
import org.ajax4jsf.webapp.BaseFilter;
import org.ajax4jsf.webapp.FilterServletResponseWrapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.richfaces.skin.Skin;
import org.richfaces.skin.SkinFactory;
import org.richfaces.skin.SkinNotFoundException;

/**
 * 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 RESOURCES_PROCESSED = "org.ajax4jsf.framework.HEADER_PROCESSED";

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

	private static ComponentInvoker invoker;

	private static Map contextClasses = new HashMap();

	Set ajaxAreasToRender = new HashSet();

	Set ajaxRenderedAreas = new LinkedHashSet();

	boolean ajaxRequest = false;

	boolean ajaxRequestSet = false;

	boolean selfRender = false;

	Integer viewSequence = new Integer(1);

	String submittedRegionClientId = null;

	boolean submittedRegionSet = false;

	ViewIdHolder viewIdHolder = null;

	Map responseDataMap = new HashMap();

	Map commonAjaxParameters = new HashMap();
	
	Object oncomplete = null;

	static {
		try {
			// Attempt to create JSF1.2 specific invoker.
			invoker = new JsfOneOneInvoker();
		} catch (Exception e) {
			invoker = new JsfOneOneInvoker();
		}
	}
	
	public void release() {
		
		ajaxAreasToRender = new HashSet();

		ajaxRenderedAreas = new LinkedHashSet();

		ajaxRequest = false;

		ajaxRequestSet = false;

		selfRender = false;

		viewSequence = new Integer(1);

		submittedRegionClientId = null;

		submittedRegionSet = false;

		viewIdHolder = null;

		responseDataMap = new HashMap();

		commonAjaxParameters = new HashMap();
		
	}

	/**
	 * @param root
	 * @param context
	 * @param callback
	 * @param regionId
	 * @return
	 * @see org.ajax4jsf.context.JsfOneOneInvoker#invokeOnComponent(javax.faces.component.UIComponent,
	 *      javax.faces.context.FacesContext,
	 *      org.ajax4jsf.context.InvokerCallback, java.lang.String)
	 */
	public static boolean invokeOnComponent(UIComponent root,
			FacesContext context, InvokerCallback callback, String regionId) {
		return invoker.invokeOnComponent(root, context, callback, regionId);
	}

	/**
	 * @param viewRoot
	 * @param context
	 * @param callback
	 * @see org.ajax4jsf.context.JsfOneOneInvoker#invokeOnRegionOrRoot(org.ajax4jsf.component.AjaxViewRoot,
	 *      javax.faces.context.FacesContext,
	 *      org.ajax4jsf.context.InvokerCallback)
	 */
	public static void invokeOnRegionOrRoot(AjaxViewRoot viewRoot,
			FacesContext context, InvokerCallback callback) {
		invoker.invokeOnRegionOrRoot(viewRoot, context, callback);
	}

	private InvokerCallback _ajaxInvoker = new InvokerCallback() {

		public void invoke(FacesContext context, UIComponent component) {
			if (component instanceof AjaxContainer) {
				AjaxContainer ajax = (AjaxContainer) component;
				renderAjaxRegion(context, component, true);
			} else {
				// Container not found, use Root for encode.
				renderAjaxRegion(context, context.getViewRoot(), true);
			}
		}

		public void invokeRoot(FacesContext context) {
			renderAjaxRegion(context, context.getViewRoot(), true);
		}

	};

	public void renderSubmittedAjaxRegion(FacesContext context) {
		renderSubmittedAjaxRegion(context, true);
	}

	public void renderSubmittedAjaxRegion(FacesContext context,
			final boolean useFilterWriter) {
		InvokerCallback ajaxInvoker = new InvokerCallback() {

			public void invoke(FacesContext context, UIComponent component) {
				if (component instanceof AjaxContainer) {
					renderAjaxRegion(context, component, useFilterWriter);
				} else {
					// Container not found, use Root for encode.
					renderAjaxRegion(context, context.getViewRoot(),
							useFilterWriter);
				}
			}

			public void invokeRoot(FacesContext context) {
				renderAjaxRegion(context, context.getViewRoot(),
						useFilterWriter);
			}

		};
		if (!invokeOnComponent(context.getViewRoot(), context, ajaxInvoker,
				getSubmittedRegionClientId(context))) {
			renderAjaxRegion(context, context.getViewRoot(), useFilterWriter);
		}

	}

	/**
	 * @param context
	 * @param useFilterWriter
	 *            TODO
	 * @throws AbortProcessingException
	 */
	public void renderAjaxRegion(FacesContext context, UIComponent component,
			boolean useFilterWriter) throws FacesException {
		if (log.isDebugEnabled()) {
			log.debug(Messages.getMessage(Messages.RENDER_AJAX_REQUEST,
					component.getId()));
		}
		try {
			setSelfRender(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();
				// HACK - bypass MyFaces ( and other ) extensions filter.

				// 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;
			if (useFilterWriter
					&& extContext.getRequestMap().containsKey(
							BaseFilter.RESPONSE_WRAPPER_ATTRIBUTE)) {
				// HACK - Special case for MyFaces, since <f:view don't call
				// encode methods,
				// encode response as for self-rendered region directly to
				// filter response wrpper.
				// to avoid exceptions, inform wrapper to ignore illegal states
				// for getWriter/Stream.
				ServletResponse servletResponse = (ServletResponse) extContext
						.getRequestMap().get(
								BaseFilter.RESPONSE_WRAPPER_ATTRIBUTE);
				servletResponse.resetBuffer();
				servletWriter = servletResponse.getWriter();
				((FilterServletResponseWrapper) servletResponse)
						.setUseNullStream(true);
			} else {
				servletWriter = getWriter(extContext);
			}
			ResponseWriter writer = renderKit.createResponseWriter(
					servletWriter, null, encoding);
			context.setResponseWriter(writer);
			// make response
			writer.startDocument();
			encodeAjaxBegin(context, component);
			component.encodeBegin(context);
			((AjaxContainer) component).encodeAjax(context);
			component.encodeEnd(context);
			saveViewState(context);
			encodeAjaxEnd(context, component);
			writer.endDocument();
			writer.flush();
			writer.close();
			servletWriter.close();
			// Save tree state.
		} catch (IOException e) {
			throw new FacesException(Messages.getMessage(
					Messages.RENDERING_AJAX_REGION_ERROR, component
							.getClientId(context)), e);
		} finally {
			context.responseComplete();
			// component.setRendererType(defaultRenderer);
		}
	}

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

	/**
	 * End encoding of AJAX response. Render tag with included areas and close
	 * &lt;/body&gt;&lt;/html&gt;
	 * 
	 * @param context
	 * @param component
	 * @throws IOException
	 */
	public void encodeAjaxEnd(FacesContext context, UIComponent component)
			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);
	}

	/**
	 * @param context
	 * @param root
	 * @throws FacesException
	 */
	public void processHeadResources(FacesContext context)
			throws FacesException {
		ExternalContext externalContext = context.getExternalContext();
		Map requestMap = externalContext.getRequestMap();
		if (!Boolean.TRUE.equals(requestMap.get(RESOURCES_PROCESSED))) {
			if (null != requestMap.get(BaseFilter.RESPONSE_WRAPPER_ATTRIBUTE)) {
				if (log.isDebugEnabled()) {
					log
							.debug("Process component tree for collect used scripts and styles");
				}
				UIViewRoot root = context.getViewRoot();
				ViewResources viewResources = new ViewResources();
				try {
					Skin skin = SkinFactory.getInstance().getSkin(
							context);
					// Set default style sheet for current skin.
					String styleSheetUri = (String) skin.getParameter(context,
							Skin.generalStyleSheet);
					if(null != styleSheetUri){
						String resourceURL = context.getApplication()
						.getViewHandler().getResourceURL(context,
								styleSheetUri);
						viewResources.addStyle(resourceURL);
					}
					// For a "NULL" skin, do not collect components stylesheets
					if("false".equals(skin.getParameter(context, Skin.loadStyleSheets))){
						viewResources.setProcessStyles(false);
					}
				} catch (SkinNotFoundException e) {
					log.warn("Current Skin is not found", e);
				}
				InternetResourceBuilder internetResourceBuilder = InternetResourceBuilder.getInstance();
				// Check init parameters for a resources processing.
				String scriptStrategy = externalContext.getInitParameter(InternetResourceBuilder.LOAD_SCRIPT_STRATEGY_PARAM);
				if(null != scriptStrategy){
					if(InternetResourceBuilder.LOAD_NONE.equals(scriptStrategy)){
						viewResources.setProcessScripts(false);
					} else if (InternetResourceBuilder.LOAD_ALL.equals(scriptStrategy)) {
						viewResources.setProcessScripts(false);
						viewResources.addScript(internetResourceBuilder.createResource(this, InternetResourceBuilder.COMMON_FRAMEWORK_SCRIPT).getUri(context, null));
						viewResources.addScript(internetResourceBuilder.createResource(this, InternetResourceBuilder.COMMON_UI_SCRIPT).getUri(context, null));
					}
				}
				String styleStrategy = externalContext.getInitParameter(InternetResourceBuilder.LOAD_STYLE_STRATEGY_PARAM);
				if(null != styleStrategy){
					if(InternetResourceBuilder.LOAD_NONE.equals(styleStrategy)){
						viewResources.setProcessStyles(false);
					} else if (InternetResourceBuilder.LOAD_ALL.equals(styleStrategy)) {
						viewResources.setProcessStyles(false);
						viewResources.addStyle(internetResourceBuilder.createResource(this, InternetResourceBuilder.COMMON_STYLE).getUri(context, null));
					}
				}
				viewResources.collect(context);
				Set scripts = viewResources.getScripts();
				if (scripts.size() > 0) {
					if (log.isDebugEnabled()) {
						StringBuffer buff = new StringBuffer(
								"Scripts for insert into head : \n");
						for (Iterator iter = scripts.iterator(); iter.hasNext();) {
							String script = (String) iter.next();
							buff.append(script).append("\n");
						}
						log.debug(buff.toString());
					}
					requestMap.put(SCRIPTS_PARAMETER, scripts);
				}
				
				Set styles = viewResources.getStyles();
				if (styles.size() > 0) {
					if (log.isDebugEnabled()) {
						StringBuffer buff = new StringBuffer(
								"Styles for insert into head : \n");
						for (Iterator iter = styles.iterator(); iter.hasNext();) {
							String style = (String) iter.next();
							buff.append(style).append("\n");
						}
						log.debug(buff.toString());
					}
					requestMap.put(STYLES_PARAMETER, styles);
				}
				
				Set usersStyles = viewResources.getUserStyles();
				if (usersStyles.size() > 0) {
					if (log.isDebugEnabled()) {
						StringBuffer buff = new StringBuffer(
								"User styles for insert into head : \n");
						for (Iterator iter = usersStyles.iterator(); iter.hasNext();) {
							String style = (String) iter.next();
							buff.append(style).append("\n");
						}
						log.debug(buff.toString());
					}
					requestMap.put(USER_STYLES_PARAMETER, usersStyles);
				}
				// Mark as processed.
				requestMap.put(RESOURCES_PROCESSED, Boolean.TRUE);
				// Save viewId for a parser selection
				requestMap.put(AjaxViewHandler.VIEW_ID_KEY, root.getViewId());
			}

		}
	}

	public void saveViewState(FacesContext context) throws IOException {
		// TODO - for facelets environment, we need to remove transient
		// components.
		try {
			Application.class.getMethod("getExpressionFactory", null);
		} catch (NoSuchMethodException e) {
			// JSF 1.1 !
		}
		ResponseWriter writer = context.getResponseWriter();
		StateManager stateManager = context.getApplication().getStateManager();
		SerializedView serializedView = stateManager
				.saveSerializedView(context);
		if (null != writer && null != serializedView) {
			StringWriter bufWriter = new StringWriter();
			ResponseWriter cloneWithWriter = writer.cloneWithWriter(bufWriter);
			context.setResponseWriter(cloneWithWriter);
			stateManager.writeState(context, serializedView);
			cloneWithWriter.flush();
			if (bufWriter.getBuffer().length() > 0) {
				context.getExternalContext().getRequestMap().put(
						AjaxViewHandler.SERIALIZED_STATE_KEY,
						bufWriter.toString());
			}
			// Restore original writer.
			context.setResponseWriter(writer);
		}
	}

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

	/**
	 * @return Returns the ajaxRequest.
	 */
	public boolean isAjaxRequest(FacesContext context) {
		if (!this.ajaxRequestSet) {
			ajaxRequest = null != getSubmittedRegionClientId(context);
			ajaxRequestSet = true;
		}
		return ajaxRequest;
	}

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

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

	/**
	 * Add affected regions's ID to ajaxView component.
	 * 
	 * @param component
	 */
	public void addRegionsFromComponent(UIComponent component) {
		// First step - find parent ajax view
		Set 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 iter = ajaxRegions.iterator(); iter.hasNext();) {
				String id = iter.next().toString();
				ajaxAreasToRender.add(convertId(component, id));
			}
		}
	}

	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 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(FacesContext context) {
		if (!this.submittedRegionSet) {
			ExternalContext externalContext = context.getExternalContext();
			if (null == externalContext.getRequestMap().get(
					"javax.servlet.error.exception")) {
				Map requestParameterMap = externalContext
						.getRequestParameterMap();
				this.submittedRegionClientId = (String) requestParameterMap
						.get(AjaxContainerRenderer.AJAX_PARAMETER_NAME);

			} else {
				// Error page, always parsed as non-ajax request.
				this.submittedRegionClientId = null;
			}
			this.submittedRegionSet = true;
			if (!this.ajaxRequestSet) {
				setAjaxRequest(this.submittedRegionClientId != null);
			}
		}
		return this.submittedRegionClientId;
	}

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

	/**
	 * @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 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() {
		return getAjaxActionURL(FacesContext.getCurrentInstance());
	}

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

}