import { toRadians, roundTo as roundToNumber } from "./shared";

export function lengthSquared(p) {
  return p.x * p.x + p.y * p.y;
}

export function length(p) {
  return Math.sqrt(lengthSquared(p));
}

export function normalize(p) {
  const l = length(p);
  return divide(p, l);
}

export function dot(a, b) {
  return a.x * b.x + a.y * b.y;
}

export function subtract(a, b) {
  if (typeof b === "number") {
    return { x: a.x - b, y: a.y - b };
  } else {
    return { x: a.x - b.x, y: a.y - b.y };
  }
}

export function add(a, b) {
  if (typeof b === "number") {
    return { x: a.x + b, y: a.y + b };
  } else {
    return { x: a.x + b.x, y: a.y + b.y };
  }
}

export function multiply(a, b) {
  if (typeof b === "number") {
    return { x: a.x * b, y: a.y * b };
  } else {
    return { x: a.x * b.x, y: a.y * b.y };
  }
}

export function divide(a, b) {
  if (typeof b === "number") {
    return { x: a.x / b, y: a.y / b };
  } else {
    return { x: a.x / b.x, y: a.y / b.y };
  }
}

export function rotate(point, origin, angle) {
  const cos = Math.cos(toRadians(angle));
  const sin = Math.sin(toRadians(angle));
  const dif = subtract(point, origin);
  return {
    x: origin.x + cos * dif.x - sin * dif.y,
    y: origin.y + sin * dif.x + cos * dif.y,
  };
}

export function rotateDirection(direction, angle) {
  return rotate(direction, { x: 0, y: 0 }, angle);
}

export function min(a) {
  return a.x < a.y ? a.x : a.y;
}

export function max(a) {
  return a.x > a.y ? a.x : a.y;
}

export function roundTo(p, to) {
  return {
    x: roundToNumber(p.x, to.x),
    y: roundToNumber(p.y, to.y),
  };
}

export function sign(a) {
  return { x: Math.sign(a.x), y: Math.sign(a.y) };
}

export function abs(a) {
  return { x: Math.abs(a.x), y: Math.abs(a.y) };
}

export function pow(a, b) {
  if (typeof b === "number") {
    return { x: Math.pow(a.x, b), y: Math.pow(a.y, b) };
  } else {
    return { x: Math.pow(a.x, b.x), y: Math.pow(a.y, b.y) };
  }
}

export function dot2(a) {
  return dot(a, a);
}

export function clamp(a, min, max) {
  return {
    x: Math.min(Math.max(a.x, min), max),
    y: Math.min(Math.max(a.y, min), max),
  };
}

// https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d
export function distanceToLine(p, a, b) {
  const pa = subtract(p, a);
  const ba = subtract(b, a);
  const h = Math.min(Math.max(dot(pa, ba) / dot(ba, ba), 0), 1);
  const distance = length(subtract(pa, multiply(ba, h)));
  const point = add(a, multiply(ba, h));
  return { distance, point };
}

// TODO: Fix the robustness of this to allow smoothing on fog layers
// https://www.shadertoy.com/view/MlKcDD
export function distanceToQuadraticBezier(pos, A, B, C) {
  let distance = 0;
  let point = { x: pos.x, y: pos.y };

  const a = subtract(B, A);
  const b = add(subtract(A, multiply(B, 2)), C);
  const c = multiply(a, 2);
  const d = subtract(A, pos);

  // Solve cubic roots to find closest points
  const kk = 1 / dot(b, b);
  const kx = kk * dot(a, b);
  const ky = (kk * (2 * dot(a, a) + dot(d, b))) / 3;
  const kz = kk * dot(d, a);

  const p = ky - kx * kx;
  const p3 = p * p * p;
  const q = kx * (2 * kx * kx - 3 * ky) + kz;
  let h = q * q + 4 * p3;

  if (h >= 0) {
    // 1 root
    h = Math.sqrt(h);
    const x = divide(subtract({ x: h, y: -h }, q), 2);
    const uv = multiply(sign(x), pow(abs(x), 1 / 3));
    const t = Math.min(Math.max(uv.x + uv.y - kx, 0), 1);
    point = add(A, multiply(add(c, multiply(b, t)), t));
    distance = dot2(add(d, multiply(add(c, multiply(b, t)), t)));
  } else {
    // 3 roots but ignore the 3rd one as it will never be closest
    // https://www.shadertoy.com/view/MdXBzB
    const z = Math.sqrt(-p);
    const v = Math.acos(q / (p * z * 2)) / 3;
    const m = Math.cos(v);
    const n = Math.sin(v) * 1.732050808;

    const t = clamp(subtract(multiply({ x: m + m, y: -n - m }, z), kx), 0, 1);
    const d1 = dot2(add(d, multiply(add(c, multiply(b, t.x)), t.x)));
    const d2 = dot2(add(d, multiply(add(c, multiply(b, t.y)), t.y)));
    distance = Math.min(d1, d2);
    if (d1 < d2) {
      point = add(d, multiply(add(c, multiply(b, t.x)), t.x));
    } else {
      point = add(d, multiply(add(c, multiply(b, t.y)), t.y));
    }
  }

  return { distance: Math.sqrt(distance), point: point };
}

export function getBounds(points) {
  let minX = Number.MAX_VALUE;
  let maxX = Number.MIN_VALUE;
  let minY = Number.MAX_VALUE;
  let maxY = Number.MIN_VALUE;
  for (let point of points) {
    minX = point.x < minX ? point.x : minX;
    maxX = point.x > maxX ? point.x : maxX;
    minY = point.y < minY ? point.y : minY;
    maxY = point.y > maxY ? point.y : maxY;
  }
  return { minX, maxX, minY, maxY };
}

// Check bounds then use ray casting algorithm
// https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm
// https://stackoverflow.com/questions/217578/how-can-i-determine-whether-a-2d-point-is-within-a-polygon/2922778
export function pointInPolygon(p, points) {
  const { minX, maxX, minY, maxY } = getBounds(points);
  if (p.x < minX || p.x > maxX || p.y < minY || p.y > maxY) {
    return false;
  }

  let isInside = false;
  for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
    const a = points[i].y > p.y;
    const b = points[j].y > p.y;
    if (
      a !== b &&
      p.x <
        ((points[j].x - points[i].x) * (p.y - points[i].y)) /
          (points[j].y - points[i].y) +
          points[i].x
    ) {
      isInside = !isInside;
    }
  }
  return isInside;
}

/**
 * Returns true if a the distance between a and b is under threshold
 * @param {Vector2} a
 * @param {Vector2} b
 * @param {number} threshold
 */
export function compare(a, b, threshold) {
  return lengthSquared(subtract(a, b)) < threshold * threshold;
}
