/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */
/*global define, $, brackets, window */

/**
 * LiveDevelopment manages the Inspector, all Agents, and the active LiveDocument
 *
 * # STARTING
 *
 * To start a session call `open`. This will read the currentDocument from brackets,
 * launch the LiveBrowser (currently Chrome) with the remote debugger port open,
 * establish the Inspector connection to the remote debugger, and finally load all
 * agents.
 *
 * # STOPPING
 *
 * To stop a session call `close`. This will close the active browser window,
 * disconnect the Inspector, unload all agents, and clean up.
 *
 * # STATUS
 *
 * Status updates are dispatched as `statusChange` jQuery events. The status
 * is passed as the first parameter and the reason for the change as the second
 * parameter. Currently only the "Inactive" status supports the reason parameter.
 * The status codes are:
 *
 * -1: Error
 *  0: Inactive
 *  1: Connecting to the remote debugger
 *  2: Loading agents
 *  3: Active
 *  4: Out of sync
 *
 * The reason codes are:
 * - null (Unknown reason)
 * - "explicit_close" (LiveDevelopment.close() was called)
 * - "navigated_away" (The browser changed to a location outside of the project)
 * - "detached_target_closed" (The tab or window was closed)
 * - "detached_replaced_with_devtools" (The developer tools were opened in the browser)
 */
