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

/* global content */
/* exported startup, shutdown, install, uninstall */

"use strict";

const Cu = Components.utils;
const Ci = Components.interfaces;
const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});

function actionOccurred(id) {
  let {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
  let Telemetry = require("devtools/client/shared/telemetry");
  let telemetry = new Telemetry();
  telemetry.actionOccurred(id);
}

// Helper to listen to a key on all windows
function MultiWindowKeyListener({ keyCode, ctrlKey, altKey, callback }) {
  let keyListener = function (event) {
    if (event.ctrlKey == !!ctrlKey &&
        event.altKey == !!altKey &&
        event.keyCode === keyCode) {
      callback(event);

      // Call preventDefault to avoid duplicated events when
      // doing the key stroke within a tab.
      event.preventDefault();
    }
  };

  let observer = function (window, topic, data) {
    // Listen on keyup to call keyListener only once per stroke
    if (topic === "domwindowopened") {
      window.addEventListener("keyup", keyListener);
    } else {
      window.removeEventListener("keyup", keyListener);
    }
  };

  return {
    start: function () {
      // Automatically process already opened windows
      let e = Services.ww.getWindowEnumerator();
      while (e.hasMoreElements()) {
        let window = e.getNext();
        observer(window, "domwindowopened", null);
      }
      // And listen for new ones to come
      Services.ww.registerNotification(observer);
    },

    stop: function () {
      Services.ww.unregisterNotification(observer);
      let e = Services.ww.getWindowEnumerator();
      while (e.hasMoreElements()) {
        let window = e.getNext();
        observer(window, "domwindowclosed", null);
      }
    }
  };
}

let getTopLevelWindow = function (window) {
  return window.QueryInterface(Ci.nsIInterfaceRequestor)
               .getInterface(Ci.nsIWebNavigation)
               .QueryInterface(Ci.nsIDocShellTreeItem)
               .rootTreeItem
               .QueryInterface(Ci.nsIInterfaceRequestor)
               .getInterface(Ci.nsIDOMWindow);
};

function reload(event) {
  // We automatically reload the toolbox if we are on a browser tab
  // with a toolbox already opened
  let top = getTopLevelWindow(event.view);
  let isBrowser = top.location.href.includes("/browser.xul");
  let reloadToolbox = false;
  if (isBrowser && top.gBrowser) {
    // We do not use any devtools code before the call to Loader.jsm reload as
    // any attempt to use Loader.jsm to load a module will instanciate a new
    // Loader.
    let nbox = top.gBrowser.getNotificationBox();
    reloadToolbox =
      top.document.getAnonymousElementByAttribute(nbox, "class",
        "devtools-toolbox-bottom-iframe") ||
      top.document.getAnonymousElementByAttribute(nbox, "class",
        "devtools-toolbox-side-iframe") ||
      Services.wm.getMostRecentWindow("devtools:toolbox");
  }
  let browserConsole = Services.wm.getMostRecentWindow("devtools:webconsole");
  let reopenBrowserConsole = false;
  if (browserConsole) {
    browserConsole.close();
    reopenBrowserConsole = true;
  }

  dump("Reload DevTools.  (reload-toolbox:" + reloadToolbox + ")\n");

  // Invalidate xul cache in order to see changes made to chrome:// files
  Services.obs.notifyObservers(null, "startupcache-invalidate", null);

  // This frame script is going to be executed in all processes:
  // parent and child
  Services.ppmm.loadProcessScript("data:,new " + function () {
    /* Flush message manager cached frame scripts as well as chrome locales */
    let obs = Components.classes["@mozilla.org/observer-service;1"]
                        .getService(Components.interfaces.nsIObserverService);
    obs.notifyObservers(null, "message-manager-flush-caches", null);

    /* Also purge cached modules in child processes, we do it a few lines after
       in the parent process */
    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
      Services.obs.notifyObservers(null, "devtools-unload", "reload");
    }
  }, false);

  // As we can't get a reference to existing Loader.jsm instances, we send them
  // an observer service notification to unload them.
  Services.obs.notifyObservers(null, "devtools-unload", "reload");

  // Then spawn a brand new Loader.jsm instance and start the main module
  Cu.unload("resource://devtools/shared/Loader.jsm");
  // Also unload all resources loaded as jsm, hopefully all of them are going
  // to be converted into regular modules
  Cu.unload("resource://devtools/client/shared/browser-loader.js");
  Cu.unload("resource://devtools/client/framework/ToolboxProcess.jsm");
  Cu.unload("resource://devtools/shared/apps/Devices.jsm");
  Cu.unload("resource://devtools/client/scratchpad/scratchpad-manager.jsm");
  Cu.unload("resource://devtools/shared/Parser.jsm");
  Cu.unload("resource://devtools/client/shared/DOMHelpers.jsm");
  Cu.unload("resource://devtools/client/shared/widgets/VariablesView.jsm");
  Cu.unload("resource://devtools/client/responsivedesign/responsivedesign.jsm");
  Cu.unload("resource://devtools/client/shared/widgets/AbstractTreeItem.jsm");
  Cu.unload("resource://devtools/shared/deprecated-sync-thenables.js");
  const {devtools} = Cu.import("resource://devtools/shared/Loader.jsm", {});
  devtools.require("devtools/client/framework/devtools-browser");

  // Go over all top level windows to reload all devtools related things
  let windowsEnum = Services.wm.getEnumerator(null);
  while (windowsEnum.hasMoreElements()) {
    let window = windowsEnum.getNext();
    let windowtype = window.document.documentElement.getAttribute("windowtype");
    if (windowtype == "navigator:browser" && window.gBrowser) {
      // Enumerate tabs on firefox windows
      for (let tab of window.gBrowser.tabs) {
        let browser = tab.linkedBrowser;
        let location = browser.documentURI.spec;
        let mm = browser.messageManager;
        // To reload JSON-View tabs and any devtools document
        if (location.startsWith("about:debugging") ||
            location.startsWith("chrome://devtools/")) {
          browser.reload();
        }
        // We have to use a frame script to query "baseURI"
        mm.loadFrameScript("data:text/javascript,new " + function () {
          let isJSONView =
            content.document.baseURI.startsWith("resource://devtools/");
          if (isJSONView) {
            content.location.reload();
          }
        }, false);
      }
    } else if (windowtype === "devtools:webide") {
      window.location.reload();
    }
  }

  if (reloadToolbox) {
    // Reopen the toolbox automatically if we are reloading from toolbox
    // shortcut and are on a browser window.
    // Wait for a second before opening the toolbox to avoid races
    // between the old and the new one.
    let {setTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {});
    setTimeout(() => {
      let { TargetFactory } = devtools.require("devtools/client/framework/target");
      let { gDevTools } = devtools.require("devtools/client/framework/devtools");
      let target = TargetFactory.forTab(top.gBrowser.selectedTab);
      gDevTools.showToolbox(target);
    }, 1000);
  }

  // Browser console document can't just be reloaded.
  // HUDService is going to close it on unload.
  // Instead we have to manually toggle it.
  if (reopenBrowserConsole) {
    let {HUDService} = devtools.require("devtools/client/webconsole/hudservice");
    HUDService.toggleBrowserConsole();
  }

  actionOccurred("reloadAddonReload");
}

