/**
 * Tests if the given child and grand child accessibles at the given point are
 * expected.
 *
 * @param aID            [in] accessible identifier
 * @param aX             [in] x coordinate of the point relative accessible
 * @param aY             [in] y coordinate of the point relative accessible
 * @param aChildID       [in] expected child accessible
 * @param aGrandChildID  [in] expected child accessible
 */
function testChildAtPoint(aID, aX, aY, aChildID, aGrandChildID) {
  var child = getChildAtPoint(aID, aX, aY, false);
  var expectedChild = getAccessible(aChildID);

  var msg = "Wrong direct child accessible at the point (" + aX + ", " + aY +
    ") of " + prettyName(aID);
  isObject(child, expectedChild, msg);

  var grandChild = getChildAtPoint(aID, aX, aY, true);
  var expectedGrandChild = getAccessible(aGrandChildID);

  msg = "Wrong deepest child accessible at the point (" + aX + ", " + aY +
    ") of " + prettyName(aID);
  isObject(grandChild, expectedGrandChild, msg);
}

/**
 * Test if getChildAtPoint returns the given child and grand child accessibles
 * at coordinates of child accessible (direct and deep hit test).
 */
function hitTest(aContainerID, aChildID, aGrandChildID) {
  var container = getAccessible(aContainerID);
  var child = getAccessible(aChildID);
  var grandChild = getAccessible(aGrandChildID);

  var [x, y] = getBoundsForDOMElm(child);

  var actualChild = container.getChildAtPoint(x + 1, y + 1);
  isObject(actualChild, child,
           "Wrong direct child of " + prettyName(aContainerID));

  var actualGrandChild = container.getDeepestChildAtPoint(x + 1, y + 1);
  isObject(actualGrandChild, grandChild,
           "Wrong deepest child of " + prettyName(aContainerID));
}

/**
 * Test if getOffsetAtPoint returns the given text offset at given coordinates.
 */
function testOffsetAtPoint(aHyperTextID, aX, aY, aCoordType, aExpectedOffset) {
  var hyperText = getAccessible(aHyperTextID, [nsIAccessibleText]);
  var offset = hyperText.getOffsetAtPoint(aX, aY, aCoordType);
  is(offset, aExpectedOffset,
     "Wrong offset at given point (" + aX + ", " + aY + ") for " +
     prettyName(aHyperTextID));
}

/**
 * Zoom the given document.
 */
function zoomDocument(aDocument, aZoom) {
  var docShell = aDocument.defaultView.
    QueryInterface(Ci.nsIInterfaceRequestor).
    getInterface(Ci.nsIWebNavigation).
    QueryInterface(Ci.nsIDocShell);
  var docViewer = docShell.contentViewer;

  docViewer.fullZoom = aZoom;
}

/**
 * Set the relative resolution of this document. This is what apz does.
 * On non-mobile platforms you won't see a visible change.
 */
function setResolution(aDocument, aZoom) {
  var windowUtils = aDocument.defaultView.
    QueryInterface(Ci.nsIInterfaceRequestor).
    getInterface(Ci.nsIDOMWindowUtils);

  windowUtils.setResolutionAndScaleTo(aZoom);
}

/**
 * Return child accessible at the given point.
 *
 * @param aIdentifier        [in] accessible identifier
 * @param aX                 [in] x coordinate of the point relative accessible
 * @param aY                 [in] y coordinate of the point relative accessible
 * @param aFindDeepestChild  [in] points whether deepest or nearest child should
 *                           be returned
 * @return                   the child accessible at the given point
 */
function getChildAtPoint(aIdentifier, aX, aY, aFindDeepestChild) {
  var acc = getAccessible(aIdentifier);
  if (!acc)
    return;

  var [screenX, screenY] = getBoundsForDOMElm(acc.DOMNode);

  var x = screenX + aX;
  var y = screenY + aY;

  try {
    if (aFindDeepestChild)
      return acc.getDeepestChildAtPoint(x, y);
    return acc.getChildAtPoint(x, y);
  } catch (e) { }

  return null;
}

/**
 * Test the accessible position.
 */
function testPos(aID, aPoint) {
  var [expectedX, expectedY] =
    (aPoint != undefined) ? aPoint : getBoundsForDOMElm(aID);

  var [x, y] = getBounds(aID);
  is(x, expectedX, "Wrong x coordinate of " + prettyName(aID));
  is(y, expectedY, "Wrong y coordinate of " + prettyName(aID));
}

/**
 * Test the accessible boundaries.
 */
function testBounds(aID, aRect) {
  var [expectedX, expectedY, expectedWidth, expectedHeight] =
    (aRect != undefined) ? aRect : getBoundsForDOMElm(aID);

  var [x, y, width, height] = getBounds(aID);
  is(x, expectedX, "Wrong x coordinate of " + prettyName(aID));
  is(y, expectedY, "Wrong y coordinate of " + prettyName(aID));
  is(width, expectedWidth, "Wrong width of " + prettyName(aID));
  is(height, expectedHeight, "Wrong height of " + prettyName(aID));
}

/**
 * Test text position at the given offset.
 */
