// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

Cu.import("resource://gre/modules/PageThumbs.jsm");

/**
 * Constants
 */

// BrowserUI.update(state) constants. Currently passed in
// but update doesn't pay attention to them. Can we remove?
const TOOLBARSTATE_LOADING  = 1;
const TOOLBARSTATE_LOADED   = 2;

// delay for ContextUI's dismissWithDelay
const kHideContextAndTrayDelayMsec = 3000;

// delay when showing the tab bar briefly as a new tab opens
const kNewTabAnimationDelayMsec = 500;


// Page for which the start UI is shown
const kStartOverlayURI = "about:start";

/**
 * Cache of commonly used elements.
 */

let Elements = {};
[
  ["contentShowing",     "bcast_contentShowing"],
  ["urlbarState",        "bcast_urlbarState"],
  ["windowState",        "bcast_windowState"],
  ["mainKeyset",         "mainKeyset"],
  ["stack",              "stack"],
  ["tabList",            "tabs"],
  ["tabs",               "tabs-container"],
  ["controls",           "browser-controls"],
  ["panelUI",            "panel-container"],
  ["startUI",            "start-container"],
  ["tray",               "tray"],
  ["toolbar",            "toolbar"],
  ["browsers",           "browsers"],
  ["navbar",             "navbar"],
  ["contextappbar",      "contextappbar"],
  ["contentViewport",    "content-viewport"],
  ["progress",           "progress-control"],
  ["progressContainer",  "progress-container"],
  ["contentNavigator",   "content-navigator"],
  ["aboutFlyout",        "about-flyoutpanel"],
  ["prefsFlyout",        "prefs-flyoutpanel"],
  ["syncFlyout",         "sync-flyoutpanel"]
].forEach(function (aElementGlobal) {
  let [name, id] = aElementGlobal;
  XPCOMUtils.defineLazyGetter(Elements, name, function() {
    return document.getElementById(id);
  });
});

/**
 * Cache of commonly used string bundles.
 */

var Strings = {};
[
  ["browser",    "chrome://browser/locale/browser.properties"],
  ["brand",      "chrome://branding/locale/brand.properties"]
].forEach(function (aStringBundle) {
  let [name, bundle] = aStringBundle;
  XPCOMUtils.defineLazyGetter(Strings, name, function() {
    return Services.strings.createBundle(bundle);
  });
});

var BrowserUI = {
  get _edit() { return document.getElementById("urlbar-edit"); },
  get _back() { return document.getElementById("cmd_back"); },
  get _forward() { return document.getElementById("cmd_forward"); },

  lastKnownGoodURL: "", //used when the user wants to escape unfinished url entry
  init: function() {
    // listen content messages
    messageManager.addMessageListener("DOMTitleChanged", this);
    messageManager.addMessageListener("DOMWillOpenModalDialog", this);
    messageManager.addMessageListener("DOMWindowClose", this);

    messageManager.addMessageListener("Browser:OpenURI", this);
    messageManager.addMessageListener("Browser:SaveAs:Return", this);
    messageManager.addMessageListener("Content:StateChange", this);

    // listening escape to dismiss dialog on VK_ESCAPE
    window.addEventListener("keypress", this, true);

    window.addEventListener("MozPrecisePointer", this, true);
    window.addEventListener("MozImprecisePointer", this, true);

    Services.prefs.addObserver("browser.cache.disk_cache_ssl", this, false);
    Services.prefs.addObserver("browser.urlbar.formatting.enabled", this, false);
    Services.prefs.addObserver("browser.urlbar.trimURLs", this, false);
    Services.obs.addObserver(this, "metro_viewstate_changed", false);
    
    this._edit.inputField.controllers.insertControllerAt(0, this._copyCutURIController);

    // Init core UI modules
    ContextUI.init();
    StartUI.init();
    PanelUI.init();
    FlyoutPanelsUI.init();
    PageThumbs.init();
    SettingsCharm.init();
    NavButtonSlider.init();

    // show the right toolbars, awesomescreen, etc for the os viewstate
    BrowserUI._adjustDOMforViewState();

    // We can delay some initialization until after startup.  We wait until
    // the first page is shown, then dispatch a UIReadyDelayed event.
    messageManager.addMessageListener("pageshow", function() {
      if (getBrowser().currentURI.spec == "about:blank")
        return;

      messageManager.removeMessageListener("pageshow", arguments.callee, true);

      setTimeout(function() {
        let event = document.createEvent("Events");
        event.initEvent("UIReadyDelayed", true, false);
        window.dispatchEvent(event);
      }, 0);
    });

    // Only load IndexedDB.js when we actually need it. A general fix will happen in bug 647079.
    messageManager.addMessageListener("IndexedDB:Prompt", function(aMessage) {
      return IndexedDB.receiveMessage(aMessage);
    });

    // Delay the panel UI and Sync initialization
    window.addEventListener("UIReadyDelayed", function(aEvent) {
      Util.dumpLn("* delay load started...");
      window.removeEventListener(aEvent.type, arguments.callee, false);

      // Login Manager and Form History initialization
      Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
      Cc["@mozilla.org/satchel/form-history;1"].getService(Ci.nsIFormHistory2);

      messageManager.addMessageListener("Browser:MozApplicationManifest", OfflineApps);

      try {
        Downloads.init();
        DialogUI.init();
        FormHelperUI.init();
        FindHelperUI.init();
        PdfJs.init();
#ifdef MOZ_SERVICES_SYNC
        Sync.init();
#endif
      } catch(ex) {
        Util.dumpLn("Exception in delay load module:", ex.message);
      }

      BrowserUI._pullDesktopControlledPrefs();

      // check for left over crash reports and submit them if found.
      if (BrowserUI.startupCrashCheck()) {
        Browser.selectedTab = BrowserUI.newOrSelectTab("about:crash");
      }
      Util.dumpLn("* delay load complete.");
    }, false);

#ifndef MOZ_OFFICIAL_BRANDING
    setTimeout(function() {
      let startup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup).getStartupInfo();
      for (let name in startup) {
        if (name != "process")
          Services.console.logStringMessage("[timing] " + name + ": " + (startup[name] - startup.process) + "ms");
      }
    }, 3000);
#endif
  },

  uninit: function() {
    messageManager.removeMessageListener("Browser:MozApplicationManifest", OfflineApps);

    PanelUI.uninit();
    StartUI.uninit();
    Downloads.uninit();
    SettingsCharm.uninit();
    messageManager.removeMessageListener("Content:StateChange", this);
    PageThumbs.uninit();
  },


  /*********************************
   * Content visibility
   */

  get isContentShowing() {
    return Elements.contentShowing.getAttribute("disabled") != true;
  },

  showContent: function showContent(aURI) {
    DialogUI.closeAllDialogs();
    StartUI.update(aURI);
    ContextUI.dismissTabs();
    ContextUI.dismissAppbar();
    FlyoutPanelsUI.hide();
    PanelUI.hide();
  },

  /*********************************
   * Crash reporting
   */

  get CrashSubmit() {
    delete this.CrashSubmit;
    Cu.import("resource://gre/modules/CrashSubmit.jsm", this);
    return this.CrashSubmit;
  },

  startupCrashCheck: function startupCrashCheck() {
#ifdef MOZ_CRASHREPORTER
    if (!Services.prefs.getBoolPref("app.reportCrashes"))
      return false;
    if (CrashReporter.enabled) {
      var lastCrashID = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo).lastRunCrashID;
      if (lastCrashID && lastCrashID.length) {
        Util.dumpLn("Submitting last crash id:", lastCrashID);
        try {
          this.CrashSubmit.submit(lastCrashID);
        } catch (ex) {
          Util.dumpLn(ex);
        }
        return true;
      }
    }
