/*
@VaadinApache2LicenseForJavaFiles@
 */

package com.vaadin.terminal.gwt.client;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.gwt.core.client.Duration;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.regexp.shared.MatchResult;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.terminal.gwt.client.ApplicationConfiguration.ErrorMessage;
import com.vaadin.terminal.gwt.client.communication.JsonDecoder;
import com.vaadin.terminal.gwt.client.communication.JsonEncoder;
import com.vaadin.terminal.gwt.client.communication.MethodInvocation;
import com.vaadin.terminal.gwt.client.communication.RpcManager;
import com.vaadin.terminal.gwt.client.communication.SharedState;
import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
import com.vaadin.terminal.gwt.client.ui.AbstractComponentConnector;
import com.vaadin.terminal.gwt.client.ui.VContextMenu;
import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager;
import com.vaadin.terminal.gwt.client.ui.notification.VNotification;
import com.vaadin.terminal.gwt.client.ui.notification.VNotification.HideEvent;
import com.vaadin.terminal.gwt.client.ui.root.RootConnector;
import com.vaadin.terminal.gwt.client.ui.window.WindowConnector;
import com.vaadin.terminal.gwt.server.AbstractCommunicationManager;

/**
 * This is the client side communication "engine", managing client-server
 * communication with its server side counterpart
 * {@link AbstractCommunicationManager}.
 * 
 * Client-side connectors receive updates from the corresponding server-side
 * connector (typically component) as state updates or RPC calls. The connector
 * has the possibility to communicate back with its server side counter part
 * through RPC calls.
 * 
 * TODO document better
 * 
 * Entry point classes (widgetsets) define <code>onModuleLoad()</code>.
 */
public class ApplicationConnection {
    // This indicates the whole page is generated by us (not embedded)
    public static final String GENERATED_BODY_CLASSNAME = "v-generated-body";

    public static final String MODIFIED_CLASSNAME = "v-modified";

    public static final String DISABLED_CLASSNAME = "v-disabled";

    public static final String REQUIRED_CLASSNAME_EXT = "-required";

    public static final String ERROR_CLASSNAME_EXT = "-error";

    public static final String UPDATE_VARIABLE_INTERFACE = "v";
    public static final String UPDATE_VARIABLE_METHOD = "v";

    public static final char VAR_BURST_SEPARATOR = '\u001d';

    public static final char VAR_ESCAPE_CHARACTER = '\u001b';

    public static final String UIDL_SECURITY_TOKEN_ID = "Vaadin-Security-Key";

    /**
     * Name of the parameter used to transmit root ids back and forth
     */
    public static final String ROOT_ID_PARAMETER = "rootId";

    /**
     * @deprecated use UIDL_SECURITY_TOKEN_ID instead
     */
    @Deprecated
    public static final String UIDL_SECURITY_HEADER = UIDL_SECURITY_TOKEN_ID;

    public static final String PARAM_UNLOADBURST = "onunloadburst";

    /**
     * A string that, if found in a non-JSON response to a UIDL request, will
     * cause the browser to refresh the page. If followed by a colon, optional
     * whitespace, and a URI, causes the browser to synchronously load the URI.
     * 
     * <p>
     * This allows, for instance, a servlet filter to redirect the application
     * to a custom login page when the session expires. For example:
     * </p>
     * 
     * <pre>
     * if (sessionExpired) {
     *     response.setHeader(&quot;Content-Type&quot;, &quot;text/html&quot;);
     *     response.getWriter().write(
     *             myLoginPageHtml + &quot;&lt;!-- Vaadin-Refresh: &quot;
     *                     + request.getContextPath() + &quot; --&gt;&quot;);
     * }
     * </pre>
     */
    public static final String UIDL_REFRESH_TOKEN = "Vaadin-Refresh";

    private final boolean debugLogging = false;

    // will hold the UIDL security key (for XSS protection) once received
    private String uidlSecurityKey = "init";

    private final HashMap<String, String> resourcesMap = new HashMap<String, String>();

    private ArrayList<MethodInvocation> pendingInvocations = new ArrayList<MethodInvocation>();

    private WidgetSet widgetSet;

    private VContextMenu contextMenu = null;

    private Timer loadTimer;
    private Timer loadTimer2;
    private Timer loadTimer3;
    private Element loadElement;

    private final RootConnector rootConnector;

    protected boolean applicationRunning = false;

    private boolean hasActiveRequest = false;

    protected boolean cssLoaded = false;

    /** Parameters for this application connection loaded from the web-page */
    private ApplicationConfiguration configuration;

    /** List of pending variable change bursts that must be submitted in order */
    private final ArrayList<ArrayList<MethodInvocation>> pendingBursts = new ArrayList<ArrayList<MethodInvocation>>();

    /** Timer for automatic refirect to SessionExpiredURL */
    private Timer redirectTimer;

    /** redirectTimer scheduling interval in seconds */
    private int sessionExpirationInterval;

    private ArrayList<Widget> componentCaptionSizeChanges = new ArrayList<Widget>();

    private Date requestStartTime;

    private boolean validatingLayouts = false;

    private Set<ComponentConnector> zeroWidthComponents = null;

    private Set<ComponentConnector> zeroHeightComponents = null;

    private final LayoutManager layoutManager;

    private final RpcManager rpcManager;

    public static class MultiStepDuration extends Duration {
        private int previousStep = elapsedMillis();

        public void logDuration(String message) {
            logDuration(message, 0);
        }

        public void logDuration(String message, int minDuration) {
            int currentTime = elapsedMillis();
            int stepDuration = currentTime - previousStep;
            if (stepDuration >= minDuration) {
                VConsole.log(message + ": " + stepDuration + " ms");
            }
            previousStep = currentTime;
        }
    }

    public ApplicationConnection() {
        rootConnector = GWT.create(RootConnector.class);
        rpcManager = GWT.create(RpcManager.class);
        layoutManager = GWT.create(LayoutManager.class);
        layoutManager.setConnection(this);
    }

    public void init(WidgetSet widgetSet, ApplicationConfiguration cnf) {
        VConsole.log("Starting application " + cnf.getRootPanelId());

        VConsole.log("Vaadin application servlet version: "
                + cnf.getServletVersion());
        VConsole.log("Application version: " + cnf.getApplicationVersion());

        if (!cnf.getServletVersion().equals(ApplicationConfiguration.VERSION)) {
            VConsole.error("Warning: your widget set seems to be built with a different "
                    + "version than the one used on server. Unexpected "
                    + "behavior may occur.");
        }

        this.widgetSet = widgetSet;
        configuration = cnf;

        ComponentLocator componentLocator = new ComponentLocator(this);

        String appRootPanelName = cnf.getRootPanelId();
        // remove the end (window name) of autogenerated rootpanel id
        appRootPanelName = appRootPanelName.replaceFirst("-\\d+$", "");

        initializeTestbenchHooks(componentLocator, appRootPanelName);

        initializeClientHooks();

        rootConnector.init(cnf.getRootPanelId(), this);
        showLoadingIndicator();
    }

    /**
     * Starts this application. Don't call this method directly - it's called by
     * {@link ApplicationConfiguration#startNextApplication()}, which should be
     * called once this application has started (first response received) or
     * failed to start. This ensures that the applications are started in order,
     * to avoid session-id problems.
     * 
     */
    public void start() {
        String jsonText = configuration.getUIDL();
        if (jsonText == null) {
            // inital UIDL not in DOM, request later
            repaintAll();
        } else {
            // Update counter so TestBench knows something is still going on
            hasActiveRequest = true;

            // initial UIDL provided in DOM, continue as if returned by request
            handleJSONText(jsonText, -1);
        }
    }

    private native void initializeTestbenchHooks(
            ComponentLocator componentLocator, String TTAppId)
    /*-{
    	var ap = this;
    	var client = {};
    	client.isActive = function() {
    		return ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::hasActiveRequest()()
    				|| ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::isExecutingDeferredCommands()();
    	}
    	var vi = ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::getVersionInfo()();
    	if (vi) {
    		client.getVersionInfo = function() {
    			return vi;
    		}
    	}

    	client.getElementByPath = function(id) {
    		return componentLocator.@com.vaadin.terminal.gwt.client.ComponentLocator::getElementByPath(Ljava/lang/String;)(id);
    	}
    	client.getPathForElement = function(element) {
    		return componentLocator.@com.vaadin.terminal.gwt.client.ComponentLocator::getPathForElement(Lcom/google/gwt/user/client/Element;)(element);
    	}

    	$wnd.vaadin.clients[TTAppId] = client;
    }-*/;

    /**
     * Helper for tt initialization
     */
    private JavaScriptObject getVersionInfo() {
        return configuration.getVersionInfoJSObject();
    }

