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

"use strict";

const EventEmitter = require("devtools/shared/event-emitter");
const { getCurrentZoom,
  setIgnoreLayoutChanges } = require("devtools/shared/layout/utils");
const {
  CanvasFrameAnonymousContentHelper,
  createSVGNode, createNode } = require("./utils/markup");

// Maximum size, in pixel, for the horizontal ruler and vertical ruler
// used by RulersHighlighter
const RULERS_MAX_X_AXIS = 10000;
const RULERS_MAX_Y_AXIS = 15000;
// Number of steps after we add a graduation, marker and text in
// RulersHighliter; currently the unit is in pixel.
const RULERS_GRADUATION_STEP = 5;
const RULERS_MARKER_STEP = 50;
const RULERS_TEXT_STEP = 100;

/**
 * The RulersHighlighter is a class that displays both horizontal and
 * vertical rules on the page, along the top and left edges, with pixel
 * graduations, useful for users to quickly check distances
 */
function RulersHighlighter(highlighterEnv) {
  this.env = highlighterEnv;
  this.markup = new CanvasFrameAnonymousContentHelper(highlighterEnv,
    this._buildMarkup.bind(this));

  let { pageListenerTarget } = highlighterEnv;
  pageListenerTarget.addEventListener("scroll", this);
  pageListenerTarget.addEventListener("pagehide", this);
}