#endif
    return false;
  },


  /*********************************
   * Navigation
   */

  update: function(aState) {
    this._updateToolbar();
  },

  getDisplayURI: function(browser) {
    let uri = browser.currentURI;
    let spec = uri.spec;

    try {
      spec = gURIFixup.createExposableURI(uri).spec;
    } catch (ex) {}

    try {
      let charset = browser.characterSet;
      let textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"].
                         getService(Ci.nsITextToSubURI);
      spec = textToSubURI.unEscapeNonAsciiURI(charset, spec);
    } catch (ex) {}

    return spec;
  },

  /**
    * Some prefs that have consequences in both Metro and Desktop such as
    * app-update prefs, are automatically pulled from Desktop here.
    */
  _pullDesktopControlledPrefs: function() {
    function pullDesktopControlledPrefType(prefType, prefFunc) {
      try {
        registry.create(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
                      "Software\\Mozilla\\Firefox\\Metro\\Prefs\\" + prefType,
                      Ci.nsIWindowsRegKey.ACCESS_ALL);
        for (let i = 0; i < registry.valueCount; i++) {
          let prefName = registry.getValueName(i);
          let prefValue = registry.readStringValue(prefName);
          if (prefType == Ci.nsIPrefBranch.PREF_BOOL) {
            prefValue = prefValue == "true";
          }
          Services.prefs[prefFunc](prefName, prefValue);
        }
      } catch (ex) {
        Util.dumpLn("Could not pull for prefType " + prefType + ": " + ex);
      } finally {
        registry.close();
      }
    }
    let registry = Cc["@mozilla.org/windows-registry-key;1"].
                   createInstance(Ci.nsIWindowsRegKey);
    pullDesktopControlledPrefType(Ci.nsIPrefBranch.PREF_INT, "setIntPref");
    pullDesktopControlledPrefType(Ci.nsIPrefBranch.PREF_BOOL, "setBoolPref");
    pullDesktopControlledPrefType(Ci.nsIPrefBranch.PREF_STRING, "setCharPref");
  },

  /* Set the location to the current content */
  updateURI: function(aOptions) {
    aOptions = aOptions || {};

    let uri = this.getDisplayURI(Browser.selectedBrowser);
    let cleanURI = Util.isURLEmpty(uri) ? "" : uri;
    this._setURI(cleanURI);

    if ("captionOnly" in aOptions && aOptions.captionOnly)
      return;

    StartUI.update(uri);
    this._updateButtons();
    this._updateToolbar();
  },

  goToURI: function(aURI) {
    aURI = aURI || this._edit.value;
    if (!aURI)
      return;

    // Make sure we're online before attempting to load
    Util.forceOnline();

    BrowserUI.showContent(aURI);
    content.focus();
    this._setURI(aURI);

    Task.spawn(function() {
      let postData = {};
      aURI = yield Browser.getShortcutOrURI(aURI, postData);
      Browser.loadURI(aURI, { flags: Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP, postData: postData });

      // Delay doing the fixup so the raw URI is passed to loadURIWithFlags
      // and the proper third-party fixup can be done
      let fixupFlags = Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
      let uri = gURIFixup.createFixupURI(aURI, fixupFlags);
      gHistSvc.markPageAsTyped(uri);

      BrowserUI._titleChanged(Browser.selectedBrowser);
    });
  },

  handleUrlbarEnter: function handleUrlbarEnter(aEvent) {
    let url = this._edit.value;
    if (aEvent instanceof KeyEvent)
      url = this._canonizeURL(url, aEvent);
    this.goToURI(url);
  },

  _canonizeURL: function _canonizeURL(aUrl, aTriggeringEvent) {
    if (!aUrl)
      return "";

    // Only add the suffix when the URL bar value isn't already "URL-like",
    // and only if we get a keyboard event, to match user expectations.
    if (/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(aUrl)) {
      let accel = aTriggeringEvent.ctrlKey;
      let shift = aTriggeringEvent.shiftKey;
      let suffix = "";

      switch (true) {
        case (accel && shift):
          suffix = ".org/";
          break;
        case (shift):
          suffix = ".net/";
          break;
        case (accel):
          try {
            suffix = gPrefService.getCharPref("browser.fixup.alternate.suffix");
            if (suffix.charAt(suffix.length - 1) != "/")
              suffix += "/";
          } catch(e) {
            suffix = ".com/";
          }
          break;
      }

      if (suffix) {
        // trim leading/trailing spaces (bug 233205)
        aUrl = aUrl.trim();

        // Tack www. and suffix on.  If user has appended directories, insert
        // suffix before them (bug 279035).  Be careful not to get two slashes.
        let firstSlash = aUrl.indexOf("/");
        if (firstSlash >= 0) {
          aUrl = aUrl.substring(0, firstSlash) + suffix + aUrl.substring(firstSlash + 1);
        } else {
          aUrl = aUrl + suffix;
        }
        aUrl = "http://www." + aUrl;
      }
    }
    return aUrl;
  },

  doOpenSearch: function doOpenSearch(aName) {
    // save the current value of the urlbar
    let searchValue = this._edit.value;

    // Make sure we're online before attempting to load
    Util.forceOnline();
    BrowserUI.showContent();

    let engine = Services.search.getEngineByName(aName);
    let submission = engine.getSubmission(searchValue, null);
    Browser.loadURI(submission.uri.spec, { postData: submission.postData });

    // loadURI may open a new tab, so get the selectedBrowser afterward.
    Browser.selectedBrowser.userTypedValue = submission.uri.spec;
    this._titleChanged(Browser.selectedBrowser);
  },

  /*********************************
   * Tab management
   */

  newTab: function newTab(aURI, aOwner) {
    aURI = aURI || kStartOverlayURI;
    let tab = Browser.addTab(aURI, true, aOwner);
    ContextUI.peekTabs();
    return tab;
  },

  newOrSelectTab: function newOrSelectTab(aURI, aOwner) {
    let tabs = Browser.tabs;
    for (let i = 0; i < tabs.length; i++) {
      if (tabs[i].browser.currentURI.spec == aURI) {
        Browser.selectedTab = tabs[i];
        return;
      }
    }
    this.newTab(aURI, aOwner);
  },

  setOnTabAnimationEnd: function setOnTabAnimationEnd(aCallback) {
    Elements.tabs.addEventListener("animationend", function onAnimationEnd() {
      Elements.tabs.removeEventListener("animationend", onAnimationEnd);
      aCallback();
    });
  },

  closeTab: function closeTab(aTab) {
    // If no tab is passed in, assume the current tab
    let tab = aTab || Browser.selectedTab;
    Browser.closeTab(tab);
  },

  animateClosingTab: function animateClosingTab(tabToClose) {
    tabToClose.chromeTab.setAttribute("closing", "true");

    let wasCollapsed = !ContextUI.isExpanded;
    if (wasCollapsed) {
      ContextUI.displayTabs();
    }

    this.setOnTabAnimationEnd(function() {
	    Browser.closeTab(tabToClose, { forceClose: true } );
        if (wasCollapsed)
          ContextUI.dismissWithDelay(kNewTabAnimationDelayMsec);
    });
  },

  /**
    * Re-open a closed tab.
    * @param aIndex
    *        The index of the tab (via nsSessionStore.getClosedTabData)
    * @returns a reference to the reopened tab.
    */
  undoCloseTab: function undoCloseTab(aIndex) {
    var tab = null;
    aIndex = aIndex || 0;
    var ss = Cc["@mozilla.org/browser/sessionstore;1"].
                getService(Ci.nsISessionStore);
    if (ss.getClosedTabCount(window) > (aIndex)) {
      tab = ss.undoCloseTab(window, aIndex);
    }
    return tab;
  },

  // Useful for when we've received an event to close a particular DOM window.
  // Since we don't have windows, we want to close the corresponding tab.
  closeTabForBrowser: function closeTabForBrowser(aBrowser) {
    // Find the relevant tab, and close it.
    let browsers = Browser.browsers;
    for (let i = 0; i < browsers.length; i++) {
      if (browsers[i] == aBrowser) {
        Browser.closeTab(Browser.getTabAtIndex(i));
        return { preventDefault: true };
      }
    }

    return {};
  },

  selectTab: function selectTab(aTab) {
    Browser.selectedTab = aTab;
  },

  selectTabAndDismiss: function selectTabAndDismiss(aTab) {
    this.selectTab(aTab);
    ContextUI.dismiss();
  },

  selectTabAtIndex: function selectTabAtIndex(aIndex) {
    // count backwards for aIndex < 0
    if (aIndex < 0)
      aIndex += Browser._tabs.length;

    if (aIndex >= 0 && aIndex < Browser._tabs.length)
      Browser.selectedTab = Browser._tabs[aIndex];
  },

  selectNextTab: function selectNextTab() {
    if (Browser._tabs.length == 1 || !Browser.selectedTab) {
     return;
    }

    let tabIndex = Browser._tabs.indexOf(Browser.selectedTab) + 1;
    if (tabIndex >= Browser._tabs.length) {
      tabIndex = 0;
    }

    Browser.selectedTab = Browser._tabs[tabIndex];
  },

  selectPreviousTab: function selectPreviousTab() {
    if (Browser._tabs.length == 1 || !Browser.selectedTab) {
      return;
    }

    let tabIndex = Browser._tabs.indexOf(Browser.selectedTab) - 1;
    if (tabIndex < 0) {
      tabIndex = Browser._tabs.length - 1;
    }

    Browser.selectedTab = Browser._tabs[tabIndex];
  },

  // Used for when we're about to open a modal dialog,
  // and want to ensure the opening tab is in front.
  selectTabForBrowser: function selectTabForBrowser(aBrowser) {
    for (let i = 0; i < Browser.tabs.length; i++) {
      if (Browser._tabs[i].browser == aBrowser) {
        Browser.selectedTab = Browser.tabs[i];
        break;
      }
    }
  },

  updateUIFocus: function _updateUIFocus() {
    if (Elements.contentShowing.getAttribute("disabled") == "true" && Browser.selectedBrowser)
      Browser.selectedBrowser.messageManager.sendAsyncMessage("Browser:Blur", { });
  },

  blurFocusedElement: function blurFocusedElement() {
    let focusedElement = document.commandDispatcher.focusedElement;
    if (focusedElement)
      focusedElement.blur();
  },

  blurNavBar: function blurNavBar() {
    if (this._edit.focused) {
      this._edit.blur();
      return true;
    }
    return false;
  },

  // If the user types in the address bar, cancel pending
  // navbar autohide if set.
  navEditKeyPress: function navEditKeyPress() {
    ContextUI.cancelDismiss();
  },

  observe: function BrowserUI_observe(aSubject, aTopic, aData) {
    switch (aTopic) {
      case "nsPref:changed":
        switch (aData) {
          case "browser.cache.disk_cache_ssl":
            this._sslDiskCacheEnabled = Services.prefs.getBoolPref(aData);
            break;
          case "browser.urlbar.formatting.enabled":
            this._formattingEnabled = Services.prefs.getBoolPref(aData);
            break;
          case "browser.urlbar.trimURLs":
            this._mayTrimURLs = Services.prefs.getBoolPref(aData);
            break;
        }
        break;
      case "metro_viewstate_changed":
        this._adjustDOMforViewState();
        let autocomplete = document.getElementById("start-autocomplete");
        if (aData == "snapped") {
          FlyoutPanelsUI.hide();
          // Order matters (need grids to get dimensions, etc), now
          // let snapped grid know to refresh/redraw
          Services.obs.notifyObservers(null, "metro_viewstate_dom_snapped", null);
          autocomplete.setAttribute("orient", "vertical");
        }
        else {
          autocomplete.setAttribute("orient", "horizontal");
        }
        break;
    }
  },

  /*********************************
   * Internal utils
   */

  _adjustDOMforViewState: function() {
    if (MetroUtils.immersive) {
      let currViewState = "";
      switch (MetroUtils.snappedState) {
        case Ci.nsIWinMetroUtils.fullScreenLandscape:
          currViewState = "landscape";
          break;
        case Ci.nsIWinMetroUtils.fullScreenPortrait:
          currViewState = "portrait";
          break;
        case Ci.nsIWinMetroUtils.filled:
          currViewState = "filled";
          break;
        case Ci.nsIWinMetroUtils.snapped:
          currViewState = "snapped";
          break;
      }
      Elements.windowState.setAttribute("viewstate", currViewState);
    }
  },

  _titleChanged: function(aBrowser) {
    let url = this.getDisplayURI(aBrowser);

    let tabCaption;
    if (aBrowser.contentTitle) {
      tabCaption = aBrowser.contentTitle;
    } else if (!Util.isURLEmpty(aBrowser.userTypedValue)) {
      tabCaption = aBrowser.userTypedValue;
    } else if (!Util.isURLEmpty(url)) {
      tabCaption = url;
    } else {
      tabCaption = Util.getEmptyURLTabTitle();
    }

    let tab = Browser.getTabForBrowser(aBrowser);
    if (tab)
      tab.chromeTab.updateTitle(tabCaption);
  },

  _updateButtons: function _updateButtons() {
    let browser = Browser.selectedBrowser;
    if (!browser) {
      return;
    }
    if (browser.canGoBack) {
      this._back.removeAttribute("disabled");
    } else {
      this._back.setAttribute("disabled", true);
    }
    if (browser.canGoForward) {
      this._forward.removeAttribute("disabled");
    } else {
      this._forward.setAttribute("disabled", true);
    }
  },

  _updateToolbar: function _updateToolbar() {
    let mode = Elements.urlbarState.getAttribute("mode");
    let isLoading = Browser.selectedTab.isLoading();

    if (isLoading && mode != "loading")
      Elements.urlbarState.setAttribute("mode", "loading");
    else if (!isLoading && mode != "edit")
      Elements.urlbarState.setAttribute("mode", "view");
  },

  _trimURL: function _trimURL(aURL) {
    // This function must not modify the given URL such that calling
    // nsIURIFixup::createFixupURI with the result will produce a different URI.
    return aURL /* remove single trailing slash for http/https/ftp URLs */
               .replace(/^((?:http|https|ftp):\/\/[^/]+)\/$/, "$1")
                /* remove http:// unless the host starts with "ftp\d*\." or contains "@" */
               .replace(/^http:\/\/((?!ftp\d*\.)[^\/@]+(?:\/|$))/, "$1");
  },

  trimURL: function trimURL(aURL) {
    return this.mayTrimURLs ? this._trimURL(aURL) : aURL;
  },

  _setURI: function _setURI(aURL) {
    this._edit.value = aURL;
    this.lastKnownGoodURL = aURL;
  },

  _getSelectedURIForClipboard: function _getSelectedURIForClipboard() {
    // Grab the actual input field's value, not our value, which could include moz-action:
    let inputVal = this._edit.inputField.value;
    let selectedVal = inputVal.substring(this._edit.selectionStart, this._edit.electionEnd);

    // If the selection doesn't start at the beginning or doesn't span the full domain or
    // the URL bar is modified, nothing else to do here.
    if (this._edit.selectionStart > 0 || this._edit.valueIsTyped)
      return selectedVal;
    // The selection doesn't span the full domain if it doesn't contain a slash and is
    // followed by some character other than a slash.
    if (!selectedVal.contains("/")) {
      let remainder = inputVal.replace(selectedVal, "");
      if (remainder != "" && remainder[0] != "/")
        return selectedVal;
    }

    let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup);

    let uri;
    try {
      uri = uriFixup.createFixupURI(inputVal, Ci.nsIURIFixup.FIXUP_FLAG_USE_UTF8);
    } catch (e) {}
    if (!uri)
      return selectedVal;

    // Only copy exposable URIs
    try {
      uri = uriFixup.createExposableURI(uri);
    } catch (ex) {}

    // If the entire URL is selected, just use the actual loaded URI.
    if (inputVal == selectedVal) {
      // ... but only if  isn't a javascript: or data: URI, since those
      // are hard to read when encoded
      if (!uri.schemeIs("javascript") && !uri.schemeIs("data")) {
        // Parentheses are known to confuse third-party applications (bug 458565).
        selectedVal = uri.spec.replace(/[()]/g, function (c) escape(c));
      }

      return selectedVal;
    }

    // Just the beginning of the URL is selected, check for a trimmed value
    let spec = uri.spec;
    let trimmedSpec = this.trimURL(spec);
    if (spec != trimmedSpec) {
      // Prepend the portion that trimURL removed from the beginning.
      // This assumes trimURL will only truncate the URL at
      // the beginning or end (or both).
      let trimmedSegments = spec.split(trimmedSpec);
      selectedVal = trimmedSegments[0] + selectedVal;
    }

    return selectedVal;
  },

  _copyCutURIController: {
    doCommand: function(aCommand) {
      let urlbar = BrowserUI._edit;
      let val = BrowserUI._getSelectedURIForClipboard();
      if (!val)
        return;

      if (aCommand == "cmd_cut" && this.isCommandEnabled(aCommand)) {
        let start = urlbar.selectionStart;
        let end = urlbar.selectionEnd;
        urlbar.inputField.value = urlbar.inputField.value.substring(0, start) +
                                  urlbar.inputField.value.substring(end);
        urlbar.selectionStart = urlbar.selectionEnd = start;
      }

      Cc["@mozilla.org/widget/clipboardhelper;1"]
        .getService(Ci.nsIClipboardHelper)
        .copyString(val, document);
    },

    supportsCommand: function(aCommand) {
      switch (aCommand) {
        case "cmd_copy":
        case "cmd_cut":
          return true;
      }
      return false;
    },

    isCommandEnabled: function(aCommand) {
      let urlbar = BrowserUI._edit;
      return this.supportsCommand(aCommand) &&
             (aCommand != "cmd_cut" || !urlbar.readOnly) &&
             urlbar.selectionStart < urlbar.selectionEnd;
    },

    onEvent: function(aEventName) {}
  },

  _editURI: function _editURI(aShouldDismiss) {
    this._edit.focus();
    this._edit.select();

    Elements.urlbarState.setAttribute("mode", "edit");
    StartUI.show();
    if (aShouldDismiss) {
      ContextUI.dismissTabs();
    }
  },

  formatURI: function formatURI() {
    if (!this.formattingEnabled ||
        Elements.urlbarState.getAttribute("mode") == "edit")
      return;

    let controller = this._edit.editor.selectionController;
    let selection = controller.getSelection(controller.SELECTION_URLSECONDARY);
    selection.removeAllRanges();

    let textNode = this._edit.editor.rootElement.firstChild;
    let value = textNode.textContent;

    let protocol = value.match(/^[a-z\d.+\-]+:(?=[^\d])/);
    if (protocol &&
        ["http:", "https:", "ftp:"].indexOf(protocol[0]) == -1)
      return;
    let matchedURL = value.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/);
    if (!matchedURL)
      return;

    let [, preDomain, domain] = matchedURL;
    let baseDomain = domain;
    let subDomain = "";
    // getBaseDomainFromHost doesn't recognize IPv6 literals in brackets as IPs (bug 667159)
    if (domain[0] != "[") {
      try {
        baseDomain = Services.eTLD.getBaseDomainFromHost(domain);
        if (!domain.endsWith(baseDomain)) {
          // getBaseDomainFromHost converts its resultant to ACE.
          let IDNService = Cc["@mozilla.org/network/idn-service;1"]
                           .getService(Ci.nsIIDNService);
          baseDomain = IDNService.convertACEtoUTF8(baseDomain);
        }
      } catch (e) {}
    }
    if (baseDomain != domain) {
      subDomain = domain.slice(0, -baseDomain.length);
    }

    let rangeLength = preDomain.length + subDomain.length;
    if (rangeLength) {
      let range = document.createRange();
      range.setStart(textNode, 0);
      range.setEnd(textNode, rangeLength);
      selection.addRange(range);
    }

    let startRest = preDomain.length + domain.length;
    if (startRest < value.length) {
      let range = document.createRange();
      range.setStart(textNode, startRest);
      range.setEnd(textNode, value.length);
      selection.addRange(range);
    }
  },

  _clearURIFormatting: function _clearURIFormatting() {
    if (!this.formattingEnabled)
      return;

    let controller = this._edit.editor.selectionController;
    let selection = controller.getSelection(controller.SELECTION_URLSECONDARY);
    selection.removeAllRanges();
  },

  _urlbarBlurred: function _urlbarBlurred() {
    let state = Elements.urlbarState;
    if (state.getAttribute("mode") == "edit")
      state.removeAttribute("mode");
    this._updateToolbar();
    this.formatURI();
  },

  _closeOrQuit: function _closeOrQuit() {
    // Close active dialog, if we have one. If not then close the application.
    if (!BrowserUI.isContentShowing()) {
      BrowserUI.showContent();
    } else {
      // Check to see if we should really close the window
      if (Browser.closing()) {
        window.close();
        let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
        appStartup.quit(Ci.nsIAppStartup.eForceQuit);
      }
    }
  },

  _onPreciseInput: function _onPreciseInput() {
    document.getElementById("bcast_preciseInput").setAttribute("input", "precise");
    let uri = Util.makeURI("chrome://browser/content/cursor.css");
    if (StyleSheetSvc.sheetRegistered(uri, Ci.nsIStyleSheetService.AGENT_SHEET)) {
      StyleSheetSvc.unregisterSheet(uri,
                                    Ci.nsIStyleSheetService.AGENT_SHEET);
    }
  },

  _onImpreciseInput: function _onImpreciseInput() {
    document.getElementById("bcast_preciseInput").setAttribute("input", "imprecise");
    let uri = Util.makeURI("chrome://browser/content/cursor.css");
    if (!StyleSheetSvc.sheetRegistered(uri, Ci.nsIStyleSheetService.AGENT_SHEET)) {
      StyleSheetSvc.loadAndRegisterSheet(uri,
                                         Ci.nsIStyleSheetService.AGENT_SHEET);
    }
  },

  /*********************************
   * Event handling
   */

  handleEvent: function handleEvent(aEvent) {
    var target = aEvent.target;
    switch (aEvent.type) {
      // Window events
      case "keypress":
        if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE)
          this.handleEscape(aEvent);
        break;
      case "MozPrecisePointer":
        this._onPreciseInput();
        break;
      case "MozImprecisePointer":
        this._onImpreciseInput();
        break;
    }
  },

  // Checks if various different parts of the UI is visible and closes
  // them one at a time.
  handleEscape: function (aEvent) {
    aEvent.stopPropagation();
    aEvent.preventDefault();

    if (this._edit.popupOpen) {
      this._edit.value = this.lastKnownGoodURL;
      this._edit.closePopup();
      StartUI.hide();
      ContextUI.dismiss();
      return;
    }

    // Check open popups
    if (DialogUI._popup) {
      DialogUI._hidePopup();
      return;
    }

    // Check open dialogs
    let dialog = DialogUI.activeDialog;
    if (dialog) {
      dialog.close();
      return;
    }

    // Check open modal elements
    if (DialogUI.modals.length > 0) {
      return;
    }

    // Check open panel
    if (PanelUI.isVisible) {
      PanelUI.hide();
      return;
    }

    // Check content helper
    let contentHelper = Elements.contentNavigator;
    if (contentHelper.isActive) {
      contentHelper.model.hide();
      return;
    }

    if (StartUI.hide()) {
      // When escaping from the start screen, hide the toolbar too.
      ContextUI.dismiss();
      return;
    }

    if (ContextUI.dismiss()) {
      return;
    }

    if (Browser.selectedTab.isLoading()) {
      Browser.selectedBrowser.stop();
      return;
    }
  },

  handleBackspace: function handleBackspace() {
    switch (Services.prefs.getIntPref("browser.backspace_action")) {
      case 0:
        CommandUpdater.doCommand("cmd_back");
        break;
      case 1:
        CommandUpdater.doCommand("cmd_scrollPageUp");
        break;
    }
  },

  handleShiftBackspace: function handleShiftBackspace() {
    switch (Services.prefs.getIntPref("browser.backspace_action")) {
      case 0:
        CommandUpdater.doCommand("cmd_forward");
        break;
      case 1:
        CommandUpdater.doCommand("cmd_scrollPageDown");
        break;
    }
  },

  openFile: function() {
    try {
      const nsIFilePicker = Ci.nsIFilePicker;
      let fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
      let self = this;
      let fpCallback = function fpCallback_done(aResult) {
        if (aResult == nsIFilePicker.returnOK) {
          self.goToURI(fp.fileURL.spec);
        }
      };

      let windowTitle = Strings.browser.GetStringFromName("browserForOpenLocation");
      fp.init(window, windowTitle, nsIFilePicker.modeOpen);
      fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText |
                       nsIFilePicker.filterImages | nsIFilePicker.filterXML |
                       nsIFilePicker.filterHTML);
      fp.open(fpCallback);
    } catch (ex) {
      dump ('BrowserUI openFile exception: ' + ex + '\n');
    }
  },

  savePage: function() {
    Browser.savePage();
  },

  receiveMessage: function receiveMessage(aMessage) {
    let browser = aMessage.target;
    let json = aMessage.json;
    switch (aMessage.name) {
      case "DOMTitleChanged":
        this._titleChanged(browser);
        break;
      case "DOMWillOpenModalDialog":
        this.selectTabForBrowser(browser);
        break;
      case "DOMWindowClose":
        return this.closeTabForBrowser(browser);
        break;
      // XXX this and content's sender are a little warped
      case "Browser:OpenURI":
        let referrerURI = null;
        if (json.referrer)
          referrerURI = Services.io.newURI(json.referrer, null, null);
        //Browser.addTab(json.uri, json.bringFront, Browser.selectedTab, { referrerURI: referrerURI });
        this.goToURI(json.uri);
        break;
      case "Content:StateChange":
        let currBrowser = Browser.selectedBrowser;
        if (this.shouldCaptureThumbnails(currBrowser)) {
          PageThumbs.captureAndStore(currBrowser);
          let currPage = currBrowser.currentURI.spec;
          Services.obs.notifyObservers(null, "Metro:RefreshTopsiteThumbnail", currPage);
        }
        break;
    }

    return {};
  },

  // Private Browsing is not supported on metro at this time, when it is added
  //  this function must be updated to skip capturing those pages
  shouldCaptureThumbnails: function shouldCaptureThumbnails(aBrowser) {
    // Capture only if it's the currently selected tab.
    if (aBrowser != Browser.selectedBrowser) {
      return false;
    }
    // FIXME Bug 720575 - Don't capture thumbnails for SVG or XML documents as
    //       that currently regresses Talos SVG tests.
    let doc = aBrowser.contentDocument;
    if (doc instanceof SVGDocument || doc instanceof XMLDocument) {
      return false;
    }

    // Don't capture pages in snapped mode, this produces 2/3 black
    // thumbs or stretched out ones
    //   Ci.nsIWinMetroUtils.snapped is inaccessible on
    //   desktop/nonwindows systems
    if(Elements.windowState.getAttribute("viewstate") == "snapped") {
      return false;
    }
    // There's no point in taking screenshot of loading pages.
    if (aBrowser.docShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE) {
      return false;
    }

    // Don't take screenshots of about: pages.
    if (aBrowser.currentURI.schemeIs("about")) {
      return false;
    }

    // No valid document channel. We shouldn't take a screenshot.
    let channel = aBrowser.docShell.currentDocumentChannel;
    if (!channel) {
      return false;
    }

    // Don't take screenshots of internally redirecting about: pages.
    // This includes error pages.
    let uri = channel.originalURI;
    if (uri.schemeIs("about")) {
      return false;
    }

    // http checks
    let httpChannel;
    try {
      httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
    } catch (e) { /* Not an HTTP channel. */ }

    if (httpChannel) {
      // Continue only if we have a 2xx status code.
      try {
        if (Math.floor(httpChannel.responseStatus / 100) != 2) {
          return false;
        }
      } catch (e) {
        // Can't get response information from the httpChannel
        // because mResponseHead is not available.
        return false;
      }

      // Cache-Control: no-store.
      if (httpChannel.isNoStoreResponse()) {
        return false;
      }

      // Don't capture HTTPS pages unless the user enabled it.
      if (uri.schemeIs("https") && !this.sslDiskCacheEnabled) {
        return false;
      }
    }

    return true;
  },

  _sslDiskCacheEnabled: null,

  get sslDiskCacheEnabled() {
    if (this._sslDiskCacheEnabled === null) {
      this._sslDiskCacheEnabled = Services.prefs.getBoolPref("browser.cache.disk_cache_ssl");
    }
    return this._sslDiskCacheEnabled;
  },

  _formattingEnabled: null,

  get formattingEnabled() {
    if (this._formattingEnabled === null) {
      this._formattingEnabled = Services.prefs.getBoolPref("browser.urlbar.formatting.enabled");
    }
    return this._formattingEnabled;
  },

  _mayTrimURLs: null,

  get mayTrimURLs() {
    if (this._mayTrimURLs === null) {
      this._mayTrimURLs = Services.prefs.getBoolPref("browser.urlbar.trimURLs");
    }
    return this._mayTrimURLs;
  },

  supportsCommand : function(cmd) {
    var isSupported = false;
    switch (cmd) {
      case "cmd_back":
      case "cmd_forward":
      case "cmd_reload":
      case "cmd_forceReload":
      case "cmd_stop":
      case "cmd_go":
      case "cmd_home":
      case "cmd_openLocation":
      case "cmd_addBookmark":
      case "cmd_bookmarks":
      case "cmd_history":
      case "cmd_remoteTabs":
      case "cmd_quit":
      case "cmd_close":
      case "cmd_newTab":
      case "cmd_closeTab":
      case "cmd_undoCloseTab":
      case "cmd_actions":
      case "cmd_panel":
      case "cmd_flyout_back":
      case "cmd_sanitize":
      case "cmd_volumeLeft":
      case "cmd_volumeRight":
      case "cmd_openFile":
      case "cmd_savePage":
        isSupported = true;
        break;
      default:
        isSupported = false;
        break;
    }
    return isSupported;
  },

  isCommandEnabled : function(cmd) {
    let elem = document.getElementById(cmd);
    if (elem && elem.getAttribute("disabled") == "true")
      return false;
    return true;
  },

  doCommand : function(cmd) {
    if (!this.isCommandEnabled(cmd))
      return;
    let browser = getBrowser();
    switch (cmd) {
      case "cmd_back":
        browser.goBack();
        break;
      case "cmd_forward":
        browser.goForward();
        break;
      case "cmd_reload":
        browser.reload();
        break;
      case "cmd_forceReload":
      {
        // Simulate a new page
        browser.lastLocation = null;

        const reloadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY |
                            Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
        browser.reloadWithFlags(reloadFlags);
        break;
      }
      case "cmd_stop":
        browser.stop();
        break;
      case "cmd_go":
        this.goToURI();
        break;
      case "cmd_home":
        this.goToURI(Browser.getHomePage());
        break;
      case "cmd_openLocation":
        ContextUI.displayNavbar();
        this._editURI(true);
        break;
      case "cmd_addBookmark":
        Elements.navbar.show();
        Appbar.onStarButton(true);
        break;
      case "cmd_bookmarks":
        PanelUI.show("bookmarks-container");
        break;
      case "cmd_history":
        PanelUI.show("history-container");
        break;
      case "cmd_remoteTabs":
        if (Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED) {
          Sync.open();
        } else {
          PanelUI.show("remotetabs-container");
        }
        break;
      case "cmd_quit":
        // Only close one window
        this._closeOrQuit();
        break;
      case "cmd_close":
        this._closeOrQuit();
        break;
      case "cmd_newTab":
        this.newTab();
        this._editURI(false);
        break;
      case "cmd_closeTab":
        this.closeTab();
        break;
      case "cmd_undoCloseTab":
        this.undoCloseTab();
        break;
      case "cmd_sanitize":
        SanitizeUI.onSanitize();
        break;
      case "cmd_flyout_back":
        FlyoutPanelsUI.hide();
        MetroUtils.showSettingsFlyout();
        break;
      case "cmd_panel":
        PanelUI.toggle();
        break;
      case "cmd_volumeLeft":
        // Zoom in (portrait) or out (landscape)
        Browser.zoom(Util.isPortrait() ? -1 : 1);
        break;
      case "cmd_volumeRight":
        // Zoom out (portrait) or in (landscape)
        Browser.zoom(Util.isPortrait() ? 1 : -1);
        break;
      case "cmd_openFile":
        this.openFile();
        break;
      case "cmd_savePage":
        this.savePage();
        break;
    }
  },

  crashReportingPrefChanged: function crashReportingPrefChanged(aState) {
    CrashReporter.submitReports = aState;
  }
};