    /**
     * Publishes a JavaScript API for mash-up applications.
     * <ul>
     * <li><code>vaadin.forceSync()</code> sends pending variable changes, in
     * effect synchronizing the server and client state. This is done for all
     * applications on host page.</li>
     * <li><code>vaadin.postRequestHooks</code> is a map of functions which gets
     * called after each XHR made by vaadin application. Note, that it is
     * attaching js functions responsibility to create the variable like this:
     * 
     * <code><pre>
     * if(!vaadin.postRequestHooks) {vaadin.postRequestHooks = new Object();}
     * postRequestHooks.myHook = function(appId) {
     *          if(appId == "MyAppOfInterest") {
     *                  // do the staff you need on xhr activity
     *          }
     * }
     * </pre></code> First parameter passed to these functions is the identifier
     * of Vaadin application that made the request.
     * </ul>
     * 
     * TODO make this multi-app aware
     */
    private native void initializeClientHooks()
    /*-{
    	var app = this;
    	var oldSync;
    	if ($wnd.vaadin.forceSync) {
    		oldSync = $wnd.vaadin.forceSync;
    	}
    	$wnd.vaadin.forceSync = function() {
    		if (oldSync) {
    			oldSync();
    		}
    		app.@com.vaadin.terminal.gwt.client.ApplicationConnection::sendPendingVariableChanges()();
    	}
    	var oldForceLayout;
    	if ($wnd.vaadin.forceLayout) {
    		oldForceLayout = $wnd.vaadin.forceLayout;
    	}
    	$wnd.vaadin.forceLayout = function() {
    		if (oldForceLayout) {
    			oldForceLayout();
    		}
    		app.@com.vaadin.terminal.gwt.client.ApplicationConnection::forceLayout()();
    	}
    }-*/;

    /**
     * Runs possibly registered client side post request hooks. This is expected
     * to be run after each uidl request made by Vaadin application.
     * 
     * @param appId
     */
    private static native void runPostRequestHooks(String appId)
    /*-{
    	if ($wnd.vaadin.postRequestHooks) {
    		for ( var hook in $wnd.vaadin.postRequestHooks) {
    			if (typeof ($wnd.vaadin.postRequestHooks[hook]) == "function") {
    				try {
    					$wnd.vaadin.postRequestHooks[hook](appId);
    				} catch (e) {
    				}
    			}
    		}
    	}
    }-*/;

    /**
     * Get the active Console for writing debug messages. May return an actual
     * logging console, or the NullConsole if debugging is not turned on.
     * 
     * @deprecated Developers should use {@link VConsole} since 6.4.5
     * 
     * @return the active Console
     */
    @Deprecated
    public static Console getConsole() {
        return VConsole.getImplementation();
    }

    /**
     * Checks if client side is in debug mode. Practically this is invoked by
     * adding ?debug parameter to URI.
     * 
     * @deprecated use ApplicationConfiguration isDebugMode instead.
     * 
     * @return true if client side is currently been debugged
     */
    @Deprecated
    public static boolean isDebugMode() {
        return ApplicationConfiguration.isDebugMode();
    }

    /**
     * Gets the application base URI. Using this other than as the download
     * action URI can cause problems in Portlet 2.0 deployments.
     * 
     * @return application base URI
     */
    public String getAppUri() {
        return configuration.getApplicationUri();
    };

    /**
     * Indicates whether or not there are currently active UIDL requests. Used
     * internally to sequence requests properly, seldom needed in Widgets.
     * 
     * @return true if there are active requests
     */
    public boolean hasActiveRequest() {
        return hasActiveRequest;
    }

    private String getRepaintAllParameters() {
        // collect some client side data that will be sent to server on
        // initial uidl request
        String nativeBootstrapParameters = getNativeBrowserDetailsParameters(getConfiguration()
                .getRootPanelId());
        String widgetsetVersion = ApplicationConfiguration.VERSION;

        // TODO figure out how client and view size could be used better on
        // server. screen size can be accessed via Browser object, but other
        // values currently only via transaction listener.
        String parameters = "repaintAll=1&" + nativeBootstrapParameters
                + "&wsver=" + widgetsetVersion;
        return parameters;
    }

    /**
     * Gets the browser detail parameters that are sent by the bootstrap
     * javascript for two-request initialization.
     * 
     * @param parentElementId
     * @return
     */
    private static native String getNativeBrowserDetailsParameters(
            String parentElementId)
    /*-{
       return $wnd.vaadin.getBrowserDetailsParameters(parentElementId);
    }-*/;

    protected void repaintAll() {
        String repainAllParameters = getRepaintAllParameters();
        makeUidlRequest("", repainAllParameters, false);
    }

    /**
     * Requests an analyze of layouts, to find inconsistencies. Exclusively used
     * for debugging during development.
     */
    public void analyzeLayouts() {
        String params = getRepaintAllParameters() + "&analyzeLayouts=1";
        makeUidlRequest("", params, false);
    }

    /**
     * Sends a request to the server to print details to console that will help
     * developer to locate component in the source code.
     * 
     * @param componentConnector
     */
    void highlightComponent(ComponentConnector componentConnector) {
        String params = getRepaintAllParameters() + "&highlightComponent="
                + componentConnector.getConnectorId();
        makeUidlRequest("", params, false);
    }

    /**
     * Makes an UIDL request to the server.
     * 
     * @param requestData
     *            Data that is passed to the server.
     * @param extraParams
     *            Parameters that are added as GET parameters to the url.
     *            Contains key=value pairs joined by & characters or is empty if
     *            no parameters should be added. Should not start with any
     *            special character.
     * @param forceSync
     *            true if the request should be synchronous, false otherwise
     */
    protected void makeUidlRequest(final String requestData,
            final String extraParams, final boolean forceSync) {
        startRequest();
        // Security: double cookie submission pattern
        final String payload = uidlSecurityKey + VAR_BURST_SEPARATOR
                + requestData;
        VConsole.log("Making UIDL Request with params: " + payload);
        String uri;
        if (configuration.usePortletURLs()) {
            uri = configuration.getPortletUidlURLBase();
        } else {
            uri = getAppUri() + "UIDL";
        }

        if (extraParams != null && extraParams.length() > 0) {
            uri = addGetParameters(uri, extraParams);
        }
        uri = addGetParameters(uri,
                ROOT_ID_PARAMETER + "=" + configuration.getRootId());

        doUidlRequest(uri, payload, forceSync);

    }

    /**
     * Sends an asynchronous or synchronous UIDL request to the server using the
     * given URI.
     * 
     * @param uri
     *            The URI to use for the request. May includes GET parameters
     * @param payload
     *            The contents of the request to send
     * @param synchronous
     *            true if the request should be synchronous, false otherwise
     */
    protected void doUidlRequest(final String uri, final String payload,
            final boolean synchronous) {
        if (!synchronous) {
            RequestCallback requestCallback = new RequestCallback() {
                public void onError(Request request, Throwable exception) {
                    showCommunicationError(exception.getMessage(), -1);
                    endRequest();
                }

                public void onResponseReceived(Request request,
                        Response response) {
                    VConsole.log("Server visit took "
                            + String.valueOf((new Date()).getTime()
                                    - requestStartTime.getTime()) + "ms");

                    int statusCode = response.getStatusCode();

                    switch (statusCode) {
                    case 0:
                        showCommunicationError(
                                "Invalid status code 0 (server down?)",
                                statusCode);
                        endRequest();
                        return;

                    case 401:
                        /*
                         * Authorization has failed. Could be that the session
                         * has timed out and the container is redirecting to a
                         * login page.
                         */
                        showAuthenticationError("");
                        endRequest();
                        return;

                    case 503:
                        // We'll assume msec instead of the usual seconds
                        int delay = Integer.parseInt(response
                                .getHeader("Retry-After"));
                        VConsole.log("503, retrying in " + delay + "msec");
                        (new Timer() {
                            @Override
                            public void run() {
                                // TODO why? Here used to be "activeRequests--;"
                                // but can't see why exactly
                                hasActiveRequest = false;
                                doUidlRequest(uri, payload, synchronous);
                            }
                        }).schedule(delay);
                        return;

                    }

                    if ((statusCode / 100) == 4) {
                        // Handle all 4xx errors the same way as (they are
                        // all permanent errors)
                        showCommunicationError(
                                "UIDL could not be read from server. Check servlets mappings. Error code: "
                                        + statusCode, statusCode);
                        endRequest();
                        return;
                    }

                    String contentType = response.getHeader("Content-Type");
                    if (contentType == null
                            || !contentType.startsWith("application/json")) {
                        /*
                         * A servlet filter or equivalent may have intercepted
                         * the request and served non-UIDL content (for
                         * instance, a login page if the session has expired.)
                         * If the response contains a magic substring, do a
                         * synchronous refresh. See #8241.
                         */
                        MatchResult refreshToken = RegExp.compile(
                                UIDL_REFRESH_TOKEN + "(:\\s*(.*?))?(\\s|$)")
                                .exec(response.getText());
                        if (refreshToken != null) {
                            redirect(refreshToken.getGroup(2));
                            return;
                        }
                    }

                    // for(;;);[realjson]
                    final String jsonText = response.getText().substring(9,
                            response.getText().length() - 1);
                    handleJSONText(jsonText, statusCode);
                }

            };
            try {
                doAsyncUIDLRequest(uri, payload, requestCallback);
            } catch (RequestException e) {
                VConsole.error(e);
                endRequest();
            }
        } else {
            // Synchronized call, discarded response (leaving the page)
            SynchronousXHR syncXHR = (SynchronousXHR) SynchronousXHR.create();
            syncXHR.synchronousPost(uri + "&" + PARAM_UNLOADBURST + "=1",
                    payload);
            /*
             * Although we are in theory leaving the page, the page may still
             * stay open. End request properly here too. See #3289
             */
            endRequest();
        }

    }

