/* AbstractExecution.java

	Purpose:
		
	Description:
		
	History:
		Mon Jun  6 12:18:25     2005, Created by tomyeh

Copyright (C) 2005 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
	This program is distributed under LGPL Version 2.1 in the hope that
	it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zk.ui.impl;

import java.io.IOException;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.zkoss.idom.Document;
import org.zkoss.util.CollectionsX;
import org.zkoss.web.servlet.Servlets;
import org.zkoss.xel.VariableResolver;
import org.zkoss.xel.VariableResolverX;
import org.zkoss.xel.XelContext;
import org.zkoss.zk.au.AuResponse;
import org.zkoss.zk.au.http.AuRedirect;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Execution;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.Session;
import org.zkoss.zk.ui.Sessions;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.metainfo.PageDefinition;
import org.zkoss.zk.ui.sys.DesktopCtrl;
import org.zkoss.zk.ui.sys.ExecutionCtrl;
import org.zkoss.zk.ui.sys.ExecutionInfo;
import org.zkoss.zk.ui.sys.UiEngine;
import org.zkoss.zk.ui.sys.Visualizer;
import org.zkoss.zk.ui.sys.WebAppCtrl;
import org.zkoss.zk.ui.util.Callback;

/**
 * A skeletal implementation of {@link Execution}.
 *
 * @author tomyeh
 */
public abstract class AbstractExecution implements Execution, ExecutionCtrl {
	private static final Logger _zklog = LoggerFactory.getLogger("org.zkoss.zk.log");

	private Desktop _desktop;
	private Page _curpage;
	private PageDefinition _curpgdef;

	/* A list of EventInfo within the same priority.
	 */
	private final Map<Integer, List<EventInfo>> _evtInfos = new TreeMap<Integer, List<EventInfo>>(
			new Comparator<Integer>() {
				public int compare(Integer o1, Integer o2) {
					// reverse it, returning the greater one first.
					return o2.compareTo(o1);
				}
			});

	/** A stack of args being pushed by {@link #pushArg}. */
	private List<Map<?, ?>> _args;
	//private Event _evtInProc;
	/** Which page is being created, or null if all in update mode. */
	private final Page _creating;
	/** The sequence ID of the current request. */
	private String _reqId;
	/** A collection of the AU responses that shall be generated to client */
	private Collection<AuResponse> _resps;
	/** The information of the event being served, or null if not under event processing. */
	private ExecutionInfo _execinf;
	private List<VariableResolver> _resolvers;

	protected static final String Add_ON_ACTIVATE = "org.zkoss.zk.ui.executions.addOnActivate";
	protected static final String Add_ON_DEACTIVATE = "org.zkoss.zk.ui.executions.addOnDeactivate";

	/** Constructs an execution.
	 * @param creating which page is being creating for this execution, or
	 * null if none is being created.
	 * {@link #isAsyncUpdate} returns based on this.
	 */
	protected AbstractExecution(Desktop desktop, Page creating) {
		_desktop = desktop; //it is null if it is created by WebManager.newDesktop
		_curpage = _creating = creating;
		if (_curpage == null)
			_curpage = getPage(desktop);
	}

	private static Page getPage(Desktop desktop) {
		return desktop != null ? desktop.getFirstPage() : null;
	}

	//-- Execution --//
	public final boolean isAsyncUpdate(Page page) {
		if (page != null)
			return _creating != page;
		final Visualizer uv;
		return (uv = getVisualizer()) != null && uv.isEverAsyncUpdate();
	}

	public Desktop getDesktop() {
		return _desktop;
	}

	public Session getSession() {
		return _desktop != null ? _desktop.getSession() : Sessions.getCurrent();
	}

	public void postEvent(Event evt) {
		postEvent(0, evt);
	}

	public void postEvent(int priority, Event evt) {
		if (evt == null)
			throw new IllegalArgumentException("null");

		evt = ((DesktopCtrl) _desktop).beforePostEvent(evt);
		if (evt == null)
			return; //done (ignored)

		List<EventInfo> eventInfos = _evtInfos.get(priority);
		if (eventInfos != null) {
			eventInfos.add(new EventInfo(priority, evt));
		} else {
			eventInfos = new LinkedList<EventInfo>();
			eventInfos.add(new EventInfo(priority, evt));
			_evtInfos.put(priority, eventInfos);
		}
	}