/**
 * Tracks whether context UI (app bar, tab bar, url bar) is shown or hidden.
 * Manages events to summon and hide the context UI.
 */
var ContextUI = {
  _expandable: true,
  _hidingId: 0,

  /*******************************************
   * init
   */

  init: function init() {
    Elements.browsers.addEventListener("mousedown", this, true);
    Elements.browsers.addEventListener("touchstart", this, true);
    Elements.browsers.addEventListener("AlertActive", this, true);
    window.addEventListener("MozEdgeUIStarted", this, true);
    window.addEventListener("MozEdgeUICanceled", this, true);
    window.addEventListener("MozEdgeUICompleted", this, true);
    window.addEventListener("keypress", this, true);
    window.addEventListener("KeyboardChanged", this, false);

    Elements.tray.addEventListener("transitionend", this, true);

    Appbar.init();
  },

  /*******************************************
   * Context UI state getters & setters
   */

  get isVisible() {
    return (Elements.navbar.hasAttribute("visible") ||
            Elements.navbar.hasAttribute("startpage"));
  },
  get isExpanded() { return Elements.tray.hasAttribute("expanded"); },
  get isExpandable() { return this._expandable; },

  set isExpandable(aFlag) {
    this._expandable = aFlag;
    if (!this._expandable)
      this.dismiss();
  },

  /*******************************************
   * Context UI state control
   */

  toggle: function toggle() {
    if (!this._expandable) {
      // exandable setter takes care of resetting state
      // so if we're not expandable, there's nothing to do here
      return;
    }
    // if we're not showing, show
    if (!this.dismiss()) {
      dump("* ContextUI is hidden, show it\n");
      this.show();
    }
  },

  // show all context UI
  // returns true if any non-visible UI was shown
  show: function() {
    let shown = false;
    if (!this.isExpanded) {
      // show the tab tray
      this._setIsExpanded(true);
      shown = true;
    }
    if (!Elements.navbar.isShowing) {
      // show the navbar
      Elements.navbar.show();
      shown = true;
    }

    this._clearDelayedTimeout();
    if (shown) {
      ContentAreaObserver.update(window.innerWidth, window.innerHeight);
    }
    return shown;
  },

  // Display the nav bar
  displayNavbar: function displayNavbar() {
    this._clearDelayedTimeout();
    Elements.navbar.show();
  },

  // Display the toolbar and tabs
  displayTabs: function displayTabs() {
    this._clearDelayedTimeout();
    this._setIsExpanded(true, true);
  },

  /** Briefly show the tab bar and then hide it */
  peekTabs: function peekTabs() {
    if (this.isExpanded) {
      setTimeout(function () {
        ContextUI.dismissWithDelay(kNewTabAnimationDelayMsec);
      }, 0);
    } else {
      BrowserUI.setOnTabAnimationEnd(function () {
        ContextUI.dismissWithDelay(kNewTabAnimationDelayMsec);
      });

      this.displayTabs();
    }
  },

  // Dismiss all context UI.
  // Returns true if any visible UI was dismissed.
  dismiss: function dismiss() {
    let dismissed = false;
    if (this.isExpanded) {
      this._setIsExpanded(false);
      dismissed = true;
    }
    if (Elements.navbar.isShowing) {
      this.dismissAppbar();
      dismissed = true;
    }
    this._clearDelayedTimeout();
    if (dismissed) {
      ContentAreaObserver.update(window.innerWidth, window.innerHeight);
    }
    return dismissed;
  },

  // Dismiss all context ui after a delay
  dismissWithDelay: function dismissWithDelay(aDelay) {
    aDelay = aDelay || kHideContextAndTrayDelayMsec;
    this._clearDelayedTimeout();
    this._hidingId = setTimeout(function () {
        ContextUI.dismiss();
      }, aDelay);
  },

  // Cancel any pending delayed dismiss
  cancelDismiss: function cancelDismiss() {
    this._clearDelayedTimeout();
  },

  dismissTabs: function dimissTabs() {
    this._clearDelayedTimeout();
    this._setIsExpanded(false, true);
  },

  dismissAppbar: function dismissAppbar() {
    this._fire("MozAppbarDismiss");
  },

  /*******************************************
   * Internal tray state setters
   */

  // tab tray state
  _setIsExpanded: function _setIsExpanded(aFlag, setSilently) {
    // if the tray can't be expanded, don't expand it.
    if (!this.isExpandable || this.isExpanded == aFlag)
      return;

    if (aFlag)
      Elements.tray.setAttribute("expanded", "true");
    else
      Elements.tray.removeAttribute("expanded");

    if (!setSilently)
      this._fire("MozContextUIExpand");
  },

  /*******************************************
   * Internal utils
   */

  _clearDelayedTimeout: function _clearDelayedTimeout() {
    if (this._hidingId) {
      clearTimeout(this._hidingId);
      this._hidingId = 0;
    }
  },

  /*******************************************
   * Events
   */

  _onEdgeUIStarted: function(aEvent) {
    this._hasEdgeSwipeStarted = true;
    this._clearDelayedTimeout();

    if (StartUI.hide()) {
      this.dismiss();
      return;
    }
    this.toggle();
  },

  _onEdgeUICanceled: function(aEvent) {
    this._hasEdgeSwipeStarted = false;
    StartUI.hide();
    this.dismiss();
  },

  _onEdgeUICompleted: function(aEvent) {
    if (this._hasEdgeSwipeStarted) {
      this._hasEdgeSwipeStarted = false;
      return;
    }

    this._clearDelayedTimeout();
    if (StartUI.hide()) {
      this.dismiss();
      return;
    }
    this.toggle();
  },

  handleEvent: function handleEvent(aEvent) {
    switch (aEvent.type) {
      case "MozEdgeUIStarted":
        this._onEdgeUIStarted(aEvent);
        break;
      case "MozEdgeUICanceled":
        this._onEdgeUICanceled(aEvent);
        break;
      case "MozEdgeUICompleted":
        this._onEdgeUICompleted(aEvent);
        break;
      case "mousedown":
        if (aEvent.button == 0 && this.isVisible)
          this.dismiss();
        break;
      case "touchstart":
      // ContextUI can hide the notification bar. Workaround until bug 845348 is fixed.
      case "AlertActive":
        this.dismiss();
        break;
      case "keypress":
        if (String.fromCharCode(aEvent.which) == "z" &&
            aEvent.getModifierState("Win"))
          this.toggle();
        break;
      case "transitionend":
        setTimeout(function () {
          ContentAreaObserver.updateContentArea();
        }, 0);
        break;
      case "KeyboardChanged":
        this.dismissTabs();
        break;
    }
  },

  _fire: function (name) {
    let event = document.createEvent("Events");
    event.initEvent(name, true, true);
    window.dispatchEvent(event);
  }
};