    /**
     * Handles received UIDL JSON text, parsing it, and passing it on to the
     * appropriate handlers, while logging timiing information.
     * 
     * @param jsonText
     * @param statusCode
     */
    private void handleJSONText(String jsonText, int statusCode) {
        final Date start = new Date();
        final ValueMap json;
        try {
            json = parseJSONResponse(jsonText);
        } catch (final Exception e) {
            endRequest();
            showCommunicationError(e.getMessage() + " - Original JSON-text:"
                    + jsonText, statusCode);
            return;
        }

        VConsole.log("JSON parsing took "
                + (new Date().getTime() - start.getTime()) + "ms");
        if (applicationRunning) {
            handleReceivedJSONMessage(start, jsonText, json);
        } else {
            applicationRunning = true;
            handleWhenCSSLoaded(jsonText, json);
        }
    }

    /**
     * Sends an asynchronous UIDL request to the server using the given URI.
     * 
     * @param uri
     *            The URI to use for the request. May includes GET parameters
     * @param payload
     *            The contents of the request to send
     * @param requestCallback
     *            The handler for the response
     * @throws RequestException
     *             if the request could not be sent
     */
    protected void doAsyncUIDLRequest(String uri, String payload,
            RequestCallback requestCallback) throws RequestException {
        RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, uri);
        // TODO enable timeout
        // rb.setTimeoutMillis(timeoutMillis);
        rb.setHeader("Content-Type", "text/plain;charset=utf-8");
        rb.setRequestData(payload);
        rb.setCallback(requestCallback);

