/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
 http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

// A helper actor for inspector and markupview tests.

var { Cc, Ci, Cu, Cr } = require("chrome");
const {getRect, getElementFromPoint, getAdjustedQuads} = require("devtools/shared/layout/utils");
const promise = require("promise");
const {Task} = require("devtools/shared/task");
var DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
            .getService(Ci.mozIJSSubScriptLoader);

// Set up a dummy environment so that EventUtils works. We need to be careful to
// pass a window object into each EventUtils method we call rather than having
// it rely on the |window| global.
let EventUtils = {};
EventUtils.window = {};
EventUtils.parent = {};
EventUtils._EU_Ci = Components.interfaces;
EventUtils._EU_Cc = Components.classes;
loader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);

const protocol = require("devtools/shared/protocol");
const {Arg, Option, method, RetVal, types} = protocol;

var dumpn = msg => {
  dump(msg + "\n");
};

/**
 * Get the instance of CanvasFrameAnonymousContentHelper used by a given
 * highlighter actor.
 * The instance provides methods to get/set attributes/text/style on nodes of
 * the highlighter, inserted into the nsCanvasFrame.
 * @see /devtools/server/actors/highlighters.js
 * @param {String} actorID
 */
function getHighlighterCanvasFrameHelper(conn, actorID) {
  let actor = conn.getActor(actorID);
  if (actor && actor._highlighter) {
    return actor._highlighter.markup;
  }
}