var StartUI = {
  get isVisible() { return this.isStartPageVisible || this.isFiltering; },
  get isStartPageVisible() { return Elements.windowState.hasAttribute("startpage"); },
  get isFiltering() { return Elements.windowState.hasAttribute("filtering"); },

  get maxResultsPerSection() {
    return Services.prefs.getIntPref("browser.display.startUI.maxresults");
  },

  sections: [
    "TopSitesStartView",
    "TopSitesSnappedView",
    "BookmarksStartView",
    "HistoryStartView",
    "RemoteTabsStartView"
  ],

  init: function init() {
    Elements.startUI.addEventListener("autocompletestart", this, false);
    Elements.startUI.addEventListener("autocompleteend", this, false);
    Elements.startUI.addEventListener("contextmenu", this, false);
    Elements.startUI.addEventListener("click", this, false);
    Elements.startUI.addEventListener("MozMousePixelScroll", this, false);

    this.sections.forEach(function (sectionName) {
      let section = window[sectionName];
      if (section.init)
        section.init();
    });
  },

  uninit: function() {
    this.sections.forEach(function (sectionName) {
      let section = window[sectionName];
      if (section.uninit)
        section.uninit();
    });
  },

  /** Show the Firefox start page / "new tab" page */
  show: function show() {
    if (this.isStartPageVisible)
      return false;

    ContextUI.displayNavbar();

    Elements.contentShowing.setAttribute("disabled", "true");
    Elements.windowState.setAttribute("startpage", "true");

    this.sections.forEach(function (sectionName) {
      let section = window[sectionName];
      if (section.show)
        section.show();
    });
    return true;
  },

  /** Show the autocomplete popup */
  filter: function filter() {
    if (this.isFiltering)
      return;

    BrowserUI._edit.openPopup();
    Elements.windowState.setAttribute("filtering", "true");
  },

  /** Hide the autocomplete popup */
  unfilter: function unfilter() {
    if (!this.isFiltering)
      return;

    BrowserUI._edit.closePopup();
    Elements.windowState.removeAttribute("filtering");
  },

  /** Hide the Firefox start page */
  hide: function hide(aURI) {
    aURI = aURI || Browser.selectedBrowser.currentURI.spec;
    if (!this.isStartPageVisible || this.isStartURI(aURI))
      return false;

    Elements.contentShowing.removeAttribute("disabled");
    Elements.windowState.removeAttribute("startpage");

    this.unfilter();
    return true;
  },

  /** Is the current tab supposed to show the Firefox start page? */
  isStartURI: function isStartURI(aURI) {
    aURI = aURI || Browser.selectedBrowser.currentURI.spec;
    return aURI == kStartOverlayURI || aURI == "about:home";
  },

  /** Call this to show or hide the start page when switching tabs or pages */
  update: function update(aURI) {
    aURI = aURI || Browser.selectedBrowser.currentURI.spec;
    if (this.isStartURI(aURI)) {
      this.show();
    } else if (aURI != "about:blank") { // about:blank is loaded briefly for new tabs; ignore it
      this.hide(aURI);
    }
  },

  onClick: function onClick(aEvent) {
    // If someone clicks / taps in empty grid space, take away
    // focus from the nav bar edit so the soft keyboard will hide.
    if (BrowserUI.blurNavBar()) {
      // Advanced notice to CAO, so we can shuffle the nav bar in advance
      // of the keyboard transition.
      ContentAreaObserver.navBarWillBlur();
    }
  },

  handleEvent: function handleEvent(aEvent) {
    switch (aEvent.type) {
      case "autocompletestart":
        this.filter();
        break;
      case "autocompleteend":
        this.unfilter();
        break;
      case "contextmenu":
        let event = document.createEvent("Events");
        event.initEvent("MozEdgeUICompleted", true, false);
        window.dispatchEvent(event);
        break;
      case "click":
        this.onClick(aEvent);
        break;

      case "MozMousePixelScroll":
        let startBox = document.getElementById("start-scrollbox");
        let [, scrollInterface] = ScrollUtils.getScrollboxFromElement(startBox);

        scrollInterface.scrollBy(aEvent.detail, 0);

        aEvent.preventDefault();
        aEvent.stopPropagation();
        break;
    }
  }
};

