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

/**
 * This is the telemetry module to report metrics for tools.
 *
 * Comprehensive documentation is in docs/frontend/telemetry.md
 */

"use strict";

const TOOLS_OPENED_PREF = "devtools.telemetry.tools.opened.version";

function Telemetry() {
  // Bind pretty much all functions so that callers do not need to.
  this.toolOpened = this.toolOpened.bind(this);
  this.toolClosed = this.toolClosed.bind(this);
  this.log = this.log.bind(this);
  this.logScalar = this.logScalar.bind(this);
  this.logKeyedScalar = this.logKeyedScalar.bind(this);
  this.logOncePerBrowserVersion = this.logOncePerBrowserVersion.bind(this);
  this.destroy = this.destroy.bind(this);

  this._timers = new Map();
}

module.exports = Telemetry;

var Services = require("Services");

Telemetry.prototype = {
  _histograms: {
    toolbox: {
      histogram: "DEVTOOLS_TOOLBOX_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_TOOLBOX_TIME_ACTIVE_SECONDS"
    },
    options: {
      histogram: "DEVTOOLS_OPTIONS_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_OPTIONS_TIME_ACTIVE_SECONDS"
    },
    webconsole: {
      histogram: "DEVTOOLS_WEBCONSOLE_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_WEBCONSOLE_TIME_ACTIVE_SECONDS"
    },
    browserconsole: {
      histogram: "DEVTOOLS_BROWSERCONSOLE_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_BROWSERCONSOLE_TIME_ACTIVE_SECONDS"
    },
    inspector: {
      histogram: "DEVTOOLS_INSPECTOR_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_INSPECTOR_TIME_ACTIVE_SECONDS"
    },
    ruleview: {
      histogram: "DEVTOOLS_RULEVIEW_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_RULEVIEW_TIME_ACTIVE_SECONDS"
    },
    computedview: {
      histogram: "DEVTOOLS_COMPUTEDVIEW_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_COMPUTEDVIEW_TIME_ACTIVE_SECONDS"
    },
    layoutview: {
      histogram: "DEVTOOLS_LAYOUTVIEW_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_LAYOUTVIEW_TIME_ACTIVE_SECONDS"
    },
    fontinspector: {
      histogram: "DEVTOOLS_FONTINSPECTOR_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_FONTINSPECTOR_TIME_ACTIVE_SECONDS"
    },
    animationinspector: {
      histogram: "DEVTOOLS_ANIMATIONINSPECTOR_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_ANIMATIONINSPECTOR_TIME_ACTIVE_SECONDS"
    },
    jsdebugger: {
      histogram: "DEVTOOLS_JSDEBUGGER_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_JSDEBUGGER_TIME_ACTIVE_SECONDS"
    },
    jsbrowserdebugger: {
      histogram: "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_JSBROWSERDEBUGGER_TIME_ACTIVE_SECONDS"
    },
    styleeditor: {
      histogram: "DEVTOOLS_STYLEEDITOR_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_STYLEEDITOR_TIME_ACTIVE_SECONDS"
    },
    shadereditor: {
      histogram: "DEVTOOLS_SHADEREDITOR_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_SHADEREDITOR_TIME_ACTIVE_SECONDS"
    },
    webaudioeditor: {
      histogram: "DEVTOOLS_WEBAUDIOEDITOR_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_WEBAUDIOEDITOR_TIME_ACTIVE_SECONDS"
    },
    canvasdebugger: {
      histogram: "DEVTOOLS_CANVASDEBUGGER_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_CANVASDEBUGGER_TIME_ACTIVE_SECONDS"
    },
    performance: {
      histogram: "DEVTOOLS_JSPROFILER_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_JSPROFILER_TIME_ACTIVE_SECONDS"
    },
    memory: {
      histogram: "DEVTOOLS_MEMORY_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_MEMORY_TIME_ACTIVE_SECONDS"
    },
    netmonitor: {
      histogram: "DEVTOOLS_NETMONITOR_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_NETMONITOR_TIME_ACTIVE_SECONDS"
    },
    storage: {
      histogram: "DEVTOOLS_STORAGE_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_STORAGE_TIME_ACTIVE_SECONDS"
    },
    dom: {
      histogram: "DEVTOOLS_DOM_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_DOM_TIME_ACTIVE_SECONDS"
    },
    paintflashing: {
      histogram: "DEVTOOLS_PAINTFLASHING_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_PAINTFLASHING_TIME_ACTIVE_SECONDS"
    },
    scratchpad: {
      histogram: "DEVTOOLS_SCRATCHPAD_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_SCRATCHPAD_TIME_ACTIVE_SECONDS"
    },
    "scratchpad-window": {
      histogram: "DEVTOOLS_SCRATCHPAD_WINDOW_OPENED_COUNT",
    },
    responsive: {
      histogram: "DEVTOOLS_RESPONSIVE_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_RESPONSIVE_TIME_ACTIVE_SECONDS"
    },
    eyedropper: {
      histogram: "DEVTOOLS_EYEDROPPER_OPENED_COUNT",
    },
    menueyedropper: {
      histogram: "DEVTOOLS_MENU_EYEDROPPER_OPENED_COUNT",
    },
    pickereyedropper: {
      histogram: "DEVTOOLS_PICKER_EYEDROPPER_OPENED_COUNT",
    },
    toolbareyedropper: {
      scalar: "devtools.toolbar.eyedropper.opened",
    },
    copyuniquecssselector: {
      scalar: "devtools.copy.unique.css.selector.opened",
    },
    copyfullcssselector: {
      scalar: "devtools.copy.full.css.selector.opened",
    },
    copyxpath: {
      scalar: "devtools.copy.xpath.opened",
    },
    developertoolbar: {
      histogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_TIME_ACTIVE_SECONDS"
    },
    aboutdebugging: {
      histogram: "DEVTOOLS_ABOUTDEBUGGING_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_ABOUTDEBUGGING_TIME_ACTIVE_SECONDS"
    },
    custom: {
      histogram: "DEVTOOLS_CUSTOM_OPENED_COUNT",
      timerHistogram: "DEVTOOLS_CUSTOM_TIME_ACTIVE_SECONDS"
    },
    gridInspectorShowGridAreasOverlayChecked: {
      scalar: "devtools.grid.showGridAreasOverlay.checked",
    },
    gridInspectorShowGridLineNumbersChecked: {
      scalar: "devtools.grid.showGridLineNumbers.checked",
    },
    gridInspectorShowInfiniteLinesChecked: {
      scalar: "devtools.grid.showInfiniteLines.checked",
    },
  },

  /**
   * Add an entry to a histogram.
   *
   * @param  {String} id
   *         Used to look up the relevant histogram ID and log true to that
   *         histogram.
   */
  toolOpened: function(id) {
    let charts = this._histograms[id] || this._histograms.custom;

    if (charts.histogram) {
      this.log(charts.histogram, true);
    }
    if (charts.timerHistogram) {
      this.startTimer(charts.timerHistogram);
    }
    if (charts.scalar) {
      this.logScalar(charts.scalar, 1);
    }
  },

  /**
   * Record that an action occurred.  Aliases to `toolOpened`, so it's just for
   * readability at the call site for cases where we aren't actually opening
   * tools.
   */
  actionOccurred(id) {
    this.toolOpened(id);
  },

  toolClosed: function(id) {
    let charts = this._histograms[id];

    if (!charts || !charts.timerHistogram) {
      return;
    }

    this.stopTimer(charts.timerHistogram);
  },

  /**
   * Record the start time for a timing-based histogram entry.
   *
   * @param String histogramId
   *        Histogram in which the data is to be stored.
   */
  startTimer: function(histogramId) {
    this._timers.set(histogramId, new Date());
  },

  /**
   * Stop the timer and log elasped time for a timing-based histogram entry.
   *
   * @param String histogramId
   *        Histogram in which the data is to be stored.
   * @param String key [optional]
   *        Optional key for a keyed histogram.
   */
  stopTimer: function(histogramId, key) {
    let startTime = this._timers.get(histogramId);
    if (startTime) {
      let time = (new Date() - startTime) / 1000;
      if (!key) {
        this.log(histogramId, time);
      } else {
        this.logKeyed(histogramId, key, time);
      }
      this._timers.delete(histogramId);
    }
  },

  /**
   * Log a value to a histogram.
   *
   * @param  {String} histogramId
   *         Histogram in which the data is to be stored.
   * @param  value
   *         Value to store.
   */
  log: function(histogramId, value) {
    if (!histogramId) {
      return;
    }

    try {
      let histogram = Services.telemetry.getHistogramById(histogramId);
      histogram.add(value);
    } catch (e) {
      dump(`Warning: An attempt was made to write to the ${histogramId} ` +
           `histogram, which is not defined in Histograms.json\n`);
    }
  },

  /**
   * Log a value to a scalar.
   *
   * @param  {String} scalarId
   *         Scalar in which the data is to be stored.
   * @param  value
   *         Value to store.
   */
  logScalar: function(scalarId, value) {
    if (!scalarId) {
      return;
    }

    try {
      if (isNaN(value)) {
        dump(`Warning: An attempt was made to write a non-numeric value ` +
             `${value} to the ${scalarId} scalar. Only numeric values are ` +
             `allowed.`);

        return;
      }
      Services.telemetry.scalarSet(scalarId, value);
    } catch (e) {
      dump(`Warning: An attempt was made to write to the ${scalarId} ` +
           `scalar, which is not defined in Scalars.yaml\n`);
    }
  },

  /**
   * Log a value to a keyed count scalar.
   *
   * @param  {String} scalarId
   *         Scalar in which the data is to be stored.
   * @param  {String} key
   *         The key within the  scalar.
   * @param  value
   *         Value to store.
   */
  logKeyedScalar: function(scalarId, key, value) {
    if (!scalarId) {
      return;
    }

    try {
      if (isNaN(value)) {
        dump(`Warning: An attempt was made to write a non-numeric value ` +
             `${value} to the ${scalarId} scalar. Only numeric values are ` +
             `allowed.`);

        return;
      }
      Services.telemetry.keyedScalarAdd(scalarId, key, value);
    } catch (e) {
      dump(`Warning: An attempt was made to write to the ${scalarId} ` +
           `scalar, which is not defined in Scalars.yaml\n`);
    }
  },

  /**
   * Log a value to a keyed histogram.
   *
   * @param  {String} histogramId
   *         Histogram in which the data is to be stored.
   * @param  {String} key
   *         The key within the single histogram.
   * @param  [value]
   *         Optional value to store.
   */
  logKeyed: function(histogramId, key, value) {
    if (histogramId) {
      try {
        let histogram = Services.telemetry.getKeyedHistogramById(histogramId);

        if (typeof value === "undefined") {
          histogram.add(key);
        } else {
          histogram.add(key, value);
        }
      } catch (e) {
        dump(`Warning: An attempt was made to write to the ${histogramId} ` +
             `histogram, which is not defined in Histograms.json\n`);
      }
    }
  },

  /**
   * Log info about usage once per browser version. This allows us to discover
   * how many individual users are using our tools for each browser version.
   *
   * @param  {String} perUserHistogram
   *         Histogram in which the data is to be stored.
   */
  logOncePerBrowserVersion: function(perUserHistogram, value) {
    let currentVersion = Services.appinfo.version;
    let latest = Services.prefs.getCharPref(TOOLS_OPENED_PREF);
    let latestObj = JSON.parse(latest);

    let lastVersionHistogramUpdated = latestObj[perUserHistogram];

    if (typeof lastVersionHistogramUpdated == "undefined" ||
        lastVersionHistogramUpdated !== currentVersion) {
      latestObj[perUserHistogram] = currentVersion;
      latest = JSON.stringify(latestObj);
      Services.prefs.setCharPref(TOOLS_OPENED_PREF, latest);
      this.log(perUserHistogram, value);
    }
  },

  destroy: function() {
    for (let histogramId of this._timers.keys()) {
      this.stopTimer(histogramId);
    }
  }
};
