/* 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, APP_SHUTDOWN */
/* exported startup, shutdown, install, uninstall */

"use strict";

const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
const {AppConstants} = Cu.import("resource://gre/modules/AppConstants.jsm", {});

// MultiWindowKeyListener instance for Ctrl+Alt+R key
let listener;
// nsIURI to the addon root folder
let resourceURI;

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

// Synchronously fetch the content of a given URL
function readURI(uri) {
  let stream = NetUtil.newChannel({
    uri: NetUtil.newURI(uri, "UTF-8"),
    loadUsingSystemPrincipal: true}
  ).open2();
  let count = stream.available();
  let data = NetUtil.readInputStreamToString(stream, count, {
    charset: "UTF-8"
  });

  stream.close();

  return data;
}

/**
 * Interpret the processing instructions contained in a preferences file, based on a
 * limited set of supported #if statements. After we ship as an addon, we don't want to
 * introduce anymore processing instructions, so all unrecognized preprocessing
 * instructions will be treated as an error.
 *
 * This function is mostly copied from devtools/client/inspector/webpack/prefs-loader.js
 *
 * @param  {String} content
 *         The string content of a preferences file.
 * @return {String} the content stripped of preprocessing instructions.
 */
function interpretPreprocessingInstructions(content) {
  const ifMap = {
    "#if MOZ_UPDATE_CHANNEL == beta": AppConstants.MOZ_UPDATE_CHANNEL === "beta",
    "#if defined(NIGHTLY_BUILD)": AppConstants.NIGHTLY_BUILD,
    "#ifdef MOZ_DEV_EDITION": AppConstants.MOZ_DEV_EDITION,
    "#ifdef RELEASE_OR_BETA": AppConstants.RELEASE_OR_BETA,
  };

  let lines = content.split("\n");
  let ignoring = false;
  let newLines = [];
  let continuation = false;
  for (let line of lines) {
    if (line.startsWith("#if")) {
      if (!(line in ifMap)) {
        throw new Error("missing line in ifMap: " + line);
      }
      ignoring = !ifMap[line];
    } else if (line.startsWith("#else")) {
      ignoring = !ignoring;
    } else if (line.startsWith("#endif")) {
      ignoring = false;
    }

    let isPrefLine = /^ *(sticky_)?pref\("([^"]+)"/.test(line);
    if (continuation || (!ignoring && isPrefLine)) {
      newLines.push(line);

      // The call to pref(...); might span more than one line.
      continuation = !/\);/.test(line);
    }
  }
  return newLines.join("\n");
}

// Read a preference file and set all of its defined pref as default values
// (This replicates the behavior of preferences files from mozilla-central)
function processPrefFile(url) {
  let content = readURI(url);
  content = interpretPreprocessingInstructions(content);
  content.match(/pref\("[^"]+",\s*.+\s*\)/g).forEach(item => {
    let m = item.match(/pref\("([^"]+)",\s*(.+)\s*\)/);
    let name = m[1];
    let val = m[2].trim();

    // Prevent overriding prefs that have been changed by the user
    if (Services.prefs.prefHasUserValue(name)) {
      return;
    }
    let defaultBranch = Services.prefs.getDefaultBranch("");
    if ((val.startsWith("\"") && val.endsWith("\"")) ||
        (val.startsWith("'") && val.endsWith("'"))) {
      val = val.substr(1, val.length - 2);
      val = val.replace(/\\"/g, '"');
      defaultBranch.setCharPref(name, val);
    } else if (val.match(/[0-9]+/)) {
      defaultBranch.setIntPref(name, parseInt(val, 10));
    } else if (val == "true" || val == "false") {
      defaultBranch.setBoolPref(name, val == "true");
    } else {
      console.log("Unable to match preference type for value:", val);
    }
  });
}

function setPrefs() {
  processPrefFile(resourceURI.spec + "./client/preferences/devtools.js");
  processPrefFile(resourceURI.spec + "./client/preferences/debugger.js");
}

// 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 unload(reason) {
  // This frame script is going to be executed in all processes:
  // parent and child
  Services.ppmm.loadProcessScript("data:,(" + function (scriptReason) {
    /* Flush message manager cached frame scripts as well as chrome locales */
    let obs = Cc["@mozilla.org/observer-service;1"]
                .getService(Ci.nsIObserverService);
    obs.notifyObservers(null, "message-manager-flush-caches");

    /* 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", scriptReason);
    }
  } + ")(\"" + reason.replace(/"/g, '\\"') + "\")", 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", reason);

  // 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/shared/widgets/AbstractTreeItem.jsm");
  Cu.unload("resource://devtools/shared/deprecated-sync-thenables.js");
}

function reload(event) {
  // We automatically reload the toolbox if we are on a browser tab
  // with a toolbox already opened
  let reloadToolbox = false;
  if (event) {
    let top = getTopLevelWindow(event.view);
    let isBrowser = top.location.href.includes("/browser.xul");
    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");

  unload("reload");

  // Update the preferences before starting new code
  setPrefs();

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

  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 top = getTopLevelWindow(event.view);
      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");
}

function startup(data) {
  dump("DevTools addon started.\n");

  resourceURI = data.resourceURI;

  listener = new MultiWindowKeyListener({
    keyCode: Ci.nsIDOMKeyEvent.DOM_VK_R, ctrlKey: true, altKey: true,
    callback: reload
  });
  listener.start();

  reload();
}
function shutdown(data, reason) {
  // On browser shutdown, do not try to cleanup anything
  if (reason == APP_SHUTDOWN) {
    return;
  }

  listener.stop();
  listener = null;

  unload("disable");
}
function install() {
  try {
    actionOccurred("reloadAddonInstalled");
  } catch (e) {
    // When installing on Firefox builds without devtools, telemetry doesn't
    // work yet and throws.
  }
}
function uninstall() {}