var SyncPanelUI = {
  init: function() {
    // Run some setup code the first time the panel is shown.
    Elements.syncFlyout.addEventListener("PopupChanged", function onShow(aEvent) {
      if (aEvent.detail && aEvent.target === Elements.syncFlyout) {
        Elements.syncFlyout.removeEventListener("PopupChanged", onShow, false);
        Sync.init();
      }
    }, false);
  }
};

var FlyoutPanelsUI = {
  init: function() {
    AboutPanelUI.init();
    PreferencesPanelView.init();
    SyncPanelUI.init();

    // make sure to hide all flyouts when window is deactivated
    window.addEventListener("deactivate", function(window) {
      FlyoutPanelsUI.hide();
    });
  },

  hide: function() {
    Elements.aboutFlyout.hide();
    Elements.prefsFlyout.hide();
    Elements.syncFlyout.hide();
  }
};

var PanelUI = {
  get _panels() { return document.getElementById("panel-items"); },
  get _switcher() { return document.getElementById("panel-view-switcher"); },

  get isVisible() {
    return !Elements.panelUI.hidden;
  },

  views: {
    "bookmarks-container": "BookmarksPanelView",
    "downloads-container": "DownloadsPanelView",
    "console-container": "ConsolePanelView",
    "remotetabs-container": "RemoteTabsPanelView",
    "history-container" : "HistoryPanelView"
  },

  init: function() {
    // Perform core init soon
    setTimeout(function () {
      for each (let viewName in this.views) {
        let view = window[viewName];
        if (view.init)
          view.init();
      }
    }.bind(this), 0);

    // Lazily run other initialization tasks when the views are shown
    this._panels.addEventListener("ToolPanelShown", function(aEvent) {
      let viewName = this.views[this._panels.selectedPanel.id];
      let view = window[viewName];
      if (view.show)
        view.show();
    }.bind(this), true);
  },

  uninit: function() {
    for each (let viewName in this.views) {
      let view = window[viewName];
      if (view.uninit)
        view.uninit();
    }
  },

  switchPane: function switchPane(aPanelId) {
    BrowserUI.blurFocusedElement();

    let panel = aPanelId ? document.getElementById(aPanelId) : this._panels.selectedPanel;
    let oldPanel = this._panels.selectedPanel;

    if (oldPanel != panel) {
      this._panels.selectedPanel = panel;
      this._switcher.value = panel.id;

      this._fire("ToolPanelHidden", oldPanel);
    }

    this._fire("ToolPanelShown", panel);
  },

  isPaneVisible: function isPaneVisible(aPanelId) {
    return this.isVisible && this._panels.selectedPanel.id == aPanelId;
  },

  show: function show(aPanelId) {
    Elements.panelUI.hidden = false;
    Elements.contentShowing.setAttribute("disabled", "true");

    this.switchPane(aPanelId);
  },

  hide: function hide() {
    if (!this.isVisible)
      return;

    Elements.panelUI.hidden = true;
    Elements.contentShowing.removeAttribute("disabled");
    BrowserUI.blurFocusedElement();

    this._fire("ToolPanelHidden", this._panels);
  },

  toggle: function toggle() {
    if (this.isVisible) {
      this.hide();
    } else {
      this.show();
    }
  },

  _fire: function _fire(aName, anElement) {
    let event = document.createEvent("Events");
    event.initEvent(aName, true, true);
    anElement.dispatchEvent(event);
  }
};

