"use strict";

const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
const { AbstractCanvasGraph } = require("devtools/client/shared/widgets/Graphs");

// Bar graph constants.

const GRAPH_DAMPEN_VALUES_FACTOR = 0.9;

const GRAPH_BACKGROUND_COLOR = "#ddd";
// px
const GRAPH_STROKE_WIDTH = 1;
const GRAPH_STROKE_COLOR = "rgba(255,255,255,0.9)";
// px
const GRAPH_HELPER_LINES_DASH = [5];
const GRAPH_HELPER_LINES_WIDTH = 1;

const GRAPH_CLIPHEAD_LINE_COLOR = "#fff";
const GRAPH_SELECTION_LINE_COLOR = "#fff";
const GRAPH_SELECTION_BACKGROUND_COLOR = "rgba(44,187,15,0.25)";
const GRAPH_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
const GRAPH_REGION_BACKGROUND_COLOR = "transparent";
const GRAPH_REGION_STRIPES_COLOR = "rgba(237,38,85,0.2)";

/**
 * A mountain graph, plotting sets of values as line graphs.
 *
 * @see AbstractCanvasGraph for emitted events and other options.
 *
 * Example usage:
 *   let graph = new MountainGraphWidget(node);
 *   graph.format = ...;
 *   graph.once("ready", () => {
 *     graph.setData(src);
 *   });
 *
 * The `graph.format` traits are mandatory and will determine how each
 * section of the moutain will be styled:
 *   [
 *     { color: "#f00", ... },
 *     { color: "#0f0", ... },
 *     ...
 *     { color: "#00f", ... }
 *   ]
 *
 * Data source format:
 *   [
 *     { delta: x1, values: [y11, y12, ... y1n] },
 *     { delta: x2, values: [y21, y22, ... y2n] },
 *     ...
 *     { delta: xm, values: [ym1, ym2, ... ymn] }
 *   ]
 * where the [ymn] values is assumed to aready be normalized from [0..1].
 *
 * @param nsIDOMNode parent
 *        The parent node holding the graph.
 */
this.MountainGraphWidget = function (parent, ...args) {
  AbstractCanvasGraph.apply(this, [parent, "mountain-graph", ...args]);
};

MountainGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
  backgroundColor: GRAPH_BACKGROUND_COLOR,
  strokeColor: GRAPH_STROKE_COLOR,
  strokeWidth: GRAPH_STROKE_WIDTH,
  clipheadLineColor: GRAPH_CLIPHEAD_LINE_COLOR,
  selectionLineColor: GRAPH_SELECTION_LINE_COLOR,
  selectionBackgroundColor: GRAPH_SELECTION_BACKGROUND_COLOR,
  selectionStripesColor: GRAPH_SELECTION_STRIPES_COLOR,
  regionBackgroundColor: GRAPH_REGION_BACKGROUND_COLOR,
  regionStripesColor: GRAPH_REGION_STRIPES_COLOR,

  /**
   * List of rules used to style each section of the mountain.
   * @see constructor
   * @type array
   */
  format: null,

  /**
   * Optionally offsets the `delta` in the data source by this scalar.
   */
  dataOffsetX: 0,

  /**
   * Optionally uses this value instead of the last tick in the data source
   * to compute the horizontal scaling.
   */
  dataDuration: 0,

  /**
   * The scalar used to multiply the graph values to leave some headroom
   * on the top.
   */
  dampenValuesFactor: GRAPH_DAMPEN_VALUES_FACTOR,

  /**
   * Renders the graph's background.
   * @see AbstractCanvasGraph.prototype.buildBackgroundImage
   */
  buildBackgroundImage: function () {
    let { canvas, ctx } = this._getNamedCanvas("mountain-graph-background");
    let width = this._width;
    let height = this._height;

    ctx.fillStyle = this.backgroundColor;
    ctx.fillRect(0, 0, width, height);

    return canvas;
  },

  /**
   * Renders the graph's data source.
   * @see AbstractCanvasGraph.prototype.buildGraphImage
   */
  buildGraphImage: function () {
    if (!this.format || !this.format.length) {
      throw new Error("The graph format traits are mandatory to style " +
                      "the data source.");
    }
    let { canvas, ctx } = this._getNamedCanvas("mountain-graph-data");
    let width = this._width;
    let height = this._height;

    let totalSections = this.format.length;
    let totalTicks = this._data.length;
    let firstTick = totalTicks ? this._data[0].delta : 0;
    let lastTick = totalTicks ? this._data[totalTicks - 1].delta : 0;

    let duration = this.dataDuration || lastTick;
    let dataScaleX = this.dataScaleX = width / (duration - this.dataOffsetX);
    let dataScaleY = this.dataScaleY = height * this.dampenValuesFactor;

    // Draw the graph.

    let prevHeights = Array.from({ length: totalTicks }).fill(0);

    ctx.globalCompositeOperation = "destination-over";
    ctx.strokeStyle = this.strokeColor;
    ctx.lineWidth = this.strokeWidth * this._pixelRatio;

    for (let section = 0; section < totalSections; section++) {
      ctx.fillStyle = this.format[section].color || "#000";
      ctx.beginPath();

      for (let tick = 0; tick < totalTicks; tick++) {
        let { delta, values } = this._data[tick];
        let currX = (delta - this.dataOffsetX) * dataScaleX;
        let currY = values[section] * dataScaleY;
        let prevY = prevHeights[tick];

        if (delta == firstTick) {
          ctx.moveTo(-GRAPH_STROKE_WIDTH, height);
          ctx.lineTo(-GRAPH_STROKE_WIDTH, height - currY - prevY);
        }

        ctx.lineTo(currX, height - currY - prevY);

        if (delta == lastTick) {
          ctx.lineTo(width + GRAPH_STROKE_WIDTH, height - currY - prevY);
          ctx.lineTo(width + GRAPH_STROKE_WIDTH, height);
        }

        prevHeights[tick] += currY;
      }

      ctx.fill();
      ctx.stroke();
    }

    ctx.globalCompositeOperation = "source-over";
    ctx.lineWidth = GRAPH_HELPER_LINES_WIDTH;
    ctx.setLineDash(GRAPH_HELPER_LINES_DASH);

    // Draw the maximum value horizontal line.

    ctx.beginPath();
    let maximumY = height * this.dampenValuesFactor;
    ctx.moveTo(0, maximumY);
    ctx.lineTo(width, maximumY);
    ctx.stroke();

    // Draw the average value horizontal line.

    ctx.beginPath();
    let averageY = height / 2 * this.dampenValuesFactor;
    ctx.moveTo(0, averageY);
    ctx.lineTo(width, averageY);
    ctx.stroke();

    return canvas;
  }
});

module.exports = MountainGraphWidget;
