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

/* globals XPCOMUtils, NewTabPrefsProvider, Services,
  Locale, UpdateUtils, NewTabRemoteResources
*/
"use strict";

const {utils: Cu, interfaces: Ci} = Components;

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

XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
                                  "resource://gre/modules/UpdateUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
                                  "resource:///modules/NewTabPrefsProvider.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Locale",
                                  "resource://gre/modules/Locale.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NewTabRemoteResources",
                                  "resource:///modules/NewTabRemoteResources.jsm");

const LOCAL_NEWTAB_URL = "chrome://browser/content/newtab/newTab.xhtml";

const REMOTE_NEWTAB_PATH = "/newtab/v%VERSION%/%CHANNEL%/%LOCALE%/index.html";

const ABOUT_URL = "about:newtab";

// Pref that tells if remote newtab is enabled
const PREF_REMOTE_ENABLED = "browser.newtabpage.remote";

// Pref branch necesssary for testing
const PREF_REMOTE_CS_TEST = "browser.newtabpage.remote.content-signing-test";

// The preference that tells whether to match the OS locale
const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";

// The preference that tells what locale the user selected
const PREF_SELECTED_LOCALE = "general.useragent.locale";

// The preference that tells what remote mode is enabled.
const PREF_REMOTE_MODE = "browser.newtabpage.remote.mode";

// The preference that tells which remote version is expected.
const PREF_REMOTE_VERSION = "browser.newtabpage.remote.version";

const VALID_CHANNELS = new Set(["esr", "release", "beta", "aurora", "nightly"]);

function AboutNewTabService() {
  NewTabPrefsProvider.prefs.on(PREF_REMOTE_ENABLED, this._handleToggleEvent.bind(this));

  this._updateRemoteMaybe = this._updateRemoteMaybe.bind(this);

  // trigger remote change if needed, according to pref
  this.toggleRemote(Services.prefs.getBoolPref(PREF_REMOTE_ENABLED));
}

/*
 * A service that allows for the overriding, at runtime, of the newtab page's url.
 * Additionally, the service manages pref state between a remote and local newtab page.
 *
 * There is tight coupling with browser/about/AboutRedirector.cpp.
 *
 * 1. Browser chrome access:
 *
 * When the user issues a command to open a new tab page, usually clicking a button
 * in the browser chrome or using shortcut keys, the browser chrome code invokes the
 * service to obtain the newtab URL. It then loads that URL in a new tab.
 *
 * When not overridden, the default URL emitted by the service is "about:newtab".
 * When overridden, it returns the overriden URL.
 *
 * 2. Redirector Access:
 *
 * When the URL loaded is about:newtab, the default behavior, or when entered in the
 * URL bar, the redirector is hit. The service is then called to return either of
 * two URLs, a chrome or remote one, based on the browser.newtabpage.remote pref.
 *
 * NOTE: "about:newtab" will always result in a default newtab page, and never an overridden URL.
 *
 * Access patterns:
 *
 * The behavior is different when accessing the service via browser chrome or via redirector
 * largely to maintain compatibility with expectations of add-on developers.
 *
 * Loading a chrome resource, or an about: URL in the redirector with either the
 * LOAD_NORMAL or LOAD_REPLACE flags yield unexpected behaviors, so a roundtrip
 * to the redirector from browser chrome is avoided.
 */