var DialogUI = {
  _dialogs: [],
  _popup: null,

  init: function() {
    window.addEventListener("mousedown", this, true);
  },

  /*******************************************
   * Modal popups
   */

  get modals() {
    return document.getElementsByClassName("modal-block");
  },

  importModal: function importModal(aParent, aSrc, aArguments) {
  // load the dialog with a synchronous XHR
    let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
    xhr.open("GET", aSrc, false);
    xhr.overrideMimeType("text/xml");
    xhr.send(null);
    if (!xhr.responseXML)
      return null;

    let currentNode;
    let nodeIterator = xhr.responseXML.createNodeIterator(xhr.responseXML, NodeFilter.SHOW_TEXT, null, false);
    while (!!(currentNode = nodeIterator.nextNode())) {
      let trimmed = currentNode.nodeValue.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
      if (!trimmed.length)
        currentNode.parentNode.removeChild(currentNode);
    }

    let doc = xhr.responseXML.documentElement;

    let dialog  = null;

    // we need to insert before context-container if we want allow pasting (using
    //  the context menu) into dialogs
    let contentMenuContainer = document.getElementById("context-container");
    let parentNode = contentMenuContainer.parentNode;

    // emit DOMWillOpenModalDialog event
    let event = document.createEvent("Events");
    event.initEvent("DOMWillOpenModalDialog", true, false);
    let dispatcher = aParent || getBrowser();
    dispatcher.dispatchEvent(event);

    // create a full-screen semi-opaque box as a background
    let back = document.createElement("box");
    back.setAttribute("class", "modal-block");
    dialog = back.appendChild(document.importNode(doc, true));
    parentNode.insertBefore(back, contentMenuContainer);

    dialog.arguments = aArguments;
    dialog.parent = aParent;
    return dialog;
  },

  /*******************************************
   * Dialogs
   */

  get activeDialog() {
    // Return the topmost dialog
    if (this._dialogs.length)
      return this._dialogs[this._dialogs.length - 1];
    return null;
  },

  closeAllDialogs: function closeAllDialogs() {
    while (this.activeDialog)
      this.activeDialog.close();
  },

  pushDialog: function pushDialog(aDialog) {
    // If we have a dialog push it on the stack and set the attr for CSS
    if (aDialog) {
      this._dialogs.push(aDialog);
      Elements.contentShowing.setAttribute("disabled", "true");
    }
  },

  popDialog: function popDialog() {
    if (this._dialogs.length)
      this._dialogs.pop();

    // If no more dialogs are being displayed, remove the attr for CSS
    if (!this._dialogs.length)
      Elements.contentShowing.removeAttribute("disabled");
  },

  /*******************************************
   * Popups
   */

  pushPopup: function pushPopup(aPanel, aElements, aParent) {
    this._hidePopup();
    this._popup =  { "panel": aPanel,
                     "elements": (aElements instanceof Array) ? aElements : [aElements] };
    this._dispatchPopupChanged(true, aPanel);
  },

  popPopup: function popPopup(aPanel) {
    if (!this._popup || aPanel != this._popup.panel)
      return;
    this._popup = null;
    this._dispatchPopupChanged(false, aPanel);
  },

  _hidePopup: function _hidePopup() {
    if (!this._popup)
      return;
    let panel = this._popup.panel;
    if (panel.hide)
      panel.hide();
  },

  /*******************************************
   * Events
   */

  handleEvent: function (aEvent) {
    switch (aEvent.type) {
      case "mousedown":
        if (!this._isEventInsidePopup(aEvent))
          this._hidePopup();
        break;
      default:
        break;
    }
  },

  _dispatchPopupChanged: function _dispatchPopupChanged(aVisible, aElement) {
    let event = document.createEvent("UIEvents");
    event.initUIEvent("PopupChanged", true, true, window, aVisible);
    aElement.dispatchEvent(event);
  },

  _isEventInsidePopup: function _isEventInsidePopup(aEvent) {
    if (!this._popup)
      return false;
    let elements = this._popup.elements;
    let targetNode = aEvent.target;
    while (targetNode && elements.indexOf(targetNode) == -1) {
      if (targetNode instanceof Element && targetNode.hasAttribute("for"))
        targetNode = document.getElementById(targetNode.getAttribute("for"));
      else
        targetNode = targetNode.parentNode;
    }
    return targetNode ? true : false;
  }
};