let prefs = {
  // Enable dump as some errors are only printed on the stdout
  "browser.dom.window.dump.enabled": true,
  // Enable the browser toolbox and various chrome-only features
  "devtools.chrome.enabled": true,
  "devtools.debugger.remote-enabled": true,
  // Disable the prompt to ease usage of the browser toolbox
  "devtools.debugger.prompt-connection": false,
};
let originalPrefValues = {};

let listener;
function startup() {
  dump("DevTools addon started.\n");
  listener = new MultiWindowKeyListener({
    keyCode: Ci.nsIDOMKeyEvent.DOM_VK_R, ctrlKey: true, altKey: true,
    callback: reload
  });
  listener.start();

  // Toggle development prefs and save original values
  originalPrefValues = {};
  for (let name in prefs) {
    let value = prefs[name];
    let userValue = Services.prefs.getBoolPref(name);
    // Only toggle if the pref isn't already set to the right value
    if (userValue != value) {
      Services.prefs.setBoolPref(name, value);
      originalPrefValues[name] = userValue;
    }
  }
}
function shutdown() {
  listener.stop();
  listener = null;

  // Restore preferences that used to be before the addon was installed
  for (let name in originalPrefValues) {
    let userValue = Services.prefs.getBoolPref(name);
    // Only reset the pref if it hasn't changed
    if (userValue == prefs[name]) {
      Services.prefs.setBoolPref(name, originalPrefValues[name]);
    }
  }
}
function install() {
  actionOccurred("reloadAddonInstalled");
}
function uninstall() {}