	public void postEvent(int priority, Component realTarget, Event evt) {
		postEvent(priority, realTarget != evt.getTarget() ? new ProxyEvent(realTarget, evt) : evt);
	}

	//-- ExecutionCtrl --//
	public Object getAttribute(String name, boolean recurse) {
		Object val = getAttribute(name);
		Desktop desktop;
		return val != null || !recurse || (desktop = getDesktop()) == null ? val : desktop.getAttribute(name, true);
	}

	public boolean hasAttribute(String name, boolean recurse) {
		Desktop desktop;
		return hasAttribute(name) || (recurse && (desktop = getDesktop()) != null && desktop.hasAttribute(name, true));
	}

	public Object setAttribute(String name, Object value, boolean recurse) {
		if (recurse && !hasAttribute(name)) {
			Desktop desktop = getDesktop();
			if (desktop != null) {
				if (desktop.hasAttribute(name, true))
					return desktop.setAttribute(name, value, true);
			}
		}
		return setAttribute(name, value);
	}

	public Object removeAttribute(String name, boolean recurse) {
		if (recurse && !hasAttribute(name)) {
			Desktop desktop = getDesktop();
			if (desktop != null) {
				if (desktop.hasAttribute(name, true))
					return desktop.removeAttribute(name, true);
			}
			return null;
		}
		return removeAttribute(name);
	}

	public final Page getCurrentPage() {
		if (_curpage == null)
			_curpage = getPage(_desktop);
		return _curpage;
	}

	public final void setCurrentPage(Page curpage) {
		if (_curpage != null && curpage != null && _curpage != curpage) {
			Desktop _curdt = _curpage.getDesktop(), curdt = curpage.getDesktop();
			if (_curdt != null && curdt != null && _curdt != curdt)
				throw new IllegalStateException("Change current page to another desktop? " + curpage);
		}
		_curpage = curpage;
	}

	public PageDefinition getCurrentPageDefinition() {
		return _curpgdef;
	}

	public void setCurrentPageDefinition(PageDefinition pgdef) {
		_curpgdef = pgdef;
	}

	public Event getNextEvent() {
		if (!_evtInfos.isEmpty()) {
			for (Map.Entry<Integer, List<EventInfo>> me : _evtInfos.entrySet()) {
				List<EventInfo> value = me.getValue();
				EventInfo remove = value.remove(0);
				if (value.isEmpty()) {
					_evtInfos.remove(me.getKey());
				}
				return remove.event;
			}
		}

		// ZK-770: EventQueue has extra delay if scope is SESSION
		((DesktopCtrl) _desktop).onPiggyback();

		if (!_evtInfos.isEmpty()) {
			for (Map.Entry<Integer, List<EventInfo>> me : _evtInfos.entrySet()) {
				List<EventInfo> value = me.getValue();
				EventInfo remove = value.remove(0);
				if (value.isEmpty()) {
					_evtInfos.remove(me.getKey());
				}
				return remove.event;
			}
		}
		return null;
	}

	public boolean isActivated() {
		return getVisualizer() != null;
	}

	@SuppressWarnings("unchecked")
	public void onActivate() {
		if (_desktop != null) {
			List<Callback> callbacks = (List<Callback>) _desktop.getAttribute(Add_ON_ACTIVATE);
			if (callbacks != null) {
				for (Iterator<Callback> it = callbacks.iterator(); it.hasNext();) {
					Callback callback = it.next();
					callback.call(null);
					it.remove();
				}
			}
		}
	}

	@SuppressWarnings("unchecked")
	public void onBeforeDeactivate() {
		if (_desktop != null) {
			List<Callback> callbacks = (List<Callback>) _desktop.getAttribute(Add_ON_DEACTIVATE);
			if (callbacks != null) {
				for (Iterator<Callback> it = callbacks.iterator(); it.hasNext();) {
					Callback callback = it.next();
					callback.call(null);
					it.remove();
				}
			}
		}
	}

	public void onDeactivate() {
	}

	public boolean isRecovering() {
		Visualizer uv = getVisualizer();
		return uv != null && uv.isRecovering();
	}