function testTextPos(aID, aOffset, aPoint, aCoordOrigin) {
  var [expectedX, expectedY] = aPoint;

  var xObj = {}, yObj = {};
  var hyperText = getAccessible(aID, [nsIAccessibleText]);
  hyperText.getCharacterExtents(aOffset, xObj, yObj, {}, {}, aCoordOrigin);
  is(xObj.value, expectedX,
     "Wrong x coordinate at offset " + aOffset + " for " + prettyName(aID));
  ok(yObj.value - expectedY < 2 && expectedY - yObj.value < 2,
     "Wrong y coordinate at offset " + aOffset + " for " + prettyName(aID) +
     " - got " + yObj.value + ", expected " + expectedY +
     "The difference doesn't exceed 1.");
}

/**
 * Test text bounds that is enclosed betwene the given offsets.
 */
function testTextBounds(aID, aStartOffset, aEndOffset, aRect, aCoordOrigin) {
  var [expectedX, expectedY, expectedWidth, expectedHeight] = aRect;

  var xObj = {}, yObj = {}, widthObj = {}, heightObj = {};
  var hyperText = getAccessible(aID, [nsIAccessibleText]);
  hyperText.getRangeExtents(aStartOffset, aEndOffset,
                            xObj, yObj, widthObj, heightObj, aCoordOrigin);

  // x
  is(xObj.value, expectedX,
     "Wrong x coordinate of text between offsets (" + aStartOffset + ", " +
     aEndOffset + ") for " + prettyName(aID));

  // y
  isWithin(yObj.value, expectedY, 1,
           `y coord of text between offsets (${aStartOffset}, ${aEndOffset}) ` +
           `for ${prettyName(aID)}`);

  // Width
  var msg = "Wrong width of text between offsets (" + aStartOffset + ", " +
    aEndOffset + ") for " + prettyName(aID);
  if (widthObj.value == expectedWidth)
    ok(true, msg);
  else
    todo(false, msg); // fails on some windows machines

  // Height
  isWithin(heightObj.value, expectedHeight, 1,
           `height of text between offsets (${aStartOffset}, ${aEndOffset}) ` +
           `for ${prettyName(aID)}`);
}

/**
 * Return the accessible coordinates relative to the screen in device pixels.
 */
function getPos(aID) {
  var accessible = getAccessible(aID);
  var x = {}, y = {};
  accessible.getBounds(x, y, {}, {});
  return [x.value, y.value];
}

/**
 * Return the accessible coordinates and size relative to the screen in device
 * pixels. This methods also retrieves coordinates in CSS pixels and ensures that they
 * match Dev pixels with a given device pixel ratio.
 */
function getBounds(aID, aDPR = window.devicePixelRatio) {
  const accessible = getAccessible(aID);
  let x = {}, y = {}, width = {}, height = {};
  let xInCSS = {}, yInCSS = {}, widthInCSS = {}, heightInCSS = {};
  accessible.getBounds(x, y, width, height);
  accessible.getBoundsInCSSPixels(xInCSS, yInCSS, widthInCSS, heightInCSS);

  isWithin(x.value / aDPR, xInCSS.value, 1,
    "Heights in CSS pixels is calculated correctly");
  isWithin(y.value / aDPR, yInCSS.value, 1,
    "Heights in CSS pixels is calculated correctly");
  isWithin(width.value / aDPR, widthInCSS.value, 1,
    "Heights in CSS pixels is calculated correctly");
  isWithin(height.value / aDPR, heightInCSS.value, 1,
    "Heights in CSS pixels is calculated correctly");

  return [x.value, y.value, width.value, height.value];
}

function getRangeExtents(aID, aStartOffset, aEndOffset, aCoordOrigin) {
  var hyperText = getAccessible(aID, [nsIAccessibleText]);
  var x = {}, y = {}, width = {}, height = {};
  hyperText.getRangeExtents(aStartOffset, aEndOffset,
                            x, y, width, height, aCoordOrigin);
  return [x.value, y.value, width.value, height.value];
}

/**
 * Return DOM node coordinates relative the screen and its size in device
 * pixels.
 */
function getBoundsForDOMElm(aID) {
  var x = 0, y = 0, width = 0, height = 0;

  var elm = getNode(aID);
  if (elm.localName == "area") {
    var mapName = elm.parentNode.getAttribute("name");
    var selector = "[usemap='#" + mapName + "']";
    var img = elm.ownerDocument.querySelector(selector);

    var areaCoords = elm.coords.split(",");
    var areaX = parseInt(areaCoords[0]);
    var areaY = parseInt(areaCoords[1]);
    var areaWidth = parseInt(areaCoords[2]) - areaX;
    var areaHeight = parseInt(areaCoords[3]) - areaY;

    let rect = img.getBoundingClientRect();
    x = rect.left + areaX;
    y = rect.top + areaY;
    width = areaWidth;
    height = areaHeight;
  } else {
    let rect = elm.getBoundingClientRect();
    x = rect.left;
    y = rect.top;
    width = rect.width;
    height = rect.height;
  }

  var elmWindow = elm.ownerGlobal;
  return CSSToDevicePixels(elmWindow,
                           x + elmWindow.mozInnerScreenX,
                           y + elmWindow.mozInnerScreenY,
                           width,
                           height);
}

function CSSToDevicePixels(aWindow, aX, aY, aWidth, aHeight) {
  var winUtil = aWindow.
    QueryInterface(Ci.nsIInterfaceRequestor).
    getInterface(Ci.nsIDOMWindowUtils);

  var ratio = winUtil.screenPixelsPerCSSPixel;

  // CSS pixels and ratio can be not integer. Device pixels are always integer.
  // Do our best and hope it works.
  return [ Math.round(aX * ratio), Math.round(aY * ratio),
           Math.round(aWidth * ratio), Math.round(aHeight * ratio) ];
}