var TestActor = exports.TestActor = protocol.ActorClass({
  typeName: "testActor",

  initialize: function (conn, tabActor, options) {
    this.conn = conn;
    this.tabActor = tabActor;
  },

  get content() {
    return this.tabActor.window;
  },

  /**
   * Helper to retrieve a DOM element.
   * @param {string | array} selector Either a regular selector string
   *   or a selector array. If an array, each item, except the last one
   *   are considered matching an iframe, so that we can query element
   *   within deep iframes.
   */
  _querySelector: function (selector) {
    let document = this.content.document;
    if (Array.isArray(selector)) {
      let fullSelector = selector.join(" >> ");
      while (selector.length > 1) {
        let str = selector.shift();
        let iframe = document.querySelector(str);
        if (!iframe) {
          throw new Error("Unable to find element with selector \"" + str + "\"" +
                          " (full selector:" + fullSelector + ")");
        }
        if (!iframe.contentWindow) {
          throw new Error("Iframe selector doesn't target an iframe \"" + str + "\"" +
                          " (full selector:" + fullSelector + ")");
        }
        document = iframe.contentWindow.document;
      }
      selector = selector.shift();
    }
    let node = document.querySelector(selector);
    if (!node) {
      throw new Error("Unable to find element with selector \"" + selector + "\"");
    }
    return node;
  },
  /**
   * Helper to get the number of elements matching a selector
   * @param {string} CSS selector.
   */
  getNumberOfElementMatches: protocol.method(function (selector,
                                                       root = this.content.document) {
    return root.querySelectorAll(selector).length;
  }, {
    request: {
      selector: Arg(0, "string"),
    },
    response: {
      value: RetVal("number")
    }
  }),

  /**
   * Get a value for a given attribute name, on one of the elements of the box
   * model highlighter, given its ID.
   * @param {Object} msg The msg.data part expects the following properties
   * - {String} nodeID The full ID of the element to get the attribute for
   * - {String} name The name of the attribute to get
   * - {String} actorID The highlighter actor ID
   * @return {String} The value, if found, null otherwise
   */
  getHighlighterAttribute: protocol.method(function (nodeID, name, actorID) {
    let helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
    if (helper) {
      return helper.getAttributeForElement(nodeID, name);
    }
  }, {
    request: {
      nodeID: Arg(0, "string"),
      name: Arg(1, "string"),
      actorID: Arg(2, "string")
    },
    response: {
      value: RetVal("string")
    }
  }),

  /**
   * Get the textcontent of one of the elements of the box model highlighter,
   * given its ID.
   * @param {String} nodeID The full ID of the element to get the attribute for
   * @param {String} actorID The highlighter actor ID
   * @return {String} The textcontent value
   */
  getHighlighterNodeTextContent: protocol.method(function (nodeID, actorID) {
    let value;
    let helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
    if (helper) {
      value = helper.getTextContentForElement(nodeID);
    }
    return value;
  }, {
    request: {
      nodeID: Arg(0, "string"),
      actorID: Arg(1, "string")
    },
    response: {
      value: RetVal("string")
    }
  }),

  /**
   * Get the number of box-model highlighters created by the SelectorHighlighter
   * @param {String} actorID The highlighter actor ID
   * @return {Number} The number of box-model highlighters created, or null if the
   * SelectorHighlighter was not found.
   */
  getSelectorHighlighterBoxNb: protocol.method(function (actorID) {
    let highlighter = this.conn.getActor(actorID);
    let {_highlighter: h} = highlighter;
    if (!h || !h._highlighters) {
      return null;
    } else {
      return h._highlighters.length;
    }
  }, {
    request: {
      highlighter: Arg(0, "string"),
    },
    response: {
      value: RetVal("number")
    }
  }),

  /**
   * Subscribe to the box-model highlighter's update event, modify an attribute of
   * the currently highlighted node and send a message when the highlighter has
   * updated.
   * @param {String} the name of the attribute to be changed
   * @param {String} the new value for the attribute
   * @param {String} actorID The highlighter actor ID
   */
  changeHighlightedNodeWaitForUpdate: protocol.method(function (name, value, actorID) {
    let deferred = promise.defer();

    let highlighter = this.conn.getActor(actorID);
    let {_highlighter: h} = highlighter;

    h.once("updated", () => {
      deferred.resolve();
    });

    h.currentNode.setAttribute(name, value);

    return deferred.promise;
  }, {
    request: {
      name: Arg(0, "string"),
      value: Arg(1, "string"),
      actorID: Arg(2, "string")
    },
    response: {}
  }),

  /**
   * Subscribe to a given highlighter event and respond when the event is received.
   * @param {String} event The name of the highlighter event to listen to
   * @param {String} actorID The highlighter actor ID
   */
  waitForHighlighterEvent: protocol.method(function (event, actorID) {
    let highlighter = this.conn.getActor(actorID);
    let {_highlighter: h} = highlighter;

    return h.once(event);
  }, {
    request: {
      event: Arg(0, "string"),
      actorID: Arg(1, "string")
    },
    response: {}
  }),

  /**
   * Wait for a specific event on a node matching the provided selector.
   * @param {String} eventName The name of the event to listen to
   * @param {String} selector Optional:  css selector of the node which should
   *        trigger the event. If ommitted, target will be the content window
   */
  waitForEventOnNode: protocol.method(function (eventName, selector) {
    return new Promise(resolve => {
      let node = selector ? this._querySelector(selector) : this.content;
      node.addEventListener(eventName, function onEvent() {
        node.removeEventListener(eventName, onEvent);
        resolve();
      });
    });
  }, {
    request: {
      eventName: Arg(0, "string"),
      selector: Arg(1, "nullable:string")
    },
    response: {}
  }),

  /**
   * Change the zoom level of the page.
   * Optionally subscribe to the box-model highlighter's update event and waiting
   * for it to refresh before responding.
   * @param {Number} level The new zoom level
   * @param {String} actorID Optional. The highlighter actor ID
   */
  changeZoomLevel: protocol.method(function (level, actorID) {
    dumpn("Zooming page to " + level);
    let deferred = promise.defer();

    if (actorID) {
      let actor = this.conn.getActor(actorID);
      let {_highlighter: h} = actor;
      h.once("updated", () => {
        deferred.resolve();
      });
    } else {
      deferred.resolve();
    }

    let docShell = this.content.QueryInterface(Ci.nsIInterfaceRequestor)
                               .getInterface(Ci.nsIWebNavigation)
                               .QueryInterface(Ci.nsIDocShell);
    docShell.contentViewer.fullZoom = level;

    return deferred.promise;
  }, {
    request: {
      level: Arg(0, "string"),
      actorID: Arg(1, "string"),
    },
    response: {}
  }),

  assertElementAtPoint: protocol.method(function (x, y, selector) {
    let elementAtPoint = getElementFromPoint(this.content.document, x, y);
    if (!elementAtPoint) {
      throw new Error("Unable to find element at (" + x + ", " + y + ")");
    }
    let node = this._querySelector(selector);
    return node == elementAtPoint;
  }, {
    request: {
      x: Arg(0, "number"),
      y: Arg(1, "number"),
      selector: Arg(2, "string")
    },
    response: {
      value: RetVal("boolean")
    }
  }),


  /**
   * Get all box-model regions' adjusted boxquads for the given element
   * @param {String} selector The node selector to target a given element
   * @return {Object} An object with each property being a box-model region, each
   * of them being an object with the p1/p2/p3/p4 properties
   */
  getAllAdjustedQuads: protocol.method(function (selector) {
    let regions = {};
    let node = this._querySelector(selector);
    for (let boxType of ["content", "padding", "border", "margin"]) {
      regions[boxType] = getAdjustedQuads(this.content, node, boxType);
    }

    return regions;
  }, {
    request: {
      selector: Arg(0, "string")
    },
    response: {
      value: RetVal("json")
    }
  }),

  /**
   * Synthesize a mouse event on an element, after ensuring that it is visible
   * in the viewport. This handler doesn't send a message back. Consumers
   * should listen to specific events on the inspector/highlighter to know when
   * the event got synthesized.
   * @param {String} selector The node selector to get the node target for the event
   * @param {Number} x
   * @param {Number} y
   * @param {Boolean} center If set to true, x/y will be ignored and
   *                  synthesizeMouseAtCenter will be used instead
   * @param {Object} options Other event options
   */
  synthesizeMouse: protocol.method(function ({ selector, x, y, center, options }) {
    let node = this._querySelector(selector);
    node.scrollIntoView();
    if (center) {
      EventUtils.synthesizeMouseAtCenter(node, options, node.ownerDocument.defaultView);
    } else {
      EventUtils.synthesizeMouse(node, x, y, options, node.ownerDocument.defaultView);
    }
  }, {
    request: {
      object: Arg(0, "json")
    },
    response: {}
  }),

  /**
  * Synthesize a key event for an element. This handler doesn't send a message
  * back. Consumers should listen to specific events on the inspector/highlighter
  * to know when the event got synthesized.
  */
  synthesizeKey: protocol.method(function ({key, options, content}) {
    EventUtils.synthesizeKey(key, options, this.content);
  }, {
    request: {
      args: Arg(0, "json")
    },
    response: {}
  }),

  /**
   * Check that an element currently has a pseudo-class lock.
   * @param {String} selector The node selector to get the pseudo-class from
   * @param {String} pseudo The pseudoclass to check for
   * @return {Boolean}
   */
  hasPseudoClassLock: protocol.method(function (selector, pseudo) {
    let node = this._querySelector(selector);
    return DOMUtils.hasPseudoClassLock(node, pseudo);
  }, {
    request: {
      selector: Arg(0, "string"),
      pseudo: Arg(1, "string")
    },
    response: {
      value: RetVal("boolean")
    }
  }),

  loadAndWaitForCustomEvent: protocol.method(function (url) {
    let deferred = promise.defer();
    let self = this;
    // Wait for DOMWindowCreated first, as listening on the current outerwindow
    // doesn't allow receiving test-page-processing-done.
    this.tabActor.chromeEventHandler.addEventListener("DOMWindowCreated", function onWindowCreated() {
      self.tabActor.chromeEventHandler.removeEventListener("DOMWindowCreated", onWindowCreated);
      self.content.addEventListener("test-page-processing-done", function onEvent() {
        self.content.removeEventListener("test-page-processing-done", onEvent);
        deferred.resolve();
      });
    });

    this.content.location = url;
    return deferred.promise;
  }, {
    request: {
      url: Arg(0, "string")
    },
    response: {}
  }),

  hasNode: protocol.method(function (selector) {
    try {
      // _querySelector throws if the node doesn't exists
      this._querySelector(selector);
      return true;
    } catch (e) {
      return false;
    }
  }, {
    request: {
      selector: Arg(0, "string")
    },
    response: {
      value: RetVal("boolean")
    }
  }),

  /**
   * Get the bounding rect for a given DOM node once.
   * @param {String} selector selector identifier to select the DOM node
   * @return {json} the bounding rect info
   */
  getBoundingClientRect: protocol.method(function (selector) {
    let node = this._querySelector(selector);
    return node.getBoundingClientRect();
  }, {
    request: {
      selector: Arg(0, "string"),
    },
    response: {
      value: RetVal("json")
    }
  }),

  /**
   * Set a JS property on a DOM Node.
   * @param {String} selector The node selector
   * @param {String} property The property name
   * @param {String} value The attribute value
   */
  setProperty: protocol.method(function (selector, property, value) {
    let node = this._querySelector(selector);
    node[property] = value;
  }, {
    request: {
      selector: Arg(0, "string"),
      property: Arg(1, "string"),
      value: Arg(2, "string")
    },
    response: {}
  }),

  /**
   * Get a JS property on a DOM Node.
   * @param {String} selector The node selector
   * @param {String} property The property name
   * @return {String} value The attribute value
   */
  getProperty: protocol.method(function (selector, property) {
    let node = this._querySelector(selector);
    return node[property];
  }, {
    request: {
      selector: Arg(0, "string"),
      property: Arg(1, "string")
    },
    response: {
      value: RetVal("string")
    }
  }),

  /**
   * Get an attribute on a DOM Node.
   * @param {String} selector The node selector
   * @param {String} attribute The attribute name
   * @return {String} value The attribute value
   */
  getAttribute: protocol.method(function (selector, attribute) {
    let node = this._querySelector(selector);
    return node.getAttribute(attribute);
  }, {
    request: {
      selector: Arg(0, "string"),
      property: Arg(1, "string")
    },
    response: {
      value: RetVal("string")
    }
  }),

  /**
   * Set an attribute on a DOM Node.
   * @param {String} selector The node selector
   * @param {String} attribute The attribute name
   * @param {String} value The attribute value
   */
  setAttribute: protocol.method(function (selector, attribute, value) {
    let node = this._querySelector(selector);
    node.setAttribute(attribute, value);
  }, {
    request: {
      selector: Arg(0, "string"),
      property: Arg(1, "string"),
      value: Arg(2, "string")
    },
    response: {}
  }),

  /**
   * Remove an attribute from a DOM Node.
   * @param {String} selector The node selector
   * @param {String} attribute The attribute name
   */
  removeAttribute: protocol.method(function (selector, attribute) {
    let node = this._querySelector(selector);
    node.removeAttribute(attribute);
  }, {
    request: {
      selector: Arg(0, "string"),
      property: Arg(1, "string")
    },
    response: {}
  }),

  /**
   * Reload the content window.
   */
  reload: protocol.method(function () {
    this.content.location.reload();
  }, {
    request: {},
    response: {}
  }),

  /**
   * Reload an iframe and wait for its load event.
   * @param {String} selector The node selector
   */
  reloadFrame: protocol.method(function (selector) {
    let node = this._querySelector(selector);

    let deferred = promise.defer();

    let onLoad = function () {
      node.removeEventListener("load", onLoad);
      deferred.resolve();
    };
    node.addEventListener("load", onLoad);

    node.contentWindow.location.reload();
    return deferred.promise;
  }, {
    request: {
      selector: Arg(0, "string"),
    },
    response: {}
  }),

  /**
   * Evaluate a JS string in the context of the content document.
   * @param {String} js JS string to evaluate
   * @return {json} The evaluation result
   */
  eval: protocol.method(function (js) {
    // We have to use a sandbox, as CSP prevent us from using eval on apps...
    let sb = Cu.Sandbox(this.content, { sandboxPrototype: this.content });
    return Cu.evalInSandbox(js, sb);
  }, {
    request: {
      js: Arg(0, "string")
    },
    response: {
      value: RetVal("nullable:json")
    }
  }),

  /**
   * Scrolls the window to a particular set of coordinates in the document, or
   * by the given amount if `relative` is set to `true`.
   *
   * @param {Number} x
   * @param {Number} y
   * @param {Boolean} relative
   *
   * @return {Object} An object with x / y properties, representing the number
   * of pixels that the document has been scrolled horizontally and vertically.
   */
  scrollWindow: protocol.method(function (x, y, relative) {
    if (isNaN(x) || isNaN(y)) {
      return {};
    }

    let deferred = promise.defer();
    this.content.addEventListener("scroll", function onScroll(event) {
      this.removeEventListener("scroll", onScroll);

      let data = {x: this.content.scrollX, y: this.content.scrollY};
      deferred.resolve(data);
    });

    this.content[relative ? "scrollBy" : "scrollTo"](x, y);

    return deferred.promise;
  }, {
    request: {
      x: Arg(0, "number"),
      y: Arg(1, "number"),
      relative: Arg(2, "nullable:boolean"),
    },
    response: {
      value: RetVal("json")
    }
  }),

  /**
   * Forces the reflow and waits for the next repaint.
   */
  reflow: protocol.method(function () {
    let deferred = promise.defer();
    this.content.document.documentElement.offsetWidth;
    this.content.requestAnimationFrame(deferred.resolve);

    return deferred.promise;
  }),

  getNodeRect: protocol.method(Task.async(function* (selector) {
    let node = this._querySelector(selector);
    return getRect(this.content, node, this.content);
  }), {
    request: {
      selector: Arg(0, "string")
    },
    response: {
      value: RetVal("json")
    }
  }),

  /**
   * Get information about a DOM element, identified by a selector.
   * @param {String} selector The CSS selector to get the node (can be an array
   * of selectors to get elements in an iframe).
   * @return {Object} data Null if selector didn't match any node, otherwise:
   * - {String} tagName.
   * - {String} namespaceURI.
   * - {Number} numChildren The number of children in the element.
   * - {Array} attributes An array of {name, value, namespaceURI} objects.
   * - {String} outerHTML.
   * - {String} innerHTML.
   * - {String} textContent.
   */
  getNodeInfo: protocol.method(function (selector) {
    let node = this._querySelector(selector);
    let info = null;

    if (node) {
      info = {
        tagName: node.tagName,
        namespaceURI: node.namespaceURI,
        numChildren: node.children.length,
        attributes: [...node.attributes].map(({name, value, namespaceURI}) => {
          return {name, value, namespaceURI};
        }),
        outerHTML: node.outerHTML,
        innerHTML: node.innerHTML,
        textContent: node.textContent
      };
    }

    return info;
  }, {
    request: {
      selector: Arg(0, "string")
    },
    response: {
      value: RetVal("json")
    }
  })
});