	public Visualizer getVisualizer() {
		return _desktop != null ? ((DesktopCtrl) _desktop).getVisualizer() : null;
	}

	public String toAbsoluteURI(String uri, boolean skipInclude) {
		if (uri != null && uri.length() > 0) {
			final char cc = uri.charAt(0);
			if (cc != '/' && cc != '~' && !(skipInclude && isIncluded()) && !Servlets.isUniversalURL(uri)) {
				final String dir = getDesktop().getCurrentDirectory();
				if (dir != null)
					return dir + uri;
			}
		}
		return uri;
		//we ignore _creating, because Servlet's include cannot handle
		//related URI correctly (even though it is by the layout servlet)
	}

	private final UiEngine getUiEngine() {
		return ((WebAppCtrl) _desktop.getWebApp()).getUiEngine();
	}

	public Component createComponents(String uri, Component parent, Map<?, ?> arg) {
		return createComponents0(uri, parent, null, null, arg);
	}

	public Component createComponents(String uri, Component parent, Component insertBefore, VariableResolver resolver) {
		return createComponents0(uri, parent, insertBefore, resolver, null);
	}

	public Component[] createComponents(String uri, Component parent, Component insertBefore, VariableResolver resolver,
			Map<?, ?> arg) {
		final Component[] cs = getUiEngine().createComponents(this, getPageDefinition(uri), getCurrentPage(), parent,
				insertBefore, resolver, arg);
		return cs.length > 0 ? cs : null;
	}

	public Component createComponents(PageDefinition pagedef, Component parent, Map<?, ?> arg) {
		return createComponents0(pagedef, parent, null, null, arg);
	}

	public Component createComponents(PageDefinition pagedef, Component parent, Component insertBefore,
			VariableResolver resolver) {
		return createComponents0(pagedef, parent, insertBefore, resolver, null);
	}

	public Component[] createComponents(String uri, Map<?, ?> arg) {
		return getUiEngine().createComponents(this, getPageDefinition(uri), null, null, null, null, arg);
	}

	public Component[] createComponents(String uri, Page page, VariableResolver resolver, Map<?, ?> arg) {
		return getUiEngine().createComponents(this, getPageDefinition(uri), page, null, null, resolver, arg);
	}

	public Component[] createComponents(PageDefinition pagedef, Map<?, ?> arg) {
		if (pagedef == null)
			throw new IllegalArgumentException("pagedef cannot be null");
		return getUiEngine().createComponents(this, pagedef, null, null, null, null, arg);
	}

	private Component createComponents0(String uri, Component parent, Component insertBefore, VariableResolver resolver,
			Map<?, ?> arg) {
		final Component[] cs = getUiEngine().createComponents(this, getPageDefinition(uri), getCurrentPage(), parent,
				insertBefore, resolver, arg);
		return cs.length > 0 ? cs[0] : null;
	}

	private Component createComponents0(PageDefinition pagedef, Component parent, Component insertBefore,
			VariableResolver resolver, Map<?, ?> arg) {
		if (pagedef == null)
			throw new IllegalArgumentException("pagedef cannot be null");
		final Component[] cs = getUiEngine().createComponents(this, pagedef, getCurrentPage(), parent, insertBefore,
				resolver, arg);
		return cs.length > 0 ? cs[0] : null;
	}

	public Component createComponentsDirectly(String content, String ext, Component parent, Map<?, ?> arg) {
		return createComponentsDirectly0(content, ext, parent, null, null, arg);
	}

	public Component createComponentsDirectly(String content, String ext, Component parent, Component insertBefore,
			VariableResolver resolver) {
		return createComponentsDirectly0(content, ext, parent, insertBefore, resolver, null);
	}

	public Component createComponentsDirectly(Document content, String ext, Component parent, Map<?, ?> arg) {
		return createComponentsDirectly0(content, ext, parent, null, null, arg);
	}

	public Component createComponentsDirectly(Document content, String ext, Component parent, Component insertBefore,
			VariableResolver resolver) {
		return createComponentsDirectly0(content, ext, parent, insertBefore, resolver, null);
	}