        rb.send();
    }

    int cssWaits = 0;
    static final int MAX_CSS_WAITS = 100;

    protected void handleWhenCSSLoaded(final String jsonText,
            final ValueMap json) {
        if (!isCSSLoaded() && cssWaits < MAX_CSS_WAITS) {
            (new Timer() {
                @Override
                public void run() {
                    handleWhenCSSLoaded(jsonText, json);
                }
            }).schedule(50);
            VConsole.log("Assuming CSS loading is not complete, "
                    + "postponing render phase. "
                    + "(.v-loading-indicator height == 0)");
            cssWaits++;
        } else {
            cssLoaded = true;
            handleReceivedJSONMessage(new Date(), jsonText, json);
            if (cssWaits >= MAX_CSS_WAITS) {
                VConsole.error("CSS files may have not loaded properly.");
            }
        }
    }

    /**
     * Checks whether or not the CSS is loaded. By default checks the size of
     * the loading indicator element.
     * 
     * @return
     */
    protected boolean isCSSLoaded() {
        return cssLoaded
                || DOM.getElementPropertyInt(loadElement, "offsetHeight") != 0;
    }

    /**
     * Shows the communication error notification.
     * 
     * @param details
     *            Optional details for debugging.
     * @param statusCode
     *            The status code returned for the request
     * 
     */
    protected void showCommunicationError(String details, int statusCode) {
        VConsole.error("Communication error: " + details);
        ErrorMessage communicationError = configuration.getCommunicationError();
        showError(details, communicationError.getCaption(),
                communicationError.getMessage(), communicationError.getUrl());
    }

    /**
     * Shows the authentication error notification.
     * 
     * @param details
     *            Optional details for debugging.
     */
    protected void showAuthenticationError(String details) {
        VConsole.error("Authentication error: " + details);
        ErrorMessage authorizationError = configuration.getAuthorizationError();
        showError(details, authorizationError.getCaption(),
                authorizationError.getMessage(), authorizationError.getUrl());
    }

    /**
     * Shows the error notification.
     * 
     * @param details
     *            Optional details for debugging.
     */
    private void showError(String details, String caption, String message,
            String url) {

        StringBuilder html = new StringBuilder();
        if (caption != null) {
            html.append("<h1>");
            html.append(caption);
            html.append("</h1>");
        }
        if (message != null) {
            html.append("<p>");
            html.append(message);
            html.append("</p>");
        }

        if (html.length() > 0) {

            // Add error description
            html.append("<br/><p><I style=\"font-size:0.7em\">");
            html.append(details);
            html.append("</I></p>");

            VNotification n = VNotification.createNotification(1000 * 60 * 45);
            n.addEventListener(new NotificationRedirect(url));
            n.show(html.toString(), VNotification.CENTERED_TOP,
                    VNotification.STYLE_SYSTEM);
        } else {
            redirect(url);
        }
    }

    protected void startRequest() {
        if (hasActiveRequest) {
            VConsole.error("Trying to start a new request while another is active");
        }
        hasActiveRequest = true;
        requestStartTime = new Date();
        // show initial throbber
        if (loadTimer == null) {
            loadTimer = new Timer() {
                @Override
                public void run() {
                    /*
                     * IE7 does not properly cancel the event with
                     * loadTimer.cancel() so we have to check that we really
                     * should make it visible
                     */
                    if (loadTimer != null) {
                        showLoadingIndicator();
                    }

                }
            };
            // First one kicks in at 300ms
        }
        loadTimer.schedule(300);
    }

    protected void endRequest() {
        if (!hasActiveRequest) {
            VConsole.error("No active request");
        }
        // After checkForPendingVariableBursts() there may be a new active
        // request, so we must set hasActiveRequest to false before, not after,
        // the call. Active requests used to be tracked with an integer counter,
        // so setting it after used to work but not with the #8505 changes.
        hasActiveRequest = false;
        if (applicationRunning) {
            checkForPendingVariableBursts();
            runPostRequestHooks(configuration.getRootPanelId());
        }
        // deferring to avoid flickering
        Scheduler.get().scheduleDeferred(new Command() {
            public void execute() {
                if (!hasActiveRequest()) {
                    hideLoadingIndicator();
                }
            }
        });
    }

    /**
     * This method is called after applying uidl change set to application.
     * 
     * It will clean current and queued variable change sets. And send next
     * change set if it exists.
     */
    private void checkForPendingVariableBursts() {
        cleanVariableBurst(pendingInvocations);
        if (pendingBursts.size() > 0) {
            for (Iterator<ArrayList<MethodInvocation>> iterator = pendingBursts
                    .iterator(); iterator.hasNext();) {
                cleanVariableBurst(iterator.next());
            }
            ArrayList<MethodInvocation> nextBurst = pendingBursts.get(0);
            pendingBursts.remove(0);
            buildAndSendVariableBurst(nextBurst, false);
        }
    }

    /**
     * Cleans given queue of variable changes of such changes that came from
     * components that do not exist anymore.
     * 
     * @param variableBurst
     */
    private void cleanVariableBurst(ArrayList<MethodInvocation> variableBurst) {
        for (int i = 1; i < variableBurst.size(); i++) {
            String id = variableBurst.get(i).getConnectorId();
            if (!getConnectorMap().hasConnector(id)
                    && !getConnectorMap().isDragAndDropPaintable(id)) {
                // variable owner does not exist anymore
                variableBurst.remove(i);
                VConsole.log("Removed variable from removed component: " + id);
            }
        }
    }

    private void showLoadingIndicator() {
        // show initial throbber
        if (loadElement == null) {
            loadElement = DOM.createDiv();
            DOM.setStyleAttribute(loadElement, "position", "absolute");
            DOM.appendChild(rootConnector.getWidget().getElement(), loadElement);
            VConsole.log("inserting load indicator");
        }
        DOM.setElementProperty(loadElement, "className", "v-loading-indicator");
        DOM.setStyleAttribute(loadElement, "display", "block");
        // Initialize other timers
        loadTimer2 = new Timer() {
            @Override
            public void run() {
                DOM.setElementProperty(loadElement, "className",
                        "v-loading-indicator-delay");
            }
        };
        // Second one kicks in at 1500ms from request start
        loadTimer2.schedule(1200);

        loadTimer3 = new Timer() {
            @Override
            public void run() {
                DOM.setElementProperty(loadElement, "className",
                        "v-loading-indicator-wait");
            }
        };
        // Third one kicks in at 5000ms from request start
        loadTimer3.schedule(4700);
    }

    private void hideLoadingIndicator() {
        if (loadTimer != null) {
            loadTimer.cancel();
            loadTimer = null;
        }
        if (loadTimer2 != null) {
            loadTimer2.cancel();
            loadTimer3.cancel();
            loadTimer2 = null;
            loadTimer3 = null;
        }
        if (loadElement != null) {
            DOM.setStyleAttribute(loadElement, "display", "none");
        }
    }

    /**
     * Checks if deferred commands are (potentially) still being executed as a
     * result of an update from the server. Returns true if a deferred command
     * might still be executing, false otherwise. This will not work correctly
     * if a deferred command is added in another deferred command.
     * <p>
     * Used by the native "client.isActive" function.
     * </p>
     * 
     * @return true if deferred commands are (potentially) being executed, false
     *         otherwise
     */
    private boolean isExecutingDeferredCommands() {
        Scheduler s = Scheduler.get();
        if (s instanceof VSchedulerImpl) {
            return ((VSchedulerImpl) s).hasWorkQueued();
        } else {
            return false;
        }
    }

    /**
     * Determines whether or not the loading indicator is showing.
     * 
     * @return true if the loading indicator is visible
     */
    public boolean isLoadingIndicatorVisible() {
        if (loadElement == null) {
            return false;
        }
        if (loadElement.getStyle().getProperty("display").equals("none")) {
            return false;
        }

        return true;
    }

    private static native ValueMap parseJSONResponse(String jsonText)
    /*-{
    	try {
    		return JSON.parse(jsonText);
    	} catch (ignored) {
    		return eval('(' + jsonText + ')');
    	}
    }-*/;

    private void handleReceivedJSONMessage(Date start, String jsonText,
            ValueMap json) {
        handleUIDLMessage(start, jsonText, json);
    }

    protected void handleUIDLMessage(final Date start, final String jsonText,
            final ValueMap json) {
        VConsole.log("Handling message from server");
        // Handle redirect
        if (json.containsKey("redirect")) {
            String url = json.getValueMap("redirect").getString("url");
            VConsole.log("redirecting to " + url);
            redirect(url);
            return;
        }

        final MultiStepDuration handleUIDLDuration = new MultiStepDuration();

        // Get security key
        if (json.containsKey(UIDL_SECURITY_TOKEN_ID)) {
            uidlSecurityKey = json.getString(UIDL_SECURITY_TOKEN_ID);
        }
        VConsole.log(" * Handling resources from server");

        if (json.containsKey("resources")) {
            ValueMap resources = json.getValueMap("resources");
            JsArrayString keyArray = resources.getKeyArray();
            int l = keyArray.length();
            for (int i = 0; i < l; i++) {
                String key = keyArray.get(i);
                resourcesMap.put(key, resources.getAsString(key));
            }
        }
        handleUIDLDuration.logDuration(
                " * Handling resources from server completed", 10);

        VConsole.log(" * Handling type inheritance map from server");

        if (json.containsKey("typeInheritanceMap")) {
            configuration.addComponentInheritanceInfo(json
                    .getValueMap("typeInheritanceMap"));
        }
        handleUIDLDuration.logDuration(
                " * Handling type inheritance map from server completed", 10);

        VConsole.log("Handling type mappings from server");

        if (json.containsKey("typeMappings")) {
            configuration.addComponentMappings(
                    json.getValueMap("typeMappings"), widgetSet);
        }

        handleUIDLDuration.logDuration(
                " * Handling type mappings from server completed", 10);

        Command c = new Command() {
            public void execute() {
                handleUIDLDuration.logDuration(" * Loading widgets completed",
                        10);

                MultiStepDuration updateDuration = new MultiStepDuration();

                if (debugLogging) {
                    VConsole.log(" * Dumping UIDL to the console");
                    VConsole.dirUIDL(json, configuration);

                    updateDuration.logDuration(
                            " * Dumping UIDL to the console completed", 10);
                }

                if (json.containsKey("locales")) {
                    VConsole.log(" * Handling locales");
                    // Store locale data
                    JsArray<ValueMap> valueMapArray = json
                            .getJSValueMapArray("locales");
                    LocaleService.addLocales(valueMapArray);
                }

                updateDuration.logDuration(" * Handling locales completed", 10);

                boolean repaintAll = false;
                ValueMap meta = null;
                if (json.containsKey("meta")) {
                    VConsole.log(" * Handling meta information");
                    meta = json.getValueMap("meta");
                    if (meta.containsKey("repaintAll")) {
                        repaintAll = true;
                        rootConnector.getWidget().clear();
                        getConnectorMap().clear();
                        if (meta.containsKey("invalidLayouts")) {
                            validatingLayouts = true;
                            zeroWidthComponents = new HashSet<ComponentConnector>();
                            zeroHeightComponents = new HashSet<ComponentConnector>();
                        }
                    }
                    if (meta.containsKey("timedRedirect")) {
                        final ValueMap timedRedirect = meta
                                .getValueMap("timedRedirect");
                        redirectTimer = new Timer() {
                            @Override
                            public void run() {
                                redirect(timedRedirect.getString("url"));
                            }
                        };
                        sessionExpirationInterval = timedRedirect
                                .getInt("interval");
                    }
                }

                updateDuration.logDuration(
                        " * Handling meta information completed", 10);

                if (redirectTimer != null) {
                    redirectTimer.schedule(1000 * sessionExpirationInterval);
                }

                componentCaptionSizeChanges.clear();

                int startProcessing = updateDuration.elapsedMillis();

                // Ensure that all connectors that we are about to update exist
                createConnectorsIfNeeded(json);

                updateDuration.logDuration(" * Creating connectors completed",
                        10);

                // Update states, do not fire events
                Collection<StateChangeEvent> pendingStateChangeEvents = updateConnectorState(json);

                updateDuration.logDuration(
                        " * Update of connector states completed", 10);

                // Update hierarchy, do not fire events
                Collection<ConnectorHierarchyChangeEvent> pendingHierarchyChangeEvents = updateConnectorHierarchy(json);

                updateDuration.logDuration(
                        " * Update of connector hierarchy completed", 10);

                // Fire hierarchy change events
                sendHierarchyChangeEvents(pendingHierarchyChangeEvents);

                updateDuration.logDuration(
                        " * Hierarchy state change event processing completed",
                        10);

                // Fire state change events.
                sendStateChangeEvents(pendingStateChangeEvents);

                updateDuration.logDuration(
                        " * State change event processing completed", 10);

                // Update of legacy (UIDL) style connectors
                updateVaadin6StyleConnectors(json);

                updateDuration
                        .logDuration(
                                " * Vaadin 6 style connector updates (updateFromUidl) completed",
                                10);

                // Handle any RPC invocations done on the server side
                handleRpcInvocations(json);

                updateDuration.logDuration(
                        " * Processing of RPC invocations completed", 10);

                if (json.containsKey("dd")) {
                    // response contains data for drag and drop service
                    VDragAndDropManager.get().handleServerResponse(
                            json.getValueMap("dd"));
                }

                updateDuration
                        .logDuration(
                                " * Processing of drag and drop server response completed",
                                10);

                unregisterRemovedConnectors();

                updateDuration.logDuration(
                        " * Unregistering of removed components completed", 10);

                VConsole.log("handleUIDLMessage: "
                        + (updateDuration.elapsedMillis() - startProcessing)
                        + " ms");

                LayoutManager layoutManager = getLayoutManager();
                layoutManager.setEverythingNeedsMeasure();
                layoutManager.layoutNow();

                updateDuration
                        .logDuration(" * Layout processing completed", 10);

                if (meta != null) {
                    if (meta.containsKey("appError")) {
                        ValueMap error = meta.getValueMap("appError");
                        String html = "";
                        if (error.containsKey("caption")
                                && error.getString("caption") != null) {
                            html += "<h1>" + error.getAsString("caption")
                                    + "</h1>";
                        }
                        if (error.containsKey("message")
                                && error.getString("message") != null) {
                            html += "<p>" + error.getAsString("message")
                                    + "</p>";
                        }
                        String url = null;
                        if (error.containsKey("url")) {
                            url = error.getString("url");
                        }

                        if (html.length() != 0) {
                            /* 45 min */
                            VNotification n = VNotification
                                    .createNotification(1000 * 60 * 45);
                            n.addEventListener(new NotificationRedirect(url));
                            n.show(html, VNotification.CENTERED_TOP,
                                    VNotification.STYLE_SYSTEM);
                        } else {
                            redirect(url);
                        }
                        applicationRunning = false;
                    }
                    if (validatingLayouts) {
                        VConsole.printLayoutProblems(meta,
                                ApplicationConnection.this,
                                zeroHeightComponents, zeroWidthComponents);
                        zeroHeightComponents = null;
                        zeroWidthComponents = null;
                        validatingLayouts = false;

                    }
                }

                updateDuration.logDuration(" * Error handling completed", 10);

                // TODO build profiling for widget impl loading time

                final long prosessingTime = (new Date().getTime())
                        - start.getTime();
                VConsole.log(" Processing time was "
                        + String.valueOf(prosessingTime) + "ms for "
                        + jsonText.length() + " characters of JSON");
                VConsole.log("Referenced paintables: " + connectorMap.size());

                endRequest();

            }

            /**
             * Sends the state change events created while updating the state
             * information.
             * 
             * This must be called after hierarchy change listeners have been
             * called. At least caption updates for the parent are strange if
             * fired from state change listeners and thus calls the parent
             * BEFORE the parent is aware of the child (through a
             * ConnectorHierarchyChangedEvent)
             * 
             * @param pendingStateChangeEvents
             *            The events to send
             */
            private void sendStateChangeEvents(
                    Collection<StateChangeEvent> pendingStateChangeEvents) {
                VConsole.log(" * Sending state change events");

                for (StateChangeEvent sce : pendingStateChangeEvents) {
                    try {
                        sce.getConnector().fireEvent(sce);
                    } catch (final Throwable e) {
                        VConsole.error(e);
                    }
                }

            }

            private void unregisterRemovedConnectors() {
                int unregistered = 0;
                List<ServerConnector> currentConnectors = new ArrayList<ServerConnector>(
                        connectorMap.getConnectors());
                for (ServerConnector c : currentConnectors) {
                    if (c instanceof ComponentConnector) {
                        ComponentConnector cc = (ComponentConnector) c;
                        if (cc.getParent() != null) {
                            if (!cc.getParent().getChildren().contains(cc)) {
                                VConsole.error("ERROR: Connector is connected to a parent but the parent does not contain the connector");
                            }
                        } else if ((cc instanceof RootConnector && cc == getRootConnector())) {
                            // RootConnector for this connection, leave as-is
                        } else if (cc instanceof WindowConnector
                                && getRootConnector().hasSubWindow(
                                        (WindowConnector) cc)) {
                            // Sub window attached to this RootConnector, leave
                            // as-is
                        } else {
                            // The connector has been detached from the
                            // hierarchy, unregister it and any possible
                            // children. The RootConnector should never be
                            // unregistered even though it has no parent.
                            connectorMap.unregisterConnector(cc);
                            unregistered++;
                        }
                    }

                }

                VConsole.log("* Unregistered " + unregistered + " connectors");
            }

            private void createConnectorsIfNeeded(ValueMap json) {
                VConsole.log(" * Creating connectors (if needed)");

                if (!json.containsKey("types")) {
                    return;
                }

                ValueMap types = json.getValueMap("types");
                JsArrayString keyArray = types.getKeyArray();
                for (int i = 0; i < keyArray.length(); i++) {
                    try {
                        String connectorId = keyArray.get(i);
                        int connectorType = Integer.parseInt(types
                                .getString((connectorId)));
                        ServerConnector connector = connectorMap
                                .getConnector(connectorId);
                        if (connector != null) {
                            continue;
                        }

                        Class<? extends ComponentConnector> connectorClass = configuration
                                .getWidgetClassByEncodedTag(connectorType);

                        // Connector does not exist so we must create it
                        if (connectorClass != RootConnector.class) {
                            // create, initialize and register the paintable
                            getConnector(connectorId, connectorType);
                        } else {
                            // First RootConnector update. Before this the
                            // RootConnector has been created but not
                            // initialized as the connector id has not been
                            // known
                            connectorMap.registerConnector(connectorId,
                                    rootConnector);
                            rootConnector.doInit(connectorId,
                                    ApplicationConnection.this);
                        }
                    } catch (final Throwable e) {
                        VConsole.error(e);
                    }
                }
            }

            private void updateVaadin6StyleConnectors(ValueMap json) {
                JsArray<ValueMap> changes = json.getJSValueMapArray("changes");
                int length = changes.length();

                VConsole.log(" * Passing UIDL to Vaadin 6 style connectors");
                // update paintables
                for (int i = 0; i < length; i++) {
                    try {
                        final UIDL change = changes.get(i).cast();
                        final UIDL uidl = change.getChildUIDL(0);
                        String connectorId = uidl.getId();

                        final ComponentConnector legacyConnector = (ComponentConnector) connectorMap
                                .getConnector(connectorId);
                        if (legacyConnector instanceof Paintable) {
                            ((Paintable) legacyConnector).updateFromUIDL(uidl,
                                    ApplicationConnection.this);
                        } else if (legacyConnector == null) {
                            VConsole.error("Received update for "
                                    + uidl.getTag()
                                    + ", but there is no such paintable ("
                                    + connectorId + ") rendered.");
                        } else {
                            VConsole.error("Server sent Vaadin 6 style updates for "
                                    + Util.getConnectorString(legacyConnector)
                                    + " but this is not a Vaadin 6 Paintable");
                        }

                    } catch (final Throwable e) {
                        VConsole.error(e);
                    }
                }
            }

            private void sendHierarchyChangeEvents(
                    Collection<ConnectorHierarchyChangeEvent> pendingHierarchyChangeEvents) {
                if (pendingHierarchyChangeEvents.isEmpty()) {
                    return;
                }

                VConsole.log(" * Sending hierarchy change events");
                for (ConnectorHierarchyChangeEvent event : pendingHierarchyChangeEvents) {
                    try {
                        event.getConnector().fireEvent(event);
                    } catch (final Throwable e) {
                        VConsole.error(e);
                    }
                }

            }

            private Collection<StateChangeEvent> updateConnectorState(
                    ValueMap json) {
                ArrayList<StateChangeEvent> events = new ArrayList<StateChangeEvent>();
                VConsole.log(" * Updating connector states");
                if (!json.containsKey("state")) {
                    return events;
                }
                // set states for all paintables mentioned in "state"
                ValueMap states = json.getValueMap("state");
                JsArrayString keyArray = states.getKeyArray();
                for (int i = 0; i < keyArray.length(); i++) {
                    try {
                        String connectorId = keyArray.get(i);
                        ServerConnector connector = connectorMap
                                .getConnector(connectorId);
                        if (null != connector) {

                            JSONArray stateDataAndType = new JSONArray(
                                    states.getJavaScriptObject(connectorId));

                            Object state = JsonDecoder.decodeValue(
                                    stateDataAndType, connectorMap,
                                    ApplicationConnection.this);

                            connector.setState((SharedState) state);
                            StateChangeEvent event = GWT
                                    .create(StateChangeEvent.class);
                            event.setConnector(connector);
                            events.add(event);
                        }
                    } catch (final Throwable e) {
                        VConsole.error(e);
                    }
                }

                return events;
            }

            /**
             * Updates the connector hierarchy and returns a list of events that
             * should be fired after update of the hierarchy and the state is
             * done.
             * 
             * @param json
             *            The JSON containing the hierarchy information
             * @return A collection of events that should be fired when update
             *         of hierarchy and state is complete
             */
            private Collection<ConnectorHierarchyChangeEvent> updateConnectorHierarchy(
                    ValueMap json) {
                List<ConnectorHierarchyChangeEvent> events = new LinkedList<ConnectorHierarchyChangeEvent>();

                VConsole.log(" * Updating connector hierarchy");
                if (!json.containsKey("hierarchy")) {
                    return events;
                }

                ValueMap hierarchies = json.getValueMap("hierarchy");
                JsArrayString hierarchyKeys = hierarchies.getKeyArray();
                for (int i = 0; i < hierarchyKeys.length(); i++) {
                    try {
                        String connectorId = hierarchyKeys.get(i);
                        ServerConnector connector = connectorMap
                                .getConnector(connectorId);
                        if (!(connector instanceof ComponentContainerConnector)) {
                            VConsole.error("Retrieved a hierarchy update for a connector ("
                                    + connectorId
                                    + ") that is not a ComponentContainerConnector");
                            continue;
                        }
                        ComponentContainerConnector ccc = (ComponentContainerConnector) connector;

                        JsArrayString childConnectorIds = hierarchies
                                .getJSStringArray(connectorId);
                        int childConnectorSize = childConnectorIds.length();

                        List<ServerConnector> newChildren = new ArrayList<ServerConnector>();
                        for (int connectorIndex = 0; connectorIndex < childConnectorSize; connectorIndex++) {
                            String childConnectorId = childConnectorIds
                                    .get(connectorIndex);
                            ComponentConnector childConnector = (ComponentConnector) connectorMap
                                    .getConnector(childConnectorId);
                            if (childConnector == null) {
                                VConsole.error("Hierarchy claims that "
                                        + childConnectorId + " is a child for "
                                        + connectorId + " ("
                                        + connector.getClass().getName()
                                        + ") but no connector with id "
                                        + childConnectorId
                                        + " has been registered");
                                continue;
                            }
                            newChildren.add(childConnector);
                            if (childConnector.getParent() != ccc) {
                                // Avoid extra calls to setParent
                                childConnector.setParent(ccc);
                            }
                        }

                        // TODO This check should be done on the server side in
                        // the future so the hierarchy update is only sent when
                        // something actually has changed
                        List<ComponentConnector> oldChildren = ccc
                                .getChildren();
                        boolean actuallyChanged = !Util.collectionsEquals(
                                oldChildren, newChildren);

                        if (!actuallyChanged) {
                            continue;
                        }

                        // Fire change event if the hierarchy has changed
                        ConnectorHierarchyChangeEvent event = GWT
                                .create(ConnectorHierarchyChangeEvent.class);
                        event.setOldChildren(oldChildren);
                        event.setConnector(ccc);
                        ccc.setChildren((List) newChildren);
                        events.add(event);

                        // Remove parent for children that are no longer
                        // attached to this (avoid updating children if they
                        // have already been assigned to a new parent)
                        for (ComponentConnector oldChild : oldChildren) {
                            if (oldChild.getParent() != ccc) {
                                continue;
                            }

                            // TODO This could probably be optimized
                            if (!newChildren.contains(oldChild)) {
                                oldChild.setParent(null);
                            }
                        }
                    } catch (final Throwable e) {
                        VConsole.error(e);
                    }
                }
                return events;

            }

            private void handleRpcInvocations(ValueMap json) {
                if (json.containsKey("rpc")) {
                    VConsole.log(" * Performing server to client RPC calls");

                    JSONArray rpcCalls = new JSONArray(
                            json.getJavaScriptObject("rpc"));

                    int rpcLength = rpcCalls.size();
                    for (int i = 0; i < rpcLength; i++) {
                        try {
                            JSONArray rpcCall = (JSONArray) rpcCalls.get(i);
                            MethodInvocation invocation = parseMethodInvocation(rpcCall);
                            VConsole.log("Server to client RPC call: "
                                    + invocation);
                            rpcManager.applyInvocation(invocation,
                                    getConnectorMap());
                        } catch (final Throwable e) {
                            VConsole.error(e);
                        }
                    }
                }

            }

        };
        ApplicationConfiguration.runWhenWidgetsLoaded(c);
    }

    private MethodInvocation parseMethodInvocation(JSONArray rpcCall) {
        String connectorId = ((JSONString) rpcCall.get(0)).stringValue();
        String interfaceName = ((JSONString) rpcCall.get(1)).stringValue();
        String methodName = ((JSONString) rpcCall.get(2)).stringValue();
        JSONArray parametersJson = (JSONArray) rpcCall.get(3);
        Object[] parameters = new Object[parametersJson.size()];
        for (int j = 0; j < parametersJson.size(); ++j) {
            parameters[j] = JsonDecoder.decodeValue(
                    (JSONArray) parametersJson.get(j), getConnectorMap(), this);
        }
        return new MethodInvocation(connectorId, interfaceName, methodName,
                parameters);
    }

    // Redirect browser, null reloads current page
    private static native void redirect(String url)
    /*-{
    	if (url) {
    		$wnd.location = url;
    	} else {
    		$wnd.location.reload(false);
    	}
    }-*/;

    private void addVariableToQueue(String connectorId, String variableName,
            Object value, boolean immediate) {
        // note that type is now deduced from value
        // TODO could eliminate invocations of same shared variable setter
        addMethodInvocationToQueue(new MethodInvocation(connectorId,
                UPDATE_VARIABLE_INTERFACE, UPDATE_VARIABLE_METHOD,
                new Object[] { variableName, value }), immediate);
    }

    /**
     * Adds an explicit RPC method invocation to the send queue.
     * 
     * @since 7.0
     * 
     * @param invocation
     *            RPC method invocation
     * @param immediate
     *            true to trigger sending within a short time window (possibly
     *            combining subsequent calls to a single request), false to let
     *            the framework delay sending of RPC calls and variable changes
     *            until the next immediate change
     */
    public void addMethodInvocationToQueue(MethodInvocation invocation,
            boolean immediate) {
        pendingInvocations.add(invocation);
        if (immediate) {
            sendPendingVariableChanges();
        }
    }

    /**
     * This method sends currently queued variable changes to server. It is
     * called when immediate variable update must happen.
     * 
     * To ensure correct order for variable changes (due servers multithreading
     * or network), we always wait for active request to be handler before
     * sending a new one. If there is an active request, we will put varible
     * "burst" to queue that will be purged after current request is handled.
     * 
     */
    public void sendPendingVariableChanges() {
        if (!deferedSendPending) {
            deferedSendPending = true;
            Scheduler.get().scheduleDeferred(sendPendingCommand);
        }
    }

    private final ScheduledCommand sendPendingCommand = new ScheduledCommand() {
        public void execute() {
            deferedSendPending = false;
            doSendPendingVariableChanges();
        }
    };
    private boolean deferedSendPending = false;

    @SuppressWarnings("unchecked")
    private void doSendPendingVariableChanges() {
        if (applicationRunning) {
            if (hasActiveRequest()) {
                // skip empty queues if there are pending bursts to be sent
                if (pendingInvocations.size() > 0 || pendingBursts.size() == 0) {
                    pendingBursts.add(pendingInvocations);
                    pendingInvocations = new ArrayList<MethodInvocation>();
                }
            } else {
                buildAndSendVariableBurst(pendingInvocations, false);
            }
        }
    }

    /**
     * Build the variable burst and send it to server.
     * 
     * When sync is forced, we also force sending of all pending variable-bursts
     * at the same time. This is ok as we can assume that DOM will never be
     * updated after this.
     * 
     * @param pendingInvocations
     *            List of RPC method invocations to send
     * @param forceSync
     *            Should we use synchronous request?
     */
    private void buildAndSendVariableBurst(
            ArrayList<MethodInvocation> pendingInvocations, boolean forceSync) {
        final StringBuffer req = new StringBuffer();

        while (!pendingInvocations.isEmpty()) {
            if (ApplicationConfiguration.isDebugMode()) {
                Util.logVariableBurst(this, pendingInvocations);
            }

            JSONArray reqJson = new JSONArray();

            for (MethodInvocation invocation : pendingInvocations) {
                JSONArray invocationJson = new JSONArray();
                invocationJson.set(0,
                        new JSONString(invocation.getConnectorId()));
                invocationJson.set(1,
                        new JSONString(invocation.getInterfaceName()));
                invocationJson.set(2,
                        new JSONString(invocation.getMethodName()));
                JSONArray paramJson = new JSONArray();
                for (int i = 0; i < invocation.getParameters().length; ++i) {
                    // TODO non-static encoder? type registration?
                    paramJson.set(i, JsonEncoder.encode(
                            invocation.getParameters()[i], getConnectorMap(),
                            this));
                }
                invocationJson.set(3, paramJson);
                reqJson.set(reqJson.size(), invocationJson);
            }

            // escape burst separators (if any)
            req.append(escapeBurstContents(reqJson.toString()));

            pendingInvocations.clear();
            // Append all the bursts to this synchronous request
            if (forceSync && !pendingBursts.isEmpty()) {
                pendingInvocations = pendingBursts.get(0);
                pendingBursts.remove(0);
                req.append(VAR_BURST_SEPARATOR);
            }
        }

        // Include the browser detail parameters if they aren't already sent
        String extraParams;
        if (!getConfiguration().isBrowserDetailsSent()) {
            extraParams = getNativeBrowserDetailsParameters(getConfiguration()
                    .getRootPanelId());
            getConfiguration().setBrowserDetailsSent();
        } else {
            extraParams = "";
        }

        makeUidlRequest(req.toString(), extraParams, forceSync);
    }

    /**
     * Sends a new value for the given paintables given variable to the server.
     * <p>
     * The update is actually queued to be sent at a suitable time. If immediate
     * is true, the update is sent as soon as possible. If immediate is false,
     * the update will be sent along with the next immediate update.
     * </p>
     * 
     * @param paintableId
     *            the id of the paintable that owns the variable
     * @param variableName
     *            the name of the variable
     * @param newValue
     *            the new value to be sent
     * @param immediate
     *            true if the update is to be sent as soon as possible
     */
    public void updateVariable(String paintableId, String variableName,
            ServerConnector newValue, boolean immediate) {
        addVariableToQueue(paintableId, variableName, newValue, immediate);
    }

    /**
     * Sends a new value for the given paintables given variable to the server.
     * <p>
     * The update is actually queued to be sent at a suitable time. If immediate
     * is true, the update is sent as soon as possible. If immediate is false,
     * the update will be sent along with the next immediate update.
     * </p>
     * 
     * @param paintableId
     *            the id of the paintable that owns the variable
     * @param variableName
     *            the name of the variable
     * @param newValue
     *            the new value to be sent
     * @param immediate
     *            true if the update is to be sent as soon as possible
     */

    public void updateVariable(String paintableId, String variableName,
            String newValue, boolean immediate) {
        addVariableToQueue(paintableId, variableName, newValue, immediate);
    }

    /**
     * Sends a new value for the given paintables given variable to the server.
     * <p>
     * The update is actually queued to be sent at a suitable time. If immediate
     * is true, the update is sent as soon as possible. If immediate is false,
     * the update will be sent along with the next immediate update.
     * </p>
     * 
     * @param paintableId
     *            the id of the paintable that owns the variable
     * @param variableName
     *            the name of the variable
     * @param newValue
     *            the new value to be sent
     * @param immediate
     *            true if the update is to be sent as soon as possible
     */

    public void updateVariable(String paintableId, String variableName,
            int newValue, boolean immediate) {
        addVariableToQueue(paintableId, variableName, newValue, immediate);
    }

    /**
     * Sends a new value for the given paintables given variable to the server.
     * <p>
     * The update is actually queued to be sent at a suitable time. If immediate
     * is true, the update is sent as soon as possible. If immediate is false,
     * the update will be sent along with the next immediate update.
     * </p>
     * 
     * @param paintableId
     *            the id of the paintable that owns the variable
     * @param variableName
     *            the name of the variable
     * @param newValue
     *            the new value to be sent
     * @param immediate
     *            true if the update is to be sent as soon as possible
     */

    public void updateVariable(String paintableId, String variableName,
            long newValue, boolean immediate) {
        addVariableToQueue(paintableId, variableName, newValue, immediate);
    }

    /**
     * Sends a new value for the given paintables given variable to the server.
     * <p>
     * The update is actually queued to be sent at a suitable time. If immediate
     * is true, the update is sent as soon as possible. If immediate is false,
     * the update will be sent along with the next immediate update.
     * </p>
     * 
     * @param paintableId
     *            the id of the paintable that owns the variable
     * @param variableName
     *            the name of the variable
     * @param newValue
     *            the new value to be sent
     * @param immediate
     *            true if the update is to be sent as soon as possible
     */

    public void updateVariable(String paintableId, String variableName,
            float newValue, boolean immediate) {
        addVariableToQueue(paintableId, variableName, newValue, immediate);
    }

    /**
     * Sends a new value for the given paintables given variable to the server.
     * <p>
     * The update is actually queued to be sent at a suitable time. If immediate
     * is true, the update is sent as soon as possible. If immediate is false,
     * the update will be sent along with the next immediate update.
     * </p>
     * 
     * @param paintableId
     *            the id of the paintable that owns the variable
     * @param variableName
     *            the name of the variable
     * @param newValue
     *            the new value to be sent
     * @param immediate
     *            true if the update is to be sent as soon as possible
     */

    public void updateVariable(String paintableId, String variableName,
            double newValue, boolean immediate) {
        addVariableToQueue(paintableId, variableName, newValue, immediate);
    }

    /**
     * Sends a new value for the given paintables given variable to the server.
     * <p>
     * The update is actually queued to be sent at a suitable time. If immediate
     * is true, the update is sent as soon as possible. If immediate is false,
     * the update will be sent along with the next immediate update.
     * </p>
     * 
     * @param paintableId
     *            the id of the paintable that owns the variable
     * @param variableName
     *            the name of the variable
     * @param newValue
     *            the new value to be sent
     * @param immediate
     *            true if the update is to be sent as soon as possible
     */

    public void updateVariable(String paintableId, String variableName,
            boolean newValue, boolean immediate) {
        addVariableToQueue(paintableId, variableName, newValue, immediate);
    }

    /**
     * Sends a new value for the given paintables given variable to the server.
     * <p>
     * The update is actually queued to be sent at a suitable time. If immediate
     * is true, the update is sent as soon as possible. If immediate is false,
     * the update will be sent along with the next immediate update.
     * </p>
     * 
     * @param paintableId
     *            the id of the paintable that owns the variable
     * @param variableName
     *            the name of the variable
     * @param map
     *            the new values to be sent
     * @param immediate
     *            true if the update is to be sent as soon as possible
     */
    public void updateVariable(String paintableId, String variableName,
            Map<String, Object> map, boolean immediate) {
        addVariableToQueue(paintableId, variableName, map, immediate);
    }

    /**
     * Sends a new value for the given paintables given variable to the server.
     * 
     * The update is actually queued to be sent at a suitable time. If immediate
     * is true, the update is sent as soon as possible. If immediate is false,
     * the update will be sent along with the next immediate update.
     * 
     * A null array is sent as an empty array.
     * 
     * @param paintableId
     *            the id of the paintable that owns the variable
     * @param variableName
     *            the name of the variable
     * @param values
     *            the new value to be sent
     * @param immediate
     *            true if the update is to be sent as soon as possible
     */
    public void updateVariable(String paintableId, String variableName,
            String[] values, boolean immediate) {
        addVariableToQueue(paintableId, variableName, values, immediate);
    }

    /**
     * Sends a new value for the given paintables given variable to the server.
     * 
     * The update is actually queued to be sent at a suitable time. If immediate
     * is true, the update is sent as soon as possible. If immediate is false,
     * the update will be sent along with the next immediate update. </p>
     * 
     * A null array is sent as an empty array.
     * 
     * 
     * @param paintableId
     *            the id of the paintable that owns the variable
     * @param variableName
     *            the name of the variable
     * @param values
     *            the new value to be sent
     * @param immediate
     *            true if the update is to be sent as soon as possible
     */
    public void updateVariable(String paintableId, String variableName,
            Object[] values, boolean immediate) {
        addVariableToQueue(paintableId, variableName, values, immediate);
    }

    /**
     * Encode burst separator characters in a String for transport over the
     * network. This protects from separator injection attacks.
     * 
     * @param value
     *            to encode
     * @return encoded value
     */
    protected String escapeBurstContents(String value) {
        final StringBuilder result = new StringBuilder();
        for (int i = 0; i < value.length(); ++i) {
            char character = value.charAt(i);
            switch (character) {
            case VAR_ESCAPE_CHARACTER:
                // fall-through - escape character is duplicated
            case VAR_BURST_SEPARATOR:
                result.append(VAR_ESCAPE_CHARACTER);
                // encode as letters for easier reading
                result.append(((char) (character + 0x30)));
                break;
            default:
                // the char is not a special one - add it to the result as is
                result.append(character);
                break;
            }
        }
        return result.toString();
    }

    private boolean runningLayout = false;

    /**
     * Causes a re-calculation/re-layout of all paintables in a container.
     * 
     * @param container
     */
    public void runDescendentsLayout(HasWidgets container) {
        if (runningLayout) {
            return;
        }
        runningLayout = true;
        internalRunDescendentsLayout(container);
        runningLayout = false;
    }

    /**
     * This will cause re-layouting of all components. Mainly used for
     * development. Published to JavaScript.
     */
    public void forceLayout() {
        Duration duration = new Duration();

        layoutManager.forceLayout();

        VConsole.log("forceLayout in " + duration.elapsedMillis() + " ms");
    }

    private void internalRunDescendentsLayout(HasWidgets container) {
        // getConsole().log(
        // "runDescendentsLayout(" + Util.getSimpleName(container) + ")");
        final Iterator<Widget> childWidgets = container.iterator();
        while (childWidgets.hasNext()) {
            final Widget child = childWidgets.next();

            if (getConnectorMap().isConnector(child)) {

                if (handleComponentRelativeSize(child)) {
                    /*
                     * Only need to propagate event if "child" has a relative
                     * size
                     */

                    if (child instanceof ContainerResizedListener) {
                        ((ContainerResizedListener) child).iLayout();
                    }

                    if (child instanceof HasWidgets) {
                        final HasWidgets childContainer = (HasWidgets) child;
                        internalRunDescendentsLayout(childContainer);
                    }
                }
            } else if (child instanceof HasWidgets) {
                // propagate over non Paintable HasWidgets
                internalRunDescendentsLayout((HasWidgets) child);
            }

        }
    }

    /**
     * Converts relative sizes into pixel sizes.
     * 
     * @param child
     * @return true if the child has a relative size
     */
    private boolean handleComponentRelativeSize(ComponentConnector paintable) {
        return false;
    }

    /**
     * Converts relative sizes into pixel sizes.
     * 
     * @param child
     * @return true if the child has a relative size
     */
    public boolean handleComponentRelativeSize(Widget widget) {
        return handleComponentRelativeSize(connectorMap.getConnector(widget));

    }

    @Deprecated
    public ComponentConnector getPaintable(UIDL uidl) {
        return getConnector(uidl.getId(), Integer.parseInt(uidl.getTag()));
    }

    /**
     * Get either an existing ComponentConnector or create a new
     * ComponentConnector with the given type and id.
     * 
     * If a ComponentConnector with the given id already exists, returns it.
     * Otherwise creates and registers a new ComponentConnector of the given
     * type.
     * 
     * @param connectorId
     *            Id of the paintable
     * @param connectorType
     *            Type of the connector, as passed from the server side
     * 
     * @return Either an existing ComponentConnector or a new ComponentConnector
     *         of the given type
     */
    public ComponentConnector getConnector(String connectorId, int connectorType) {
        if (!connectorMap.hasConnector(connectorId)) {
            return createAndRegisterConnector(connectorId, connectorType);
        }
        return (ComponentConnector) connectorMap.getConnector(connectorId);
    }

    /**
     * Creates a new ComponentConnector with the given type and id.
     * 
     * Creates and registers a new ComponentConnector of the given type. Should
     * never be called with the connector id of an existing connector.
     * 
     * @param connectorId
     *            Id of the new connector
     * @param connectorType
     *            Type of the connector, as passed from the server side
     * 
     * @return A new ComponentConnector of the given type
     */
    private ComponentConnector createAndRegisterConnector(String connectorId,
            int connectorType) {
        // Create and register a new connector with the given type
        ComponentConnector p = widgetSet.createWidget(connectorType,
                configuration);
        connectorMap.registerConnector(connectorId, p);
        p.doInit(connectorId, this);

        return p;
    }

    /**
     * Gets a recource that has been pre-loaded via UIDL, such as custom
     * layouts.
     * 
     * @param name
     *            identifier of the resource to get
     * @return the resource
     */
    public String getResource(String name) {
        return resourcesMap.get(name);
    }

    /**
     * Singleton method to get instance of app's context menu.
     * 
     * @return VContextMenu object
     */
    public VContextMenu getContextMenu() {
        if (contextMenu == null) {
            contextMenu = new VContextMenu();
            DOM.setElementProperty(contextMenu.getElement(), "id",
                    "PID_VAADIN_CM");
        }
        return contextMenu;
    }

    /**
     * Translates custom protocols in UIDL URI's to be recognizable by browser.
     * All uri's from UIDL should be routed via this method before giving them
     * to browser due URI's in UIDL may contain custom protocols like theme://.
     * 
     * @param uidlUri
     *            Vaadin URI from uidl
     * @return translated URI ready for browser
     */
    public String translateVaadinUri(String uidlUri) {
        if (uidlUri == null) {
            return null;
        }
        if (uidlUri.startsWith("theme://")) {
            final String themeUri = configuration.getThemeUri();
            if (themeUri == null) {
                VConsole.error("Theme not set: ThemeResource will not be found. ("
                        + uidlUri + ")");
            }
            uidlUri = themeUri + uidlUri.substring(7);
        }
        if (uidlUri.startsWith("app://")) {
            uidlUri = getAppUri() + uidlUri.substring(6);
        }
        return uidlUri;
    }

    /**
     * Gets the URI for the current theme. Can be used to reference theme
     * resources.
     * 
     * @return URI to the current theme
     */
    public String getThemeUri() {
        return configuration.getThemeUri();
    }

    /**
     * Listens for Notification hide event, and redirects. Used for system
     * messages, such as session expired.
     * 
     */
    private class NotificationRedirect implements VNotification.EventListener {
        String url;

        NotificationRedirect(String url) {
            this.url = url;
        }

        public void notificationHidden(HideEvent event) {
            redirect(url);
        }

    }

    /* Extended title handling */

    /**
     * Data showed in tooltips are stored centrilized as it may be needed in
     * varios place: caption, layouts, and in owner components themselves.
     * 
     * Updating TooltipInfo is done in updateComponent method.
     * 
     */
    public TooltipInfo getTooltipTitleInfo(ComponentConnector titleOwner,
            Object key) {
        if (null == titleOwner) {
            return null;
        }
        return connectorMap.getTooltipInfo(titleOwner, key);
    }

    private final VTooltip tooltip = new VTooltip(this);

    /**
     * Component may want to delegate Tooltip handling to client. Layouts add
     * Tooltip (description, errors) to caption, but some components may want
     * them to appear one other elements too.
     * 
     * Events wanted by this handler are same as in Tooltip.TOOLTIP_EVENTS
     * 
     * @param event
     * @param owner
     */
    public void handleTooltipEvent(Event event, ComponentConnector owner) {
        tooltip.handleTooltipEvent(event, owner, null);

    }

    /**
     * Component may want to delegate Tooltip handling to client. Layouts add
     * Tooltip (description, errors) to caption, but some components may want
     * them to appear one other elements too.
     * 
     * Events wanted by this handler are same as in Tooltip.TOOLTIP_EVENTS
     * 
     * @param event
     * @param owner
     * @param key
     *            the key for tooltip if this is "additional" tooltip, null for
     *            components "main tooltip"
     */
    public void handleTooltipEvent(Event event, ComponentConnector owner,
            Object key) {
        tooltip.handleTooltipEvent(event, owner, key);

    }

    private ConnectorMap connectorMap = GWT.create(ConnectorMap.class);

    protected String getUidlSecurityKey() {
        return uidlSecurityKey;
    }

    /**
     * Use to notify that the given component's caption has changed; layouts may
     * have to be recalculated.
     * 
     * @param component
     *            the Paintable whose caption has changed
     */
    public void captionSizeUpdated(Widget widget) {
        componentCaptionSizeChanges.add(widget);
    }

    /**
     * Gets the main view
     * 
     * @return the main view
     */
    public RootConnector getRootConnector() {
        return rootConnector;
    }

    /**
     * If component has several tooltips in addition to the one provided by
     * {@link com.vaadin.ui.AbstractComponent}, component can register them with
     * this method.
     * <p>
     * Component must also pipe events to
     * {@link #handleTooltipEvent(Event, ComponentConnector, Object)} method.
     * <p>
     * This method can also be used to deregister tooltips by using null as
     * tooltip
     * 
     * @param paintable
     *            Paintable "owning" this tooltip
     * @param key
     *            key assosiated with given tooltip. Can be any object. For
     *            example a related dom element. Same key must be given for
     *            {@link #handleTooltipEvent(Event, ComponentConnector, Object)}
     *            method.
     * 
     * @param tooltip
     *            the TooltipInfo object containing details shown in tooltip,
     *            null if deregistering tooltip
     */
    public void registerTooltip(ComponentConnector paintable, Object key,
            TooltipInfo tooltip) {
        connectorMap.registerTooltip(paintable, key, tooltip);
    }

    /**
     * Gets the {@link ApplicationConfiguration} for the current application.
     * 
     * @see ApplicationConfiguration
     * @return the configuration for this application
     */
    public ApplicationConfiguration getConfiguration() {
        return configuration;
    }

    /**
     * Checks if there is a registered server side listener for the event. The
     * list of events which has server side listeners is updated automatically
     * before the component is updated so the value is correct if called from
     * updatedFromUIDL.
     * 
     * @param paintable
     *            The connector to register event listeners for
     * @param eventIdentifier
     *            The identifier for the event
     * @return true if at least one listener has been registered on server side
     *         for the event identified by eventIdentifier.
     * @deprecated Use {@link ComponentState#hasEventListener(String)} instead
     */
    @Deprecated
    public boolean hasEventListeners(ComponentConnector paintable,
            String eventIdentifier) {
        return paintable.hasEventListener(eventIdentifier);
    }

    /**
     * Adds the get parameters to the uri and returns the new uri that contains
     * the parameters.
     * 
     * @param uri
     *            The uri to which the parameters should be added.
     * @param extraParams
     *            One or more parameters in the format "a=b" or "c=d&e=f". An
     *            empty string is allowed but will not modify the url.
     * @return The modified URI with the get parameters in extraParams added.
     */
    public static String addGetParameters(String uri, String extraParams) {
        if (extraParams == null || extraParams.length() == 0) {
            return uri;
        }
        // RFC 3986: The query component is indicated by the first question
        // mark ("?") character and terminated by a number sign ("#") character
        // or by the end of the URI.
        String fragment = null;
        int hashPosition = uri.indexOf('#');
        if (hashPosition != -1) {
            // Fragment including "#"
            fragment = uri.substring(hashPosition);
            // The full uri before the fragment
            uri = uri.substring(0, hashPosition);
        }

        if (uri.contains("?")) {
            uri += "&";
        } else {
            uri += "?";
        }
        uri += extraParams;

        if (fragment != null) {
            uri += fragment;
        }

        return uri;
    }

    ConnectorMap getConnectorMap() {
        return connectorMap;
    }

    @Deprecated
    public void unregisterPaintable(ServerConnector p) {
        System.out.println("unregisterPaintable (unnecessarily) called for "
                + Util.getConnectorString(p));
        // connectorMap.unregisterConnector(p);
    }

    public VTooltip getVTooltip() {
        return tooltip;
    }

    @Deprecated
    public void handleTooltipEvent(Event event, Widget owner, Object key) {
        handleTooltipEvent(event, getConnectorMap().getConnector(owner), key);
    }

    /**
     * Method provided for backwards compatibility. Duties previously done by
     * this method is now handled by the state change event handler in
     * AbstractComponentConnector. The only function this method has is to
     * return true if the UIDL is a "cached" update.
     * 
     * @param component
     * @param uidl
     * @param manageCaption
     * @return
     */
    @Deprecated
    public boolean updateComponent(Widget component, UIDL uidl,
            boolean manageCaption) {
        ComponentConnector connector = getConnectorMap()
                .getConnector(component);
        if (!AbstractComponentConnector.isRealUpdate(uidl)) {
            return true;
        }

        if (!manageCaption) {
            VConsole.error(Util.getConnectorString(connector)
                    + " called updateComponent with manageCaption=false. The parameter was ignored - override delegateCaption() to return false instead. It is however not recommended to use caption this way at all.");
        }
        return false;
    }

    @Deprecated
    public void handleTooltipEvent(Event event, Widget owner) {
        handleTooltipEvent(event, getConnectorMap().getConnector(owner));

    }

    @Deprecated
    public void registerTooltip(Widget owner, Object key, TooltipInfo info) {
        registerTooltip(getConnectorMap().getConnector(owner), key, info);
    }

    @Deprecated
    public boolean hasEventListeners(Widget widget, String eventIdentifier) {
        return hasEventListeners(getConnectorMap().getConnector(widget),
                eventIdentifier);
    }

    LayoutManager getLayoutManager() {
        return layoutManager;
    }
}
