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

function compStyle(property) {
  return getComputedStyle(window.document.body).getPropertyValue(property);
}

function toHumanReadable(num, fpDecimals) {
  const prefixes = [..." kMGTPEYZYRQ"];
  const inner = (curr, remainingPrefixes) => {
    return Math.abs(curr >= 1000)
      ? inner(curr / 1000, remainingPrefixes.slice(1, -1))
      : [curr.toFixed(fpDecimals), remainingPrefixes[0].trimEnd()];
  };
  return inner(num, prefixes);
}

class GraphImpl {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  // The returns the earliest time to graph
  startTime = dataSet => (dataSet.earliest() || { time: 0 }).time;

  // Returns the latest time to graph
  stopTime = dataSet => (dataSet.latest() || { time: 0 }).time;

  // The default background color
  bgColor = () => compStyle("--background-color-canvas");
  // The color to use for value graph lines
  valueLineColor = () => "grey";
  // The color to use for average graph lines and text
  averageLineColor = () => "green";
  // The color to use for the max value
  maxColor = () => "grey";
  // The color to use for the min value
  minColor = () => "grey";
  // Title color
  titleColor = () => compStyle("--text-color");
  // The color to use for a data point at a time.
  // The destination x coordinate and graph width are also provided.
  datumColor = () => "red";

  // Returns an SVG element that needs to be inserted into the DOM for display
  drawSparseValues = (dataSet, title, config) => {
    const { width, height } = this;
    // Clear the canvas
    const bgColor = this.bgColor();
    const mkSvgElem = type =>
      document.createElementNS("http://www.w3.org/2000/svg", type);
    const svgText = (x, y, text, color, subclass) => {
      const txt = mkSvgElem("text");
      txt.setAttribute("x", x);
      txt.setAttribute("y", y);
      txt.setAttribute("stroke", bgColor);
      txt.setAttribute("fill", color);
      txt.setAttribute("paint-order", "stroke");
      txt.textContent = text;
      txt.classList.add(["graph-text", ...[subclass]].join("-"));
      return txt;
    };
    const svg = mkSvgElem("svg");
    svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
    svg.setAttribute("version", "1.1");
    svg.setAttribute("width", width);
    svg.setAttribute("height", height);
    svg.classList.add("svg-graph");
    const rect = mkSvgElem("rect");
    rect.setAttribute("fill", bgColor);
    rect.setAttribute("width", width);
    rect.setAttribute("height", height);
    svg.appendChild(rect);

    if (config.toRate) {
      dataSet = dataSet.toRateDataSet();
    }

    const startTime = this.startTime(dataSet);
    const stopTime = this.stopTime(dataSet);
    let timeFilter = ({ time }) => time >= startTime && time <= stopTime;

    let avgDataSet = { dataPoints: [] };
    if (!config.noAvg) {
      avgDataSet = dataSet.toRollingAverageDataSet(config.avgPoints);
    }

    let filtered = dataSet.filter(timeFilter);
    if (filtered.dataPoints == []) {
      return svg;
    }

    let range = filtered.dataRange();
    if (range === undefined) {
      return svg;
    }
    let { min: rangeMin, max: rangeMax } = range;

    // Adjust the _display_ range to lift flat lines towards the center
    if (rangeMin == rangeMax) {
      rangeMin = rangeMin - 1;
      rangeMax = rangeMax + 1;
    }
    const yFactor = (height - 26) / (1 + rangeMax - rangeMin);
    const yPos = ({ value }) =>
      this.height - 1 - (value - rangeMin) * yFactor - 13;
    const xFactor = width / (1 + stopTime - startTime);
    const xPos = ({ time }) => (time - startTime) * xFactor;

    const toPathStr = dataPoints =>
      [...dataPoints]
        .map(
          (datum, index) => `${index ? "L" : "M"}${xPos(datum)} ${yPos(datum)}`
        )
        .join(" ");
    const valuePath = mkSvgElem("path");
    valuePath.setAttribute("d", toPathStr(filtered.dataPoints));
    valuePath.setAttribute("stroke", this.valueLineColor());
    valuePath.setAttribute("fill", "none");
    svg.appendChild(valuePath);

    const avgPath = mkSvgElem("path");
    avgPath.setAttribute("d", toPathStr(avgDataSet.dataPoints));
    avgPath.setAttribute("stroke", this.averageLineColor());
    avgPath.setAttribute("fill", "none");
    svg.appendChild(avgPath);
    const fixed = num => num.toFixed(config.fixedPointDecimals);
    const formatValue = value =>
      config.toHuman
        ? toHumanReadable(value, config.fixedPointDecimals).join("")
        : fixed(value);

    // Draw rolling average text
    avgDataSet.dataPoints.slice(-1).forEach(({ value }) => {
      svg.appendChild(
        svgText(
          5,
          height - 4,
          `AVG: ${formatValue(value)}`,
          this.averageLineColor(),
          "avg"
        )
      );
    });

    // Draw title text
    if (title) {
      svg.appendChild(
        svgText(
          5,
          12,
          `${title}${config.toRate ? "/s" : ""}`,
          this.titleColor(this),
          "title"
        )
      );
    }

    // Draw max value text
    const maxText = svgText(
      width - 5,
      12,
      `Max: ${formatValue(range.max)}`,
      this.maxColor(range.max),
      "max"
    );
    maxText.setAttribute("text-anchor", "end");
    svg.appendChild(maxText);

    // Draw min value text
    const minText = svgText(
      width - 5,
      height - 4,
      `Min: ${formatValue(range.min)}`,
      this.minColor(range.min),
      "min"
    );
    minText.setAttribute("text-anchor", "end");
    svg.appendChild(minText);
    return svg;
  };
}

export { GraphImpl };
