/**
 * 
 */
package org.ajax4jsf.context;

import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
import javax.faces.FactoryFinder;
import javax.faces.application.Application;
import javax.faces.component.UIComponent;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.render.RenderKit;
import javax.faces.render.RenderKitFactory;
import javax.faces.render.Renderer;

import org.ajax4jsf.application.AjaxViewHandler;
import org.ajax4jsf.renderkit.HeaderResourceProducer;
import org.ajax4jsf.renderkit.UserResourceRenderer;
import org.ajax4jsf.resource.InternetResourceBuilder;
import org.ajax4jsf.resource.ResourceNotFoundException;
import org.ajax4jsf.util.ELUtils;
import org.ajax4jsf.webapp.BaseFilter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.richfaces.event.RenderPhaseComponentVisitor;
import org.richfaces.skin.Skin;
import org.richfaces.skin.SkinFactory;
import org.richfaces.skin.SkinNotFoundException;

public class RenderPhaseViewResourcesVisitor implements
		RenderPhaseComponentVisitor {
	
	private class State {
		
		private RenderKit renderKit;
		private boolean processStyles;
		private boolean processScripts;
		private LinkedHashSet<String> scripts;
		private LinkedHashSet<String> styles;
		private LinkedHashSet<String> userScripts;
		private LinkedHashSet<String> userStyles;
		
		private State(boolean processScripts, boolean processStyles,
				RenderKit renderKit, LinkedHashSet<String> scripts,
				LinkedHashSet<String> styles,
				LinkedHashSet<String> userScripts,
				LinkedHashSet<String> userStyles) {
			super();
			this.processScripts = processScripts;
			this.processStyles = processStyles;
			this.renderKit = renderKit;
			this.scripts = scripts;
			this.styles = styles;
			this.userScripts = userScripts;
			this.userStyles = userStyles;
		}
	}

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

	//TODO remove this in other
	public static final String RESOURCES_PROCESSED = "org.ajax4jsf.framework.HEADER_PROCESSED";
	private static final String INIT_PARAMETER_PREFIX = "_init_parameter_";
	private static final Object NULL = new Object();
	private static final Pattern USER_AGENTS = Pattern.compile(" AppleWebKit/|^Opera/| Opera ");
	//todo
	
	/* (non-Javadoc)
	 * @see org.richfaces.event.ComponentPhaseEventHandler#beforePhaseBegin(javax.faces.event.PhaseEvent)
	 */
	public Object beforeRoot(PhaseEvent event) {
		Object result = null;
		FacesContext context = event.getFacesContext();

		ExternalContext externalContext = context.getExternalContext();
		Map<String,Object> requestMap = externalContext.getRequestMap();
		if (!Boolean.TRUE.equals(requestMap.get(RESOURCES_PROCESSED))) {
			if (null != requestMap.get(BaseFilter.RESPONSE_WRAPPER_ATTRIBUTE)) {
				
				Boolean processStyles = true;
				Boolean processScripts = true;

				LinkedHashSet<String> scripts = new LinkedHashSet<String>();
				LinkedHashSet<String> styles = new LinkedHashSet<String>();
				LinkedHashSet<String> userScripts = new LinkedHashSet<String>();
				LinkedHashSet<String> userStyles = new LinkedHashSet<String>();

				boolean ajaxRequest = AjaxContext.getCurrentInstance(context).isAjaxRequest(context);
				
				if (log.isDebugEnabled()) {
					log
							.debug("Process component tree for collect used scripts and styles");
				}
				try {
					Skin skin = SkinFactory.getInstance().getSkin(context);
					// For a "NULL" skin, do not collect components stylesheets
					if ("false".equals(skin.getParameter(context,
							Skin.loadStyleSheets))) {
						processStyles = 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)) {
						processScripts = false;
					} else if (InternetResourceBuilder.LOAD_ALL
							.equals(scriptStrategy)) {
						processScripts = false;
						// For an "ALL" strategy, it is not necessary to load scripts in the ajax request
						if (!ajaxRequest) {
							try {
								scripts.add(internetResourceBuilder
												.createResource(
														this,
														InternetResourceBuilder.COMMON_FRAMEWORK_SCRIPT)
												.getUri(context, null));
								scripts.add(internetResourceBuilder
												.createResource(
														this,
														InternetResourceBuilder.COMMON_UI_SCRIPT)
												.getUri(context, null));

							} catch (ResourceNotFoundException e) {
								if (log.isWarnEnabled()) {
									log
											.warn("No aggregated javaScript library found "
													+ e.getMessage());
								}
							}

						}
					}
				}

				boolean useStdControlsSkinning = false;

				String stdControlsSkinning = getInitParameterValue(context, InternetResourceBuilder.STD_CONTROLS_SKINNING_PARAM);
				if (stdControlsSkinning != null) {
					useStdControlsSkinning = InternetResourceBuilder.ENABLE.equals(stdControlsSkinning);
				}
				
				boolean useStdControlsSkinningClasses = true;

				String stdControlsSkinningClasses = getInitParameterValue(context, InternetResourceBuilder.STD_CONTROLS_SKINNING_CLASSES_PARAM);
				if (stdControlsSkinningClasses != null) {
					useStdControlsSkinningClasses = InternetResourceBuilder.ENABLE.equals(stdControlsSkinningClasses);
				}
				
				boolean useExtendedSkinning = isExtendedSkinningEnabled(externalContext);
				
				String styleStrategy = externalContext
						.getInitParameter(InternetResourceBuilder.LOAD_STYLE_STRATEGY_PARAM);
				
				if (InternetResourceBuilder.LOAD_NONE.equals(styleStrategy)) {
					processStyles = false;
				} else if (InternetResourceBuilder.LOAD_ALL
						.equals(styleStrategy)) {
					processStyles = false;
					// For an "ALL" strategy, it is not necessary to load styles
					// in the ajax request
					if (!ajaxRequest) {
						String commonStyle = InternetResourceBuilder.COMMON_STYLE_PREFIX;

						if (useStdControlsSkinning
								|| useStdControlsSkinningClasses) {
							if (isExtendedSkinningEnabled(externalContext)) {
								commonStyle += "-ext";
							} else {
								commonStyle += "-bas";
							}

							if (useStdControlsSkinning
									&& useStdControlsSkinningClasses) {
								commonStyle += "-both";
							} else if (useStdControlsSkinning) {
								commonStyle += "-styles";
							} else if (useStdControlsSkinningClasses) {
								commonStyle += "-classes";
							}
						}

						commonStyle += InternetResourceBuilder.COMMON_STYLE_EXTENSION;

						try {
							styles.add(internetResourceBuilder
									.createResource(this, commonStyle).getUri(
											context, null));

						} catch (ResourceNotFoundException e) {
							if (log.isWarnEnabled()) {
								log.warn("No aggregated stylesheet found "
										+ e.getMessage());
							}
						}

					}
				} else {
					if (useStdControlsSkinning) {
						styles.add(
								internetResourceBuilder.createResource(
										this, InternetResourceBuilder.STD_CONTROLS_BASIC_STYLE)
										.getUri(context, null));
						
						if (useExtendedSkinning) {
							styles.add(
									internetResourceBuilder.createResource(
											this, InternetResourceBuilder.STD_CONTROLS_EXTENDED_STYLE)
											.getUri(context, null));
						}
					}
					
					if (useStdControlsSkinningClasses) {
						styles.add(
								internetResourceBuilder.createResource(
										this, InternetResourceBuilder.STD_CONTROLS_BASIC_CLASSES_STYLE)
										.getUri(context, null));
						
						if (useExtendedSkinning) {
							styles.add(
									internetResourceBuilder.createResource(
											this, InternetResourceBuilder.STD_CONTROLS_EXTENDED_CLASSES_STYLE)
											.getUri(context, null));
						}
					}
				}

				RenderKitFactory rkFactory = (RenderKitFactory) FactoryFinder
				.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
				RenderKit renderKit = rkFactory.getRenderKit(context, context
						.getViewRoot().getRenderKitId());
				
				result = new State(processScripts, processStyles, renderKit, scripts, styles, userScripts, userStyles);
			}
		}
		return result;
	}

	/* (non-Javadoc)
	 * @see org.richfaces.event.ComponentPhaseEventHandler#componentBegin(javax.faces.component.UIComponent, javax.faces.event.PhaseEvent, java.lang.Object)
	 */
	public void beforeComponent(UIComponent component, PhaseEvent event,
			Object object) {
		if (object != null) {
			State state = (State) object;
			FacesContext context = event.getFacesContext();
			Renderer renderer = getRenderer(context, component, state.renderKit);
			if (null != renderer) {
				if ((state.processScripts || state.processStyles)
						&& renderer instanceof HeaderResourceProducer) {
					HeaderResourceProducer producer = (HeaderResourceProducer) renderer;
					if (state.processScripts) {
						Set<String> set = producer.getHeaderScripts(context,
								component);
						if (null != set) {
							state.scripts.addAll(set);
						}

					}
					if (state.processStyles) {
						Set<String> set = producer.getHeaderStyles(context,
								component);
						if (null != set) {
							state.styles.addAll(set);
						}

					}
				} else if (renderer instanceof UserResourceRenderer) {
					UserResourceRenderer producer = (UserResourceRenderer) renderer;
					Set<String> set = producer.getHeaderScripts(context,
							component);
					if (null != set) {
						state.userScripts.addAll(set);
					}
					set = producer.getHeaderStyles(context, component);
					if (null != set) {
						state.userStyles.addAll(set);
					}
				}

			}
		}
	}

	/* (non-Javadoc)
	 * @see org.richfaces.event.ComponentPhaseEventHandler#componentEnd(javax.faces.component.UIComponent, javax.faces.event.PhaseEvent, java.lang.Object)
	 */
	public void afterComponent(UIComponent component, PhaseEvent event,
			Object state) {}

	/* (non-Javadoc)
	 * @see org.richfaces.event.ComponentPhaseEventHandler#beforePhaseEnd(javax.faces.event.PhaseEvent, java.lang.Object)
	 */
	public void afterRoot(PhaseEvent event, Object object) {
		if (object != null) {
			FacesContext context = event.getFacesContext();
			ExternalContext externalContext = context.getExternalContext();
			Map<String,Object> requestMap = externalContext.getRequestMap();
			State state = (State) object;
			state.scripts.addAll(state.userScripts);
			if (state.scripts.size() > 0) {
				if (log.isDebugEnabled()) {
					StringBuffer buff = new StringBuffer(
							"Scripts for insert into head : \n");
					for (Iterator<String> iter = state.scripts.iterator(); iter.hasNext();) {
						String script = iter.next();
						buff.append(script).append("\n");
					}
					log.debug(buff.toString());
				}
				requestMap.put(AjaxContext.SCRIPTS_PARAMETER, state.scripts);
			}
			
			String skinStyleSheetUri = null;
			String skinExtendedStyleSheetUri = null;
			try {
				Skin skin = SkinFactory.getInstance().getSkin(context);
				// Set default style sheet for current skin.
				skinStyleSheetUri = (String) skin.getParameter(context,
						Skin.generalStyleSheet);
				// Set default style sheet for current skin.
				skinExtendedStyleSheetUri = (String) skin.getParameter(context,
						Skin.extendedStyleSheet);
			} catch (SkinNotFoundException e) {
				log.warn("Current Skin is not found", e);
			}

			// Append Skin StyleSheet after a
			if (null != skinStyleSheetUri) {
				String resourceURL = context.getApplication()
						.getViewHandler().getResourceURL(context,
								skinStyleSheetUri);
				state.styles.add(resourceURL);
			}
			
			if (null != skinExtendedStyleSheetUri && isExtendedSkinningEnabled(externalContext)) {
				String resourceURL = context.getApplication().getViewHandler().getResourceURL(context,
						skinExtendedStyleSheetUri);
				state.styles.add(resourceURL);
			}
			if (state.styles.size() > 0) {
				if (log.isDebugEnabled()) {
					StringBuffer buff = new StringBuffer(
							"Styles for insert into head : \n");
					for (Iterator<String> iter = state.styles.iterator(); iter.hasNext();) {
						String style = (String) iter.next();
						buff.append(style).append("\n");
					}
					log.debug(buff.toString());
				}
				requestMap.put(AjaxContext.STYLES_PARAMETER, state.styles);
			}
			
			if (state.userStyles.size() > 0) {
				if (log.isDebugEnabled()) {
					StringBuffer buff = new StringBuffer(
							"User styles for insert into head : \n");
					for (Iterator<String> iter = state.userStyles.iterator(); iter.hasNext();) {
						String style = (String) iter.next();
						buff.append(style).append("\n");
					}
					log.debug(buff.toString());
				}
				requestMap.put(AjaxContext.USER_STYLES_PARAMETER, state.userStyles);
			}
			// Mark as processed.
			requestMap.put(RESOURCES_PROCESSED, Boolean.TRUE);
			// Save viewId for a parser selection
			requestMap.put(AjaxViewHandler.VIEW_ID_KEY, context.getViewRoot().getViewId());
		}
	}

	/**
	 * Find renderer for given component.
	 * 
	 * @param context
	 * @param comp
	 * @param renderKit
	 * @return
	 */
	private static Renderer getRenderer(FacesContext context, UIComponent comp, RenderKit renderKit) {
		String rendererType = comp.getRendererType();
		if (rendererType != null) {
			return (renderKit.getRenderer(comp.getFamily(), rendererType));
		} else {
			return (null);
		}

	}
	
	private static String getInitParameterValue(FacesContext context, String parameterName) {
		
		String key = INIT_PARAMETER_PREFIX + parameterName;
		
		ExternalContext externalContext = context.getExternalContext();
		Map<String, Object> applicationMap = externalContext.getApplicationMap();
		Object mutex = externalContext.getRequest();
		Object parameterValue = null;
		
		synchronized (mutex) {
			parameterValue = applicationMap.get(key);

			if (parameterValue == null) {

				String initParameter = externalContext.getInitParameter(parameterName);
				if (initParameter != null) {
					
					if (ELUtils.isValueReference(initParameter)) {
						Application application = context.getApplication();
						ExpressionFactory expressionFactory = application.getExpressionFactory();
						
						parameterValue = expressionFactory.createValueExpression(context.getELContext(), 
								initParameter,
								String.class);
					} else {
						parameterValue = initParameter;
					}
					
				} else {
					parameterValue = NULL;
				}
				
				applicationMap.put(key, parameterValue);
			}
		}
		
		return evaluate(context, parameterValue);
	}
	
	private static String evaluate(FacesContext context, Object parameterValue) {
		if (parameterValue == NULL || parameterValue == null) {
			return null;
		} else if (parameterValue instanceof ValueExpression) {
			ValueExpression expression = (ValueExpression) parameterValue;
			
			return (String) expression.getValue(context.getELContext());
		} else {
			return parameterValue.toString();
		}
	}
	
	private static boolean isExtendedSkinningEnabled(ExternalContext context) {
		String userAgent = context.getRequestHeaderMap().get("User-Agent");
		if (userAgent != null) {
			boolean apply = !USER_AGENTS.matcher(userAgent).find();

			if (log.isDebugEnabled()) {
				log.debug("Got User-Agent: " + userAgent);
				log.debug("Applying extended CSS controls styling = " + apply);
			}
			
			return apply;
		} else {
			if (log.isDebugEnabled()) {
				log.debug("User-Agent is null, applying extended CSS controls styling");
			}

			return true;
		}
	}
}
