package org.seasar.jsf.lifecycle;

import java.io.IOException;
import java.util.Iterator;
import java.util.Map;

import javax.faces.FacesException;
import javax.faces.application.Application;
import javax.faces.application.ViewHandler;
import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.component.UIViewRoot;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.el.EvaluationException;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.faces.lifecycle.Lifecycle;

import org.seasar.framework.util.ArrayUtil;
import org.seasar.jsf.component.S2UIViewRoot;
import org.seasar.jsf.util.ExternalContextUtil;

public class LifecycleImpl extends Lifecycle {

	private static final String VIEW_ID_ATTR = LifecycleImpl.class.getName()
			+ ".VIEW_ID";

	private static final String EXECUTED_ATTR = LifecycleImpl.class.getName()
			+ ".EXECUTED";

	private PhaseListener[] phaseListeners = new PhaseListener[0];

	public LifecycleImpl() {
	}

	public void execute(FacesContext context) throws FacesException {
		try {
			boolean postback = restoreView(context);
			if (isFinished(context)) {
				return;
			}
			Map requestMap = context.getExternalContext().getRequestMap();
			if (requestMap.containsKey(EXECUTED_ATTR)) {
				context.renderResponse();
				return;
			} else {
				requestMap.put(EXECUTED_ATTR, null);
			}
			applyRequestValues(context);
			if (isFinished(context)) {
				return;
			}
			if (postback || hasEvent(context)) {
				processValidations(context);
				if (isFinished(context)) {
					return;
				}
			}
			updateModelValues(context);
			if (isFinished(context)) {
				return;
			}
			invokeApplication(context);
		} catch (EvaluationException ex) {
			Throwable cause = ex.getCause();
			if (cause instanceof RuntimeException) {
				throw (RuntimeException) cause;
			} else if (cause instanceof Error) {
				throw (Error) cause;
			} else {
				throw ex;
			}
		}
	}

	protected boolean restoreView(FacesContext context) throws FacesException {
		beforePhase(context, PhaseId.RESTORE_VIEW);
		ExternalContext externalContext = context.getExternalContext();
		String viewId = ExternalContextUtil.getViewId(externalContext);
		Application application = context.getApplication();
		ViewHandler viewHandler = application.getViewHandler();
		UIViewRoot viewRoot = viewHandler.restoreView(context, viewId);
		if (viewRoot == null) {
			viewRoot = viewHandler.createView(context, viewId);
			context.renderResponse();
		}
		String previousViewId = getViewIdFromSession(externalContext);
		context.setViewRoot(viewRoot);
		saveViewIdToSession(externalContext, viewId);
		setValid(context, viewRoot);
		if (externalContext.getRequestParameterMap().isEmpty()) {
			context.renderResponse();
		}
		afterPhase(context, PhaseId.RESTORE_VIEW);
		return viewId.equals(previousViewId);
	}

	protected void applyRequestValues(FacesContext context)
			throws FacesException {

		beforePhase(context, PhaseId.APPLY_REQUEST_VALUES);
		context.getViewRoot().processDecodes(context);
		afterPhase(context, PhaseId.APPLY_REQUEST_VALUES);
	}

	protected void processValidations(FacesContext context)
			throws FacesException {

		beforePhase(context, PhaseId.PROCESS_VALIDATIONS);
		context.getViewRoot().processValidators(context);
		afterPhase(context, PhaseId.PROCESS_VALIDATIONS);
	}

	protected void updateModelValues(FacesContext context)
			throws FacesException {

		beforePhase(context, PhaseId.UPDATE_MODEL_VALUES);
		context.getViewRoot().processUpdates(context);
		afterPhase(context, PhaseId.UPDATE_MODEL_VALUES);
	}

	protected void invokeApplication(FacesContext context)
			throws FacesException {

		beforePhase(context, PhaseId.INVOKE_APPLICATION);
		context.getViewRoot().processApplication(context);
		afterPhase(context, PhaseId.INVOKE_APPLICATION);
	}

	public void render(FacesContext context) throws FacesException {
		if (context.getResponseComplete()) {
			return;
		}
		beforePhase(context, PhaseId.RENDER_RESPONSE);
		Application application = context.getApplication();
		ViewHandler viewHandler = application.getViewHandler();
		try {
			viewHandler.renderView(context, context.getViewRoot());
		} catch (IOException e) {
			throw new FacesException(e.getMessage(), e);
		} catch (EvaluationException ex) {
			Throwable cause = ex.getCause();
			if (cause instanceof RuntimeException) {
				throw (RuntimeException) cause;
			} else if (cause instanceof Error) {
				throw (Error) cause;
			} else {
				throw ex;
			}
		}
		afterPhase(context, PhaseId.RENDER_RESPONSE);
	}

	protected String getViewIdFromSession(ExternalContext externalContext) {

		return (String) externalContext.getSessionMap().get(VIEW_ID_ATTR);
	}

	protected void saveViewIdToSession(ExternalContext externalContext,
			String viewId) {

		externalContext.getSessionMap().put(VIEW_ID_ATTR, viewId);
	}

	protected void setValid(FacesContext context, UIComponent component) {
		for (Iterator i = component.getFacetsAndChildren(); i.hasNext();) {
			UIComponent child = (UIComponent) i.next();
			if (child instanceof UIInput) {
				((UIInput) child).setValid(true);
			}
			setValid(context, child);
		}
	}

	protected boolean isFinished(FacesContext context) throws FacesException {
		return context.getResponseComplete() || context.getRenderResponse();
	}

	public void addPhaseListener(PhaseListener listener) {
		phaseListeners = (PhaseListener[]) ArrayUtil.add(phaseListeners,
				listener);
	}

	public void removePhaseListener(PhaseListener listener) {
		phaseListeners = (PhaseListener[]) ArrayUtil.remove(phaseListeners,
				listener);
	}

	public PhaseListener[] getPhaseListeners() {
		return phaseListeners;
	}

	protected void beforePhase(FacesContext context, PhaseId phaseId) {
		for (int i = 0; i < phaseListeners.length; i++) {
			PhaseListener listener = phaseListeners[i];
			if (isTargetListener(listener, phaseId)) {
				listener.beforePhase(new PhaseEvent(context, phaseId, this));
			}
		}
	}

	protected void afterPhase(FacesContext context, PhaseId phaseId) {
		for (int i = 0; i < phaseListeners.length; i++) {
			PhaseListener listener = phaseListeners[i];
			if (isTargetListener(listener, phaseId)) {
				listener.afterPhase(new PhaseEvent(context, phaseId, this));
			}
		}
	}

	protected boolean isTargetListener(PhaseListener listener, PhaseId phaseId) {
		int listenerOrdinal = listener.getPhaseId().getOrdinal();
		return listenerOrdinal == PhaseId.ANY_PHASE.getOrdinal()
				|| listenerOrdinal == phaseId.getOrdinal();
	}

	protected boolean hasEvent(FacesContext context) {
		S2UIViewRoot viewRoot = (S2UIViewRoot) context.getViewRoot();
		return viewRoot.getEventSize() > 0;
	}
}