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

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Map.Entry;

import javax.el.MethodExpression;
import javax.faces.FacesException;
import javax.faces.FactoryFinder;
import javax.faces.component.ContextCallback;
import javax.faces.component.UIComponent;
import javax.faces.component.UIComponentBase;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.FacesEvent;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.faces.lifecycle.Lifecycle;
import javax.faces.lifecycle.LifecycleFactory;
import javax.faces.render.Renderer;
import javax.faces.webapp.FacesServlet;

import org.ajax4jsf.Messages;
import org.ajax4jsf.context.AjaxContext;
import org.ajax4jsf.context.InvokerCallback;
import org.ajax4jsf.context.ViewIdHolder;
import org.ajax4jsf.event.AjaxListener;
import org.ajax4jsf.event.EventsQueue;
import org.ajax4jsf.model.KeepAlive;
import org.ajax4jsf.renderkit.AjaxContainerRenderer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Custom ViewRoot for support render parts of tree for Ajax requests. Main
 * difference from default ViewRoot - store events queue and unique id counter
 * in request-scope variables. In common realisation, store request-scope
 * variables in component produce errors in case of concurrent requests to same
 * view.
 * 
 * @author asmirnov@exadel.com (latest modification by $Author: alexsmirnov $)
 * @version $Revision: 1.1.2.4 $ $Date: 2007/02/28 17:01:01 $
 * 
 */
public class AjaxViewRoot extends UIViewRoot implements AjaxContainer {

	public static final String ROOT_ID = "_viewRoot";
	
	private Lifecycle lifecycle;

	private AjaxRegionBrige _brige;

	private static final Log _log = LogFactory.getLog(AjaxViewRoot.class);