/**
 * Manage the contents of the Windows 8 "Settings" charm.
 */
var SettingsCharm = {
  _entries: new Map(),
  _nextId: 0,

  /**
   * Add a new item to the "Settings" menu in the Windows 8 charms.
   * @param aEntry Object with a "label" property (string that will appear in the UI)
   *    and an "onselected" property (function to be called when the user chooses this entry)
   */
  addEntry: function addEntry(aEntry) {
    try {
      let id = MetroUtils.addSettingsPanelEntry(aEntry.label);
      this._entries.set(id, aEntry);
    } catch (e) {
      // addSettingsPanelEntry does not work on non-Metro platforms
      Cu.reportError(e);
    }
  },

  init: function SettingsCharm_init() {
    Services.obs.addObserver(this, "metro-settings-entry-selected", false);

    // Options
    this.addEntry({
        label: Strings.browser.GetStringFromName("optionsCharm"),
        onselected: function() Elements.prefsFlyout.show()
    });
    // Sync 
    this.addEntry({
        label: Strings.browser.GetStringFromName("syncCharm"),
        onselected: function() Elements.syncFlyout.show()
    });
    // About
    this.addEntry({
        label: Strings.browser.GetStringFromName("aboutCharm1"),
        onselected: function() Elements.aboutFlyout.show()
    });
    // Help
    this.addEntry({
        label: Strings.browser.GetStringFromName("helpOnlineCharm"),
        onselected: function() {
          let url = Services.urlFormatter.formatURLPref("app.support.baseURL");
          BrowserUI.newTab(url, Browser.selectedTab);
        }
    });
  },

  observe: function SettingsCharm_observe(aSubject, aTopic, aData) {
    if (aTopic == "metro-settings-entry-selected") {
      let entry = this._entries.get(parseInt(aData, 10));
      if (entry)
        entry.onselected();
    }
  },

  uninit: function SettingsCharm_uninit() {
    Services.obs.removeObserver(this, "metro-settings-entry-selected");
  }
};
