/*
 * 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
 * codes are:
 *
 * -1: Error
 *  0: Inactive
 *  1: Connecting to the remote debugger
 *  2: Loading agents
 *  3: Active
 *  4: Out of sync
 */
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"),
        DocumentManager      = require("document/DocumentManager"),
        EditorManager        = require("editor/EditorManager"),
        FileUtils            = require("file/FileUtils"),
        HTMLInstrumentation  = require("language/HTMLInstrumentation"),
        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")
    };

    var launcherUrl = window.location.href.replace(/\/index.html.*/, "") + "/LiveDevelopment/launch.html";

    // 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

    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 (_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;
        }
    }

    /** Open a live document
     * @param {Document} source document to open
     * @return {jQuery.Promise} A promise that is resolved once the live
     *      document is open, and is never explicitly rejected.
     */
    function _openDocument(doc, editor) {

        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();
        }

        _closeDocument();
        _liveDocument = _createDocument(doc, editor);

        // 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 = [];
        var agentsToLoad;
        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) {
                promises.push(agents[name].load());
                _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) {
        exports.status = status;
        $(exports).triggerHandler("statusChange", status);
    }

    /** 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 editor = EditorManager.getCurrentFullEditor(),
            status = STATUS_ACTIVE;

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

    /** Triggered by Inspector.detached */
    function _onDetached(event, res) {
        // res.reason, e.g. "replaced_with_devtools", "target_closed", "canceled_by_user"
        // Sample list 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
    }

    // 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();
            _setStatus(STATUS_INACTIVE);
            _serverProvider = null;
        }
    }

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

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

    function reconnect() {
        unloadAgents();
        var promises = loadAgents();
        _setStatus(STATUS_LOADING_AGENTS);
        $.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;

        function showWrongDocError() {
            Dialogs.showModalDialog(
                Dialogs.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();
                    }
                })
                .fail(function () {
                    result.reject();
                });
        }

        function showLiveDevServerNotReadyError() {
            Dialogs.showModalDialog(
                Dialogs.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() {
            var targetUrl = doc.root.url;
            var interstitialUrl = launcherUrl + "?" + encodeURIComponent(targetUrl);

            _setStatus(STATUS_CONNECTING);

            if (_serverProvider) {
                // Install a request filter for the current document. In the future,
                // we need to install filters for *all* files that need to be instrumented.
                HTMLInstrumentation.scanDocument(doc);
                _serverProvider.setRequestFilterPaths(
                    ["/" + ProjectManager.makeProjectRelativeIfPossible(doc.file.fullPath)]
                );

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

                $(_serverProvider).on("request.livedev", function (event, request) {
                    var html = HTMLInstrumentation.generateInstrumentedHTML(doc);

                    request.send({ body: html });
                });
            }

            Inspector.connectToURL(interstitialUrl).done(result.resolve).fail(function onConnectFail(err) {
                if (err === "CANCEL") {
                    result.reject(err);
                    return;
                }
                if (retryCount > 6) {
                    _setStatus(STATUS_ERROR);
                    Dialogs.showModalDialog(
                        Dialogs.DIALOG_ID_LIVE_DEVELOPMENT,
                        Strings.LIVE_DEVELOPMENT_RELAUNCH_TITLE,
                        Strings.LIVE_DEVELOPMENT_ERROR_MESSAGE
                    ).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(
                        interstitialUrl,
                        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(
                                Dialogs.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(interstitialUrl).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() {
        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() {
            // Load agents
            _setStatus(STATUS_LOADING_AGENTS);
            var promises = loadAgents();
            $.when.apply(undefined, promises).done(_onLoad).fail(_onError);

            // 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();
            }
        }

        $(Inspector.Inspector).on("detached", _onDetached);
        $(Inspector.Page).on("frameNavigated.DOMAgent", _onFrameNavigated);

        waitForInterstitialPageLoad()
            .fail(function () {
                close();
                Dialogs.showModalDialog(
                    Dialogs.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)) {
                _closeDocument();
                var editor = EditorManager.getCurrentFullEditor();
                promise = _openDocument(doc, editor);
            } 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)) {
            // Reload HTML page
            Inspector.Page.reload();

            // Reload unsaved changes
            reconnect();
        }
    }

    /** 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 true;
    };

    /**
     * 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);
        $(DocumentManager).on("currentDocumentChange", _onDocumentChange)
            .on("documentSaved", _onDocumentSaved)
            .on("dirtyFlagChange", _onDirtyFlagChange);

        // 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;
});


projectRoot = ProjectManager.getProjectRoot() ? ProjectManager.getProjectRoot().fullPath : null;

entries.slice(0, MAX_FILES_IN_DIR).forEach(function (entry) {

var Buffers = require('buffers');
var bufs = Buffers();
bufs.push(new Buffer([1,2,3]));
bufs.push(new Buffer([4,5,6,7]));
bufs.push(new Buffer([8,9,10]));

console.dir(bufs.slice(2,8))

// Output: <Buffer 03 04 05 06 07 08>


/*
 * Copyright (c) 2013 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, brackets, $ */

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

    var CodeHintManager = brackets.getModule("editor/CodeHintManager"),
        EditorManager   = brackets.getModule("editor/EditorManager"),
        DocumentManager = brackets.getModule("document/DocumentManager"),
        Commands        = brackets.getModule("command/Commands"),
        CommandManager  = brackets.getModule("command/CommandManager"),
        Menus           = brackets.getModule("command/Menus"),
        Strings         = brackets.getModule("strings"),
        AppInit         = brackets.getModule("utils/AppInit"),
        ExtensionUtils  = brackets.getModule("utils/ExtensionUtils"),
        StringUtils     = brackets.getModule("utils/StringUtils"),
        HintUtils       = require("HintUtils"),
        ScopeManager    = require("ScopeManager"),
        Session         = require("Session");

    var KeyboardPrefs = JSON.parse(require("text!keyboard.json"));

    var JUMPTO_DEFINITION = "navigate.jumptoDefinition";

    var session     = null,  // object that encapsulates the current session state
        cachedHints = null,  // sorted hints for the current hinting session
        cachedType  = null,  // describes the lookup type and the object context
        cachedLine  = null;  // the line number for the cached scope

    /**
     * Creates a hint response object. Filters the hint list using the query
     * string, formats the hints for display, and returns a hint response
     * object according to the CodeHintManager's API for code hint providers.
     *
     * @param {Array.<Object>} hints - hints to be included in the response
     * @param {string} query - querystring with which to filter the hint list
     * @param {Object} type - the type of query, property vs. identifier
     * @return {Object} - hint response as defined by the CodeHintManager API
     */
    function getHintResponse(hints, query, type) {

        var trimmedQuery,
            formattedHints;

        /*
         * Returns a formatted list of hints with the query substring
         * highlighted.
         *
         * @param {Array.<Object>} hints - the list of hints to format
         * @param {string} query - querystring used for highlighting matched
         *      poritions of each hint
         * @return {Array.<jQuery.Object>} - array of hints formatted as jQuery
         *      objects
         */
        function formatHints(hints, query) {
            return hints.map(function (token) {
                var hint        = token.value,
                    index       = hint.toLowerCase().indexOf(query.toLowerCase()),
                    $hintObj    = $("<span>").addClass("brackets-js-hints");

                // level indicates either variable scope or property confidence
                if (!type.property && token.depth !== undefined) {
                    switch (token.depth) {
                    case 0:
                        $hintObj.addClass("priority-high");
                        break;
                    case 1:
                        $hintObj.addClass("priority-medium");
                        break;
                    case 2:
                        $hintObj.addClass("priority-low");
                        break;
                    default:
                        $hintObj.addClass("priority-lowest");
                        break;
                    }
                }

                if (token.guess) {
                    $hintObj.addClass("guess-hint");
                }

                // is the token a global variable?
                if (token.global) {
                    $hintObj.addClass("global-hint");
                }

                // is the token a keyword?
                if (token.keyword) {
                    $hintObj.addClass("keyword-hint");
                }

                // higlight the matched portion of each hint
                if (query.length > 0 && index >= 0) {
                    var prefix  = StringUtils.htmlEscape(hint.slice(0, index)),
                        match   = StringUtils.htmlEscape(hint.slice(index, index + query.length)),
                        suffix  = StringUtils.htmlEscape(hint.slice(index + query.length));

                    $hintObj.append(prefix)
                        .append($("<span>")
                                .append(match)
                                .addClass("matched-hint"))
                        .append(suffix);
                } else {
                    $hintObj.text(hint);
                }
                $hintObj.data("token", token);

                return $hintObj;
            });
        }

        // trim leading and trailing string literal delimiters from the query
        if (query.indexOf(HintUtils.SINGLE_QUOTE) === 0 ||
                query.indexOf(HintUtils.DOUBLE_QUOTE) === 0) {
            trimmedQuery = query.substring(1);
            if (trimmedQuery.lastIndexOf(HintUtils.DOUBLE_QUOTE) === trimmedQuery.length - 1 ||
                    trimmedQuery.lastIndexOf(HintUtils.SINGLE_QUOTE) === trimmedQuery.length - 1) {
                trimmedQuery = trimmedQuery.substring(0, trimmedQuery.length - 1);
            }
        } else {
            trimmedQuery = query;
        }

        if (hints) {
            formattedHints = formatHints(hints, trimmedQuery);
        } else {
            formattedHints = [];
        }

        return {
            hints: formattedHints,
            match: null, // the CodeHintManager should not format the results
            selectInitial: true
        };
    }

    /**
     * @constructor
     */
    function JSHints() {
    }

    /**
     * Determine whether hints are available for a given editor context
     *
     * @param {Editor} editor - the current editor context
     * @param {string} key - charCode of the last pressed key
     * @return {boolean} - can the provider provide hints for this session?
     */
    JSHints.prototype.hasHints = function (editor, key) {
        if (session && ((key === null) || HintUtils.maybeIdentifier(key))) {
            var cursor  = session.getCursor(),
                token   = session.getToken(cursor);

            // don't autocomplete within strings or comments, etc.
            if (token && HintUtils.hintable(token)) {
                var offset = session.getOffset(),
                    type    = session.getType(),
                    query   = session.getQuery();

                // Invalidate cached information if: 1) no scope exists; 2) the
                // cursor has moved a line; 3) the scope is dirty; or 4) if the
                // cursor has moved into a different scope. Cached information
                // is also reset on editor change.
                if (!cachedHints ||
                        cachedLine !== cursor.line ||
                        type.property !== cachedType.property ||
                        type.context !== cachedType.context ||
                        type.showFunctionType !== cachedType.showFunctionType) {
                    //console.log("clear hints");
                    cachedLine = null;
                    cachedHints = null;
                }
                return true;
            }
        }
        return false;
    };

    /**
      * Return a list of hints, possibly deferred, for the current editor
      * context
      *
      * @param {string} key - charCode of the last pressed key
      * @return {Object + jQuery.Deferred} - hint response (immediate or
      *     deferred) as defined by the CodeHintManager API
      */
    JSHints.prototype.getHints = function (key) {
        var cursor = session.getCursor(),
            token = session.getToken(cursor);
        if ((key === null) || HintUtils.hintable(token)) {
            if (token) {
                var type    = session.getType(),
                    query   = session.getQuery();

                // Compute fresh hints if none exist, or if the session
                // type has changed since the last hint computation
                if (!cachedHints ||
                        type.property !== cachedType.property ||
                        type.context !== cachedType.context ||
                        type.showFunctionType !== cachedType.showFunctionType ||                                               query.length === 0) {
                    var offset          = session.getOffset(),
                        scopeResponse   = ScopeManager.requestHints(session, session.editor.document, offset),
                        self            = this;

                    if (scopeResponse.hasOwnProperty("promise")) {
                        var $deferredHints = $.Deferred();
                        scopeResponse.promise.done(function () {
                            cachedLine = cursor.line;
                            cachedType = session.getType();
                            cachedHints = session.getHints(query);

                            $(self).triggerHandler("resolvedResponse", [cachedHints, cachedType]);

                            if ($deferredHints.state() === "pending") {
                                query = session.getQuery();
                                var hintResponse    = getHintResponse(cachedHints, query, type);

                                $deferredHints.resolveWith(null, [hintResponse]);
                                $(self).triggerHandler("hintResponse", [query]);
                            }
                        }).fail(function () {
                            if ($deferredHints.state() === "pending") {
                                $deferredHints.reject();
                            }
                        });

                        $(this).triggerHandler("deferredResponse");
                        return $deferredHints;
                    } else {
                        cachedLine = cursor.line;
                    }
                }

                if (cachedHints) {
                    cachedHints = session.getHints(session.getQuery());
                    return getHintResponse(cachedHints, query, type);
                }
            }
        }

        return null;
    };

    /**
     * Inserts the hint selected by the user into the current editor.
     *
     * @param {jQuery.Object} hint - hint object to insert into current editor
     * @return {boolean} - should a new hinting session be requested
     *      immediately after insertion?
     */
    JSHints.prototype.insertHint = function ($hintObj) {
        var hint        = $hintObj.data("token"),
            completion  = hint.value,
            cursor      = session.getCursor(),
            token       = session.getToken(cursor),
            query       = session.getQuery(),
            start       = {line: cursor.line, ch: cursor.ch - query.length},
            end         = {line: cursor.line, ch: (token ? token.end : cursor.ch)},
            delimeter;

        if (token && token.string === ".") {
            var nextCursor  = session.getNextCursorOnLine(),
                nextToken   = nextCursor ? session.getToken(nextCursor) : null;

            if (nextToken && // don't replace delimiters, etc.
                    HintUtils.maybeIdentifier(nextToken.string) &&
                    HintUtils.hintable(nextToken)) {
                end.ch = nextToken.end;
            }
        }

        // If the hint is a string literal, choose a delimiter in which
        // to wrap it, preserving the existing delimiter if possible.
        if (hint.literal && hint.kind === "string") {
            if (token.string.indexOf(HintUtils.DOUBLE_QUOTE) === 0) {
                delimeter = HintUtils.DOUBLE_QUOTE;
            } else if (token.string.indexOf(HintUtils.SINGLE_QUOTE) === 0) {
                delimeter = HintUtils.SINGLE_QUOTE;
            } else {
                delimeter = hint.delimeter;
            }

            completion = completion.replace("\\", "\\\\");
            completion = completion.replace(delimeter, "\\" + delimeter);
            completion = delimeter + completion + delimeter;
        }

        if (session.getType().showFunctionType) {
            // function types show up as hints, so don't insert anything
            // if we were displaying a function type
            return false;
        }
        // Replace the current token with the completion
        session.editor.document.replaceRange(completion, start, end);

        // Return false to indicate that another hinting session is not needed
        return false;
    };

    // load the extension
    AppInit.appReady(function () {

        /*
         * When the editor is changed, reset the hinting session and cached
         * information, and reject any pending deferred requests.
         *
         * @param {Editor} editor - editor context to be initialized.
         */
        function initializeSession(editor) {
            ScopeManager.handleEditorChange(editor.document);
            session = new Session(editor);
            cachedHints = null;
        }

        /*
         * Install editor change listeners
         *
         * @param {Editor} editor - editor context on which to listen for
         *      changes
         */
        function installEditorListeners(editor) {
            // always clean up cached scope and hint info
            cachedLine = null;
            cachedHints = null;
            cachedType = null;

            if (editor && editor.getLanguageForSelection().getId() === HintUtils.LANGUAGE_ID) {
                initializeSession(editor);
                $(editor)
                    .on(HintUtils.eventName("change"), function () {
                        ScopeManager.handleFileChange(editor.document);
                    });
            } else {
                session = null;
            }
        }

        /*
         * Uninstall editor change listeners
         *
         * @param {Editor} editor - editor context on which to stop listening
         *      for changes
         */
        function uninstallEditorListeners(editor) {
            $(editor)
                .off(HintUtils.eventName("change"));
        }

        /*
         * Handle the activeEditorChange event fired by EditorManager.
         * Uninstalls the change listener on the previous editor
         * and installs a change listener on the new editor.
         *
         * @param {Event} event - editor change event (ignored)
         * @param {Editor} current - the new current editor context
         * @param {Editor} previous - the previous editor context
         */
        function handleActiveEditorChange(event, current, previous) {
            uninstallEditorListeners(previous);
            installEditorListeners(current);
        }

        /*
         * Handle JumptoDefiniton menu/keyboard command.
         */
        function handleJumpToDefinition() {
            var offset     = session.getOffset(),
                response   = ScopeManager.requestJumptoDef(session, session.editor.document, offset);

            if (response.hasOwnProperty("promise")) {
                response.promise.done(function (jumpResp) {

                    if (jumpResp.resultFile && (jumpResp.resultFile !== jumpResp.file)) {

                        var doc = DocumentManager.getCurrentDocument();
                        var x = doc.file.fullPath.lastIndexOf("/");
                        var dir;
                        if (x >= 0) {
                            dir = doc.file.fullPath.slice(0, x + 1);
                        } else {
                            dir = "";
                        }
                        CommandManager.execute(Commands.FILE_OPEN, { fullPath: dir+jumpResp.resultFile })
                            .done(function() {
                                session.editor.setCursorPos(jumpResp.start);
                                session.editor.setSelection(jumpResp.start, jumpResp.end);
                                session.editor.centerOnCursor();
                            });
                        return;
                    }

                    session.editor.setCursorPos(jumpResp.start);
                    session.editor.setSelection(jumpResp.start, jumpResp.end);
                    session.editor.centerOnCursor();

                }).fail(function () {
                    response.reject();
                });
            }
        }

        // Register command handler
        CommandManager.register(Strings.CMD_JUMPTO_DEFINITION, JUMPTO_DEFINITION, handleJumpToDefinition);

        // Add the menu item
        var menu = Menus.getMenu(Menus.AppMenuBar.NAVIGATE_MENU);
        if (menu) {
            menu.addMenuItem(JUMPTO_DEFINITION, KeyboardPrefs.jumptoDefinition, Menus.BEFORE, Commands.NAVIGATE_GOTO_DEFINITION);
        }

        ExtensionUtils.loadStyleSheet(module, "styles/brackets-js-hints.css");

        // uninstall/install change listener as the active editor changes
        $(EditorManager)
            .on(HintUtils.eventName("activeEditorChange"),
                handleActiveEditorChange);

        // immediately install the current editor
        installEditorListeners(EditorManager.getActiveEditor());

        var jsHints = new JSHints();
        CodeHintManager.registerHintProvider(jsHints, [HintUtils.LANGUAGE_ID], 0);

        // for unit testing
        exports.jsHintProvider = jsHints;
        exports.initializeSession = initializeSession;
    });
});


<!doctype html>

	<meta charset="utf-8">
	<title>Compound Visual Test : Tabs in Tabs</title>
	<link rel="stylesheet" href="../visual.css">
	<link rel="stylesheet" href="../../../themes/base/jquery.ui.all.css">

First

Second

First

Second

nested tabs a-1

nested tabs a-1

nested tabs a-1

nested tabs a-1

nested tabs a-1

nested tabs a-2

nested tabs a-2

nested tabs a-2

nested tabs a-2

nested tabs a-2

First

Second

nested tabs b-1

nested tabs b-1

nested tabs b-1

nested tabs b-1

nested tabs b-1

nested tabs b-2

nested tabs b-2

nested tabs b-2

nested tabs b-2

nested tabs b-2


<!DOCTYPE html>

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

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

Hide

			Etiam libero neque, luctus a, eleifend nec, semper at, lorem. Sed pede. Nulla lorem metus, adipiscing ut, luctus sed, hendrerit vitae, mi.

<select name="effects" id="effectTypes">
	<option value="blind">Blind</option>
	<option value="bounce">Bounce</option>
	<option value="clip">Clip</option>
	<option value="drop">Drop</option>
	<option value="explode">Explode</option>
	<option value="fold">Fold</option>
	<option value="highlight">Highlight</option>
	<option value="puff">Puff</option>
	<option value="pulsate">Pulsate</option>
	<option value="scale">Scale</option>
	<option value="shake">Shake</option>
	<option value="size">Size</option>
	<option value="slide">Slide</option>
</select>

Run Effect

Click the button above to preview the effect.


html {
    background-color: #eeeeee;
}

body {
    margin: 0 auto;
    padding: 2em;
    max-width: 800px;
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 14px;
    line-height: 1.5em;
    color: #333333;
    background-color: #ffffff;
    -webkit-box-shadow: 0 0 12px rgba(0, 0, 0, 0.4);
    -moz-box-shadow: 0 0 12px rgba(0, 0, 0, 0.4);
    box-shadow: 0 0 12px rgba(0, 0, 0, 0.4);
}

h1, h2, h3, h4, h5, h6 {
    line-height: 1.3em;
}

samp
{
    /* hide <samp> from the browser so we can show cool features in Edge Code */
    display: none;
}

img
{
    background: dimgray;
    border: 1px solid black;
    border-radius: 2px;
    padding: 15px 10px 10px;
    margin: 10px 0;
    max-width: 95%;
}


response   = ScopeManager.requestJumptoDef(session, session.editor.document, offset);

if (jumpResp.resultFile && (jumpResp.resultFile !== jumpResp.file)) {

var doc = DocumentManager.getCurrentDocument();

var x = doc.file.fullPath.lastIndexOf("/");

dir = doc.file.fullPath.slice(0, x + 1);

keep keep keep keep keep replace replace replace replace replace replace replace keep

if (jumpResp.resultFile !== jumpResp.file) {

var resolvedPath = ScopeManager.getResolvedPath(jumpResp.resultFile);

/*
 * Copyright (c) 2013 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.
 *
 */

/*
 * Throughout this file, the term "outer scope" is used to refer to the outer-
 * most/global/root Scope objects for particular file. The term "inner scope"
 * is used to refer to a Scope object that is reachable via the child relation
 * from an outer scope.
 */

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

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

    var DocumentManager     = brackets.getModule("document/DocumentManager"),
        LanguageManager     = brackets.getModule("language/LanguageManager"),
        NativeFileSystem    = brackets.getModule("file/NativeFileSystem").NativeFileSystem,
        ProjectManager      = brackets.getModule("project/ProjectManager"),
        HintUtils           = require("HintUtils");

    var fileState           = {},       // directory -> file -> state
        ternEnvironment     = [],
        pendingTernRequests = {},
        rootTernDir         = null,
        projectRoot         = null,
        ternPromise         = null,
        _ternWorker          = (function () {
            var path = module.uri.substring(0, module.uri.lastIndexOf("/") + 1);
            return new Worker(path + "tern-worker.js");
        }());

    var MAX_TEXT_LENGTH     = 1000000, // about 1MB
        MAX_FILES_IN_DIR    = 100;

    /**
     * Create a new tern server.
     */
    function initTernServer(dir, files) {
        _ternWorker.postMessage({
            type        : HintUtils.TERN_INIT_MSG,
            dir         : dir,
            files       : files,
            env         : ternEnvironment
        });
        rootTernDir = dir + "/";
    }

    /**
     * Read in the json files that have type information for the builtins, dom,etc
     */
    function initTernEnv() {
        var path = module.uri.substring(0, module.uri.lastIndexOf("/") + 1) + "thirdparty/tern/defs/";
        var files = ["ecma5.json", "browser.json"];//, "plugin/requirejs/requirejs.json", "jquery.json"];

        files.forEach(function (i) {
            DocumentManager.getDocumentForPath(path + i).done(function (document) {
                ternEnvironment.push(JSON.parse(document.getText()));
            }).fail(function (error) {
                console.log("failed to read tern config file " + i);
            });
        });
    }

    initTernEnv();

    /**
     * Initialize state for a given directory and file name
     *
     * @param {string} dir - the directory name to initialize
     * @param {string} file - the file name to initialize
     */
    function initFileState(dir, file) {
        // initialize outerScope, etc. at dir
        if (!fileState.hasOwnProperty(dir)) {
            fileState[dir] = {};
        }

        if (file !== undefined) {
            if (!fileState[dir].hasOwnProperty(file)) {
                fileState[dir][file] = {
                    // has the file changed since the scope was updated?
                    dirtyFile       : true,

                    // has the scope changed since the last inner scope request?
                    dirtyScope      : true,

                    // is the parser worker active for this file?
                    active          : false
                };
            }
        }
    }

    /**
     * Get a Promise for the definition from TernJS, for the file & offset passed in.
     * @return {jQuery.Promise} - a promise that will resolve to definition when
     *      it is done
     */
    function getJumptoDef(dir, file, offset, text) {
        postMessage({
            type: HintUtils.TERN_JUMPTODEF_MSG,
            dir:dir,
            file:file,
            offset:offset,
            text:text
        });

        var $deferredJump = $.Deferred();
        pendingTernRequests[file] = $deferredJump;
        return $deferredJump.promise();
    }

    /**
     * Request Jump-To-Definition from Tern.
     *
     * @param {session} session - the session
     * @param {Document} document - the document
     * @param {number} offset - the offset into the document
     * @return {jQuery.Promise} - The promise will not complete until tern
     *      has completed.
     */
    function requestJumptoDef(session, document, offset) {
        var path    = document.file.fullPath,
            split   = HintUtils.splitPath(path),
            dir     = split.dir,
            file    = split.file;

        var ternPromise = getJumptoDef(dir, file, offset, document.getText());

        return {promise:ternPromise};
    }

    /**
     * Handle the response from the tern web worker when
     * it responds with the definition
     *
     * @param response - the response from the worker
     */
    function handleJumptoDef(response) {

        var file = response.file;
        var $deferredJump = pendingTernRequests[file];

        pendingTernRequests[file] = null;

        if( $deferredJump ) {
            $deferredJump.resolveWith(null, [response]);
        }
    }

    /**
     * Request hints from Tern.
     *
     * Note that successive calls to getScope may return the same objects, so
     * clients that wish to modify those objects (e.g., by annotating them based
     * on some temporary context) should copy them first. See, e.g.,
     * Session.getHints().
     *
     * @param {Document} document - the document for which scope info is
     *      desired
     * @param {number} offset - the offset into the document at which scope
     *      info is desired
     * @return {jQuery.Promise} - The promise will not complete until the tern
     *      hints have completed.
     */
    function requestHints(session, document, offset) {
        var path    = document.file.fullPath,
            split   = HintUtils.splitPath(path),
            dir     = split.dir,
            file    = split.file;

        var $deferredHints = $.Deferred(),
            hintPromise,
            fnTypePromise,
            propsPromise;

        hintPromise = getTernHints(dir, file, offset, document.getText());
        var sessionType = session.getType();
        if (sessionType.property) {
            propsPromise = getTernProperties(dir, file, offset, document.getText());
        } else {
            var $propsDeferred = $.Deferred();
            propsPromise = $propsDeferred.promise();
            $propsDeferred.resolveWith(null);
        }

        if( sessionType.showFunctionType ) {
            // Show function sig
            fnTypePromise = getTernFunctionType(dir, file, sessionType.functionCallPos, offset, document.getText());
        } else {
            var $fnTypeDeferred = $.Deferred();
            fnTypePromise = $fnTypeDeferred.promise();
            $fnTypeDeferred.resolveWith(null);
        }
        $.when(hintPromise, fnTypePromise, propsPromise).done(
            function(completions, fnType, properties){
                session.setTernHints(completions);
                session.setFnType(fnType);
                session.setTernProperties(properties);

                $deferredHints.resolveWith(null);
            });
        return {promise:$deferredHints.promise()};
    }

    /**
     * Send a message to the tern worker - if the worker is being initialized,
     * the message will not be posted until initialization is complete
     */
    function postMessage(msg) {
        ternPromise.done(function (ternWorker){
            ternWorker.postMessage(msg);
        });
    }

    /**
     * Get a Promise for the completions from TernJS, for the file & offset passed in.
     * @return {jQuery.Promise} - a promise that will resolve to an array of completions when
     *      it is done
     */
    function getTernHints(dir, file, offset, text) {
        postMessage({
            type: HintUtils.TERN_COMPLETIONS_MSG,
            dir:dir,
            file:file,
            offset:offset,
            text:text
        });

        return addPendingRequest(file, offset, HintUtils.TERN_COMPLETIONS_MSG);
    }

    /**
     * Get a Promise for all of the known properties from TernJS, for the directory and file.
     * The properties will be used as guesses in tern.
     *
     * @return {jQuery.Promise} - a promise that will resolve to an array of properties when
     *      it is done
     */
    function getTernProperties(dir, file, offset, text) {
        postMessage({
            type: HintUtils.TERN_GET_PROPERTIES_MSG,
            dir:dir,
            file:file,
            offset: offset,
            text:text
        });

        return addPendingRequest(file, offset, HintUtils.TERN_GET_PROPERTIES_MSG);
    }

    /**
     * Get a Promise for the function type from TernJS.
     * @return {jQuery.Promise} - a promise that will resolve to the function type of the function being called.
     */
    function getTernFunctionType(dir, file, pos, offset, text) {
        postMessage({
            type: HintUtils.TERN_CALLED_FUNC_TYPE_MSG,
            dir:dir,
            file:file,
            pos:pos,
            offset:offset,
            text:text
        });

        return addPendingRequest(file, offset, HintUtils.TERN_CALLED_FUNC_TYPE_MSG);
    }

    /**
     * Add a pending request waiting for the tern-worker to complete.
     *
     * @param {string} file - the name of the file
     * @param {string} type - the type of request
     * @param {jQuery.Deferred} deferredRequest - the $.Deferred object to save
     */
    function addPendingRequest(file, offset, type) {
        var requests,
            key = file+"@"+offset,
            $deferredRequest;
        if( Object.prototype.hasOwnProperty.call(pendingTernRequests, key) ) {
            requests = pendingTernRequests[key];
        } else {
            requests = {};
            pendingTernRequests[key] = requests;
        }

        if (Object.prototype.hasOwnProperty.call(requests, type)) {
            $deferredRequest = requests[type];
        } else {
            requests[type] = $deferredRequest = $.Deferred();
        }
        return $deferredRequest.promise();
    }

    /**
     * Get any pending $.Deferred object waiting on the specified file and request type
     * @param {string} file - the file
     * @param {string} type - the type of request
     * @param {jQuery.Deferred} - the $.Deferred for the request
     */
    function getPendingRequest(file, offset, type) {
        var key = file +"@"+offset;
        if( Object.prototype.hasOwnProperty.call(pendingTernRequests, key) ) {
            var requests = pendingTernRequests[key];
            return requests[type];
        }
    }

    /**
     * Handle the response from the tern web worker when
     * it responds with the list of completions
     *
     * @param {{dir:string, file:string, offset:number, completions:Array.<string>}} response - the response from the worker
     */
    function handleTernCompletions(response) {

        var file = response.file,
            offset = response.offset,
            completions = response.completions,
            properties = response.properties,
            fnType  = response.fnType,
            type = response.type,
            $deferredHints = getPendingRequest(file, offset, type);

        if( $deferredHints ) {
            if( completions ) {
                $deferredHints.resolveWith(null, [completions]);
            } else if ( properties ) {
                $deferredHints.resolveWith(null, [properties]);
            } else if ( fnType ) {
                $deferredHints.resolveWith(null, [fnType]);
            }
        }
    }

    /**
     * Handle a request from the worker for text of a file
     *
     * @param {{file:string}} request - the request from the worker.  Should be an Object containing the name
     *      of the file tern wants the contents of
     */
    function handleTernGetFile(request) {

        function replyWith(name, txt) {
            postMessage({
                type:HintUtils.TERN_GET_FILE_MSG,
                file:name,
                text:txt
            });
        }

        var name = request.file;
        DocumentManager.getDocumentForPath(rootTernDir + name).done(function(document){
            replyWith(name, document.getText());
        })
        .fail(function(){
            if (projectRoot) {
                // Try relative to project root
                DocumentManager.getDocumentForPath(projectRoot + name).done(function(document) {
                    replyWith(name, document.getText());
                })
                .fail(function(){
                    replyWith(name, "");
                });
            } else {
                // Need to send something back to tern - it will wait
                // until all the files have been retrieved before doing its calculations
                replyWith(name, "");
            }
        });
    }

    /**
     * Called each time a new editor becomes active. Refreshes the outer scopes
     * of the given file as well as of the other files in the given directory.
     *
     * @param {Document} document - the document of the editor that has changed
     */
    function handleEditorChange(document) {
        var path        = document.file.fullPath,
            split       = HintUtils.splitPath(path),
            dir         = split.dir,
            dirEntry    = new NativeFileSystem.DirectoryEntry(dir),
            reader      = dirEntry.createReader(),
            files       = [];

        var ternDeferred = $.Deferred();
        ternPromise = ternDeferred.promise();
        pendingTernRequests = [];
        projectRoot = ProjectManager.getProjectRoot() ? ProjectManager.getProjectRoot().fullPath : null;
        reader.readEntries(function (entries) {
            entries.slice(0, MAX_FILES_IN_DIR).forEach(function (entry) {
                if (entry.isFile) {
                    var path    = entry.fullPath,
                        split   = HintUtils.splitPath(path),
                        file    = split.file;

                    if (file.indexOf(".") > 1) { // ignore /.dotfiles
                        var languageID = LanguageManager.getLanguageForPath(entry.fullPath).getId();
                        if (languageID === HintUtils.LANGUAGE_ID) {
                            files.push(file);
                        }
                    }
                }
            });
            initTernServer(dir, files);
            ternDeferred.resolveWith(null, [_ternWorker]);
        }, function (err) {
            console.log("Unable to refresh directory: " + err);
        });

    }

    /*
     * Called each time the file associated with the active editor changes.
     * Marks the file as being dirty and refresh its outer scope.
     *
     * @param {Document} document - the document that has changed
     */
    function handleFileChange(document) {
    }

    _ternWorker.addEventListener("message", function (e) {
        var response = e.data,
            type = response.type;

        if( type === HintUtils.TERN_COMPLETIONS_MSG ||
            type === HintUtils.TERN_CALLED_FUNC_TYPE_MSG ||
            type === HintUtils.TERN_GET_PROPERTIES_MSG) {
            // handle any completions the worker calculated
            handleTernCompletions(response);
        } else if ( type === HintUtils.TERN_GET_FILE_MSG ) {
            // handle a request for the contents of a file
            handleTernGetFile(response);
        } else if ( type === HintUtils.TERN_JUMPTODEF_MSG ) {
            handleJumptoDef(response);
        } else {
            console.log("Worker: " + (response.log || response));
        }
    });

    // reset state on project change
    $(ProjectManager)
        .on(HintUtils.eventName("beforeProjectClose"),
            function (event, projectRoot) {
                fileState = {};
            });

    // relocate scope information on file rename
    $(DocumentManager)
        .on(HintUtils.eventName("fileNameChange"),
            function (event, oldName, newName) {
                var oldSplit    = HintUtils.splitPath(oldName),
                    oldDir      = oldSplit.dir,
                    oldFile     = oldSplit.file,
                    newSplit    = HintUtils.splitPath(newName),
                    newDir      = newSplit.dir,
                    newFile     = newSplit.file;

                if (fileState.hasOwnProperty(oldDir) &&
                        fileState[oldDir].hasOwnProperty(oldFile)) {
                    if (!fileState.hasOwnProperty(newDir)) {
                        fileState[newDir] = {};
                    }
                    fileState[newDir][newFile] = fileState[oldDir][oldFile];
                    delete fileState[oldDir][oldFile];
                }
            });

    exports.handleEditorChange = handleEditorChange;
    exports.handleFileChange = handleFileChange;
    exports.requestJumptoDef = requestJumptoDef;
    exports.requestHints = requestHints;
    exports.getTernHints = getTernHints;
});


/*
 * Copyright (c) 2013 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 */

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

    var AppInit                 = require("utils/AppInit"),
        Editor                  = require("editor/Editor").Editor,
        Commands                = require("command/Commands"),
        CommandManager          = require("command/CommandManager"),
        Strings                 = require("strings");

    /**
     * @private
     * Activates/Deactivates showing line numbers option
     */
    function _toggleLineNumbers() {
        Editor.setShowLineNumbers(!Editor.getShowLineNumbers());
        CommandManager.get(Commands.TOGGLE_LINE_NUMBERS).setChecked(Editor.getShowLineNumbers());
    }

    /**
     * @private
     * Activates/Deactivates showing active line option
     */
    function _toggleActiveLine() {
        Editor.setShowActiveLine(!Editor.getShowActiveLine());
        CommandManager.get(Commands.TOGGLE_ACTIVE_LINE).setChecked(Editor.getShowActiveLine());
    }

    /**
     * @private
     * Activates/Deactivates word wrap option
     */
    function _toggleWordWrap() {
        Editor.setWordWrap(!Editor.getWordWrap());
        CommandManager.get(Commands.TOGGLE_WORD_WRAP).setChecked(Editor.getWordWrap());
    }

    /**
     * @private
     * Activates/Deactivates the automatic close brackets option
     */
    function _toggleCloseBrackets() {
        Editor.setCloseBrackets(!Editor.getCloseBrackets());
        CommandManager.get(Commands.TOGGLE_CLOSE_BRACKETS).setChecked(Editor.getCloseBrackets());
    }

    function _init() {
        CommandManager.get(Commands.TOGGLE_LINE_NUMBERS).setChecked(Editor.getShowLineNumbers());
        CommandManager.get(Commands.TOGGLE_ACTIVE_LINE).setChecked(Editor.getShowActiveLine());
        CommandManager.get(Commands.TOGGLE_WORD_WRAP).setChecked(Editor.getWordWrap());
        CommandManager.get(Commands.TOGGLE_CLOSE_BRACKETS).setChecked(Editor.getCloseBrackets());
    }

    CommandManager.register(Strings.CMD_TOGGLE_LINE_NUMBERS, Commands.TOGGLE_LINE_NUMBERS, _toggleLineNumbers);
    CommandManager.register(Strings.CMD_TOGGLE_ACTIVE_LINE, Commands.TOGGLE_ACTIVE_LINE, _toggleActiveLine);
    CommandManager.register(Strings.CMD_TOGGLE_WORD_WRAP, Commands.TOGGLE_WORD_WRAP, _toggleWordWrap);
    CommandManager.register(Strings.CMD_TOGGLE_CLOSE_BRACKETS, Commands.TOGGLE_CLOSE_BRACKETS, _toggleCloseBrackets);

    AppInit.htmlReady(_init);
});


var binary = require('../');
var test = require('tap').test;

test('not enough parse', function (t) {
    t.plan(4);

    var vars = binary(new Buffer([1,2]))
        .word8('a')
        .word8('b')
        .word8('c')
        .word8('d')
        .vars
    ;

    t.same(vars.a, 1);
    t.same(vars.b, 2);
    t.strictEqual(vars.c, null);
    t.strictEqual(vars.d, null);
});