	public Component createComponentsDirectly(Reader reader, String ext, Component parent, Map<?, ?> arg)
			throws IOException {
		return createComponentsDirectly0(reader, ext, parent, null, null, arg);
	}

	public Component createComponentsDirectly(Reader reader, String ext, Component parent, Component insertBefore,
			VariableResolver resolver) throws IOException {
		return createComponentsDirectly0(reader, ext, parent, insertBefore, resolver, null);
	}

	public Component[] createComponentsDirectly(String content, String ext, Map<?, ?> arg) {
		return getUiEngine().createComponents(this, getPageDefinitionDirectly(content, ext), null, null, null, null,
				arg);
	}

	public Component[] createComponentsDirectly(Document content, String ext, Map<?, ?> arg) {
		return getUiEngine().createComponents(this, getPageDefinitionDirectly(content, ext), null, null, null, null,
				arg);
	}

	public Component[] createComponentsDirectly(Reader reader, String ext, Map<?, ?> arg) throws IOException {
		return getUiEngine().createComponents(this, getPageDefinitionDirectly(reader, ext), null, null, null, null,
				arg);
	}

	private Component createComponentsDirectly0(String content, String ext, Component parent, Component insertBefore,
			VariableResolver resolver, Map<?, ?> arg) {
		final Component[] cs = getUiEngine().createComponents(this, getPageDefinitionDirectly(content, ext),
				getCurrentPage(), parent, insertBefore, resolver, arg);
		return cs.length > 0 ? cs[0] : null;
	}

	private Component createComponentsDirectly0(Document content, String ext, Component parent, Component insertBefore,
			VariableResolver resolver, Map<?, ?> arg) {
		final Component[] cs = getUiEngine().createComponents(this, getPageDefinitionDirectly(content, ext),
				getCurrentPage(), parent, insertBefore, resolver, arg);
		return cs.length > 0 ? cs[0] : null;
	}

	private Component createComponentsDirectly0(Reader reader, String ext, Component parent, Component insertBefore,
			VariableResolver resolver, Map<?, ?> arg) throws IOException {
		final Component[] cs = getUiEngine().createComponents(this, getPageDefinitionDirectly(reader, ext),
				getCurrentPage(), parent, insertBefore, resolver, arg);
		return cs.length > 0 ? cs[0] : null;
	}

	public void sendRedirect(String uri) {
		getUiEngine().sendRedirect(uri, null);
	}

	public void sendRedirect(String uri, String target) {
		getUiEngine().sendRedirect(uri, target);
	}

	public void sendRedirect(String uri, boolean respRedirect) {
		if (!respRedirect) {
			sendRedirect(uri);
			return;
		} else {
			uri = uri == null ? "" : uri;
			HttpServletResponse resp = (HttpServletResponse) getNativeResponse();
			try {
				String destUrl = encodeURL(uri);
				String destUrlParam = URLEncoder.encode(destUrl, "utf-8");
				String updateURI = _desktop.getUpdateURI(
						AuRedirect.URI_PREFIX + "?" + AuRedirect.REDIRECT_URL_PARAMETER + "=" + destUrlParam);
				updateURI = resp.encodeRedirectURL(updateURI);
				resp.setHeader("Location", updateURI);
				resp.setStatus(HttpServletResponse.SC_FOUND);
			} catch (UnsupportedEncodingException e) {
				_zklog.warn("", e);
			}
		}
	}

	public Map<?, ?> getArg() {
		if (_args != null)
			return _args.get(0);
		return Collections.emptyMap();
	}

	public void pushArg(Map<?, ?> arg) {
		if (_args == null)
			_args = new LinkedList<Map<?, ?>>();
		_args.add(0, arg);
	}

	public void popArg() {
		if (_args != null) {
			if (_args.size() == 1)
				_args = null;
			else
				_args.remove(0);
		}
	}

	public void addAuResponse(AuResponse response) {
		getUiEngine().addResponse(response);
	}

	public void addAuResponse(String key, AuResponse response) {
		getUiEngine().addResponse(key, response);
	}

	public void setDesktop(Desktop desktop) {
		if (desktop == null)
			throw new IllegalArgumentException("null");
		if (_desktop != null && _desktop != desktop)
			throw new IllegalStateException("assign diff desktop");

		_desktop = desktop;
	}