define(function LiveDevelopment(require, exports, module) {
    "use strict";

    require("utils/Global");

    // Status Codes
    var STATUS_ERROR          = exports.STATUS_ERROR          = -1;
    var STATUS_INACTIVE       = exports.STATUS_INACTIVE       =  0;
    var STATUS_CONNECTING     = exports.STATUS_CONNECTING     =  1;
    var STATUS_LOADING_AGENTS = exports.STATUS_LOADING_AGENTS =  2;
    var STATUS_ACTIVE         = exports.STATUS_ACTIVE         =  3;
    var STATUS_OUT_OF_SYNC    = exports.STATUS_OUT_OF_SYNC    =  4;

    var Async                = require("utils/Async"),
        Dialogs              = require("widgets/Dialogs"),
        DefaultDialogs       = require("widgets/DefaultDialogs"),
        DocumentManager      = require("document/DocumentManager"),
        EditorManager        = require("editor/EditorManager"),
        FileUtils            = require("file/FileUtils"),
        LiveDevServerManager = require("LiveDevelopment/LiveDevServerManager"),
        NativeFileError      = require("file/NativeFileError"),
        NativeApp            = require("utils/NativeApp"),
        PreferencesDialogs   = require("preferences/PreferencesDialogs"),
        ProjectManager       = require("project/ProjectManager"),
        Strings              = require("strings"),
        StringUtils          = require("utils/StringUtils");

    // Inspector
    var Inspector       = require("LiveDevelopment/Inspector/Inspector");

    // Documents
    var CSSDocument     = require("LiveDevelopment/Documents/CSSDocument"),
        HTMLDocument    = require("LiveDevelopment/Documents/HTMLDocument"),
        JSDocument      = require("LiveDevelopment/Documents/JSDocument");

    // Agents
    var agents = {
        "console"   : require("LiveDevelopment/Agents/ConsoleAgent"),
        "remote"    : require("LiveDevelopment/Agents/RemoteAgent"),
        "network"   : require("LiveDevelopment/Agents/NetworkAgent"),
        "dom"       : require("LiveDevelopment/Agents/DOMAgent"),
        "css"       : require("LiveDevelopment/Agents/CSSAgent"),
        "script"    : require("LiveDevelopment/Agents/ScriptAgent"),
        "highlight" : require("LiveDevelopment/Agents/HighlightAgent"),
        "goto"      : require("LiveDevelopment/Agents/GotoAgent"),
        "edit"      : require("LiveDevelopment/Agents/EditAgent")
    };

    // construct path to launch.html
    // window location is can be one of the following:
    // Installed:                /path/to/Brackets.app/Contents/www/index.html
    // Installed, dev:           /path/to/Brackets.app/Contents/dev/src/index.html
    // Installed, dev, test:     /path/to/Brackets.app/Contents/dev/test/SpecRunner.html
    // Arbitrary git repo:       /path/to/brackets/src/index.html
    // Arbitrary git repo, test: /path/to/brackets/test/SpecRunner.html
    var launcherUrl = window.location.pathname;

    // special case for test/SpecRunner.html since we can't tell how requirejs
    // baseUrl is configured dynamically
    launcherUrl = launcherUrl.replace("/test/SpecRunner.html", "/src/index.html");

    launcherUrl = launcherUrl.substr(0, launcherUrl.lastIndexOf("/")) + "/LiveDevelopment/launch.html";
    launcherUrl = window.location.origin + launcherUrl;

    // Some agents are still experimental, so we don't enable them all by default
    // However, extensions can enable them by calling enableAgent().
    // This object is used as a set (thus all properties have the value 'true').
    // Property names should match property names in the 'agents' object.
    var _enabledAgentNames = {
        "console"   : true,
        "remote"    : true,
        "network"   : true,
        "dom"       : true,
        "css"       : true,
        "highlight" : true
    };

    // store the names (matching property names in the 'agent' object) of agents that we've loaded
    var _loadedAgentNames = [];

    var _liveDocument;        // the document open for live editing.
    var _relatedDocuments;    // CSS and JS documents that are used by the live HTML document
    var _serverProvider;      // current LiveDevServerProvider
    var _closeReason;         // reason why live preview was closed

    function _isHtmlFileExt(ext) {
        return (FileUtils.isStaticHtmlFileExt(ext) ||
                (ProjectManager.getBaseUrl() && FileUtils.isServerHtmlFileExt(ext)));
    }

    /** Convert a URL to a local full file path */
    function _urlToPath(url) {
        var path,
            baseUrl = "";

        if (_serverProvider) {
            baseUrl = _serverProvider.getBaseUrl();
        }

        if (baseUrl !== "" && url.indexOf(baseUrl) === 0) {
            // Use base url to translate to local file path.
            // Need to use encoded project path because it's decoded below.
            path = url.replace(baseUrl, encodeURI(ProjectManager.getProjectRoot().fullPath));

        } else if (url.indexOf("file://") === 0) {
            // Convert a file URL to local file path
            path = url.slice(7);
            if (path && brackets.platform === "win" && path.charAt(0) === "/") {
                path = path.slice(1);
            }
        }
        return decodeURI(path);
    }

    /** Convert a local full file path to a URL */
    function _pathToUrl(path) {
        var url,
            baseUrl = "";

        if (_serverProvider) {
            baseUrl = _serverProvider.getBaseUrl();
        }

        // See if base url has been specified and path is within project
        if (baseUrl !== "" && ProjectManager.isWithinProject(path)) {
            // Map to server url. Base url is already encoded, so don't encode again.
            var encodedDocPath = encodeURI(path);
            var encodedProjectPath = encodeURI(ProjectManager.getProjectRoot().fullPath);
            url = encodedDocPath.replace(encodedProjectPath, baseUrl);

        } else {
            var prefix = "file://";

            if (brackets.platform === "win") {
                // The path on Windows starts with a drive letter (e.g. "C:").
                // In order to make it a valid file: URL we need to add an
                // additional slash to the prefix.
                prefix += "/";
            }

            url = encodeURI(prefix + path);
        }

        return url;
    }

    /** Augments the given Brackets document with information that's useful for live development. */
    function _setDocInfo(doc) {

        var parentUrl,
            rootUrl,
            matches;

        // FUTURE: some of these things should just be moved into core Document; others should
        // be in a LiveDevelopment-specific object attached to the doc.
        matches = /^(.*\/)(.+\.([^.]+))$/.exec(doc.file.fullPath);
        if (!matches) {
            return;
        }

        doc.extension = matches[3];

        parentUrl = _pathToUrl(matches[1]);
        doc.url = parentUrl + encodeURI(matches[2]);

        // the root represents the document that should be displayed in the browser
        // for live development (the file for HTML files)
        // TODO: Issue #2033 Improve how default page is determined
        doc.root = { url: doc.url };
    }

    /** Get the current document from the document manager
     * _adds extension, url and root to the document
     */
    function _getCurrentDocument() {
        var doc = DocumentManager.getCurrentDocument();
        if (doc) {
            _setDocInfo(doc);
        }
        return doc;
    }

    /** Determine which document class should be used for a given document
     * @param {Document} document
     */
    function _classForDocument(doc) {
        switch (doc.getLanguage().getId()) {
        case "css":
            return CSSDocument;
        case "javascript":
            return exports.config.experimental ? JSDocument : null;
        }

        if (_isHtmlFileExt(doc.extension)) {
            return HTMLDocument;
        }

        return null;
    }

    function getLiveDocForPath(path) {
        var docsToSearch = [];
        if (_relatedDocuments) {
            docsToSearch = docsToSearch.concat(_relatedDocuments);
        }
        if (_liveDocument) {
            docsToSearch = docsToSearch.concat(_liveDocument);
        }
        var foundDoc;
        docsToSearch.some(function matchesPath(ele) {
            if (ele.doc.file.fullPath === path) {
                foundDoc = ele;
                return true;
            }
            return false;
        });

        return foundDoc;
    }

    function getLiveDocForEditor(editor) {
        if (!editor) {
            return null;
        }
        return getLiveDocForPath(editor.document.file.fullPath);
    }

    /**
     * Removes the given CSS/JSDocument from _relatedDocuments. Signals that the
     * given file is no longer associated with the HTML document that is live (e.g.
     * if the related file has been deleted on disk).
     */
    function _handleRelatedDocumentDeleted(event, liveDoc) {
        var index = _relatedDocuments.indexOf(liveDoc);
        if (index !== -1) {
            $(liveDoc).on("deleted", _handleRelatedDocumentDeleted);
            _relatedDocuments.splice(index, 1);
        }
    }

    /** Close a live document */
    function _closeDocument() {
        if (_liveDocument) {
            _liveDocument.close();
            _liveDocument = undefined;
        }

        if (_serverProvider) {
            // Stop listening for requests
            if (_serverProvider.setRequestFilterPaths) {
                _serverProvider.setRequestFilterPaths([]);
            }

            // Remove any "request" listeners that were added previously
            $(_serverProvider).off(".livedev");
        }

        if (_relatedDocuments) {
            _relatedDocuments.forEach(function (liveDoc) {
                liveDoc.close();
                $(liveDoc).off("deleted", _handleRelatedDocumentDeleted);
            });
            _relatedDocuments = undefined;
        }
    }

    /** Create a live version of a Brackets document */
    function _createDocument(doc, editor) {
        var DocClass = _classForDocument(doc);
        if (DocClass) {
            return new DocClass(doc, editor);
        } else {
            return null;
        }
    }

    /**
     * @private
     * Open a live document
     * @param {Document} source document to open
     */
    function _openDocument(doc, editor) {
        _closeDocument();
        _liveDocument = _createDocument(doc, editor);

        // Enable instrumentation
        if (_liveDocument && _liveDocument.setInstrumentationEnabled) {
            var enableInstrumentation = false;

            if (_serverProvider && _serverProvider.setRequestFilterPaths) {
                enableInstrumentation = true;

                _serverProvider.setRequestFilterPaths(
                    ["/" + encodeURI(ProjectManager.makeProjectRelativeIfPossible(doc.file.fullPath))]
                );

                // Send custom HTTP response for the current live document
                $(_serverProvider).on("request.livedev", function (event, request) {
                    // response can be null in which case the StaticServerDomain reverts to simple file serving.
                    var response = _liveDocument.getResponseData ? _liveDocument.getResponseData() : null;
                    request.send(response);
                });
            }

            _liveDocument.setInstrumentationEnabled(enableInstrumentation);
        }
    }

    /**
     * @private
     * Populate array of related documents reported by the browser agent(s)
     */
    function _getRelatedDocuments() {
        function createLiveStylesheet(url) {
            var stylesheetDeferred = $.Deferred();

            DocumentManager.getDocumentForPath(_urlToPath(url))
                .fail(function () {
                    // A failure to open a related file is benign
                    stylesheetDeferred.resolve();
                })
                .done(function (doc) {
                    // CSSAgent includes containing HTMLDocument in list returned
                    // from getStyleSheetURLS() (which could be useful for collecting
                    // embedded style sheets) but we need to filter doc out here.
                    if ((_classForDocument(doc) === CSSDocument) &&
                            (!_liveDocument || (doc !== _liveDocument.doc))) {
                        _setDocInfo(doc);
                        var liveDoc = _createDocument(doc);
                        if (liveDoc) {
                            _relatedDocuments.push(liveDoc);
                            $(liveDoc).on("deleted", _handleRelatedDocumentDeleted);
                        }
                    }
                    stylesheetDeferred.resolve();
                });
            return stylesheetDeferred.promise();
        }

        // Gather related CSS documents.
        // FUTURE: Gather related JS documents as well.
        _relatedDocuments = [];

        return Async.doInParallel(agents.css.getStylesheetURLs(),
                                  createLiveStylesheet,
                                  false); // don't fail fast
    }

    /** Unload the agents */
    function unloadAgents() {
        _loadedAgentNames.forEach(function (name) {
            agents[name].unload();
        });
        _loadedAgentNames = [];
    }

    /** Load the agents */
    function loadAgents() {
        var name, promises = [], agentsToLoad, promise;

        if (exports.config.experimental) {
            // load all agents
            agentsToLoad = agents;
        } else {
            // load only enabled agents
            agentsToLoad = _enabledAgentNames;
        }
        for (name in agentsToLoad) {
            if (agentsToLoad.hasOwnProperty(name) && agents[name] && agents[name].load) {
                promise = agents[name].load();

                if (promise) {
                    promises.push(promise);
                }

                _loadedAgentNames.push(name);
            }
        }
        return promises;
    }

    /** Enable an agent. Takes effect next time a connection is made. Does not affect
     *  current live development sessions.
     *
     *  @param {string} name of agent to enable
     */
    function enableAgent(name) {
        if (agents.hasOwnProperty(name) && !_enabledAgentNames.hasOwnProperty(name)) {
            _enabledAgentNames[name] = true;
        }
    }

    /** Disable an agent. Takes effect next time a connection is made. Does not affect
     *  current live development sessions.
     *
     *  @param {string} name of agent to disable
     */
    function disableAgent(name) {
        if (_enabledAgentNames.hasOwnProperty(name)) {
            delete _enabledAgentNames[name];
        }
    }

    /** Update the status
     * @param {integer} new status
     */
    function _setStatus(status) {
        // Don't send a notification when the status didn't actually change
        if (status === exports.status) {
            return;
        }

        exports.status = status;
        var reason = status === STATUS_INACTIVE ? _closeReason : null;
        $(exports).triggerHandler("statusChange", [status, reason]);
    }

    /** Triggered by Inspector.error */
    function _onError(event, error) {
        var message;

        // Sometimes error.message is undefined
        if (!error.message) {
            console.warn("Expected a non-empty string in error.message, got this instead:", error.message);
            message = JSON.stringify(error);
        } else {
            message = error.message;
        }

        // Remove "Uncaught" from the beginning to avoid the inspector popping up
        if (message && message.substr(0, 8) === "Uncaught") {
            message = message.substr(9);
        }

        // Additional information, like exactly which parameter could not be processed.
        var data = error.data;
        if (Array.isArray(data)) {
            message += "\n" + data.join("\n");
        }

        // Show the message, but include the error object for further information (e.g. error code)
        console.error(message, error);
        _setStatus(STATUS_ERROR);
    }

    /** Run when all agents are loaded */
    function _onLoad() {
        var doc = _getCurrentDocument();
        if (!doc) {
            return;
        }

        var status = STATUS_ACTIVE;

        // Note: the following promise is never explicitly rejected, so there
        // is no failure handler. If _getRelatedDocuments is changed so that rejection
        // is possible, failure should be managed accordingly.
        _getRelatedDocuments()
            .done(function () {
                if (doc.isDirty && _classForDocument(doc) !== CSSDocument) {
                    status = STATUS_OUT_OF_SYNC;
                }
                _setStatus(status);
            });
    }

    // WebInspector Event: Page.frameNavigated
    function _onFrameNavigated(event, res) {
        // res = {frame}
        var url = res.frame.url,
            baseUrl,
            baseUrlRegExp;

        // Only check domain of root frame (with undefined parentId)
        if (res.frame.parentId) {
            return;
        }

        // Any local file is OK
        if (url.match(/^file:\/\//i) || !_serverProvider) {
            return;
        }

        // Need base url to build reg exp
        baseUrl = _serverProvider.getBaseUrl();
        if (!baseUrl) {
            return;
        }

        // Test that url is within site
        baseUrlRegExp = new RegExp("^" + StringUtils.regexEscape(baseUrl), "i");
        if (!url.match(baseUrlRegExp)) {
            // No longer in site, so terminate live dev, but don't close browser window
            Inspector.disconnect();
            _closeReason = "navigated_away";
            _setStatus(STATUS_INACTIVE);
            _serverProvider = null;
        }
    }

    /** Triggered by Inspector.disconnect */
    function _onDisconnect(event) {
        $(Inspector.Inspector).off("detached.livedev");
        $(Inspector.Page).off("frameNavigated.livedev");

        unloadAgents();
        _closeDocument();
        _setStatus(STATUS_INACTIVE);
    }

    function _onDetached(event, res) {
        // If there already is a reason for closing the session, do not overwrite it
        if (!_closeReason) {
            // Get the explanation from res.reason, e.g. "replaced_with_devtools", "target_closed", "canceled_by_user"
            // Examples taken from https://chromiumcodereview.appspot.com/10947037/patch/12001/13004
            // However, the link refers to the Chrome Extension API, it may not apply 100% to the Inspector API
            // Prefix with "detached_" to create a quasi-namespace for Chrome's reasons
            _closeReason = "detached_" + res.reason;
        }
    }

    function reconnect() {
        unloadAgents();

        _setStatus(STATUS_LOADING_AGENTS);
        var promises = loadAgents();
        $.when.apply(undefined, promises).done(_onLoad).fail(_onError);
    }

    /** Open the Connection and go live */
    function open() {
        var result = new $.Deferred(),
            promise = result.promise();
        var doc = _getCurrentDocument();
        var browserStarted = false;
        var retryCount = 0;

        _closeReason = null;

        function showWrongDocError() {
            Dialogs.showModalDialog(
                DefaultDialogs.DIALOG_ID_ERROR,
                Strings.LIVE_DEVELOPMENT_ERROR_TITLE,
                Strings.LIVE_DEV_NEED_HTML_MESSAGE
            );
            result.reject();
        }

        function showNeedBaseUrlError() {
            PreferencesDialogs.showProjectPreferencesDialog("", Strings.LIVE_DEV_NEED_BASEURL_MESSAGE)
                .done(function (id) {
                    if (id === Dialogs.DIALOG_BTN_OK && ProjectManager.getBaseUrl()) {
                        // If base url is specifed, then re-invoke open() to continue
                        open();
                        result.resolve();
                    } else {
                        result.reject();
                    }
                });
        }

        function showLiveDevServerNotReadyError() {
            Dialogs.showModalDialog(
                DefaultDialogs.DIALOG_ID_ERROR,
                Strings.LIVE_DEVELOPMENT_ERROR_TITLE,
                Strings.LIVE_DEV_SERVER_NOT_READY_MESSAGE
            );
            result.reject();
        }

        // helper function that actually does the launch once we are sure we have
        // a doc and the server for that doc is up and running.
        function doLaunchAfterServerReady() {
            _setStatus(STATUS_CONNECTING);

            _openDocument(doc, EditorManager.getCurrentFullEditor());

            Inspector.connectToURL(launcherUrl).done(result.resolve).fail(function onConnectFail(err) {
                if (err === "CANCEL") {
                    result.reject(err);
                    return;
                }
                if (retryCount > 6) {
                    _setStatus(STATUS_ERROR);
                    Dialogs.showModalDialog(
                        DefaultDialogs.DIALOG_ID_LIVE_DEVELOPMENT,
                        Strings.LIVE_DEVELOPMENT_RELAUNCH_TITLE,
                        Strings.LIVE_DEVELOPMENT_ERROR_MESSAGE,
                        [
                            {
                                className: Dialogs.DIALOG_BTN_CLASS_LEFT,
                                id:        Dialogs.DIALOG_BTN_CANCEL,
                                text:      Strings.CANCEL
                            },
                            {
                                className: Dialogs.DIALOG_BTN_CLASS_PRIMARY,
                                id:        Dialogs.DIALOG_BTN_OK,
                                text:      Strings.RELAUNCH_CHROME
                            }
                        ]
                    )
                        .done(function (id) {
                            if (id === Dialogs.DIALOG_BTN_OK) {
                                // User has chosen to reload Chrome, quit the running instance
                                _setStatus(STATUS_INACTIVE);
                                NativeApp.closeLiveBrowser()
                                    .done(function () {
                                        browserStarted = false;
                                        window.setTimeout(function () {
                                            open().done(result.resolve).fail(result.reject);
                                        });
                                    })
                                    .fail(function (err) {
                                        // Report error?
                                        _setStatus(STATUS_ERROR);
                                        browserStarted = false;
                                        result.reject("CLOSE_LIVE_BROWSER");
                                    });
                            } else {
                                result.reject("CANCEL");
                            }
                        });
                    return;
                }
                retryCount++;

                if (!browserStarted && exports.status !== STATUS_ERROR) {
                    NativeApp.openLiveBrowser(
                        launcherUrl,
                        true        // enable remote debugging
                    )
                        .done(function () {
                            browserStarted = true;
                        })
                        .fail(function (err) {
                            var message;

                            _setStatus(STATUS_ERROR);
                            if (err === NativeFileError.NOT_FOUND_ERR) {
                                message = Strings.ERROR_CANT_FIND_CHROME;
                            } else {
                                message = StringUtils.format(Strings.ERROR_LAUNCHING_BROWSER, err);
                            }

                            // Append a message to direct users to the troubleshooting page.
                            if (message) {
                                message += " " + StringUtils.format(Strings.LIVE_DEVELOPMENT_TROUBLESHOOTING, brackets.config.troubleshoot_url);
                            }

                            Dialogs.showModalDialog(
                                DefaultDialogs.DIALOG_ID_ERROR,
                                Strings.ERROR_LAUNCHING_BROWSER_TITLE,
                                message
                            );

                            result.reject("OPEN_LIVE_BROWSER");
                        });
                }

                if (exports.status !== STATUS_ERROR) {
                    window.setTimeout(function retryConnect() {
                        Inspector.connectToURL(launcherUrl).done(result.resolve).fail(onConnectFail);
                    }, 500);
                }
            });
        }

        if (!doc || !doc.root) {
            showWrongDocError();
        } else {
            _serverProvider = LiveDevServerManager.getProvider(doc.file.fullPath);

            if (!exports.config.experimental && !_serverProvider) {
                if (FileUtils.isServerHtmlFileExt(doc.extension)) {
                    showNeedBaseUrlError();
                } else if (!FileUtils.isStaticHtmlFileExt(doc.extension)) {
                    showWrongDocError();
                } else {
                    doLaunchAfterServerReady();   // fall-back to file://
                }
            } else {
                var readyPromise = _serverProvider.readyToServe();
                if (!readyPromise) {
                    showLiveDevServerNotReadyError();
                } else {
                    readyPromise.then(
                        doLaunchAfterServerReady,
                        showLiveDevServerNotReadyError
                    );
                }
            }
        }

        return promise;
    }

    /**
     * Close the connection and the associated window asynchronously
     *
     * @return {jQuery.Promise} Resolves once the connection is closed
     */
    function close() {
        _closeReason = "explicit_close";

        var deferred = $.Deferred();

        /*
         * Finish closing the live development connection, including setting
         * the status accordingly.
         */
        function cleanup() {
            _setStatus(STATUS_INACTIVE);
            _serverProvider = null;
            deferred.resolve();
        }

        if (Inspector.connected()) {
            var timer = window.setTimeout(cleanup, 5000); // 5 seconds
            Inspector.Runtime.evaluate("window.open('', '_self').close();", function (response) {
                Inspector.disconnect();
                window.clearTimeout(timer);
                cleanup();
            });
        } else {
            cleanup();
        }

        return deferred.promise();
    }

    /** Enable highlighting */
    function showHighlight() {
        var doc = getLiveDocForEditor(EditorManager.getActiveEditor());

        if (doc instanceof CSSDocument) {
            doc.updateHighlight();
        }
    }

    /** Hide any active highlighting */
    function hideHighlight() {
        if (Inspector.connected() && agents.highlight) {
            agents.highlight.hide();
        }
    }

    /** Redraw highlights **/
    function redrawHighlight() {
        if (Inspector.connected() && agents.highlight) {
            agents.highlight.redraw();
        }
    }

    /** Triggered by Inspector.connect */
    function _onConnect(event) {

        /*
         * Create a promise that resolves when the interstitial page has
         * finished loading.
         *
         * @return {jQuery.Promise}
         */
        function waitForInterstitialPageLoad() {
            var deferred    = $.Deferred(),
                keepPolling = true,
                timer       = window.setTimeout(function () {
                    keepPolling = false;
                    deferred.reject();
                }, 10000); // 10 seconds

            /*
             * Asynchronously check to see if the interstitial page has
             * finished loading; if not, check again until timing out.
             */
            function pollInterstitialPage() {
                if (keepPolling && Inspector.connected()) {
                    Inspector.Runtime.evaluate("window.isBracketsLiveDevelopmentInterstitialPageLoaded", function (response) {
                        var result = response.result;

                        if (result.type === "boolean" && result.value) {
                            window.clearTimeout(timer);
                            deferred.resolve();
                        } else {
                            window.setTimeout(pollInterstitialPage, 100);
                        }
                    });
                } else {
                    deferred.reject();
                }
            }

            pollInterstitialPage();
            return deferred.promise();
        }

        /*
         * Load agents and navigate to the target document once the
         * interstitial page has finished loading.
         */
        function onInterstitialPageLoad() {
            // Page domain must be enabled first before loading other agents
            Inspector.Page.enable().done(function () {
                // Load the right document (some agents are waiting for the page's load event)
                var doc = _getCurrentDocument();
                if (doc) {
                    Inspector.Page.navigate(doc.root.url);
                } else {
                    close();
                }
            });

            // Load agents
            _setStatus(STATUS_LOADING_AGENTS);
            var promises = loadAgents();
            $.when.apply(undefined, promises).done(_onLoad).fail(_onError);
        }

        $(Inspector.Page).on("frameNavigated.livedev", _onFrameNavigated);

        waitForInterstitialPageLoad()
            .fail(function () {
                close();
                Dialogs.showModalDialog(
                    DefaultDialogs.DIALOG_ID_ERROR,
                    Strings.LIVE_DEVELOPMENT_ERROR_TITLE,
                    Strings.LIVE_DEV_LOADING_ERROR_MESSAGE
                );
            })
            .done(onInterstitialPageLoad);
    }

    /** Triggered by a document change from the DocumentManager */
    function _onDocumentChange() {
        var doc = _getCurrentDocument(),
            status = STATUS_ACTIVE,
            promise;

        if (!doc) {
            return;
        }

        if (Inspector.connected()) {
            hideHighlight();
            if (agents.network && agents.network.wasURLRequested(doc.url)) {
                _openDocument(doc, EditorManager.getCurrentFullEditor());

                promise = _getRelatedDocuments();
            } else {
                if (exports.config.experimental || _isHtmlFileExt(doc.extension)) {
                    promise = close().done(open);
                } else {
                    promise = $.Deferred().resolve();
                }
            }

            promise
                .fail(close)
                .done(function () {
                    if (doc.isDirty && _classForDocument(doc) !== CSSDocument) {
                        status = STATUS_OUT_OF_SYNC;
                    }
                    _setStatus(status);
                });
        }
    }

    /** Triggered by a document saved from the DocumentManager */
    function _onDocumentSaved(event, doc) {
        if (doc && Inspector.connected() && _classForDocument(doc) !== CSSDocument &&
                agents.network && agents.network.wasURLRequested(doc.url)) {
            // Unload and reload agents before reloading the page
            reconnect();

            // Reload HTML page
            Inspector.Page.reload();
        }
    }

    /** Triggered by a change in dirty flag from the DocumentManager */
    function _onDirtyFlagChange(event, doc) {
        if (doc && Inspector.connected() && _classForDocument(doc) !== CSSDocument &&
                agents.network && agents.network.wasURLRequested(doc.url)) {
            // Set status to out of sync if dirty. Otherwise, set it to active status.
            _setStatus(doc.isDirty ? STATUS_OUT_OF_SYNC : STATUS_ACTIVE);
        }
    }

    /**
     * @constructor
     *
     * LiveDevServerProvider for user specified server as defined with Live Preview Base Url
     * Project setting. In a clean installation of Brackets, this is the highest priority
     * server provider, if defined.
     */
    function UserServerProvider() {}

    /**
     * Determines whether we can serve local file.
     * @param {String} localPath A local path to file being served.
     * @return {Boolean} true for yes, otherwise false.
     */
    UserServerProvider.prototype.canServe = function (localPath) {
        var baseUrl = ProjectManager.getBaseUrl();
        if (!baseUrl) {
            return false;
        }

        if (!ProjectManager.isWithinProject(localPath)) {
            return false;
        }

        return _isHtmlFileExt(localPath);
    };

    /**
     * Returns a base url for current project.
     * @return {String}  Base url for current project.
     */
    UserServerProvider.prototype.getBaseUrl = function () {
        return ProjectManager.getBaseUrl();
    };

    /**
     * # LiveDevServerProvider.readyToServe()
     *
     * Used to check if the server has finished launching after opening
     * the project. User is required to make sure their external sever
     * is ready, so indicate that we're always ready.
     *
     * @return {jQuery.Promise} Promise that is already resolved
     */
    UserServerProvider.prototype.readyToServe = function () {
        return $.Deferred().resolve().promise();
    };

    /** Initialize the LiveDevelopment Session */
    function init(theConfig) {
        exports.config = theConfig;
        $(Inspector).on("connect", _onConnect)
            .on("disconnect", _onDisconnect)
            .on("error", _onError);
        $(Inspector.Inspector).on("detached", _onDetached);
        $(DocumentManager).on("currentDocumentChange", _onDocumentChange)
            .on("documentSaved", _onDocumentSaved)
            .on("dirtyFlagChange", _onDirtyFlagChange);
        $(ProjectManager).on("beforeProjectClose beforeAppClose", close);

        // Register user defined server provider
        var userServerProvider = new UserServerProvider();
        LiveDevServerManager.registerProvider(userServerProvider, 99);

        // Initialize exports.status
        _setStatus(STATUS_INACTIVE);
    }

    function _setServerProvider(serverProvider) {
        _serverProvider = serverProvider;
    }

    // For unit testing
    exports._pathToUrl          = _pathToUrl;
    exports._urlToPath          = _urlToPath;
    exports._setServerProvider  = _setServerProvider;
    exports.launcherUrl         = launcherUrl;

    // Export public functions
    exports.agents              = agents;
    exports.open                = open;
    exports.close               = close;
    exports.reconnect           = reconnect;
    exports.enableAgent         = enableAgent;
    exports.disableAgent        = disableAgent;
    exports.getLiveDocForPath   = getLiveDocForPath;
    exports.showHighlight       = showHighlight;
    exports.hideHighlight       = hideHighlight;
    exports.redrawHighlight     = redrawHighlight;
    exports.init                = init;
});


/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, $, window */

/**
 * EditorManager owns the UI for the editor area. This essentially mirrors the 'current document'
 * property maintained by DocumentManager's model.
 *
 * Note that there is a little bit of unusual overlap between EditorManager and DocumentManager:
 * because the Document state is actually stored in the CodeMirror editor UI, DocumentManager is
 * not a pure headless model. Each Document encapsulates an editor instance, and thus EditorManager
 * must have some knowledge about Document's internal state (we access its _editor property).
 *
 * This module dispatches the following events:
 *    - activeEditorChange --  Fires after the active editor (full or inline) changes and size/visibility
 *                             are complete. Doesn't fire when editor temporarily loses focus to a non-editor
 *                             control (e.g. search toolbar or modal dialog, or window deactivation). Does
 *                             fire when focus moves between inline editor and its full-size container.
 *                             This event tracks getActiveEditor() changes, while DocumentManager's
 *                             currentDocumentChange tracks getCurrentFullEditor() changes.
 *                             The 2nd arg to the listener is which Editor became active; the 3rd arg is
 *                             which Editor is deactivated as a result. Either one may be null.
 *                             NOTE (#1257): getFocusedEditor() sometimes lags behind this event. Listeners
 *                             should use the arguments or call getActiveEditor() to reliably see which Editor
 *                             just gained focus.
 */
define(function (require, exports, module) {
    "use strict";

    // Load dependent modules
    var Commands            = require("command/Commands"),
        PanelManager        = require("view/PanelManager"),
        CommandManager      = require("command/CommandManager"),
        DocumentManager     = require("document/DocumentManager"),
        PerfUtils           = require("utils/PerfUtils"),
        Editor              = require("editor/Editor").Editor,
        InlineTextEditor    = require("editor/InlineTextEditor").InlineTextEditor,
        Strings             = require("strings");

    /** @type {jQueryObject} DOM node that contains all editors (visible and hidden alike) */
    var _editorHolder = null;

    /**
     * Currently visible full-size Editor, or null if no editors open
     * @type {?Editor}
     */
    var _currentEditor = null;
    /** @type {?Document} */
    var _currentEditorsDocument = null;

    /**
     * Currently focused Editor (full-size, inline, or otherwise)
     * @type {?Editor}
     */
    var _lastFocusedEditor = null;

    /**
     * Maps full path to scroll pos & cursor/selection info. Not kept up to date while an editor is current.
     * Only updated when switching / closing editor, or when requested explicitly via _getViewState().
     * @type {Object<string, {scrollPos:{x:number, y:number}, selection:{start:{line:number, ch:number}, end:{line:number, ch:number}}}>}
     */
    var _viewStateCache = {};

    /**
     * Last known editor area width, used to detect when the window is resized horizontally.
     */
    var _lastEditorWidth = null;

    /**
     * Registered inline-editor widget providers. See {@link #registerInlineEditProvider()}.
     * @type {Array.<function(...)>}
     */
    var _inlineEditProviders = [];

    /**
     * Registered inline documentation widget providers. See {@link #registerInlineDocsProvider()}.
     * @type {Array.<function(...)>}
     */
    var _inlineDocsProviders = [];

    /**
     * Registered jump-to-definition providers. See {@link #registerJumpToDefProvider()}.
     * @type {Array.<function(...)>}
     */
    var _jumpToDefProviders = [];

	/**
     * @private
     * @param {?Editor} current
     */
    function _notifyActiveEditorChanged(current) {
        // Skip if the Editor that gained focus was already the most recently focused editor.
        // This may happen e.g. if the window loses then regains focus.
        if (_lastFocusedEditor === current) {
            return;
        }
        var previous = _lastFocusedEditor;
        _lastFocusedEditor = current;

        $(exports).triggerHandler("activeEditorChange", [current, previous]);
    }

    /**
     * Creates a new Editor bound to the given Document.
     * The editor is appended to the given container as a visible child.
     * @param {!Document} doc  Document for the Editor's content
     * @param {!boolean} makeMasterEditor  If true, the Editor will set itself as the private "master"
     *          Editor for the Document. If false, the Editor will attach to the Document as a "slave."
     * @param {!jQueryObject} container  Container to add the editor to.
     * @param {{startLine: number, endLine: number}=} range If specified, range of lines within the document
     *          to display in this editor. Inclusive.
     * @return {Editor} the newly created editor.
     */
    function _createEditorForDocument(doc, makeMasterEditor, container, range) {
        var editor = new Editor(doc, makeMasterEditor, container, range);

        $(editor).on("focus", function () {
            _notifyActiveEditorChanged(this);
        });

        return editor;
    }

    /**
     * @private
     * Finds an inline widget provider from the given list that can offer a widget for the current cursor
     * position, and once the widget has been created inserts it into the editor.
     * @param {!Editor} editor The host editor
     * @param {!Array.<function(!Editor, !{line:Number, ch:Number}):?$.Promise>} providers
     * @return {$.Promise} a promise that will be resolved when an InlineWidget
     *      is created or rejected if no inline providers have offered one.
     */
    function _openInlineWidget(editor, providers) {
        PerfUtils.markStart(PerfUtils.INLINE_WIDGET_OPEN);

        // Run through inline-editor providers until one responds
        var pos = editor.getCursorPos(),
            inlinePromise,
            i,
            result = new $.Deferred();

        for (i = 0; i < providers.length && !inlinePromise; i++) {
            var provider = providers[i];
            inlinePromise = provider(editor, pos);
        }

        // If one of them will provide a widget, show it inline once ready
        if (inlinePromise) {
            inlinePromise.done(function (inlineWidget) {
                editor.addInlineWidget(pos, inlineWidget);
                PerfUtils.addMeasurement(PerfUtils.INLINE_WIDGET_OPEN);
                result.resolve();
            }).fail(function () {
                // terminate timer that was started above
                PerfUtils.finalizeMeasurement(PerfUtils.INLINE_WIDGET_OPEN);
                result.reject();
            });
        } else {
            // terminate timer that was started above
            PerfUtils.finalizeMeasurement(PerfUtils.INLINE_WIDGET_OPEN);
            result.reject();
        }

        return result.promise();
    }

    /**
     * Removes the given widget UI from the given hostEditor (agnostic of what the widget's content
     * is). The widget's onClosed() callback will be run as a result.
     * @param {!Editor} hostEditor The editor containing the widget.
     * @param {!InlineWidget} inlineWidget The inline widget to close.
     */
    function closeInlineWidget(hostEditor, inlineWidget) {
        // If widget has focus, return it to the hostEditor & move the cursor to where the inline used to be
        if (inlineWidget.hasFocus()) {
            // Place cursor back on the line just above the inline (the line from which it was opened)
            // If cursor's already on that line, leave it be to preserve column position
            var widgetLine = hostEditor._codeMirror.getLineNumber(inlineWidget.info.line);
            var cursorLine = hostEditor.getCursorPos().line;
            if (cursorLine !== widgetLine) {
                hostEditor.setCursorPos({ line: widgetLine, pos: 0 });
            }

            hostEditor.focus();
        }

        hostEditor.removeInlineWidget(inlineWidget);
    }

    /**
     * Registers a new inline editor provider. When Quick Edit is invoked each registered provider is
     * asked if it wants to provide an inline editor given the current editor and cursor location.
     *
     * @param {function(!Editor, !{line:Number, ch:Number}):?$.Promise} provider
     * The provider returns a promise that will be resolved with an InlineWidget, or returns null
     * to indicate the provider doesn't want to respond to this case.
     */
    function registerInlineEditProvider(provider) {
        _inlineEditProviders.push(provider);
    }

    /**
     * Registers a new inline docs provider. When Quick Docs is invoked each registered provider is
     * asked if it wants to provide inline docs given the current editor and cursor location.
     *
     * @param {function(!Editor, !{line:Number, ch:Number}):?$.Promise} provider
     * The provider returns a promise that will be resolved with an InlineWidget, or returns null
     * to indicate the provider doesn't want to respond to this case.
     */
    function registerInlineDocsProvider(provider) {
        _inlineDocsProviders.push(provider);
    }

    /**
     * Registers a new jump-to-definition provider. When jump-to-definition is invoked each
     * registered provider is asked if it wants to provide jump-to-definition results, given
     * the current editor and cursor location.
     * @param {function(!Editor, !{line:Number, ch:Number}):?$.Promise} provider
     * The provider returns a promise that will be resolved with jump-to-definition results, or
     * returns null to indicate the provider doesn't want to respond to this case.
     */
    function registerJumpToDefProvider(provider) {
        _jumpToDefProviders.push(provider);
    }

    /**
     * @private
     * Given a host editor, return a list of all Editors in all its open inline widgets. (Ignoring
     * any other inline widgets that might be open but don't contain Editors).
     * @param {!Editor} hostEditor
     * @return {Array.<Editor>}
     *
     */
    function getInlineEditors(hostEditor) {
        var inlineEditors = [];

        if (hostEditor) {
            hostEditor.getInlineWidgets().forEach(function (widget) {
                if (widget instanceof InlineTextEditor) {
                    inlineEditors = inlineEditors.concat(widget.editors);
                }
            });
        }

        return inlineEditors;
    }

    /**
     * @private
     * Creates a new "full-size" (not inline) Editor for the given Document, and sets it as the
     * Document's master backing editor. The editor is not yet visible; to show it, use
     * DocumentManager.setCurrentDocument().
     * Semi-private: should only be called within this module or by Document.
     * @param {!Document} document  Document whose main/full Editor to create
     */
    function _createFullEditorForDocument(document) {
        // Create editor; make it initially invisible
        var container = _editorHolder.get(0);
        var editor = _createEditorForDocument(document, true, container);
        editor.setVisible(false);
    }

    /** Returns the visible full-size Editor corresponding to DocumentManager.getCurrentDocument() */
    function getCurrentFullEditor() {
        // This *should* always be equivalent to DocumentManager.getCurrentDocument()._masterEditor
        return _currentEditor;
    }

    /**
     * Creates a new inline Editor instance for the given Document.
     * The editor is not yet visible or attached to a host editor.
     * @param {!Document} doc  Document for the Editor's content
     * @param {?{startLine:Number, endLine:Number}} range  If specified, all lines outside the given
     *      range are hidden from the editor. Range is inclusive. Line numbers start at 0.
     * @param {HTMLDivContainer} inlineContent
     * @param  {function(inlineWidget)} closeThisInline
     *
     * @return {{content:DOMElement, editor:Editor}}
     */
    function createInlineEditorForDocument(doc, range, inlineContent) {
        // Create the Editor
        var inlineEditor = _createEditorForDocument(doc, false, inlineContent, range);

        return { content: inlineContent, editor: inlineEditor };
    }

    /**
     * Disposes the given Document's full-size editor if the doc is no longer "open" from the user's
     * standpoint - not in the working set and not currentDocument).
     *
     * Destroying the full-size editor releases ONE ref to the Document; if inline editors or other
     * UI elements are still referencing the Document it will still be 'open' (kept alive) from
     * DocumentManager's standpoint. However, destroying the full-size editor does remove the backing
     * "master" editor from the Document, rendering it immutable until either inline-editor edits or
     * currentDocument change triggers _createFullEditorForDocument() full-size editor again.
     *
     * In certain edge cases, this is called directly by DocumentManager; see _gcDocuments() for details.
     *
     * @param {!Document} document Document whose "master" editor we may destroy
     */
    function _destroyEditorIfUnneeded(document) {
        var editor = document._masterEditor;

        if (!editor) {
            return;
        }

        // If outgoing editor is no longer needed, dispose it
        var isCurrentDocument = (DocumentManager.getCurrentDocument() === document);
        var isInWorkingSet = (DocumentManager.findInWorkingSet(document.file.fullPath) !== -1);
        if (!isCurrentDocument && !isInWorkingSet) {
            // Destroy the editor widget (which un-refs the Document and reverts it to read-only mode)
            editor.destroy();

            // Our callers should really ensure this, but just for safety...
            if (_currentEditor === editor) {
                _currentEditorsDocument = null;
                _currentEditor = null;
            }
        }
    }

    /**
     * Returns focus to the last visible editor that had focus. If no editor visible, does nothing.
     * This function should be called to restore editor focus after it has been temporarily
     * removed. For example, after a dialog with editable text is closed.
     */
    function focusEditor() {
        if (_lastFocusedEditor) {
            _lastFocusedEditor.focus();
        }
    }

    /**
     * Flag for _onEditorAreaResize() to always force refresh.
     * @const
     * @type {string}
     */
    var REFRESH_FORCE = "force";

    /**
     * Flag for _onEditorAreaResize() to never refresh.
     * @const
     * @type {string}
     */
    var REFRESH_SKIP = "skip";

    /**
     * Must be called whenever the size/visibility of editor area siblings is changed without going through
     * PanelManager or Resizer. Resizable panels created via PanelManager do not require this manual call.
     */
    function resizeEditor() {
        if (!_editorHolder) {
            return;  // still too early during init
        }
        // PanelManager computes the correct editor-holder size & calls us back with it, via _onEditorAreaResize()
        PanelManager._notifyLayoutChange();
    }

    /**
     * Update the current CodeMirror editor's size. Must be called any time the contents of the editor area
     * are swapped or any time the editor-holder area has changed height. EditorManager calls us in the swap
     * case. PanelManager calls us in the most common height-change cases (panel and/or window resize), but
     * some other cases are handled by external code calling resizeEditor() (e.g. ModalBar hide/show).
     *
     * @param {number} editorAreaHt
     * @param {string=} refreshFlag For internal use. Set to "force" to ensure the editor will refresh,
     *    "skip" to ensure the editor does not refresh, or leave undefined to let _onEditorAreaResize()
     *    determine whether it needs to refresh.
     */
    function _onEditorAreaResize(event, editorAreaHt, refreshFlag) {

        if (_currentEditor) {
            var curRoot = _currentEditor.getRootElement(),
                curWidth = $(curRoot).width();
            if (!curRoot.style.height || $(curRoot).height() !== editorAreaHt) {
                $(curRoot).height(editorAreaHt);
                if (refreshFlag === undefined) {
                    refreshFlag = REFRESH_FORCE;
                }
            } else if (curWidth !== _lastEditorWidth) {
                if (refreshFlag === undefined) {
                    refreshFlag = REFRESH_FORCE;
                }
            }
            _lastEditorWidth = curWidth;

            if (refreshFlag === REFRESH_FORCE) {
                _currentEditor.refreshAll(true);
            }
        }
    }

    /** Updates _viewStateCache from the given editor's actual current state */
    function _saveEditorViewState(editor) {
        _viewStateCache[editor.document.file.fullPath] = {
            selection: editor.getSelection(),
            scrollPos: editor.getScrollPos()
        };
    }

    /** Updates the given editor's actual state from _viewStateCache, if any state stored */
    function _restoreEditorViewState(editor) {
        // We want to ignore the current state of the editor, so don't call _getViewState()
        var viewState = _viewStateCache[editor.document.file.fullPath];
        if (viewState) {
            if (viewState.selection) {
                editor.setSelection(viewState.selection.start, viewState.selection.end);
            }
            if (viewState.scrollPos) {
                editor.setScrollPos(viewState.scrollPos.x, viewState.scrollPos.y);
            }
        }
    }

    /** Returns up-to-date view state for the given file, or null if file not open and no state cached */
    function _getViewState(fullPath) {
        if (_currentEditorsDocument && _currentEditorsDocument.file.fullPath === fullPath) {
            _saveEditorViewState(_currentEditor);
        }
        return _viewStateCache[fullPath];
    }

    /** Removes all cached view state info and replaces it with the given mapping */
    function _resetViewStates(viewStates) {
        _viewStateCache = viewStates;
    }

    /**
     * @private
     */
    function _doShow(document) {
        // Show new editor
        _currentEditorsDocument = document;
        _currentEditor = document._masterEditor;

        // Skip refreshing the editor since we're going to refresh it more explicitly below
        _currentEditor.setVisible(true, false);
        _currentEditor.focus();

        // Resize and refresh the editor, since it might have changed size or had other edits applied
        // since it was last visible.
        PanelManager._notifyLayoutChange(REFRESH_FORCE);
    }

    /**
     * Make the given document's editor visible in the UI, hiding whatever was
     * visible before. Creates a new editor if none is assigned.
     * @param {!Document} document
     */
    function _showEditor(document) {
        // Hide whatever was visible before
        if (!_currentEditor) {
            $("#not-editor").css("display", "none");
        } else {
            _saveEditorViewState(_currentEditor);
            _currentEditor.setVisible(false);
            _destroyEditorIfUnneeded(_currentEditorsDocument);
        }

        // Ensure a main editor exists for this document to show in the UI
        var createdNewEditor = false;
        if (!document._masterEditor) {
            createdNewEditor = true;
            // Editor doesn't exist: populate a new Editor with the text
            _createFullEditorForDocument(document);
        }

        _doShow(document);

        if (createdNewEditor) {
            _restoreEditorViewState(document._masterEditor);
        }
    }

    /** Hide the currently visible editor and show a placeholder UI in its place */
    function _showNoEditor() {
        if (_currentEditor) {
            _saveEditorViewState(_currentEditor);
            _currentEditor.setVisible(false);
            _destroyEditorIfUnneeded(_currentEditorsDocument);

            _currentEditorsDocument = null;
            _currentEditor = null;

            $("#not-editor").css("display", "");

            // No other Editor is gaining focus, so in this one special case we must trigger event manually
            _notifyActiveEditorChanged(null);
        }
    }

    /** Handles changes to DocumentManager.getCurrentDocument() */
    function _onCurrentDocumentChange() {
        var doc = DocumentManager.getCurrentDocument(),
            container = _editorHolder.get(0);

        var perfTimerName = PerfUtils.markStart("EditorManager._onCurrentDocumentChange():\t" + (!doc || doc.file.fullPath));

        // Update the UI to show the right editor (or nothing), and also dispose old editor if no
        // longer needed.
        if (doc) {
            _showEditor(doc);
        } else {
            _showNoEditor();
        }

        PerfUtils.addMeasurement(perfTimerName);
    }

    /** Handles removals from DocumentManager's working set list */
    function _onWorkingSetRemove(event, removedFile) {
        // There's one case where an editor should be disposed even though the current document
        // didn't change: removing a document from the working set (via the "X" button). (This may
        // also cover the case where the document WAS current, if the editor-swap happens before the
        // removal from the working set.
        var doc = DocumentManager.getOpenDocumentForPath(removedFile.fullPath);
        if (doc) {
            _destroyEditorIfUnneeded(doc);
        }
        // else, file was listed in working set but never shown in the editor - ignore
    }

    function _onWorkingSetRemoveList(event, removedFiles) {
        removedFiles.forEach(function (removedFile) {
            _onWorkingSetRemove(event, removedFile);
        });
    }

    // Note: there are several paths that can lead to an editor getting destroyed
    //  - file was in working set, but not in current editor; then closed (via working set "X" button)
    //      --> handled by _onWorkingSetRemove()
    //  - file was in current editor, but not in working set; then navigated away from
    //      --> handled by _onCurrentDocumentChange()
    //  - file was in current editor, but not in working set; then closed (via File > Close) (and thus
    //    implicitly navigated away from)
    //      --> handled by _onCurrentDocumentChange()
    //  - file was in current editor AND in working set; then closed (via File > Close OR working set
    //    "X" button) (and thus implicitly navigated away from)
    //      --> handled by _onWorkingSetRemove() currently, but could be _onCurrentDocumentChange()
    //      just as easily (depends on the order of events coming from DocumentManager)

    /**
     * Designates the DOM node that will contain the currently active editor instance. EditorManager
     * will own the content of this DOM node.
     * @param {!jQueryObject} holder
     */
    function setEditorHolder(holder) {
        if (_currentEditor) {
            console.error("Cannot change editor area after an editor has already been created!");
            return;
        }

        _editorHolder = holder;

        resizeEditor();  // if no files open at startup, we won't get called back later to resize the "no-editor" placeholder
    }

    /**
     * Returns the currently focused inline widget, if any.
     * @return {?InlineWidget}
     */
    function getFocusedInlineWidget() {
        var result = null;

        if (_currentEditor) {
            _currentEditor.getInlineWidgets().forEach(function (widget) {
                if (widget.hasFocus()) {
                    result = widget;
                }
            });
        }

        return result;
    }

    /**
     * Returns the focused Editor within an inline text editor, or null if something else has focus
     * @return {?Editor}
     */
    function _getFocusedInlineEditor() {
        var focusedWidget = getFocusedInlineWidget();
        if (focusedWidget instanceof InlineTextEditor) {
            return focusedWidget.getFocusedEditor();
        }
        return null;
    }

    /**
     * Returns the currently focused editor instance (full-sized OR inline editor).
     * This function is similar to getActiveEditor(), with one main difference: this
     * function will only return editors that currently have focus, whereas
     * getActiveEditor() will return the last visible editor that was given focus (but
     * may not currently have focus because, for example, a dialog with editable text
     * is open).
     * @returns {?Editor}
     */
    function getFocusedEditor() {
        if (_currentEditor) {

            // See if any inlines have focus
            var focusedInline = _getFocusedInlineEditor();
            if (focusedInline) {
                return focusedInline;
            }

            // otherwise, see if full-sized editor has focus
            if (_currentEditor.hasFocus()) {
                return _currentEditor;
            }
        }

        return null;
    }

    /**
     * Returns the current active editor (full-sized OR inline editor). This editor may not
     * have focus at the moment, but it is visible and was the last editor that was given
     * focus. Returns null if no editors are active.
     * @see getFocusedEditor()
     * @returns {?Editor}
     */
    function getActiveEditor() {
        return _lastFocusedEditor;
    }

    /**
     * Closes any focused inline widget. Else, asynchronously asks providers to create one.
     * @return {!Promise} A promise resolved with true if an inline widget is opened or false
     *   when closed. Rejected if there is neither an existing widget to close nor a provider
     *   willing to create a widget (or if no editor is open).
     */
    function _toggleInlineWidget(providers) {
        var result = new $.Deferred();

        if (_currentEditor) {
            var inlineWidget = getFocusedInlineWidget();

            if (inlineWidget) {
                // an inline widget's editor has focus, so close it
                PerfUtils.markStart(PerfUtils.INLINE_WIDGET_CLOSE);
                inlineWidget.close();
                PerfUtils.addMeasurement(PerfUtils.INLINE_WIDGET_CLOSE);

                // return a resolved promise to CommandManager
                result.resolve(false);
            } else {
                // main editor has focus, so create an inline editor
                _openInlineWidget(_currentEditor, providers).done(function () {
                    result.resolve(true);
                }).fail(function () {
                    result.reject();
                });
            }
        } else {
            // Can not open an inline editor without a host editor
            result.reject();
        }

        return result.promise();
    }

    /**
     * Asynchronously asks providers to handle jump-to-definition.
     * @return {!Promise} null if no appropriate provider exists. Else, returns a promise
     *  which is resolved by adjusting the editor selection to the requested definition.
     */
    function _doJumpToDef() {
        var providers = _jumpToDefProviders;
        var promise,
            i,
            result = new $.Deferred();

        if (_currentEditor) {
            // main editor has focus

            PerfUtils.markStart(PerfUtils.JUMP_TO_DEFINITION);

            // Run through providers until one responds
            for (i = 0; i < providers.length && !promise; i++) {
                var provider = providers[i];
                promise = provider();
            }

            // Will one of them will provide a result?
            if (promise) {
                promise.done(function () {
                    PerfUtils.addMeasurement(PerfUtils.JUMP_TO_DEFINITION);
                    result.resolve();
                }).fail(function () {
                    // terminate timer that was started above
                    PerfUtils.finalizeMeasurement(PerfUtils.JUMP_TO_DEFINITION);
                    result.reject();
                });
            } else {
                // terminate timer that was started above
                PerfUtils.finalizeMeasurement(PerfUtils.JUMP_TO_DEFINITION);
                result.reject();
            }

        } else {
            result.reject();
        }

        return result.promise();
    }

    // Initialize: command handlers
    CommandManager.register(Strings.CMD_TOGGLE_QUICK_EDIT, Commands.TOGGLE_QUICK_EDIT, function () {
        return _toggleInlineWidget(_inlineEditProviders);
    });
    CommandManager.register(Strings.CMD_TOGGLE_QUICK_DOCS, Commands.TOGGLE_QUICK_DOCS, function () {
        return _toggleInlineWidget(_inlineDocsProviders);
    });
    CommandManager.register(Strings.CMD_JUMPTO_DEFINITION, Commands.NAVIGATE_JUMPTO_DEFINITION, _doJumpToDef);

    // Create PerfUtils measurement
    PerfUtils.createPerfMeasurement("JUMP_TO_DEFINITION", "Jump-To-Definiiton");

    // Initialize: register listeners
    $(DocumentManager).on("currentDocumentChange", _onCurrentDocumentChange);
    $(DocumentManager).on("workingSetRemove", _onWorkingSetRemove);
    $(DocumentManager).on("workingSetRemoveList", _onWorkingSetRemoveList);
    $(PanelManager).on("editorAreaResize", _onEditorAreaResize);

    // For unit tests and internal use only
    exports._openInlineWidget = _openInlineWidget;
    exports._createFullEditorForDocument = _createFullEditorForDocument;
    exports._destroyEditorIfUnneeded = _destroyEditorIfUnneeded;
    exports._getViewState = _getViewState;
    exports._resetViewStates = _resetViewStates;
    exports._doShow = _doShow;
    exports._notifyActiveEditorChanged = _notifyActiveEditorChanged;

    exports.REFRESH_FORCE = REFRESH_FORCE;
    exports.REFRESH_SKIP = REFRESH_SKIP;

    // Define public API
    exports.setEditorHolder = setEditorHolder;
    exports.getCurrentFullEditor = getCurrentFullEditor;
    exports.createInlineEditorForDocument = createInlineEditorForDocument;
    exports.focusEditor = focusEditor;
    exports.getFocusedEditor = getFocusedEditor;
    exports.getActiveEditor = getActiveEditor;
    exports.getFocusedInlineWidget = getFocusedInlineWidget;
    exports.resizeEditor = resizeEditor;
    exports.registerInlineEditProvider = registerInlineEditProvider;
    exports.registerInlineDocsProvider = registerInlineDocsProvider;
    exports.registerJumpToDefProvider = registerJumpToDefProvider;
    exports.getInlineEditors = getInlineEditors;
    exports.closeInlineWidget = closeInlineWidget;
});


<!DOCTYPE html>

	<meta charset="utf-8">
	<title>jQuery UI Resizable - Synchronous resize</title>
	<link rel="stylesheet" href="../../themes/base/jquery.ui.all.css">

	<link rel="stylesheet" href="../demos.css">

Resize

will also resize

Resize multiple elements simultaneously by clicking and dragging the sides of one.  Pass a shared selector into the alsoResize option.


/*
 * resizable_core.js
 */

var el;

var drag = function(el, dx, dy, complete) {

	// speed = sync -> Drag syncrhonously.
	// speed = fast|slow -> Drag asyncrhonously - animated.

	//this mouseover is to work around a limitation in resizable
	//TODO: fix resizable so handle doesn't require mouseover in order to be used
	$(el).simulate("mouseover");

	return $(el).simulate("drag", {
		dx: dx||0, dy: dy||0, speed: 'sync', complete: complete
	});
};

(function($) {

module("resizable: core");

/*
test("element types", function() {
	var typeNames = ('p,h1,h2,h3,h4,h5,h6,blockquote,ol,ul,dl,div,form'
		+ ',table,fieldset,address,ins,del,em,strong,q,cite,dfn,abbr'
		+ ',acronym,code,samp,kbd,var,img,object,hr'
		+ ',input,button,label,select,iframe').split(',');

	$.each(typeNames, function(i) {
		var typeName = typeNames[i];
		el = $(document.createElement(typeName)).appendTo('body');
		(typeName == 'table' && el.append("
content
"));
		el.resizable();
		ok(true, '$("<' + typeName + '/&gt").resizable()');
		el.resizable("destroy");
		el.remove();
	});
});
*/

test("n", function() {
	expect(2);

	var handle = '.ui-resizable-n', target = $('#resizable1').resizable({ handles: 'all' });

	drag(handle, 0, -50);
	equal( target.height(), 150, "compare height" );

	drag(handle, 0, 50);
	equal( target.height(), 100, "compare height" );
});

test("s", function() {
	expect(2);

	var handle = '.ui-resizable-s', target = $('#resizable1').resizable({ handles: 'all' });

	drag(handle, 0, 50);
	equal( target.height(), 150, "compare height" );

	drag(handle, 0, -50);
	equal( target.height(), 100, "compare height" );
});

test("e", function() {
	expect(2);

	var handle = '.ui-resizable-e', target = $('#resizable1').resizable({ handles: 'all' });

	drag(handle, 50);
	equal( target.width(), 150, "compare width");

	drag(handle, -50);
	equal( target.width(), 100, "compare width" );
});

test("w", function() {
	expect(2);

	var handle = '.ui-resizable-w', target = $('#resizable1').resizable({ handles: 'all' });

	drag(handle, -50);
	equal( target.width(), 150, "compare width" );

	drag(handle, 50);
	equal( target.width(), 100, "compare width" );
});

test("ne", function() {
	expect(4);

	var handle = '.ui-resizable-ne', target = $('#resizable1').css({ overflow: 'hidden' }).resizable({ handles: 'all' });

	drag(handle, -50, -50);
	equal( target.width(), 50, "compare width" );
	equal( target.height(), 150, "compare height" );

	drag(handle, 50, 50);
	equal( target.width(), 100, "compare width" );
	equal( target.height(), 100, "compare height" );
});

test("se", function() {
	expect(4);

	var handle = '.ui-resizable-se', target = $('#resizable1').resizable({ handles: 'all' });

	drag(handle, 50, 50);
	equal( target.width(), 150, "compare width" );
	equal( target.height(), 150, "compare height" );

	drag(handle, -50, -50);
	equal( target.width(), 100, "compare width" );
	equal( target.height(), 100, "compare height" );
});

test("sw", function() {
	expect(4);

	var handle = '.ui-resizable-sw', target = $('#resizable1').resizable({ handles: 'all' });

	drag(handle, -50, -50);
	equal( target.width(), 150, "compare width" );
	equal( target.height(), 50, "compare height" );

	drag(handle, 50, 50);
	equal( target.width(), 100, "compare width" );
	equal( target.height(), 100, "compare height" );
});

test("nw", function() {
	expect(4);

	var handle = '.ui-resizable-nw', target = $('#resizable1').resizable({ handles: 'all' });

	drag(handle, -50, -50);
	equal( target.width(), 150, "compare width" );
	equal( target.height(), 150, "compare height" );

	drag(handle, 50, 50);
	equal( target.width(), 100, "compare width" );
	equal( target.height(), 100, "compare height" );
});

})(jQuery);


keep keep keep replace keep keep keep keep keep

/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, $, PathUtils, Mustache */

/**
 * PreferencesDialogs
 *
 */
define(function (require, exports, module) {
    "use strict";

    require("thirdparty/path-utils/path-utils.min");

    var Dialogs                = require("widgets/Dialogs"),
        PreferencesManager     = require("preferences/PreferencesManager"),
        ProjectManager         = require("project/ProjectManager"),
        StringUtils            = require("utils/StringUtils"),
        Strings                = require("strings"),
        SettingsDialogTemplate = require("text!htmlContent/project-settings-dialog.html");

    /**
     * Validate that text string is a valid base url which should map to a server folder
     * @param {string} url
     * @return {string} Empty string if valid, otherwise error string
     */
    function _validateBaseUrl(url) {
        var result = "";
        // Empty url means "no server mapping; use file directly"
        if (url === "") {
            return result;
        }

        var obj = PathUtils.parseUrl(url);
        if (!obj) {
            result = Strings.BASEURL_ERROR_UNKOWN_ERROR;
        } else if (obj.href.search(/^(http|https):\/\//i) !== 0) {
            result = StringUtils.format(Strings.BASEURL_ERROR_INVALID_PROTOCOL, obj.href.substring(0, obj.href.indexOf("//")));
        } else if (obj.search !== "") {
            result = StringUtils.format(Strings.BASEURL_ERROR_SEARCH_DISALLOWED, obj.search);
        } else if (obj.hash !== "") {
            result = StringUtils.format(Strings.BASEURL_ERROR_HASH_DISALLOWED, obj.hash);
        } else {
            var index = url.search(/[ \^\[\]\{\}<>\\"\?]+/);
            if (index !== -1) {
                result = StringUtils.format(Strings.BASEURL_ERROR_INVALID_CHAR, url[index]);
            }
        }

        return result;
    }

    /**
     * Show a dialog that shows the project preferences
     * @param {string} baseUrl Initial value
     * @param {string} errorMessage Error to display
     * @return {Dialog} A Dialog object with an internal promise that will be resolved with the ID
     *      of the clicked button when the dialog is dismissed. Never rejected.
     */
    function showProjectPreferencesDialog(baseUrl, errorMessage) {
        var $dlg,
            $title,
            $baseUrlControl,
            dialog;

        dialog = Dialogs.showModalDialogUsingTemplate(Mustache.render(SettingsDialogTemplate, Strings))
            .done(function (id) {
                if (id === Dialogs.DIALOG_BTN_OK) {
                    var baseUrlValue = $baseUrlControl.val();
                    var result = _validateBaseUrl(baseUrlValue);
                    if (result === "") {
                        ProjectManager.setBaseUrl(baseUrlValue);
                    } else {
                        // Re-invoke dialog with result (error message)
                        showProjectPreferencesDialog(baseUrlValue, result);
                    }
                }
            });

        // Populate project settings
        $dlg = $(".project-settings-dialog.instance");

        // Title
        $title = $dlg.find(".dialog-title");
        var projectName = "",
            projectRoot = ProjectManager.getProjectRoot(),
            title;
        if (projectRoot) {
            projectName = projectRoot.name;
        }
        title = StringUtils.format(Strings.PROJECT_SETTINGS_TITLE, projectName);
        $title.text(title);

        // Base URL
        $baseUrlControl = $dlg.find(".url");
        if (baseUrl) {
            $baseUrlControl.val(baseUrl);
        }

        // Error message
        if (errorMessage) {
            $dlg.find(".field-container").append("
" + errorMessage + "
");
        }

        // Give focus to first control
        $baseUrlControl.focus();

        return dialog;
    }

    // For unit testing
    exports._validateBaseUrl                = _validateBaseUrl;

    exports.showProjectPreferencesDialog    = showProjectPreferencesDialog;
});