RulersHighlighter.prototype = {
  typeName: "RulersHighlighter",

  ID_CLASS_PREFIX: "rulers-highlighter-",

  _buildMarkup: function() {
    let { window } = this.env;
    let prefix = this.ID_CLASS_PREFIX;

    function createRuler(axis, size) {
      let width, height;
      let isHorizontal = true;

      if (axis === "x") {
        width = size;
        height = 16;
      } else if (axis === "y") {
        width = 16;
        height = size;
        isHorizontal = false;
      } else {
        throw new Error(
          `Invalid type of axis given; expected "x" or "y" but got "${axis}"`);
      }

      let g = createSVGNode(window, {
        nodeType: "g",
        attributes: {
          id: `${axis}-axis`
        },
        parent: svg,
        prefix
      });

      createSVGNode(window, {
        nodeType: "rect",
        attributes: {
          y: isHorizontal ? 0 : 16,
          width,
          height
        },
        parent: g
      });

      let gRule = createSVGNode(window, {
        nodeType: "g",
        attributes: {
          id: `${axis}-axis-ruler`
        },
        parent: g,
        prefix
      });

      let pathGraduations = createSVGNode(window, {
        nodeType: "path",
        attributes: {
          "class": "ruler-graduations",
          width,
          height
        },
        parent: gRule,
        prefix
      });

      let pathMarkers = createSVGNode(window, {
        nodeType: "path",
        attributes: {
          "class": "ruler-markers",
          width,
          height
        },
        parent: gRule,
        prefix
      });

      let gText = createSVGNode(window, {
        nodeType: "g",
        attributes: {
          id: `${axis}-axis-text`,
          "class": (isHorizontal ? "horizontal" : "vertical") + "-labels"
        },
        parent: g,
        prefix
      });

      let dGraduations = "";
      let dMarkers = "";
      let graduationLength;

      for (let i = 0; i < size; i += RULERS_GRADUATION_STEP) {
        if (i === 0) {
          continue;
        }

        graduationLength = (i % 2 === 0) ? 6 : 4;

        if (i % RULERS_TEXT_STEP === 0) {
          graduationLength = 8;
          createSVGNode(window, {
            nodeType: "text",
            parent: gText,
            attributes: {
              x: isHorizontal ? 2 + i : -i - 1,
              y: 5
            }
          }).textContent = i;
        }

        if (isHorizontal) {
          if (i % RULERS_MARKER_STEP === 0) {
            dMarkers += `M${i} 0 L${i} ${graduationLength}`;
          } else {
            dGraduations += `M${i} 0 L${i} ${graduationLength} `;
          }
        } else if (i % 50 === 0) {
          dMarkers += `M0 ${i} L${graduationLength} ${i}`;
        } else {
          dGraduations += `M0 ${i} L${graduationLength} ${i}`;
        }
      }

      pathGraduations.setAttribute("d", dGraduations);
      pathMarkers.setAttribute("d", dMarkers);

      return g;
    }

    let container = createNode(window, {
      attributes: {"class": "highlighter-container"}
    });

    let root = createNode(window, {
      parent: container,
      attributes: {
        "id": "root",
        "class": "root"
      },
      prefix
    });

    let svg = createSVGNode(window, {
      nodeType: "svg",
      parent: root,
      attributes: {
        id: "elements",
        "class": "elements",
        width: "100%",
        height: "100%",
        hidden: "true"
      },
      prefix
    });

    createRuler("x", RULERS_MAX_X_AXIS);
    createRuler("y", RULERS_MAX_Y_AXIS);

    createNode(window, {
      parent: container,
      attributes: {
        "class": "viewport-infobar-container",
        "id": "viewport-infobar-container",
        "position": "top"
      },
      prefix
    });

    return container;
  },

  handleEvent: function(event) {
    switch (event.type) {
      case "scroll":
        this._onScroll(event);
        break;
      case "pagehide":
        // If a page hide event is triggered for current window's highlighter, hide the
        // highlighter.
        if (event.target.defaultView === this.env.window) {
          this.destroy();
        }
        break;
    }
  },

  _onScroll: function(event) {
    let prefix = this.ID_CLASS_PREFIX;
    let { scrollX, scrollY } = event.view;

    this.markup.getElement(`${prefix}x-axis-ruler`)
                        .setAttribute("transform", `translate(${-scrollX})`);
    this.markup.getElement(`${prefix}x-axis-text`)
                        .setAttribute("transform", `translate(${-scrollX})`);
    this.markup.getElement(`${prefix}y-axis-ruler`)
                        .setAttribute("transform", `translate(0, ${-scrollY})`);
    this.markup.getElement(`${prefix}y-axis-text`)
                        .setAttribute("transform", `translate(0, ${-scrollY})`);
  },

  _update: function() {
    let { window } = this.env;

    setIgnoreLayoutChanges(true);

    let zoom = getCurrentZoom(window);
    let isZoomChanged = zoom !== this._zoom;

    if (isZoomChanged) {
      this._zoom = zoom;
      this.updateViewport();
    }

    this.updateViewportInfobar();

    setIgnoreLayoutChanges(false, window.document.documentElement);

    this._rafID = window.requestAnimationFrame(() => this._update());
  },

  _cancelUpdate: function() {
    if (this._rafID) {
      this.env.window.cancelAnimationFrame(this._rafID);
      this._rafID = 0;
    }
  },
  updateViewport: function() {
    let { devicePixelRatio } = this.env.window;

    // Because `devicePixelRatio` is affected by zoom (see bug 809788),
    // in order to get the "real" device pixel ratio, we need divide by `zoom`
    let pixelRatio = devicePixelRatio / this._zoom;

    // The "real" device pixel ratio is used to calculate the max stroke
    // width we can actually assign: on retina, for instance, it would be 0.5,
    // where on non high dpi monitor would be 1.
    let minWidth = 1 / pixelRatio;
    let strokeWidth = Math.min(minWidth, minWidth / this._zoom);

    this.markup.getElement(this.ID_CLASS_PREFIX + "root").setAttribute("style",
      `stroke-width:${strokeWidth};`);
  },

  updateViewportInfobar: function() {
    let { window } = this.env;
    let { innerHeight, innerWidth } = window;
    let infobarId = this.ID_CLASS_PREFIX + "viewport-infobar-container";
    let textContent = innerHeight + "px \u00D7 " + innerWidth + "px";
    this.markup.getElement(infobarId).setTextContent(textContent);
  },

  destroy: function() {
    this.hide();

    let { pageListenerTarget } = this.env;

    if (pageListenerTarget) {
      pageListenerTarget.removeEventListener("scroll", this);
      pageListenerTarget.removeEventListener("pagehide", this);
    }

    this.markup.destroy();

    EventEmitter.emit(this, "destroy");
  },

  show: function() {
    this.markup.removeAttributeForElement(this.ID_CLASS_PREFIX + "elements",
      "hidden");

    this._update();

    return true;
  },

  hide: function() {
    this.markup.setAttributeForElement(this.ID_CLASS_PREFIX + "elements",
      "hidden", "true");

    this._cancelUpdate();
  }
};
exports.RulersHighlighter = RulersHighlighter;
