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

/*global jQuery */

// TODO: (issue #264) break out the definition of brackets into a separate module from the application controller logic

/**
 * brackets is the root of the Brackets codebase. This file pulls in all other modules as
 * dependencies (or dependencies thereof), initializes the UI, and binds global menus & keyboard
 * shortcuts to their Commands.
 *
 * Unlike other modules, this one can be accessed without an explicit require() because it exposes
 * a global object, window.brackets.
 */
define(function (require, exports, module) {
    "use strict";

    // Load dependent non-module scripts
    require("widgets/bootstrap-dropdown");
    require("widgets/bootstrap-modal");
    require("widgets/bootstrap-twipsy-mod");

    // Load CodeMirror add-ons--these attach themselves to the CodeMirror module
    require("thirdparty/CodeMirror/addon/edit/closebrackets");
    require("thirdparty/CodeMirror/addon/edit/closetag");
    require("thirdparty/CodeMirror/addon/edit/matchbrackets");
    require("thirdparty/CodeMirror/addon/edit/matchtags");
    require("thirdparty/CodeMirror/addon/fold/xml-fold");
    require("thirdparty/CodeMirror/addon/mode/multiplex");
    require("thirdparty/CodeMirror/addon/mode/overlay");
    require("thirdparty/CodeMirror/addon/mode/simple");
    require("thirdparty/CodeMirror/addon/scroll/scrollpastend");
    require("thirdparty/CodeMirror/addon/search/match-highlighter");
    require("thirdparty/CodeMirror/addon/search/searchcursor");
    require("thirdparty/CodeMirror/addon/selection/active-line");
    require("thirdparty/CodeMirror/addon/selection/mark-selection");
    require("thirdparty/CodeMirror/keymap/sublime");

    // Load dependent modules
    var AppInit             = require("utils/AppInit"),
        LanguageManager     = require("language/LanguageManager"),
        ProjectManager      = require("project/ProjectManager"),
        FileViewController  = require("project/FileViewController"),
        FileSyncManager     = require("project/FileSyncManager"),
        Commands            = require("command/Commands"),
        CommandManager      = require("command/CommandManager"),
        PerfUtils           = require("utils/PerfUtils"),
        FileSystem          = require("filesystem/FileSystem"),
        Strings             = require("strings"),
        Dialogs             = require("widgets/Dialogs"),
        DefaultDialogs      = require("widgets/DefaultDialogs"),
        ExtensionLoader     = require("utils/ExtensionLoader"),
        Async               = require("utils/Async"),
        UpdateNotification  = require("utils/UpdateNotification"),
        UrlParams           = require("utils/UrlParams").UrlParams,
        PreferencesManager  = require("preferences/PreferencesManager"),
        DragAndDrop         = require("utils/DragAndDrop"),
        NativeApp           = require("utils/NativeApp"),
        DeprecationWarning  = require("utils/DeprecationWarning"),
        ViewCommandHandlers = require("view/ViewCommandHandlers"),
        MainViewManager     = require("view/MainViewManager");

    var MainViewHTML        = require("text!htmlContent/main-view.html");

    // load modules for later use
    require("utils/Global");
    require("editor/CSSInlineEditor");
    require("project/WorkingSetSort");
    require("search/QuickOpen");
    require("search/QuickOpenHelper");
    require("file/FileUtils");
    require("project/SidebarView");
    require("utils/Resizer");
    require("LiveDevelopment/main");
    require("utils/NodeConnection");
    require("utils/NodeDomain");
    require("utils/ColorUtils");
    require("view/ThemeManager");
    require("thirdparty/lodash");
    require("language/XMLUtils");
    require("language/JSONUtils");
    require("widgets/InlineMenu");

    // DEPRECATED: In future we want to remove the global CodeMirror, but for now we
    // expose our required CodeMirror globally so as to avoid breaking extensions in the
    // interim.
    var CodeMirror = require("thirdparty/CodeMirror/lib/codemirror");

    Object.defineProperty(window, "CodeMirror", {
        get: function () {
            DeprecationWarning.deprecationWarning('Use brackets.getModule("thirdparty/CodeMirror/lib/codemirror") instead of global CodeMirror.', true);
            return CodeMirror;
        }
    });

    // DEPRECATED: In future we want to remove the global Mustache, but for now we
    // expose our required Mustache globally so as to avoid breaking extensions in the
    // interim.
    var Mustache = require("thirdparty/mustache/mustache");

    Object.defineProperty(window, "Mustache", {
        get: function () {
            DeprecationWarning.deprecationWarning('Use brackets.getModule("thirdparty/mustache/mustache") instead of global Mustache.', true);
            return Mustache;
        }
    });

    // DEPRECATED: In future we want to remove the global PathUtils, but for now we
    // expose our required PathUtils globally so as to avoid breaking extensions in the
    // interim.
    var PathUtils = require("thirdparty/path-utils/path-utils");

    Object.defineProperty(window, "PathUtils", {
        get: function () {
            DeprecationWarning.deprecationWarning('Use brackets.getModule("thirdparty/path-utils/path-utils") instead of global PathUtils.', true);
            return PathUtils;
        }
    });
    
    //load language features
    require("features/ParameterHintsManager");
    require("features/JumpToDefManager");

    // Load modules that self-register and just need to get included in the main project
    require("command/DefaultMenus");
    require("document/ChangedDocumentTracker");
    require("editor/EditorCommandHandlers");
    require("editor/EditorOptionHandlers");
    require("editor/EditorStatusBar");
    require("editor/ImageViewer");
    require("extensibility/InstallExtensionDialog");
    require("extensibility/ExtensionManagerDialog");
    require("help/HelpCommandHandlers");
    require("search/FindInFilesUI");
    require("search/FindReplace");

    //Load find References Feature Manager
    require("features/FindReferencesManager");

    //Load common JS module
    require("JSUtils/Session");
    require("JSUtils/ScopeManager");

    //load Language Tools Module
    require("languageTools/PathConverters");
    require("languageTools/LanguageTools");
    require("languageTools/ClientLoader");
    require("languageTools/BracketsToNodeInterface");
    require("languageTools/DefaultProviders");
    require("languageTools/DefaultEventHandlers");


    PerfUtils.addMeasurement("brackets module dependencies resolved");

    // Local variables
    var params = new UrlParams();

    // read URL params
    params.parse();


    /**
     * Setup test object
     */
    function _initTest() {
        // TODO: (issue #265) Make sure the "test" object is not included in final builds
        // All modules that need to be tested from the context of the application
        // must to be added to this object. The unit tests cannot just pull
        // in the modules since they would run in context of the unit test window,
        // and would not have access to the app html/css.
        brackets.test = {
            CodeHintManager         : require("editor/CodeHintManager"),
            CodeInspection          : require("language/CodeInspection"),
            CommandManager          : require("command/CommandManager"),
            Commands                : require("command/Commands"),
            CSSUtils                : require("language/CSSUtils"),
            DefaultDialogs          : require("widgets/DefaultDialogs"),
            Dialogs                 : require("widgets/Dialogs"),
            DocumentCommandHandlers : require("document/DocumentCommandHandlers"),
            DocumentManager         : require("document/DocumentManager"),
            DocumentModule          : require("document/Document"),
            DOMAgent                : require("LiveDevelopment/Agents/DOMAgent"),
            DragAndDrop             : require("utils/DragAndDrop"),
            EditorManager           : require("editor/EditorManager"),
            ExtensionLoader         : require("utils/ExtensionLoader"),
            ExtensionUtils          : require("utils/ExtensionUtils"),
            File                    : require("filesystem/File"),
            FileFilters             : require("search/FileFilters"),
            FileSyncManager         : require("project/FileSyncManager"),
            FileSystem              : require("filesystem/FileSystem"),
            FileUtils               : require("file/FileUtils"),
            FileViewController      : require("project/FileViewController"),
            FindInFiles             : require("search/FindInFiles"),
            FindInFilesUI           : require("search/FindInFilesUI"),
            HTMLInstrumentation     : require("language/HTMLInstrumentation"),
            Inspector               : require("LiveDevelopment/Inspector/Inspector"),
            InstallExtensionDialog  : require("extensibility/InstallExtensionDialog"),
            JSUtils                 : require("language/JSUtils"),
            KeyBindingManager       : require("command/KeyBindingManager"),
            LanguageManager         : require("language/LanguageManager"),
            LiveDevelopment         : require("LiveDevelopment/LiveDevelopment"),
            LiveDevMultiBrowser     : require("LiveDevelopment/LiveDevMultiBrowser"),
            LiveDevServerManager    : require("LiveDevelopment/LiveDevServerManager"),
            MainViewFactory         : require("view/MainViewFactory"),
            MainViewManager         : require("view/MainViewManager"),
            Menus                   : require("command/Menus"),
            MultiRangeInlineEditor  : require("editor/MultiRangeInlineEditor").MultiRangeInlineEditor,
            NativeApp               : require("utils/NativeApp"),
            PerfUtils               : require("utils/PerfUtils"),
            PreferencesManager      : require("preferences/PreferencesManager"),
            ProjectManager          : require("project/ProjectManager"),
            RemoteAgent             : require("LiveDevelopment/Agents/RemoteAgent"),
            ScrollTrackMarkers      : require("search/ScrollTrackMarkers"),
            UpdateNotification      : require("utils/UpdateNotification"),
            WorkingSetView          : require("project/WorkingSetView"),
            doneLoading             : false
        };

        AppInit.appReady(function () {
            brackets.test.doneLoading = true;
        });
    }

    /**
     * Setup Brackets
     */
    function _onReady() {
        PerfUtils.addMeasurement("window.document Ready");

        // Let the user know Brackets doesn't run in a web browser yet
        if (brackets.inBrowser) {
            Dialogs.showModalDialog(
                DefaultDialogs.DIALOG_ID_ERROR,
                Strings.ERROR_IN_BROWSER_TITLE,
                Strings.ERROR_IN_BROWSER
            );
        }

        brackets.app.getRemoteDebuggingPort(function (err, remote_debugging_port){
            var InfoBar = require('widgets/infobar'),
                StringUtils = require("utils/StringUtils");
            if ((!err) && remote_debugging_port && remote_debugging_port > 0) {
                InfoBar.showInfoBar({
                    type: "warning",
                    title: `${Strings.REMOTE_DEBUGGING_ENABLED}${remote_debugging_port}`,
                    description: ""
                });
            } else if (err) {
                InfoBar.showInfoBar({
                    type: "error",
                    title: StringUtils.format(Strings.REMOTE_DEBUGGING_PORT_INVALID, err, 1024, 65534),
                    description: ""
                });
            }
        });
        // Use quiet scrollbars if we aren't on Lion. If we're on Lion, only
        // use native scroll bars when the mouse is not plugged in or when
        // using the "Always" scroll bar setting.
        var osxMatch = /Mac OS X 10\D([\d+])\D/.exec(window.navigator.userAgent);
        if (osxMatch && osxMatch[1] && Number(osxMatch[1]) >= 7) {
            // test a scrolling div for scrollbars
            var $testDiv = $("<div style='position:fixed;left:-50px;width:50px;height:50px;overflow:auto;'><div style='width:100px;height:100px;'/></div>").appendTo(window.document.body);

            if ($testDiv.outerWidth() === $testDiv.get(0).clientWidth) {
                $(".sidebar").removeClass("quiet-scrollbars");
            }

            $testDiv.remove();
        }

        // Load default languages and preferences
        Async.waitForAll([LanguageManager.ready, PreferencesManager.ready]).always(function () {
            // Load all extensions. This promise will complete even if one or more
            // extensions fail to load.
            var extensionPathOverride = params.get("extensions");  // used by unit tests
            var extensionLoaderPromise = ExtensionLoader.init(extensionPathOverride ? extensionPathOverride.split(",") : null);

            // Load the initial project after extensions have loaded
            extensionLoaderPromise.always(function () {
               // Signal that extensions are loaded
                AppInit._dispatchReady(AppInit.EXTENSIONS_LOADED);

                // Finish UI initialization
                ViewCommandHandlers.restoreFontSize();
                var initialProjectPath = ProjectManager.getInitialProjectPath();
                ProjectManager.openProject(initialProjectPath).always(function () {
                    _initTest();

                    // If this is the first launch, and we have an index.html file in the project folder (which should be
                    // the samples folder on first launch), open it automatically. (We explicitly check for the
                    // samples folder in case this is the first time we're launching Brackets after upgrading from
                    // an old version that might not have set the "afterFirstLaunch" pref.)
                    var deferred = new $.Deferred();

                    if (!params.get("skipSampleProjectLoad") && !PreferencesManager.getViewState("afterFirstLaunch")) {
                        PreferencesManager.setViewState("afterFirstLaunch", "true");
                        if (ProjectManager.isWelcomeProjectPath(initialProjectPath)) {
                            FileSystem.resolve(initialProjectPath + "index.html", function (err, file) {
                                if (!err) {
                                    var promise = CommandManager.execute(Commands.CMD_ADD_TO_WORKINGSET_AND_OPEN, { fullPath: file.fullPath });
                                    promise.then(deferred.resolve, deferred.reject);
                                } else {
                                    deferred.reject();
                                }
                            });
                        } else {
                            deferred.resolve();
                        }
                    } else {
                        deferred.resolve();
                    }

                    deferred.always(function () {
                        // Signal that Brackets is loaded
                        AppInit._dispatchReady(AppInit.APP_READY);

                        PerfUtils.addMeasurement("Application Startup");

                        if (PreferencesManager._isUserScopeCorrupt()) {
                            var userPrefFullPath = PreferencesManager.getUserPrefFile();
                            // user scope can get corrupt only if the file exists, is readable,
                            // but malformed. no need to check for its existance.
                            var info = MainViewManager.findInAllWorkingSets(userPrefFullPath);
                            var paneId;
                            if (info.length) {
                                paneId = info[0].paneId;
                            }
                            FileViewController.openFileAndAddToWorkingSet(userPrefFullPath, paneId)
                                .done(function () {
                                    Dialogs.showModalDialog(
                                        DefaultDialogs.DIALOG_ID_ERROR,
                                        Strings.ERROR_PREFS_CORRUPT_TITLE,
                                        Strings.ERROR_PREFS_CORRUPT
                                    ).done(function () {
                                        // give the focus back to the editor with the pref file
                                        MainViewManager.focusActivePane();
                                    });
                                });
                        }

                    });

                    // See if any startup files were passed to the application
                    if (brackets.app.getPendingFilesToOpen) {
                        brackets.app.getPendingFilesToOpen(function (err, paths) {
                            DragAndDrop.openDroppedFiles(paths);
                        });
                    }
                });
            });
        });

        // Check for updates
        if (!brackets.inBrowser && !params.get("skipUpdateCheck")) {
            AppInit.appReady(function () {
                // launches periodic checks for updates cca every 24 hours
                UpdateNotification.launchAutomaticUpdate();
            });
        }
    }

    /**
     * Setup event handlers prior to dispatching AppInit.HTML_READY
     */
    function _beforeHTMLReady() {
        // Add the platform (mac, win or linux) to the body tag so we can have platform-specific CSS rules
        $("body").addClass("platform-" + brackets.platform);

        // Browser-hosted version may also have different CSS (e.g. since '#titlebar' is shown)
        if (brackets.inBrowser) {
            $("body").addClass("in-browser");
        } else {
            $("body").addClass("in-appshell");
        }

        // Enable/Disable HTML Menus
        if (brackets.nativeMenus) {
            $("body").addClass("has-appshell-menus");
        } else {
            // (issue #5310) workaround for bootstrap dropdown: prevent the menu item to grab
            // the focus -- override jquery focus implementation for top-level menu items
            (function () {
                var defaultFocus = $.fn.focus;
                $.fn.focus = function () {
                    if (!this.hasClass("dropdown-toggle")) {
                        return defaultFocus.apply(this, arguments);
                    }
                };
            }());
        }

        // Localize MainViewHTML and inject into <BODY> tag
        $("body").html(Mustache.render(MainViewHTML, { shouldAddAA: (brackets.platform === "mac"), Strings: Strings }));

        // Update title
        $("title").text(brackets.config.app_title);

        // Respond to dragging & dropping files/folders onto the window by opening them. If we don't respond
        // to these events, the file would load in place of the Brackets UI
        DragAndDrop.attachHandlers();

        // TODO: (issue 269) to support IE, need to listen to document instead (and even then it may not work when focus is in an input field?)
        $(window).focus(function () {
            // This call to syncOpenDocuments() *should* be a no-op now that we have
            // file watchers, but is still here as a safety net.
            FileSyncManager.syncOpenDocuments();
        });

        // Prevent unhandled middle button clicks from triggering native behavior
        // Example: activating AutoScroll (see #510)
        $("html").on("mousedown", ".inline-widget", function (e) {
            if (e.button === 1) {
                e.preventDefault();
            }
        });

        // The .no-focus style is added to clickable elements that should
        // not steal focus. Calling preventDefault() on mousedown prevents
        // focus from going to the click target.
        $("html").on("mousedown", ".no-focus", function (e) {
            // Text fields should always be focusable.
            var $target = $(e.target),
                isFormElement =
                    $target.is("input") ||
                    $target.is("textarea") ||
                    $target.is("select");

            if (!isFormElement) {
                e.preventDefault();
            }
        });

        // Prevent clicks on any link from navigating to a different page (which could lose unsaved
        // changes). We can't use a simple .on("click", "a") because of http://bugs.jquery.com/ticket/3861:
        // jQuery hides non-left clicks from such event handlers, yet middle-clicks still cause CEF to
        // navigate. Also, a capture handler is more reliable than bubble.
        window.document.body.addEventListener("click", function (e) {
            // Check parents too, in case link has inline formatting tags
            var node = e.target, url;
            while (node) {
                if (node.tagName === "A") {
                    url = node.getAttribute("href");
                    if (url && !url.match(/^#/)) {
                        NativeApp.openURLInDefaultBrowser(url);
                    }
                    e.preventDefault();
                    break;
                }
                node = node.parentElement;
            }
        }, true);

        // Prevent extensions from using window.open() to insecurely load untrusted web content
        var real_windowOpen = window.open;
        window.open = function (url) {
            // Allow file:// URLs, relative URLs (implicitly file: also), and about:blank
            if (!url.match(/^file:\/\//) && !url.match(/^about:blank/) && url.indexOf(":") !== -1) {
                throw new Error("Brackets-shell is not a secure general purpose web browser. Use NativeApp.openURLInDefaultBrowser() to open URLs in the user's main browser");
            }
            return real_windowOpen.apply(window, arguments);
        };

        // jQuery patch to shim deprecated usage of $() on EventDispatchers
        var DefaultCtor = jQuery.fn.init;
        jQuery.fn.init = function (firstArg, secondArg) {
            var jQObject = new DefaultCtor(firstArg, secondArg);

            // Is this a Brackets EventDispatcher object? (not a DOM node or other object)
            if (firstArg && firstArg._EventDispatcher) {
                // Patch the jQ wrapper object so it calls EventDispatcher's APIs instead of jQuery's
                jQObject.on  = firstArg.on.bind(firstArg);
                jQObject.one = firstArg.one.bind(firstArg);
                jQObject.off = firstArg.off.bind(firstArg);
                // Don't offer legacy support for trigger()/triggerHandler() on core model objects; extensions
                // shouldn't be doing that anyway since it's basically poking at private API

                // Console warning, since $() is deprecated for EventDispatcher objects
                // (pass true to only print once per caller, and index 4 since the extension caller is deeper in the stack than usual)
                DeprecationWarning.deprecationWarning("Deprecated: Do not use $().on/off() on Brackets modules and model objects. Call on()/off() directly on the object without a $() wrapper.", true, 4);
            }
            return jQObject;
        };
    }

    // Wait for view state to load.
    var viewStateTimer = PerfUtils.markStart("User viewstate loading");
    PreferencesManager._smUserScopeLoading.always(function () {
        PerfUtils.addMeasurement(viewStateTimer);
        // Dispatch htmlReady event
        _beforeHTMLReady();
        AppInit._dispatchReady(AppInit.HTML_READY);
        $(window.document).ready(_onReady);
    });
});
