import * as GA from "../ga";
import * as GAPoint from "../gapoints";
import * as GADirection from "../gadirections";
import * as GALine from "../galines";
import * as GATransform from "../gatransforms";

export const hitTest = (
  element,
  appState,
  x,
  y,
) => {
  // How many pixels off the shape boundary we still consider a hit
  const threshold = 5;
  const point = [x, y];

  if (
    isElementSelected(appState, element)
  ) {
    return isPointHittingElementBoundingBox(element, point, threshold);
  }

  return isHittingElementNotConsideringBoundingBox(element, point);
};

const isElementSelected = (
  appState,
  element,
) => appState.selectedElement && appState.selectedElement[element.name];

export const isPointHittingElementBoundingBox = (
  element,
  [x, y],
  threshold,
) => {
  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);

  return (
    x > x1 - threshold &&
    x < x2 + threshold &&
    y > y1 - threshold &&
    y < y2 + threshold
  );
};

const getElementAbsoluteCoords = (element) => {
  const { left, top, width, height } = element.rect
  return [left, top, left + width, top + height]
}

export const isHittingElementNotConsideringBoundingBox = (
  element,
  point,
) => {
  const threshold = 10;
  return hitTestPointAgainstElement({ element, point, threshold, check: isNearCheck });
};

const isNearCheck = (distance, threshold) => {
  return Math.abs(distance) < threshold;
};

const hitTestPointAgainstElement = (args) => {
  switch (args.element.type) {
    case "square":
    case "image":
    case "text":
    case "circle":
      const distance = distanceToBindableElement(args.element, args.point);
      return args.check(distance, args.threshold);
    case "freedraw": {
      if (
        !args.check(
          distanceToRectangle(args.element, args.point),
          args.threshold,
        )
      ) {
        return false;
      }

      return hitTestFreeDrawElement(args.element, args.point, args.threshold);
    }
    case "arrow":
    case "line":
      // return hitTestLinear(args);
      break;
    case "selection":
      console.warn(
        "This should not happen, we need to investigate why it does.",
      );
      return false;
  }
};

const coordsCenter = ([ax, ay, bx, by]) => {
  return GA.point((ax + bx) / 2, (ay + by) / 2);
};


const hitTestLinear = (args) => {
  const { element, threshold } = args;
  if (!getShapeForElement(element)) {
    return false;
  }
  const [point, pointAbs, hwidth, hheight] = pointRelativeToElement(args.element, args.point);
  const side1 = GALine.equation(0, 1, -hheight);
  const side2 = GALine.equation(1, 0, -hwidth);
  if (!isInsideCheck(GAPoint.distanceToLine(pointAbs, side1), threshold) ||
    !isInsideCheck(GAPoint.distanceToLine(pointAbs, side2), threshold)) {
    return false;
  }
  const [relX, relY] = GAPoint.toTuple(point);
  const shape = getShapeForElement(element);
  if (!shape) {
    return false;
  }
  if (args.check === isInsideCheck) {
    const hit = shape.some((subshape) => hitTestCurveInside(subshape, relX, relY, element.roundness ? "round" : "sharp"));
    if (hit) {
      return true;
    }
  }
  // hit test all "subshapes" of the linear element
  return shape.some((subshape) => hitTestRoughShape(subshape, relX, relY, threshold));
};

export const distanceToBindableElement = (
  element,
  point,
) => {
  switch (element.type) {
    case "square":
    case "image":
    case "text":
      return distanceToRectangle(element, point);
    case "circle":
      return distanceToEllipse(element, point);
  }
};

const isInsideCheck = (distance, threshold) => {
  return distance < threshold;
};

const distanceToRectangle = (
  element,
  point,
) => {
  const [x, y] = point
  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
  const [centerX, centerY] = [(x1 + x2) / 2, (y1 + y2) / 2]
  const distanceX = Math.abs(x - centerX)
  const distanceY = Math.abs(y - centerY)
  const distance = distanceX > distanceY ? Math.abs(distanceX - element.rect.width / 2) : Math.abs(distanceY - element.rect.height / 2)
  const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
  return Math.max(
    GAPoint.distanceToLine(pointRel, GALine.equation(0, 1, -hheight)),
    GAPoint.distanceToLine(pointRel, GALine.equation(1, 0, -hwidth)),
  );
};

const pointRelativeToElement = (
  element,
  pointTuple,
) => {
  const point = GAPoint.from(pointTuple);
  const elementCoords = getElementAbsoluteCoords(element);
  const [x1, y1, x2, y2] = elementCoords;
  const center = coordsCenter([x1, y1, x2, y2]);
  // GA has angle orientation opposite to `rotate`
  const rotate = GATransform.rotation(center, 0);
  const pointRotated = GATransform.apply(rotate, point);
  const pointRelToCenter = GA.sub(pointRotated, GADirection.from(center));
  const pointRelToCenterAbs = GAPoint.abs(pointRelToCenter);
  const elementPos = GA.offset(element.x, element.y);
  const pointRelToPos = GA.sub(pointRotated, elementPos);
  const [ax, ay, bx, by] = elementCoords;
  const halfWidth = (bx - ax) / 2;
  const halfHeight = (by - ay) / 2;
  return [pointRelToPos, pointRelToCenterAbs, halfWidth, halfHeight];
};

export const getElementsAtPosition = (
  elements,
  isAtPositionFn,
) => {
  // The parameter elements comes ordered from lower z-index to higher.
  // We want to preserve that order on the returned array.
  return elements.filter(
    (element) => !element.isDeleted && isAtPositionFn(element),
  );
};