AboutNewTabService.prototype = {

  _newTabURL: ABOUT_URL,
  _remoteEnabled: false,
  _remoteURL: null,
  _overridden: false,

  classID: Components.ID("{dfcd2adc-7867-4d3a-ba70-17501f208142}"),
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutNewTabService]),
  _xpcom_categories: [{
    service: true
  }],

  _handleToggleEvent(prefName, stateEnabled, forceState) { // jshint unused:false
    if (this.toggleRemote(stateEnabled, forceState)) {
      Services.obs.notifyObservers(null, "newtab-url-changed", ABOUT_URL);
    }
  },

  /**
   * React to changes to the remote newtab pref.
   *
   * If browser.newtabpage.remote is true, this will change the default URL to the
   * remote newtab page URL. If browser.newtabpage.remote is false, the default URL
   * will be a local chrome URL.
   *
   * This will only act if there is a change of state and if not overridden.
   *
   * @returns {Boolean} Returns if there has been a state change
   *
   * @param {Boolean}   stateEnabled    remote state to set to
   * @param {Boolean}   forceState      force state change
   */
  toggleRemote(stateEnabled, forceState) {

    if (!forceState && (this._overriden || stateEnabled === this._remoteEnabled)) {
      // exit there is no change of state
      return false;
    }

    let csTest = Services.prefs.getBoolPref(PREF_REMOTE_CS_TEST);
    if (stateEnabled) {
      if (!csTest) {
        this._remoteURL = this.generateRemoteURL();
      } else {
        this._remoteURL = this._newTabURL;
      }
      NewTabPrefsProvider.prefs.on(
        PREF_SELECTED_LOCALE,
        this._updateRemoteMaybe);
      NewTabPrefsProvider.prefs.on(
        PREF_MATCH_OS_LOCALE,
        this._updateRemoteMaybe);
      NewTabPrefsProvider.prefs.on(
        PREF_REMOTE_MODE,
        this._updateRemoteMaybe);
      NewTabPrefsProvider.prefs.on(
        PREF_REMOTE_VERSION,
        this._updateRemoteMaybe);
      this._remoteEnabled = true;
    } else {
      NewTabPrefsProvider.prefs.off(PREF_SELECTED_LOCALE, this._updateRemoteMaybe);
      NewTabPrefsProvider.prefs.off(PREF_MATCH_OS_LOCALE, this._updateRemoteMaybe);
      NewTabPrefsProvider.prefs.off(PREF_REMOTE_MODE, this._updateRemoteMaybe);
      NewTabPrefsProvider.prefs.off(PREF_REMOTE_VERSION, this._updateRemoteMaybe);
      this._remoteEnabled = false;
    }
    if (!csTest) {
      this._newTabURL = ABOUT_URL;
    }
    return true;
  },

  /*
   * Generate a default url based on remote mode, version, locale and update channel
   */
  generateRemoteURL() {
    let releaseName = this.releaseFromUpdateChannel(UpdateUtils.UpdateChannel);
    let path = REMOTE_NEWTAB_PATH
      .replace("%VERSION%", this.remoteVersion)
      .replace("%LOCALE%", Locale.getLocale())
      .replace("%CHANNEL%", releaseName);
    let mode = Services.prefs.getCharPref(PREF_REMOTE_MODE, "production");
    if (!(mode in NewTabRemoteResources.MODE_CHANNEL_MAP)) {
      mode = "production";
    }
    return NewTabRemoteResources.MODE_CHANNEL_MAP[mode].origin + path;
  },

  /*
   * Returns the default URL.
   *
   * This URL only depends on the browser.newtabpage.remote pref. Overriding
   * the newtab page has no effect on the result of this function.
   *
   * The result is also the remote URL if this is in a test (PREF_REMOTE_CS_TEST)
   *
   * @returns {String} the default newtab URL, remote or local depending on browser.newtabpage.remote
   */
  get defaultURL() {
    let csTest = Services.prefs.getBoolPref(PREF_REMOTE_CS_TEST);
    if (this._remoteEnabled || csTest)  {
      return this._remoteURL;
    }
    return LOCAL_NEWTAB_URL;
  },

  /*
   * Updates the remote location when the page is not overriden.
   *
   * Useful when there is a dependent pref change
   */
  _updateRemoteMaybe() {
    if (!this._remoteEnabled || this._overridden) {
      return;
    }

    let url = this.generateRemoteURL();
    if (url !== this._remoteURL) {
      this._remoteURL = url;
      Services.obs.notifyObservers(null, "newtab-url-changed",
        this._remoteURL);
    }
  },

  /**
   * Returns the release name from an Update Channel name
   *
   * @returns {String} a release name based on the update channel. Defaults to nightly
   */
  releaseFromUpdateChannel(channelName) {
    return VALID_CHANNELS.has(channelName) ? channelName : "nightly";
  },

  get newTabURL() {
    return this._newTabURL;
  },

  get remoteVersion() {
    return Services.prefs.getCharPref(PREF_REMOTE_VERSION, "1");
  },

  get remoteReleaseName() {
    return this.releaseFromUpdateChannel(UpdateUtils.UpdateChannel);
  },

  set newTabURL(aNewTabURL) {
    let csTest = Services.prefs.getBoolPref(PREF_REMOTE_CS_TEST);
    aNewTabURL = aNewTabURL.trim();
    if (aNewTabURL === ABOUT_URL) {
      // avoid infinite redirects in case one sets the URL to about:newtab
      this.resetNewTabURL();
      return;
    } else if (aNewTabURL === "") {
      aNewTabURL = "about:blank";
    }
    let remoteURL = this.generateRemoteURL();
    let prefRemoteEnabled = Services.prefs.getBoolPref(PREF_REMOTE_ENABLED);
    let isResetLocal = !prefRemoteEnabled && aNewTabURL === LOCAL_NEWTAB_URL;
    let isResetRemote = prefRemoteEnabled && aNewTabURL === remoteURL;

    if (isResetLocal || isResetRemote) {
      if (this._overriden && !csTest) {
        // only trigger a reset if previously overridden and this is no test
        this.resetNewTabURL();
      }
      return;
    }
    // turn off remote state if needed
    if (!csTest) {
      this.toggleRemote(false);
    } else {
      // if this is a test, we want the remoteURL to be set
      this._remoteURL = aNewTabURL;
    }
    this._newTabURL = aNewTabURL;
    this._overridden = true;
    Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL);
  },

  get overridden() {
    return this._overridden;
  },

  get remoteEnabled() {
    return this._remoteEnabled;
  },

  resetNewTabURL() {
    this._overridden = false;
    this._newTabURL = ABOUT_URL;
    this.toggleRemote(Services.prefs.getBoolPref(PREF_REMOTE_ENABLED), true);
    Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL);
  }
};

this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AboutNewTabService]);