var TestActorFront = exports.TestActorFront = protocol.FrontClass(TestActor, {
  initialize: function (client, { testActor }, toolbox) {
    protocol.Front.prototype.initialize.call(this, client, { actor: testActor });
    this.manage(this);
    this.toolbox = toolbox;
  },

  /**
   * Zoom the current page to a given level.
   * @param {Number} level The new zoom level.
   * @return {Promise} The returned promise will only resolve when the
   * highlighter has updated to the new zoom level.
   */
  zoomPageTo: function (level) {
    return this.changeZoomLevel(level, this.toolbox.highlighter.actorID);
  },

  changeHighlightedNodeWaitForUpdate: protocol.custom(function (name, value, highlighter) {
    return this._changeHighlightedNodeWaitForUpdate(name, value, (highlighter || this.toolbox.highlighter).actorID);
  }, {
    impl: "_changeHighlightedNodeWaitForUpdate"
  }),

  /**
   * Get the value of an attribute on one of the highlighter's node.
   * @param {String} nodeID The Id of the node in the highlighter.
   * @param {String} name The name of the attribute.
   * @param {Object} highlighter Optional custom highlither to target
   * @return {String} value
   */
  getHighlighterNodeAttribute: function (nodeID, name, highlighter) {
    return this.getHighlighterAttribute(nodeID, name, (highlighter || this.toolbox.highlighter).actorID);
  },

  getHighlighterNodeTextContent: protocol.custom(function (nodeID, highlighter) {
    return this._getHighlighterNodeTextContent(nodeID, (highlighter || this.toolbox.highlighter).actorID);
  }, {
    impl: "_getHighlighterNodeTextContent"
  }),

  /**
   * Is the highlighter currently visible on the page?
   */
  isHighlighting: function () {
    return this.getHighlighterNodeAttribute("box-model-elements", "hidden")
      .then(value => value === null);
  },

  /**
   * Assert that the box-model highlighter's current position corresponds to the
   * given node boxquads.
   * @param {String} selector The node selector to get the boxQuads from
   * @param {Function} is assertion function to call for equality checks
   * @param {String} prefix An optional prefix for logging information to the
   * console.
   */
  isNodeCorrectlyHighlighted: Task.async(function* (selector, is, prefix = "") {
    prefix += (prefix ? " " : "") + selector + " ";

    let boxModel = yield this._getBoxModelStatus();
    let regions = yield this.getAllAdjustedQuads(selector);

    for (let boxType of ["content", "padding", "border", "margin"]) {
      let [quad] = regions[boxType];
      for (let point in boxModel[boxType].points) {
        is(boxModel[boxType].points[point].x, quad[point].x,
          prefix + boxType + " point " + point + " x coordinate is correct");
        is(boxModel[boxType].points[point].y, quad[point].y,
          prefix + boxType + " point " + point + " y coordinate is correct");
      }
    }
  }),

  /**
   * Get the current rect of the border region of the box-model highlighter
   */
  getSimpleBorderRect: Task.async(function* (toolbox) {
    let {border} = yield this._getBoxModelStatus(toolbox);
    let {p1, p2, p3, p4} = border.points;

    return {
      top: p1.y,
      left: p1.x,
      width: p2.x - p1.x,
      height: p4.y - p1.y
    };
  }),

  /**
   * Get the current positions and visibility of the various box-model highlighter
   * elements.
   */
  _getBoxModelStatus: Task.async(function* () {
    let isVisible = yield this.isHighlighting();

    let ret = {
      visible: isVisible
    };

    for (let region of ["margin", "border", "padding", "content"]) {
      let points = yield this._getPointsForRegion(region);
      let visible = yield this._isRegionHidden(region);
      ret[region] = {points, visible};
    }

    ret.guides = {};
    for (let guide of ["top", "right", "bottom", "left"]) {
      ret.guides[guide] = yield this._getGuideStatus(guide);
    }

    return ret;
  }),

  assertHighlightedNode: Task.async(function* (selector) {
    // Taken and tweaked from:
    // https://github.com/iominh/point-in-polygon-extended/blob/master/src/index.js#L30-L85
    function isLeft(p0, p1, p2) {
      let l = ((p1[0] - p0[0]) * (p2[1] - p0[1])) -
              ((p2[0] - p0[0]) * (p1[1] - p0[1]));
      return l;
    }
    function isInside(point, polygon) {
      if (polygon.length === 0) {
        return false;
      }

      var n = polygon.length;
      var newPoints = polygon.slice(0);
      newPoints.push(polygon[0]);
      var wn = 0; // wn counter

      // loop through all edges of the polygon
      for (var i = 0; i < n; i++) {
        // Accept points on the edges
        let r = isLeft(newPoints[i], newPoints[i + 1], point);
        if (r === 0) {
          return true;
        }
        if (newPoints[i][1] <= point[1]) {
          if (newPoints[i + 1][1] > point[1] && r > 0) {
            wn++;
          }
        } else {
          if (newPoints[i + 1][1] <= point[1] && r < 0) {
            wn--;
          }
        }
      }
      if (wn === 0) {
        dumpn(JSON.stringify(point) + " is outside of " + JSON.stringify(polygon));
      }
      // the point is outside only when this winding number wn===0, otherwise it's inside
      return wn !== 0;
    }

    let {visible, border} = yield this._getBoxModelStatus();
    let points = border.points;
    if (visible) {
      // Check that the node is within the box model
      let { left, top, width, height } = yield this.getNodeRect(selector);
      let right = left + width;
      let bottom = top + height;

      // Converts points dictionnary into an array
      let list = [];
      for (var i = 1; i <= 4; i++) {
        let p = points["p" + i];
        list.push([p.x, p.y]);
      }
      points = list;

      // Check that each point of the node is within the box model
      if (!isInside([left, top], points) ||
          !isInside([right, top], points) ||
          !isInside([right, bottom], points) ||
          !isInside([left, bottom], points)) {
        return false;
      }
      return true;
    } else {
      return false;
    }
  }),

  /**
   * Get the coordinate (points attribute) from one of the polygon elements in the
   * box model highlighter.
   */
  _getPointsForRegion: Task.async(function* (region) {
    let d = yield this.getHighlighterNodeAttribute("box-model-" + region, "d");

    let polygons = d.match(/M[^M]+/g);
    if (!polygons) {
      return null;
    }

    let points = polygons[0].trim().split(" ").map(i => {
      return i.replace(/M|L/, "").split(",");
    });

    return {
      p1: {
        x: parseFloat(points[0][0]),
        y: parseFloat(points[0][1])
      },
      p2: {
        x: parseFloat(points[1][0]),
        y: parseFloat(points[1][1])
      },
      p3: {
        x: parseFloat(points[2][0]),
        y: parseFloat(points[2][1])
      },
      p4: {
        x: parseFloat(points[3][0]),
        y: parseFloat(points[3][1])
      }
    };
  }),

  /**
   * Is a given region polygon element of the box-model highlighter currently
   * hidden?
   */
  _isRegionHidden: Task.async(function* (region) {
    let value = yield this.getHighlighterNodeAttribute("box-model-" + region, "hidden");
    return value !== null;
  }),

  _getGuideStatus: Task.async(function* (location) {
    let id = "box-model-guide-" + location;

    let hidden = yield this.getHighlighterNodeAttribute(id, "hidden");
    let x1 = yield this.getHighlighterNodeAttribute(id, "x1");
    let y1 = yield this.getHighlighterNodeAttribute(id, "y1");
    let x2 = yield this.getHighlighterNodeAttribute(id, "x2");
    let y2 = yield this.getHighlighterNodeAttribute(id, "y2");

    return {
      visible: !hidden,
      x1: x1,
      y1: y1,
      x2: x2,
      y2: y2
    };
  }),

  /**
   * Get the coordinates of the rectangle that is defined by the 4 guides displayed
   * in the toolbox box-model highlighter.
   * @return {Object} Null if at least one guide is hidden. Otherwise an object
   * with p1, p2, p3, p4 properties being {x, y} objects.
   */
  getGuidesRectangle: Task.async(function* () {
    let tGuide = yield this._getGuideStatus("top");
    let rGuide = yield this._getGuideStatus("right");
    let bGuide = yield this._getGuideStatus("bottom");
    let lGuide = yield this._getGuideStatus("left");

    if (!tGuide.visible || !rGuide.visible || !bGuide.visible || !lGuide.visible) {
      return null;
    }

    return {
      p1: {x: lGuide.x1, y: tGuide.y1},
      p2: {x: rGuide.x1, y: tGuide. y1},
      p3: {x: rGuide.x1, y: bGuide.y1},
      p4: {x: lGuide.x1, y: bGuide.y1}
    };
  }),

  waitForHighlighterEvent: protocol.custom(function (event) {
    return this._waitForHighlighterEvent(event, this.toolbox.highlighter.actorID);
  }, {
    impl: "_waitForHighlighterEvent"
  }),

  /**
   * Get the "d" attribute value for one of the box-model highlighter's region
   * <path> elements, and parse it to a list of points.
   * @param {String} region The box model region name.
   * @param {Front} highlighter The front of the highlighter.
   * @return {Object} The object returned has the following form:
   * - d {String} the d attribute value
   * - points {Array} an array of all the polygons defined by the path. Each box
   *   is itself an Array of points, themselves being [x,y] coordinates arrays.
   */
  getHighlighterRegionPath: Task.async(function* (region, highlighter) {
    let d = yield this.getHighlighterNodeAttribute("box-model-" + region, "d", highlighter);
    if (!d) {
      return {d: null};
    }

    let polygons = d.match(/M[^M]+/g);
    if (!polygons) {
      return {d};
    }

    let points = [];
    for (let polygon of polygons) {
      points.push(polygon.trim().split(" ").map(i => {
        return i.replace(/M|L/, "").split(",");
      }));
    }

    return {d, points};
  })
});
