/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* 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/. */

const Ci = Components.interfaces;
const Cu = Components.utils;

var {loader, require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
var Telemetry = require("devtools/client/shared/telemetry");
var {showDoorhanger} = require("devtools/client/shared/doorhanger");
var {TouchEventSimulator} = require("devtools/shared/touch/simulator");
var {Task} = require("devtools/shared/task");
var promise = require("promise");
var DevToolsUtils = require("devtools/shared/DevToolsUtils");
var flags = require("devtools/shared/flags");
var Services = require("Services");
var EventEmitter = require("devtools/shared/event-emitter");
var {ViewHelpers} = require("devtools/client/shared/widgets/view-helpers");
var { LocalizationHelper } = require("devtools/shared/l10n");
var { EmulationFront } = require("devtools/shared/fronts/emulation");

loader.lazyImporter(this, "SystemAppProxy",
                    "resource://gre/modules/SystemAppProxy.jsm");
loader.lazyRequireGetter(this, "DebuggerClient",
                         "devtools/shared/client/main", true);
loader.lazyRequireGetter(this, "DebuggerServer",
                         "devtools/server/main", true);

this.EXPORTED_SYMBOLS = ["ResponsiveUIManager"];

const MIN_WIDTH = 50;
const MIN_HEIGHT = 50;

const MAX_WIDTH = 10000;
const MAX_HEIGHT = 10000;

const SLOW_RATIO = 6;
const ROUND_RATIO = 10;

const INPUT_PARSER = /(\d+)[^\d]+(\d+)/;

const SHARED_L10N = new LocalizationHelper("devtools/client/locales/shared.properties");

function debug(msg) {
  // dump(`RDM UI: ${msg}\n`);
}

var ActiveTabs = new Map();

var Manager = {
  /**
   * Check if the a tab is in a responsive mode.
   * Leave the responsive mode if active,
   * active the responsive mode if not active.
   *
   * @param aWindow the main window.
   * @param aTab the tab targeted.
   */
  toggle: function (aWindow, aTab) {
    if (this.isActiveForTab(aTab)) {
      ActiveTabs.get(aTab).close();
    } else {
      this.openIfNeeded(aWindow, aTab);
    }
  },

  /**
   * Launches the responsive mode.
   *
   * @param aWindow the main window.
   * @param aTab the tab targeted.
   * @returns {ResponsiveUI} the instance of ResponsiveUI for the current tab.
   */
  openIfNeeded: Task.async(function* (aWindow, aTab) {
    let ui;
    if (!this.isActiveForTab(aTab)) {
      ui = new ResponsiveUI(aWindow, aTab);
      yield ui.inited;
    } else {
      ui = this.getResponsiveUIForTab(aTab);
    }
    return ui;
  }),

  /**
   * Returns true if responsive view is active for the provided tab.
   *
   * @param aTab the tab targeted.
   */
  isActiveForTab: function (aTab) {
    return ActiveTabs.has(aTab);
  },

  /**
   * Return the responsive UI controller for a tab.
   */
  getResponsiveUIForTab: function (aTab) {
    return ActiveTabs.get(aTab);
  },

  /**
   * Handle gcli commands.
   *
   * @param aWindow the browser window.
   * @param aTab the tab targeted.
   * @param aCommand the command name.
   * @param aArgs command arguments.
   */
  handleGcliCommand: Task.async(function* (aWindow, aTab, aCommand, aArgs) {
    switch (aCommand) {
      case "resize to":
        let ui = yield this.openIfNeeded(aWindow, aTab);
        ui.setViewportSize(aArgs);
        break;
      case "resize on":
        this.openIfNeeded(aWindow, aTab);
        break;
      case "resize off":
        if (this.isActiveForTab(aTab)) {
          yield ActiveTabs.get(aTab).close();
        }
        break;
      case "resize toggle":
        this.toggle(aWindow, aTab);
      default:
    }
  })
};

EventEmitter.decorate(Manager);

// If the new HTML RDM UI is enabled and e10s is enabled by default (e10s is required for
// the new HTML RDM UI to function), delegate the ResponsiveUIManager API over to that
// tool instead.  Performing this delegation here allows us to contain the pref check to a
// single place.
if (Services.prefs.getBoolPref("devtools.responsive.html.enabled") &&
    Services.appinfo.browserTabsRemoteAutostart) {
  let { ResponsiveUIManager } =
    require("devtools/client/responsive.html/manager");
  this.ResponsiveUIManager = ResponsiveUIManager;
} else {
  this.ResponsiveUIManager = Manager;
}

var defaultPresets = [
  // Phones
  {key: "320x480", width: 320, height: 480},    // iPhone, B2G, with <meta viewport>
  {key: "360x640", width: 360, height: 640},    // Android 4, phones, with <meta viewport>

  // Tablets
  {key: "768x1024", width: 768, height: 1024},   // iPad, with <meta viewport>
  {key: "800x1280", width: 800, height: 1280},   // Android 4, Tablet, with <meta viewport>

  // Default width for mobile browsers, no <meta viewport>
  {key: "980x1280", width: 980, height: 1280},

  // Computer
  {key: "1280x600", width: 1280, height: 600},
  {key: "1920x900", width: 1920, height: 900},
];

function ResponsiveUI(aWindow, aTab)
{
  this.mainWindow = aWindow;
  this.tab = aTab;
  this.mm = this.tab.linkedBrowser.messageManager;
  this.tabContainer = aWindow.gBrowser.tabContainer;
  this.browser = aTab.linkedBrowser;
  this.chromeDoc = aWindow.document;
  this.container = aWindow.gBrowser.getBrowserContainer(this.browser);
  this.stack = this.container.querySelector(".browserStack");
  this._telemetry = new Telemetry();

  // Let's bind some callbacks.
  this.bound_presetSelected = this.presetSelected.bind(this);
  this.bound_handleManualInput = this.handleManualInput.bind(this);
  this.bound_addPreset = this.addPreset.bind(this);
  this.bound_removePreset = this.removePreset.bind(this);
  this.bound_rotate = this.rotate.bind(this);
  this.bound_screenshot = () => this.screenshot();
  this.bound_touch = this.toggleTouch.bind(this);
  this.bound_close = this.close.bind(this);
  this.bound_startResizing = this.startResizing.bind(this);
  this.bound_stopResizing = this.stopResizing.bind(this);
  this.bound_onDrag = this.onDrag.bind(this);
  this.bound_changeUA = this.changeUA.bind(this);
  this.bound_onContentResize = this.onContentResize.bind(this);

  this.mm.addMessageListener("ResponsiveMode:OnContentResize",
                             this.bound_onContentResize);

  // We must be ready to handle window or tab close now that we have saved
  // ourselves in ActiveTabs.  Otherwise we risk leaking the window.
  this.mainWindow.addEventListener("unload", this);
  this.tab.addEventListener("TabClose", this);
  this.tabContainer.addEventListener("TabSelect", this);

  ActiveTabs.set(this.tab, this);

  this.inited = this.init();
}

ResponsiveUI.prototype = {
  _transitionsEnabled: true,
  get transitionsEnabled() {
    return this._transitionsEnabled;
  },
  set transitionsEnabled(aValue) {
    this._transitionsEnabled = aValue;
    if (aValue && !this._resizing && this.stack.hasAttribute("responsivemode")) {
      this.stack.removeAttribute("notransition");
    } else if (!aValue) {
      this.stack.setAttribute("notransition", "true");
    }
  },

  init: Task.async(function* () {
    debug("INIT BEGINS");
    let ready = this.waitForMessage("ResponsiveMode:ChildScriptReady");
    this.mm.loadFrameScript("resource://devtools/client/responsivedesign/responsivedesign-child.js", true);
    yield ready;

    let requiresFloatingScrollbars =
      !this.mainWindow.matchMedia("(-moz-overlay-scrollbars)").matches;
    let started = this.waitForMessage("ResponsiveMode:Start:Done");
    debug("SEND START");
    this.mm.sendAsyncMessage("ResponsiveMode:Start", {
      requiresFloatingScrollbars,
      // Tests expect events on resize to yield on various size changes
      notifyOnResize: flags.testing,
    });
    yield started;

    // Load Presets
    this.loadPresets();

    // Setup the UI
    this.container.setAttribute("responsivemode", "true");
    this.stack.setAttribute("responsivemode", "true");
    this.buildUI();
    this.checkMenus();

    // Rotate the responsive mode if needed
    try {
      if (Services.prefs.getBoolPref("devtools.responsiveUI.rotate")) {
        this.rotate();
      }
    } catch (e) {}

    // Touch events support
    this.touchEnableBefore = false;
    this.touchEventSimulator = new TouchEventSimulator(this.browser);

    yield this.connectToServer();
    this.userAgentInput.hidden = false;

    // Hook to display promotional Developer Edition doorhanger.
    // Only displayed once.
    showDoorhanger({
      window: this.mainWindow,
      type: "deveditionpromo",
      anchor: this.chromeDoc.querySelector("#content")
    });

    // Notify that responsive mode is on.
    this._telemetry.toolOpened("responsive");
    ResponsiveUIManager.emit("on", { tab: this.tab });
  }),

  connectToServer: Task.async(function* () {
    if (!DebuggerServer.initialized) {
      DebuggerServer.init();
      DebuggerServer.addBrowserActors();
    }
    this.client = new DebuggerClient(DebuggerServer.connectPipe());
    yield this.client.connect();
    let {tab} = yield this.client.getTab();
    yield this.client.attachTab(tab.actor);
    this.emulationFront = EmulationFront(this.client, tab);
  }),

  loadPresets: function () {
    // Try to load presets from prefs
    let presets = defaultPresets;
    if (Services.prefs.prefHasUserValue("devtools.responsiveUI.presets")) {
      try {
        presets = JSON.parse(Services.prefs.getCharPref("devtools.responsiveUI.presets"));
      } catch (e) {
        // User pref is malformated.
        console.error("Could not parse pref `devtools.responsiveUI.presets`: " + e);
      }
    }

    this.customPreset = {key: "custom", custom: true};

    if (Array.isArray(presets)) {
      this.presets = [this.customPreset].concat(presets);
    } else {
      console.error("Presets value (devtools.responsiveUI.presets) is malformated.");
      this.presets = [this.customPreset];
    }

    try {
      let width = Services.prefs.getIntPref("devtools.responsiveUI.customWidth");
      let height = Services.prefs.getIntPref("devtools.responsiveUI.customHeight");
      this.customPreset.width = Math.min(MAX_WIDTH, width);
      this.customPreset.height = Math.min(MAX_HEIGHT, height);

      this.currentPresetKey = Services.prefs.getCharPref("devtools.responsiveUI.currentPreset");
    } catch (e) {
      // Default size. The first preset (custom) is the one that will be used.
      let bbox = this.stack.getBoundingClientRect();

      this.customPreset.width = bbox.width - 40; // horizontal padding of the container
      this.customPreset.height = bbox.height - 80; // vertical padding + toolbar height

      this.currentPresetKey = this.presets[1].key; // most common preset
    }
  },

  /**
   * Destroy the nodes. Remove listeners. Reset the style.
   */
  close: Task.async(function* () {
    debug("CLOSE BEGINS");
    if (this.closing) {
      debug("ALREADY CLOSING, ABORT");
      return;
    }
    this.closing = true;

    // If we're closing very fast (in tests), ensure init has finished.
    debug("CLOSE: WAIT ON INITED");
    yield this.inited;
    debug("CLOSE: INITED DONE");

    this.unCheckMenus();
    // Reset style of the stack.
    debug(`CURRENT SIZE: ${this.stack.getAttribute("style")}`);
    let style = "max-width: none;" +
                "min-width: 0;" +
                "max-height: none;" +
                "min-height: 0;";
    debug("RESET STACK SIZE");
    this.stack.setAttribute("style", style);

    // Wait for resize message before stopping in the child when testing,
    // but only if we should expect to still get a message.
    if (flags.testing && this.tab.linkedBrowser.messageManager) {
      yield this.waitForMessage("ResponsiveMode:OnContentResize");
    }

    if (this.isResizing)
      this.stopResizing();

    // Remove listeners.
    this.menulist.removeEventListener("select", this.bound_presetSelected, true);
    this.menulist.removeEventListener("change", this.bound_handleManualInput, true);
    this.mainWindow.removeEventListener("unload", this);
    this.tab.removeEventListener("TabClose", this);
    this.tabContainer.removeEventListener("TabSelect", this);
    this.rotatebutton.removeEventListener("command", this.bound_rotate, true);
    this.screenshotbutton.removeEventListener("command", this.bound_screenshot, true);
    this.closebutton.removeEventListener("command", this.bound_close, true);
    this.addbutton.removeEventListener("command", this.bound_addPreset, true);
    this.removebutton.removeEventListener("command", this.bound_removePreset, true);
    this.touchbutton.removeEventListener("command", this.bound_touch, true);
    this.userAgentInput.removeEventListener("blur", this.bound_changeUA, true);

    // Removed elements.
    this.container.removeChild(this.toolbar);
    if (this.bottomToolbar) {
      this.bottomToolbar.remove();
      delete this.bottomToolbar;
    }
    this.stack.removeChild(this.resizer);
    this.stack.removeChild(this.resizeBarV);
    this.stack.removeChild(this.resizeBarH);

    this.stack.classList.remove("fxos-mode");

    // Unset the responsive mode.
    this.container.removeAttribute("responsivemode");
    this.stack.removeAttribute("responsivemode");

    ActiveTabs.delete(this.tab);
    if (this.touchEventSimulator) {
      this.touchEventSimulator.stop();
    }

    yield this.client.close();
    this.client = this.emulationFront = null;

    this._telemetry.toolClosed("responsive");

    if (this.tab.linkedBrowser.messageManager) {
      let stopped = this.waitForMessage("ResponsiveMode:Stop:Done");
      this.tab.linkedBrowser.messageManager.sendAsyncMessage("ResponsiveMode:Stop");
      yield stopped;
    }

    this.inited = null;
    ResponsiveUIManager.emit("off", { tab: this.tab });
  }),

  waitForMessage(message) {
    return new Promise(resolve => {
      let listener = () => {
        this.mm.removeMessageListener(message, listener);
        resolve();
      };
      this.mm.addMessageListener(message, listener);
    });
  },

  /**
   * Emit an event when the content has been resized. Only used in tests.
   */
  onContentResize: function (msg) {
    ResponsiveUIManager.emit("content-resize", {
      tab: this.tab,
      width: msg.data.width,
      height: msg.data.height,
    });
  },

  /**
   * Handle events
   */
  handleEvent: function (aEvent) {
    switch (aEvent.type) {
      case "TabClose":
      case "unload":
        this.close();
        break;
      case "TabSelect":
        if (this.tab.selected) {
          this.checkMenus();
        } else if (!this.mainWindow.gBrowser.selectedTab.responsiveUI) {
          this.unCheckMenus();
        }
        break;
    }
  },

  getViewportBrowser() {
    return this.browser;
  },

  /**
   * Check the menu items.
   */
  checkMenus: function RUI_checkMenus() {
    this.chromeDoc.getElementById("menu_responsiveUI").setAttribute("checked", "true");
  },

  /**
   * Uncheck the menu items.
   */
  unCheckMenus: function RUI_unCheckMenus() {
    let el = this.chromeDoc.getElementById("menu_responsiveUI");
    if (el) {
      el.setAttribute("checked", "false");
    }
  },

  /**
   * Build the toolbar and the resizers.
   *
   * <vbox class="browserContainer"> From tabbrowser.xml
   *  <toolbar class="devtools-responsiveui-toolbar">
   *    <menulist class="devtools-responsiveui-menulist"/> // presets
   *    <toolbarbutton tabindex="0" class="devtools-responsiveui-toolbarbutton" tooltiptext="rotate"/> // rotate
   *    <toolbarbutton tabindex="0" class="devtools-responsiveui-toolbarbutton" tooltiptext="screenshot"/> // screenshot
   *    <toolbarbutton tabindex="0" class="devtools-responsiveui-toolbarbutton" tooltiptext="Leave Responsive Design Mode"/> // close
   *  </toolbar>
   *  <stack class="browserStack"> From tabbrowser.xml
   *    <browser/>
   *    <box class="devtools-responsiveui-resizehandle" bottom="0" right="0"/>
   *    <box class="devtools-responsiveui-resizebarV" top="0" right="0"/>
   *    <box class="devtools-responsiveui-resizebarH" bottom="0" left="0"/>
   *    // Additional button in FxOS mode:
   *    <button class="devtools-responsiveui-sleep-button" />
   *    <vbox class="devtools-responsiveui-volume-buttons">
   *      <button class="devtools-responsiveui-volume-up-button" />
   *      <button class="devtools-responsiveui-volume-down-button" />
   *    </vbox>
   *  </stack>
   *  <toolbar class="devtools-responsiveui-hardware-button">
   *    <toolbarbutton class="devtools-responsiveui-home-button" />
   *  </toolbar>
   * </vbox>
   */
  buildUI: function RUI_buildUI() {
    // Toolbar
    this.toolbar = this.chromeDoc.createElement("toolbar");
    this.toolbar.className = "devtools-responsiveui-toolbar";
    this.toolbar.setAttribute("fullscreentoolbar", "true");

    this.menulist = this.chromeDoc.createElement("menulist");
    this.menulist.className = "devtools-responsiveui-menulist";
    this.menulist.setAttribute("editable", "true");

    this.menulist.addEventListener("select", this.bound_presetSelected, true);
    this.menulist.addEventListener("change", this.bound_handleManualInput, true);

    this.menuitems = new Map();

    let menupopup = this.chromeDoc.createElement("menupopup");
    this.registerPresets(menupopup);
    this.menulist.appendChild(menupopup);

    this.addbutton = this.chromeDoc.createElement("menuitem");
    this.addbutton.setAttribute("label", this.strings.GetStringFromName("responsiveUI.addPreset"));
    this.addbutton.addEventListener("command", this.bound_addPreset, true);

    this.removebutton = this.chromeDoc.createElement("menuitem");
    this.removebutton.setAttribute("label", this.strings.GetStringFromName("responsiveUI.removePreset"));
    this.removebutton.addEventListener("command", this.bound_removePreset, true);

    menupopup.appendChild(this.chromeDoc.createElement("menuseparator"));
    menupopup.appendChild(this.addbutton);
    menupopup.appendChild(this.removebutton);

    this.rotatebutton = this.chromeDoc.createElement("toolbarbutton");
    this.rotatebutton.setAttribute("tabindex", "0");
    this.rotatebutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.rotate2"));
    this.rotatebutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-rotate";
    this.rotatebutton.addEventListener("command", this.bound_rotate, true);

    this.screenshotbutton = this.chromeDoc.createElement("toolbarbutton");
    this.screenshotbutton.setAttribute("tabindex", "0");
    this.screenshotbutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.screenshot"));
    this.screenshotbutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-screenshot";
    this.screenshotbutton.addEventListener("command", this.bound_screenshot, true);

    this.closebutton = this.chromeDoc.createElement("toolbarbutton");
    this.closebutton.setAttribute("tabindex", "0");
    this.closebutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-close";
    this.closebutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.close1"));
    this.closebutton.addEventListener("command", this.bound_close, true);

    this.toolbar.appendChild(this.closebutton);
    this.toolbar.appendChild(this.menulist);
    this.toolbar.appendChild(this.rotatebutton);

    this.touchbutton = this.chromeDoc.createElement("toolbarbutton");
    this.touchbutton.setAttribute("tabindex", "0");
    this.touchbutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.touch"));
    this.touchbutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-touch";
    this.touchbutton.addEventListener("command", this.bound_touch, true);
    this.toolbar.appendChild(this.touchbutton);

    this.toolbar.appendChild(this.screenshotbutton);

    this.userAgentInput = this.chromeDoc.createElement("textbox");
    this.userAgentInput.className = "devtools-responsiveui-textinput";
    this.userAgentInput.setAttribute("placeholder",
      this.strings.GetStringFromName("responsiveUI.userAgentPlaceholder"));
    this.userAgentInput.addEventListener("blur", this.bound_changeUA, true);
    this.userAgentInput.hidden = true;
    this.toolbar.appendChild(this.userAgentInput);

    // Resizers
    let resizerTooltip = this.strings.GetStringFromName("responsiveUI.resizerTooltip");
    this.resizer = this.chromeDoc.createElement("box");
    this.resizer.className = "devtools-responsiveui-resizehandle";
    this.resizer.setAttribute("right", "0");
    this.resizer.setAttribute("bottom", "0");
    this.resizer.setAttribute("tooltiptext", resizerTooltip);
    this.resizer.onmousedown = this.bound_startResizing;

    this.resizeBarV = this.chromeDoc.createElement("box");
    this.resizeBarV.className = "devtools-responsiveui-resizebarV";
    this.resizeBarV.setAttribute("top", "0");
    this.resizeBarV.setAttribute("right", "0");
    this.resizeBarV.setAttribute("tooltiptext", resizerTooltip);
    this.resizeBarV.onmousedown = this.bound_startResizing;

    this.resizeBarH = this.chromeDoc.createElement("box");
    this.resizeBarH.className = "devtools-responsiveui-resizebarH";
    this.resizeBarH.setAttribute("bottom", "0");
    this.resizeBarH.setAttribute("left", "0");
    this.resizeBarH.setAttribute("tooltiptext", resizerTooltip);
    this.resizeBarH.onmousedown = this.bound_startResizing;

    this.container.insertBefore(this.toolbar, this.stack);
    this.stack.appendChild(this.resizer);
    this.stack.appendChild(this.resizeBarV);
    this.stack.appendChild(this.resizeBarH);
  },

  // FxOS custom controls
  buildPhoneUI: function () {
    this.stack.classList.add("fxos-mode");

    let sleepButton = this.chromeDoc.createElement("button");
    sleepButton.className = "devtools-responsiveui-sleep-button";
    sleepButton.setAttribute("top", 0);
    sleepButton.setAttribute("right", 0);
    sleepButton.addEventListener("mousedown", () => {
      SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "Power"});
    });
    sleepButton.addEventListener("mouseup", () => {
      SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "Power"});
    });
    this.stack.appendChild(sleepButton);

    let volumeButtons = this.chromeDoc.createElement("vbox");
    volumeButtons.className = "devtools-responsiveui-volume-buttons";
    volumeButtons.setAttribute("top", 0);
    volumeButtons.setAttribute("left", 0);

    let volumeUp = this.chromeDoc.createElement("button");
    volumeUp.className = "devtools-responsiveui-volume-up-button";
    volumeUp.addEventListener("mousedown", () => {
      SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "AudioVolumeUp"});
    });
    volumeUp.addEventListener("mouseup", () => {
      SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "AudioVolumeUp"});
    });

    let volumeDown = this.chromeDoc.createElement("button");
    volumeDown.className = "devtools-responsiveui-volume-down-button";
    volumeDown.addEventListener("mousedown", () => {
      SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "AudioVolumeDown"});
    });
    volumeDown.addEventListener("mouseup", () => {
      SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "AudioVolumeDown"});
    });

    volumeButtons.appendChild(volumeUp);
    volumeButtons.appendChild(volumeDown);
    this.stack.appendChild(volumeButtons);

    let bottomToolbar = this.chromeDoc.createElement("toolbar");
    bottomToolbar.className = "devtools-responsiveui-hardware-buttons";
    bottomToolbar.setAttribute("align", "center");
    bottomToolbar.setAttribute("pack", "center");

    let homeButton = this.chromeDoc.createElement("toolbarbutton");
    homeButton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-home-button";
    homeButton.addEventListener("mousedown", () => {
      SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "Home"});
    });
    homeButton.addEventListener("mouseup", () => {
      SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "Home"});
    });
    bottomToolbar.appendChild(homeButton);
    this.bottomToolbar = bottomToolbar;
    this.container.appendChild(bottomToolbar);
  },

  /**
   * Validate and apply any user input on the editable menulist
   */
  handleManualInput: function RUI_handleManualInput() {
    let userInput = this.menulist.inputField.value;
    let value = INPUT_PARSER.exec(userInput);
    let selectedPreset = this.menuitems.get(this.selectedItem);

    // In case of an invalide value, we show back the last preset
    if (!value || value.length < 3) {
      this.setMenuLabel(this.selectedItem, selectedPreset);
      return;
    }

    this.rotateValue = false;

    if (!selectedPreset.custom) {
      let menuitem = this.customMenuitem;
      this.currentPresetKey = this.customPreset.key;
      this.menulist.selectedItem = menuitem;
    }

    let w = this.customPreset.width = parseInt(value[1], 10);
    let h = this.customPreset.height = parseInt(value[2], 10);

    this.saveCustomSize();
    this.setViewportSize({
      width: w,
      height: h,
    });
  },

  /**
   * Build the presets list and append it to the menupopup.
   *
   * @param aParent menupopup.
   */
  registerPresets: function RUI_registerPresets(aParent) {
    let fragment = this.chromeDoc.createDocumentFragment();
    let doc = this.chromeDoc;

    for (let preset of this.presets) {
      let menuitem = doc.createElement("menuitem");
      menuitem.setAttribute("ispreset", true);
      this.menuitems.set(menuitem, preset);

      if (preset.key === this.currentPresetKey) {
        menuitem.setAttribute("selected", "true");
        this.selectedItem = menuitem;
      }

      if (preset.custom) {
        this.customMenuitem = menuitem;
      }

      this.setMenuLabel(menuitem, preset);
      fragment.appendChild(menuitem);
    }
    aParent.appendChild(fragment);
  },

  /**
   * Set the menuitem label of a preset.
   *
   * @param aMenuitem menuitem to edit.
   * @param aPreset associated preset.
   */
  setMenuLabel: function RUI_setMenuLabel(aMenuitem, aPreset) {
    let size = SHARED_L10N.getFormatStr("dimensions",
      Math.round(aPreset.width), Math.round(aPreset.height));

    // .inputField might be not reachable yet (async XBL loading)
    if (this.menulist.inputField) {
      this.menulist.inputField.value = size;
    }

    if (aPreset.custom) {
      size = this.strings.formatStringFromName("responsiveUI.customResolution", [size], 1);
    } else if (aPreset.name != null && aPreset.name !== "") {
      size = this.strings.formatStringFromName("responsiveUI.namedResolution", [size, aPreset.name], 2);
    }

    aMenuitem.setAttribute("label", size);
  },

  /**
   * When a preset is selected, apply it.
   */
  presetSelected: function RUI_presetSelected() {
    if (this.menulist.selectedItem.getAttribute("ispreset") === "true") {
      this.selectedItem = this.menulist.selectedItem;

      this.rotateValue = false;
      let selectedPreset = this.menuitems.get(this.selectedItem);
      this.loadPreset(selectedPreset);
      this.currentPresetKey = selectedPreset.key;
      this.saveCurrentPreset();

      // Update the buttons hidden status according to the new selected preset
      if (selectedPreset == this.customPreset) {
        this.addbutton.hidden = false;
        this.removebutton.hidden = true;
      } else {
        this.addbutton.hidden = true;
        this.removebutton.hidden = false;
      }
    }
  },

  /**
   * Apply a preset.
   */
  loadPreset(preset) {
    this.setViewportSize(preset);
  },

  /**
   * Add a preset to the list and the memory
   */
  addPreset: function RUI_addPreset() {
    let w = this.customPreset.width;
    let h = this.customPreset.height;
    let newName = {};

    let title = this.strings.GetStringFromName("responsiveUI.customNamePromptTitle1");
    let message = this.strings.formatStringFromName("responsiveUI.customNamePromptMsg", [w, h], 2);
    let promptOk = Services.prompt.prompt(null, title, message, newName, null, {});

    if (!promptOk) {
      // Prompt has been cancelled
      this.menulist.selectedItem = this.selectedItem;
      return;
    }

    let newPreset = {
      key: w + "x" + h,
      name: newName.value,
      width: w,
      height: h
    };

    this.presets.push(newPreset);

    // Sort the presets according to width/height ascending order
    this.presets.sort(function RUI_sortPresets(aPresetA, aPresetB) {
      // We keep custom preset at first
      if (aPresetA.custom && !aPresetB.custom) {
        return 1;
      }
      if (!aPresetA.custom && aPresetB.custom) {
        return -1;
      }

      if (aPresetA.width === aPresetB.width) {
        if (aPresetA.height === aPresetB.height) {
          return 0;
        } else {
          return aPresetA.height > aPresetB.height;
        }
      } else {
        return aPresetA.width > aPresetB.width;
      }
    });

    this.savePresets();

    let newMenuitem = this.chromeDoc.createElement("menuitem");
    newMenuitem.setAttribute("ispreset", true);
    this.setMenuLabel(newMenuitem, newPreset);

    this.menuitems.set(newMenuitem, newPreset);
    let idx = this.presets.indexOf(newPreset);
    let beforeMenuitem = this.menulist.firstChild.childNodes[idx + 1];
    this.menulist.firstChild.insertBefore(newMenuitem, beforeMenuitem);

    this.menulist.selectedItem = newMenuitem;
    this.currentPresetKey = newPreset.key;
    this.saveCurrentPreset();
  },

  /**
   * remove a preset from the list and the memory
   */
  removePreset: function RUI_removePreset() {
    let selectedPreset = this.menuitems.get(this.selectedItem);
    let w = selectedPreset.width;
    let h = selectedPreset.height;

    this.presets.splice(this.presets.indexOf(selectedPreset), 1);
    this.menulist.firstChild.removeChild(this.selectedItem);
    this.menuitems.delete(this.selectedItem);

    this.customPreset.width = w;
    this.customPreset.height = h;
    let menuitem = this.customMenuitem;
    this.setMenuLabel(menuitem, this.customPreset);
    this.menulist.selectedItem = menuitem;
    this.currentPresetKey = this.customPreset.key;

    this.setViewportSize({
      width: w,
      height: h,
    });

    this.savePresets();
  },

  /**
   * Swap width and height.
   */
  rotate: function RUI_rotate() {
    let selectedPreset = this.menuitems.get(this.selectedItem);
    let width = this.rotateValue ? selectedPreset.height : selectedPreset.width;
    let height = this.rotateValue ? selectedPreset.width : selectedPreset.height;

    this.setViewportSize({
      width: height,
      height: width,
    });

    if (selectedPreset.custom) {
      this.saveCustomSize();
    } else {
      this.rotateValue = !this.rotateValue;
      this.saveCurrentPreset();
    }
  },

  /**
   * Take a screenshot of the page.
   *
   * @param aFileName name of the screenshot file (used for tests).
   */
  screenshot: function RUI_screenshot(aFileName) {
    let filename = aFileName;
    if (!filename) {
      let date = new Date();
      let month = ("0" + (date.getMonth() + 1)).substr(-2, 2);
      let day = ("0" + date.getDate()).substr(-2, 2);
      let dateString = [date.getFullYear(), month, day].join("-");
      let timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
      filename = this.strings.formatStringFromName("responsiveUI.screenshotGeneratedFilename", [dateString, timeString], 2);
    }
    let mm = this.tab.linkedBrowser.messageManager;
    let chromeWindow = this.chromeDoc.defaultView;
    let doc = chromeWindow.document;
    function onScreenshot(aMessage) {
      mm.removeMessageListener("ResponsiveMode:RequestScreenshot:Done", onScreenshot);
      chromeWindow.saveURL(aMessage.data, filename + ".png", null, true, true, doc.documentURIObject, doc);
    }
    mm.addMessageListener("ResponsiveMode:RequestScreenshot:Done", onScreenshot);
    mm.sendAsyncMessage("ResponsiveMode:RequestScreenshot");
  },

  /**
   * Enable/Disable mouse -> touch events translation.
   */
  enableTouch: function RUI_enableTouch() {
    this.touchbutton.setAttribute("checked", "true");
    return this.touchEventSimulator.start();
  },

  disableTouch: function RUI_disableTouch() {
    this.touchbutton.removeAttribute("checked");
    return this.touchEventSimulator.stop();
  },

  hideTouchNotification: function RUI_hideTouchNotification() {
    let nbox = this.mainWindow.gBrowser.getNotificationBox(this.browser);
    let n = nbox.getNotificationWithValue("responsive-ui-need-reload");
    if (n) {
      n.close();
    }
  },

  toggleTouch: Task.async(function* () {
    this.hideTouchNotification();
    if (this.touchEventSimulator.enabled) {
      this.disableTouch();
    } else {
      let isReloadNeeded = yield this.enableTouch();
      if (isReloadNeeded) {
        if (Services.prefs.getBoolPref("devtools.responsiveUI.no-reload-notification")) {
          return;
        }

        let nbox = this.mainWindow.gBrowser.getNotificationBox(this.browser);

        var buttons = [{
          label: this.strings.GetStringFromName("responsiveUI.notificationReload"),
          callback: () => {
            this.browser.reload();
          },
          accessKey: this.strings.GetStringFromName("responsiveUI.notificationReload_accesskey"),
        }, {
          label: this.strings.GetStringFromName("responsiveUI.dontShowReloadNotification"),
          callback: function () {
            Services.prefs.setBoolPref("devtools.responsiveUI.no-reload-notification", true);
          },
          accessKey: this.strings.GetStringFromName("responsiveUI.dontShowReloadNotification_accesskey"),
        }];

        nbox.appendNotification(
           this.strings.GetStringFromName("responsiveUI.needReload"),
           "responsive-ui-need-reload",
           null,
           nbox.PRIORITY_INFO_LOW,
           buttons);
      }
    }
  }),

  waitForReload() {
    let navigatedDeferred = promise.defer();
    let onNavigated = (_, { state }) => {
      if (state != "stop") {
        return;
      }
      this.client.removeListener("tabNavigated", onNavigated);
      navigatedDeferred.resolve();
    };
    this.client.addListener("tabNavigated", onNavigated);
    return navigatedDeferred.promise;
  },

  /**
   * Change the user agent string
   */
  changeUA: Task.async(function* () {
    let value = this.userAgentInput.value;
    let changed;
    if (value) {
      changed = yield this.emulationFront.setUserAgentOverride(value);
      this.userAgentInput.setAttribute("attention", "true");
    } else {
      changed = yield this.emulationFront.clearUserAgentOverride();
      this.userAgentInput.removeAttribute("attention");
    }
    if (changed) {
      let reloaded = this.waitForReload();
      this.tab.linkedBrowser.reload();
      yield reloaded;
    }
    ResponsiveUIManager.emit("userAgentChanged", { tab: this.tab });
  }),

  /**
   * Get the current width and height.
   */
  getSize() {
    let width = Number(this.stack.style.minWidth.replace("px", ""));
    let height = Number(this.stack.style.minHeight.replace("px", ""));
    return {
      width,
      height,
    };
  },

  /**
   * Change the size of the viewport.
   */
  setViewportSize({ width, height }) {
    debug(`SET SIZE TO ${width} x ${height}`);
    if (width) {
      this.setWidth(width);
    }
    if (height) {
      this.setHeight(height);
    }
  },

  setWidth: function RUI_setWidth(aWidth) {
    aWidth = Math.min(Math.max(aWidth, MIN_WIDTH), MAX_WIDTH);
    this.stack.style.maxWidth = this.stack.style.minWidth = aWidth + "px";

    if (!this.ignoreX)
      this.resizeBarH.setAttribute("left", Math.round(aWidth / 2));

    let selectedPreset = this.menuitems.get(this.selectedItem);

    if (selectedPreset.custom) {
      selectedPreset.width = aWidth;
      this.setMenuLabel(this.selectedItem, selectedPreset);
    }
  },

  setHeight: function RUI_setHeight(aHeight) {
    aHeight = Math.min(Math.max(aHeight, MIN_HEIGHT), MAX_HEIGHT);
    this.stack.style.maxHeight = this.stack.style.minHeight = aHeight + "px";

    if (!this.ignoreY)
      this.resizeBarV.setAttribute("top", Math.round(aHeight / 2));

    let selectedPreset = this.menuitems.get(this.selectedItem);
    if (selectedPreset.custom) {
      selectedPreset.height = aHeight;
      this.setMenuLabel(this.selectedItem, selectedPreset);
    }
  },
  /**
   * Start the process of resizing the browser.
   *
   * @param aEvent
   */
  startResizing: function RUI_startResizing(aEvent) {
    let selectedPreset = this.menuitems.get(this.selectedItem);

    if (!selectedPreset.custom) {
      this.customPreset.width = this.rotateValue ? selectedPreset.height : selectedPreset.width;
      this.customPreset.height = this.rotateValue ? selectedPreset.width : selectedPreset.height;

      let menuitem = this.customMenuitem;
      this.setMenuLabel(menuitem, this.customPreset);

      this.currentPresetKey = this.customPreset.key;
      this.menulist.selectedItem = menuitem;
    }
    this.mainWindow.addEventListener("mouseup", this.bound_stopResizing, true);
    this.mainWindow.addEventListener("mousemove", this.bound_onDrag, true);
    this.container.style.pointerEvents = "none";

    this._resizing = true;
    this.stack.setAttribute("notransition", "true");

    this.lastScreenX = aEvent.screenX;
    this.lastScreenY = aEvent.screenY;

    this.ignoreY = (aEvent.target === this.resizeBarV);
    this.ignoreX = (aEvent.target === this.resizeBarH);

    this.isResizing = true;
  },

  /**
   * Resizing on mouse move.
   *
   * @param aEvent
   */
  onDrag: function RUI_onDrag(aEvent) {
    let shift = aEvent.shiftKey;
    let ctrl = !aEvent.shiftKey && aEvent.ctrlKey;

    let screenX = aEvent.screenX;
    let screenY = aEvent.screenY;

    let deltaX = screenX - this.lastScreenX;
    let deltaY = screenY - this.lastScreenY;

    if (this.ignoreY)
      deltaY = 0;
    if (this.ignoreX)
      deltaX = 0;

    if (ctrl) {
      deltaX /= SLOW_RATIO;
      deltaY /= SLOW_RATIO;
    }

    let width = this.customPreset.width + deltaX;
    let height = this.customPreset.height + deltaY;

    if (shift) {
      let roundedWidth, roundedHeight;
      roundedWidth = 10 * Math.floor(width / ROUND_RATIO);
      roundedHeight = 10 * Math.floor(height / ROUND_RATIO);
      screenX += roundedWidth - width;
      screenY += roundedHeight - height;
      width = roundedWidth;
      height = roundedHeight;
    }

    if (width < MIN_WIDTH) {
      width = MIN_WIDTH;
    } else {
      this.lastScreenX = screenX;
    }

    if (height < MIN_HEIGHT) {
      height = MIN_HEIGHT;
    } else {
      this.lastScreenY = screenY;
    }

    this.setViewportSize({ width, height });
  },

  /**
   * Stop End resizing
   */
  stopResizing: function RUI_stopResizing() {
    this.container.style.pointerEvents = "auto";

    this.mainWindow.removeEventListener("mouseup", this.bound_stopResizing, true);
    this.mainWindow.removeEventListener("mousemove", this.bound_onDrag, true);

    this.saveCustomSize();

    delete this._resizing;
    if (this.transitionsEnabled) {
      this.stack.removeAttribute("notransition");
    }
    this.ignoreY = false;
    this.ignoreX = false;
    this.isResizing = false;
  },

  /**
   * Store the custom size as a pref.
   */
  saveCustomSize: function RUI_saveCustomSize() {
    Services.prefs.setIntPref("devtools.responsiveUI.customWidth", this.customPreset.width);
    Services.prefs.setIntPref("devtools.responsiveUI.customHeight", this.customPreset.height);
  },

  /**
   * Store the current preset as a pref.
   */
  saveCurrentPreset: function RUI_saveCurrentPreset() {
    Services.prefs.setCharPref("devtools.responsiveUI.currentPreset", this.currentPresetKey);
    Services.prefs.setBoolPref("devtools.responsiveUI.rotate", this.rotateValue);
  },

  /**
   * Store the list of all registered presets as a pref.
   */
  savePresets: function RUI_savePresets() {
    // We exclude the custom one
    let registeredPresets = this.presets.filter(function (aPreset) {
      return !aPreset.custom;
    });

    Services.prefs.setCharPref("devtools.responsiveUI.presets", JSON.stringify(registeredPresets));
  },
};

loader.lazyGetter(ResponsiveUI.prototype, "strings", function () {
  return Services.strings.createBundle("chrome://devtools/locale/responsiveUI.properties");
});