	/**
	 * 
	 */
	public AjaxViewRoot() {
		super();
		super.setId(ROOT_ID);
		_brige = new AjaxRegionBrige(this);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.faces.component.UIComponentBase#getId()
	 */
	// public String getId() {
	// return ROOT_ID;
	// }
	public String getRendererType() {
		return (COMPONENT_FAMILY);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.ajax4jsf.framework.ajax.AjaxViewBrige#broadcast(javax.faces.event.FacesEvent)
	 */
	public void broadcast(FacesEvent event) throws AbortProcessingException {
		super.broadcast(event);
		getBrige().broadcast(event);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.ajax4jsf.framework.ajax.AjaxViewBrige#getAjaxListener()
	 */
	public MethodExpression getAjaxListener() {
		return getBrige().getAjaxListener();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.ajax4jsf.framework.ajax.AjaxViewBrige#isImmediate()
	 */
	public boolean isImmediate() {
		return getBrige().isImmediate();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.ajax4jsf.framework.ajax.AjaxViewBrige#isSubmitted()
	 */
	public boolean isSubmitted() {
		return getBrige().isSubmitted();
	}

	/**
	 * Use own {@link PhaseListener}'s list, to use with AJAX processing.
	 * phases.
	 */
	private List<PhaseListener> phaseListeners = null;

	public void removePhaseListener(PhaseListener toRemove) {
		if (null != phaseListeners) {
			phaseListeners.remove(toRemove);
		}
		super.removePhaseListener(toRemove);
	}

	public void addPhaseListener(PhaseListener newPhaseListener) {
		if (null == phaseListeners) {
			phaseListeners = new ArrayList<PhaseListener>();
		}
		phaseListeners.add(newPhaseListener);
		super.addPhaseListener(newPhaseListener);
	}

	/**
	 * Send notification to a view-scope phase listeners.
	 * 
	 * @param context
	 * @param phase
	 * @param before
	 */
	protected void processPhaseListeners(FacesContext context, PhaseId phase,
			boolean before) {
		MethodExpression listenerExpression = before ? getBeforePhaseListener()
				: getAfterPhaseListener();
		PhaseEvent event = null;
		if (null != listenerExpression) {
			event = createPhaseEvent(context, phase);
			listenerExpression.invoke(context.getELContext(),
					new Object[] { event });
		}
		if (null != this.phaseListeners) {
			for (PhaseListener listener : phaseListeners) {
				PhaseId phaseId = listener.getPhaseId();
				if (phaseId == phase || phaseId == PhaseId.ANY_PHASE) {
					if (null == event) {
						event = createPhaseEvent(context, phase);
					}
					if (before) {
						listener.beforePhase(event);
					} else {
						listener.afterPhase(event);
					}
				}
			}
		}
	}

	protected PhaseEvent createPhaseEvent(FacesContext context, PhaseId phaseId)
			throws FacesException {
		if (lifecycle == null) {
			LifecycleFactory lifecycleFactory = (LifecycleFactory) FactoryFinder
					.getFactory(FactoryFinder.LIFECYCLE_FACTORY);
			String lifecycleId = context.getExternalContext().getInitParameter(
					FacesServlet.LIFECYCLE_ID_ATTR);
			if (lifecycleId == null) {
				lifecycleId = LifecycleFactory.DEFAULT_LIFECYCLE;
			}
			lifecycle = lifecycleFactory.getLifecycle(lifecycleId);
		}
		return (new PhaseEvent(context, phaseId, lifecycle));
	}

	protected void processPhase(FacesContext context, PhaseId phase,
			InvokerCallback callback) {
		// Process phase listeners before phase.
		processPhaseListeners(context, phase, true);
		// Process phase. Run callback method by invokeOnComponent for a
		// submitted region.
		if (!(null == callback || context.getRenderResponse() || context
				.getResponseComplete())) {
			AjaxContext ajaxContext = AjaxContext.getCurrentInstance(context);
			String submittedRegionClientId = ajaxContext
					.getSubmittedRegionClientId();
			if (ajaxContext.isAjaxRequest() && submittedRegionClientId != null && 
							!submittedRegionClientId.equals(ROOT_ID) 
							&& !submittedRegionClientId.equals(getClientId(context))) {
				invokeOnComponent(context, submittedRegionClientId, new InvokerCallbackWrapper(callback));
			} else {
				// For a root region, call invokeRoot method, then process all
				// facets and children by invoke method.
				try {
					callback.invokeRoot(context);
				} catch (RuntimeException e) {
					context.renderResponse();
					throw e;
				}
				String ajaxSingleClientId = ajaxContext.getAjaxSingleClientId();
				if (null == ajaxSingleClientId) {
					for (Iterator<UIComponent> iter = getFacetsAndChildren(); iter
							.hasNext();) {
						callback.invokeContextCallback(context, iter.next());
					}
				} else {
				    	InvokerCallback invokerCallback = new InvokerCallbackWrapper(callback);
					boolean invoked = invokeOnComponent(context, ajaxSingleClientId, invokerCallback);
					if(!invoked){
						_log.warn("No component found to process as 'ajaxSingle' for clientId "+ajaxSingleClientId);
					}
					Set<String> areasToProcess = ajaxContext.getAjaxAreasToProcess();
					if(null != areasToProcess){
						for (String areaId : areasToProcess) {
							boolean areaProcessed = invokeOnComponent(context, areaId, invokerCallback);
							if(!areaProcessed){
								_log.warn("No component found to process for clientId "+areaId);
							}
						}
					}
				}
			}
		}
		// Broadcast phase events.
		broadcastEventsForPhase(context, phase);
		// Process afterPhase listeners.
		processPhaseListeners(context, phase, false);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.faces.component.UIComponent#queueEvent(javax.faces.event.FacesEvent)
	 */
	public void queueEvent(FacesEvent event) {
		if (event == null)
			throw new NullPointerException(Messages
					.getMessage(Messages.NULL_EVENT_SUBMITTED_ERROR));
		if (event.getPhaseId().compareTo(PhaseId.RENDER_RESPONSE) == 0) {
			// HACK - Special case - Ajax Events to RenderResponse phase
			// queue.
			getAjaxEventsQueue().offer(event);
		} else {
			getEventsQueue(event.getPhaseId()).offer(event);
		}
	}

	/**
	 * Broadcast events for specified Phase
	 * 
	 * @param context
	 * @param phaseId -
	 *            phase, for which events must be processed.
	 */
	void broadcastEventsForPhase(FacesContext context, PhaseId phaseId) {
		EventsQueue[] events = getEvents();
		EventsQueue anyPhaseEvents = events[PhaseId.ANY_PHASE.getOrdinal()];
		EventsQueue phaseEvents = events[phaseId.getOrdinal()];
		if (phaseEvents.isEmpty() && anyPhaseEvents.isEmpty())
			return;
		// FacesEvent event = null;
		boolean haveAnyPhaseEvents = !anyPhaseEvents.isEmpty();
		boolean havePhaseEvents = !phaseEvents.isEmpty();
		do {
			// ANY_PHASE first
			processEvents(context, anyPhaseEvents, haveAnyPhaseEvents);

			processEvents(context, phaseEvents, havePhaseEvents);
			// Events can queued in other events processing
			haveAnyPhaseEvents = !anyPhaseEvents.isEmpty();
			havePhaseEvents = !phaseEvents.isEmpty();
		} while (haveAnyPhaseEvents || havePhaseEvents);
		if (context.getRenderResponse() || context.getResponseComplete()) {
			clearEvents();
		}

	}

	/**
	 * @param context
	 *            TODO
	 * @param phaseEventsQueue
	 * @param havePhaseEvents
	 */
	public void processEvents(FacesContext context,
			EventsQueue phaseEventsQueue, boolean havePhaseEvents) {
		FacesEvent event;
		while (havePhaseEvents) {
			try {
				event = (FacesEvent) phaseEventsQueue.remove();
				UIComponent source = event.getComponent();
				try {
					source.broadcast(event);
				} catch (AbortProcessingException e) {
					if (_log.isErrorEnabled()) {
						UIComponent component = event.getComponent();
						String id = null != component ? component
								.getClientId(context) : "";
						_log.error(
								"Error processing faces event for the component "
										+ id, e);
					}
				}
			} catch (NoSuchElementException e) {
				havePhaseEvents = false;
			}
		}
	}

	public void broadcastAjaxEvents(FacesContext context) {
		EventsQueue queue = getAjaxEventsQueue();
		processEvents(context, queue, !queue.isEmpty());
	}

	private EventsQueue[] events;

	private EventsQueue ajaxEvents = new EventsQueue();

	/**
	 * Use FIFO buffers for hold Faces Events. Hold this buffers in Request
	 * scope parameters for support any concurrent requests for same component
	 * tree ( since RI hold component tree in session ).
	 * 
	 * @param phase
	 * 
	 * @return
	 */
	protected EventsQueue getEventsQueue(PhaseId phase) {
		return getEvents()[phase.getOrdinal()];
	}

	/**
	 * @return
	 */
	protected EventsQueue[] getEvents() {
		if (events == null) {
			clearEvents();
		}
		return events;
	}

	/**
	 * Special Fifo Buffer for ajax events to Render Responce phase.
	 * 
	 * @return
	 */
	protected EventsQueue getAjaxEventsQueue() {

		return ajaxEvents;
	}

	public void clearEvents() {
		int len;
		events = new EventsQueue[len = PhaseId.VALUES.size()];
		for (int i = 0; i < len; i++) {
			events[i] = new EventsQueue();
		}
	}

	private InvokerCallback _decodeInvoker = new InvokerCallback() {

		public void invokeContextCallback(FacesContext context,
				UIComponent component) {
			component.processDecodes(context);
		}

		public void invokeRoot(FacesContext context) {
			decode(context);
		}

	};

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.ajax4jsf.framework.ajax.AjaxViewBrige#processDecodes(javax.faces.context.FacesContext)
	 */
	public void processDecodes(FacesContext context) {
		if (context == null)
			throw new NullPointerException("context");
		processPhase(context, PhaseId.APPLY_REQUEST_VALUES, _decodeInvoker);
	}

	private InvokerCallback _updatesInvoker = new InvokerCallback() {

		public void invokeContextCallback(FacesContext context,
				UIComponent component) {
			component.processUpdates(context);
		}

		public void invokeRoot(FacesContext context) {
		}

	};

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.ajax4jsf.framework.ajax.AjaxViewBrige#processUpdates(javax.faces.context.FacesContext)
	 */
	public void processUpdates(FacesContext context) {
		if (context == null)
			throw new NullPointerException("context");
		processPhase(context, PhaseId.UPDATE_MODEL_VALUES, _updatesInvoker);
	}

	private InvokerCallback _validatorsInvoker = new InvokerCallback() {

		public void invokeContextCallback(FacesContext context,
				UIComponent component) {
			component.processValidators(context);
		}

		public void invokeRoot(FacesContext context) {
		}

	};

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.ajax4jsf.framework.ajax.AjaxViewBrige#processValidators(javax.faces.context.FacesContext)
	 */
	public void processValidators(FacesContext context) {
		if (context == null)
			throw new NullPointerException("context");
		processPhase(context, PhaseId.PROCESS_VALIDATIONS, _validatorsInvoker);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.faces.component.UIViewRoot#processApplication(javax.faces.context.FacesContext)
	 */
	public void processApplication(FacesContext context) {
		if (context == null)
			throw new NullPointerException("context");
		processPhase(context, PhaseId.INVOKE_APPLICATION, null);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.faces.component.UIViewRoot#encodeBegin(javax.faces.context.FacesContext)
	 */
	// public void encodeBegin(FacesContext context) throws IOException {
	// UIComponent submittedComponent = getSubmittedRegion(context);
	// if(null == submittedComponent){
	// super.encodeBegin(context);
	// } else {
	// submittedComponent.encodeBegin(context);
	// }
	// }
	private ContextCallback _ajaxInvoker = new ContextCallback() {

		public void invokeContextCallback(FacesContext context,
				UIComponent component) {
			try {
				if (component instanceof AjaxContainer) {
					AjaxContainer ajax = (AjaxContainer) component;
					ajax.encodeAjax(context);
				} else {
					// Container not found, use Root for encode.
					encodeAjax(context);
				}
			} catch (IOException e) {
				throw new FacesException(e);
			}
		}
	};

	@Override
	public void encodeBegin(FacesContext context) throws IOException {
		processPhaseListeners(context, PhaseId.RENDER_RESPONSE, true);
		// Copy/paste from UIComponentBase, so in no way for java to call super.super method.
        String rendererType = getRendererType();
        if (rendererType != null) {
            Renderer renderer = this.getRenderer(context);
            if (renderer != null) {
                renderer.encodeBegin(context, this);
            } else {
                // TODO: log
            }
        }

	}

	@Override
	public void encodeEnd(FacesContext context) throws IOException {
		// Copy/paste from UIComponentBase, so in no way for java to call super.super method.
        String rendererType = getRendererType();
        if (rendererType != null) {
            Renderer renderer = this.getRenderer(context);
            if (renderer != null) {
                renderer.encodeEnd(context, this);
            } else {
                // TODO: log
            }
        }
		processPhaseListeners(context, PhaseId.RENDER_RESPONSE, false);
//		super.encodeEnd(context);
	}
	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.faces.component.UIComponentBase#encodeChildren(javax.faces.context.FacesContext)
	 */
	public void encodeChildren(FacesContext context) throws IOException {
		AjaxContext ajaxContext = AjaxContext.getCurrentInstance(context);
		if (ajaxContext.isAjaxRequest()) {
			String submittedRegionClientId = ajaxContext
					.getSubmittedRegionClientId();
			boolean invoked = false; 
			if (submittedRegionClientId != null && !submittedRegionClientId.equals(ROOT_ID) && !submittedRegionClientId.equals(getClientId(context))) {
				invoked = invokeOnComponent(context, submittedRegionClientId, _ajaxInvoker);
			} 
			// if container not found, use Root for encode.
			// https://jira.jboss.org/jira/browse/RF-3975			
			if (!invoked) {
			    encodeAjax(context);
			}
		} else {
			super.encodeChildren(context);
		}
	}

	@SuppressWarnings("unchecked")
	public void restoreState(FacesContext context, Object state) {
		Object[] mystate = (Object[]) state;
		super.restoreState(context, mystate[0]);
		getBrige().restoreState(context, mystate[1]);
		Object listeners = restoreAttachedState(context, mystate[2]);
		if (null != listeners) {
			phaseListeners = (List<PhaseListener>) listeners;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.ajax4jsf.framework.ajax.AjaxViewBrige#saveState(javax.faces.context.FacesContext)
	 */
	public Object saveState(FacesContext context) {
		Object[] state = new Object[3];
		state[0] = super.saveState(context);
		state[1] = getBrige().saveState(context);
		state[2] = saveAttachedState(context, phaseListeners);
		return state;
	}

	public String getViewId() {
		ViewIdHolder viewIdHolder = AjaxContext.getCurrentInstance()
				.getViewIdHolder();
		String viewId;
		if (null != viewIdHolder) {
			viewId = viewIdHolder.getViewId();
		} else {
			viewId = super.getViewId();
		}
		return viewId;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.ajax4jsf.framework.ajax.AjaxViewBrige#setAjaxListener(javax.faces.el.MethodBinding)
	 */
	public void setAjaxListener(MethodExpression ajaxListener) {
		getBrige().setAjaxListener(ajaxListener);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.ajax4jsf.framework.ajax.AjaxViewBrige#setImmediate(boolean)
	 */
	public void setImmediate(boolean immediate) {
		getBrige().setImmediate(immediate);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.ajax4jsf.framework.ajax.AjaxViewBrige#setSubmitted(boolean)
	 */
	public void setSubmitted(boolean submitted) {
		getBrige().setSubmitted(submitted);
	}

	public void addAjaxListener(AjaxListener listener) {
		addFacesListener(listener);
	}

	public AjaxListener[] getAjaxListeners() {
		return (AjaxListener[]) getFacesListeners(AjaxListener.class);
	}

	public void removeAjaxListener(AjaxListener listener) {
		removeFacesListener(listener);

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.ajax4jsf.framework.ajax.AjaxViewBrige#isSelfRendered()
	 */
	public boolean isSelfRendered() {
		return false;// _brige.isSelfRendered();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.ajax4jsf.framework.ajax.AjaxViewBrige#setSelfRendered(boolean)
	 */
	public void setSelfRendered(boolean selfRendered) {
		// _brige.setSelfRendered(selfRendered);
	}


	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.faces.component.UIComponentBase#getRendersChildren()
	 */
	public boolean getRendersChildren() {
		FacesContext context = FacesContext.getCurrentInstance();
		// For non Ajax request, view root not render children
		if (!AjaxContext.getCurrentInstance(context).isAjaxRequest()) {
			return false;
		}
		// Ajax Request. Control all output.
		return true;
	}

	public boolean isRenderRegionOnly() {
		// for viewroot it not applicable.
		return false;
	}

	public void setRenderRegionOnly(boolean reRenderPage) {
		// Ignore for a ViewRoot.
	}

	public void encodeAjax(FacesContext context) throws IOException {
		String rendererType = getRendererType();
		if (rendererType != null) {
			((AjaxContainerRenderer) getRenderer(context)).encodeAjax(context,
					this);
		}

	}

	/**
	 * @return the brige
	 */
	protected AjaxRegionBrige getBrige() {
		return _brige;
	}

	protected Object getAdditionalState(FacesContext context) {
		Map<String, Object> keepAliveBeans = new HashMap<String, Object>();
		Map<String, Object> requestMap = context.getExternalContext()
		.getRequestMap();
		// Save all objects form request map wich marked by @KeepAlive
		// annotations
		for (Entry<String, Object> requestEntry : requestMap.entrySet()) {
			Object bean = requestEntry.getValue();
			// check value for a NULL -
			// http://jira.jboss.com/jira/browse/RF-3576
			if (null != bean
					&& bean.getClass().isAnnotationPresent(KeepAlive.class)) {
				keepAliveBeans.put(requestEntry.getKey(), bean);
			}
		}
		if (keepAliveBeans.size() > 0) {
			return UIComponentBase.saveAttachedState(context, keepAliveBeans);
		} else {
			return null;
		}
	}

	@SuppressWarnings("unchecked")
	protected void restoreAdditionalState(FacesContext context, Object state) {
		if (null != state) {
			boolean isAjax = AjaxContext.getCurrentInstance(context).isAjaxRequest();

			// Append all saved beans to the request map.
			Map beansMap = (Map) UIComponentBase.restoreAttachedState(context,
					state);
			Map<String, Object> requestMap = context.getExternalContext()
			.getRequestMap();
			for (Object key : beansMap.keySet()) {
				Object bean = beansMap.get(key);
				if (bean != null) {
					KeepAlive annotation = bean.getClass().getAnnotation(KeepAlive.class);
					if (annotation != null) {
						if (!isAjax && annotation.ajaxOnly()) {

							//skip ajax-only beans for non-ajax requests
							continue;
						}
					}
				}

				requestMap.put((String) key, bean);
			}
		}
	}
	
	@Override
	public Object processSaveState(FacesContext context) {
		Object[] state = new Object[2];
		state[0] = super.processSaveState(context);
		state[1] = getAdditionalState(context);
		
		return state;
	}
	
	@Override
	public void processRestoreState(FacesContext context, Object stateObject) {
		Object[] state = (Object[]) stateObject;
		
		super.processRestoreState(context, state[0]);
		restoreAdditionalState(context, state[1]);
	}
	
}