	public void setRequestId(String reqId) {
		_reqId = reqId;
	}

	public String getRequestId() {
		return _reqId;
	}

	public Collection<AuResponse> getResponses() {
		return _resps;
	}

	public void setResponses(Collection<AuResponse> responses) {
		_resps = responses;
	}

	public ExecutionInfo getExecutionInfo() {
		return _execinf;
	}

	public void setExecutionInfo(ExecutionInfo execinf) {
		_execinf = execinf;
	}

	public boolean addVariableResolver(VariableResolver resolver) {
		if (resolver == null)
			throw new IllegalArgumentException("null");

		if (_resolvers == null)
			_resolvers = new LinkedList<VariableResolver>();
		else if (_resolvers.contains(resolver))
			return false;

		_resolvers.add(0, resolver); //FILO order
		return true;
	}

	public boolean removeVariableResolver(VariableResolver resolver) {
		return _resolvers != null && _resolvers.remove(resolver);
	}

	public boolean hasVariableResolver(VariableResolver resolver) {
		return _resolvers != null && _resolvers.contains(resolver);
	}

	public boolean hasVariableResolver(Class<? extends VariableResolver> cls) {
		if (_resolvers != null)
			for (final VariableResolver resolver : _resolvers)
				if (cls.isInstance(resolver))
					return true;
		return false;
	}

	public Object getExtraXelVariable(String name) {
		return getExtraXelVariable(null, null, name);
	}

	public Object getExtraXelVariable(XelContext ctx, Object base, Object name) {
		//Note this method searches only _resolvers
		if (_resolvers != null) {
			for (Iterator it = CollectionsX.comodifiableIterator(_resolvers); it.hasNext();) {
				final VariableResolver vr = (VariableResolver) it.next();
				final Object o = vr instanceof VariableResolverX
						? ((VariableResolverX) vr).resolveVariable(ctx, base, name)
						: base == null && name != null ? vr.resolveVariable(name.toString()) : null;
				if (o != null)
					return o;
			}
		}
		return null;
	}

	public void log(String msg) {
		if (_desktop != null)
			_desktop.getWebApp().log(msg);
		else
			_zklog.info(msg);
	}

	public void log(String msg, Throwable ex) {
		if (_desktop != null)
			_desktop.getWebApp().log(msg, ex);
		else
			_zklog.error(msg, ex);
	}

	/**
	 * Adds a callback method to be executed only once after the execution
	 * activated.
	 * @param callback
	 * @since 7.0.5
	 */
	public void addOnActivate(Callback callback) {
		Execution exec = Executions.getCurrent();
		if (exec == null)
			throw new IllegalStateException("Execution cannot be null!");
		Desktop desktop = exec.getDesktop();
		if (desktop != null) {
			List<Callback> callbacks = (List<Callback>) desktop.getAttribute(Add_ON_ACTIVATE);
			if (callbacks == null) {
				callbacks = new LinkedList<Callback>();
				desktop.setAttribute(Add_ON_ACTIVATE, callbacks);
			}
			callbacks.add(callback);
		}
	}

	/**
	 * Adds a callback method to be executed only once after the execution
	 * deactivated.
	 * @param callback
	 * @since 7.0.5
	 */
	public void addOnDeactivate(Callback callback) {
		Execution exec = Executions.getCurrent();
		if (exec == null)
			throw new IllegalStateException("Execution cannot be null!");
		Desktop desktop = exec.getDesktop();
		if (desktop != null) {
			List<Callback> callbacks = (List<Callback>) desktop.getAttribute(Add_ON_DEACTIVATE);
			if (callbacks == null) {
				callbacks = new LinkedList<Callback>();
				desktop.setAttribute(Add_ON_DEACTIVATE, callbacks);
			}
			callbacks.add(callback);
		}
	}

	//Object//
	public String toString() {
		return "[Exec" + System.identityHashCode(this) + ": " + _desktop + ']';
	}

	private static class EventInfo {
		private final int priority;
		private final Event event;

		private EventInfo(int priority, Event event) {
			this.priority = priority;
			this.event = event;
		}

		public String toString() {
			return "[" + this.priority + ": " + this.event.toString() + "]";
		}
	}
}
