/* import-globals-from common.js */

/**
 * 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) {
  SpecialPowers.setFullZoom(aDocument.defaultView, 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.windowUtils;

  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 null;
  }

  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
  isWithin(
    expectedX,
    xObj.value,
    1,
    "Wrong x coordinate of text between offsets (" +
      aStartOffset +
      ", " +
      aEndOffset +
      ") for " +
      prettyName(aID)
  );

  // y
  isWithin(
    expectedY,
    yObj.value,
    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) +
    " - Got " +
    widthObj.value +
    " Expected " +
    expectedWidth;
  if (!WIN) {
    isWithin(expectedWidth, widthObj.value, 1, msg);
  } else {
    // fails on some windows machines
    todo(false, msg);
  }

  // Height
  isWithin(
    expectedHeight,
    heightObj.value,
    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);

  info(`DPR is: ${aDPR}`);
  isWithin(
    xInCSS.value,
    x.value / aDPR,
    1,
    "X in CSS pixels is calculated correctly"
  );
  isWithin(
    yInCSS.value,
    y.value / aDPR,
    1,
    "Y in CSS pixels is calculated correctly"
  );
  isWithin(
    widthInCSS.value,
    width.value / aDPR,
    1,
    "Width in CSS pixels is calculated correctly"
  );
  isWithin(
    heightInCSS.value,
    height.value / aDPR,
    1,
    "Height 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.documentGlobal;
  return CSSToDevicePixels(
    elmWindow,
    x + elmWindow.mozInnerScreenX,
    y + elmWindow.mozInnerScreenY,
    width,
    height
  );
}

function CSSToDevicePixels(aWindow, aX, aY, aWidth, aHeight) {
  var ratio = aWindow.devicePixelRatio;

  // 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),
  ];
}
