import { cellToBoundary, cellToLatLng, getResolution } from 'h3-js';
import type { HexagonData } from '../types';

type Point = [number, number];

const EPSILON = 1e-12;

function samePoint(a: Point, b: Point): boolean {
  return Math.abs(a[0] - b[0]) <= EPSILON && Math.abs(a[1] - b[1]) <= EPSILON;
}

function cellBoundary(h3: string): Point[] {
  const boundary = cellToBoundary(h3, true) as Point[];
  if (boundary.length > 1 && samePoint(boundary[0], boundary[boundary.length - 1])) {
    return boundary.slice(0, -1);
  }
  return boundary;
}

function orientation(a: Point, b: Point, c: Point): number {
  return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
}

function pointOnSegment(point: Point, start: Point, end: Point): boolean {
  if (Math.abs(orientation(start, end, point)) > EPSILON) return false;
  return (
    point[0] >= Math.min(start[0], end[0]) - EPSILON &&
    point[0] <= Math.max(start[0], end[0]) + EPSILON &&
    point[1] >= Math.min(start[1], end[1]) - EPSILON &&
    point[1] <= Math.max(start[1], end[1]) + EPSILON
  );
}

function segmentsIntersect(a: Point, b: Point, c: Point, d: Point): boolean {
  const abC = orientation(a, b, c);
  const abD = orientation(a, b, d);
  const cdA = orientation(c, d, a);
  const cdB = orientation(c, d, b);

  if (
    ((abC > EPSILON && abD < -EPSILON) || (abC < -EPSILON && abD > EPSILON)) &&
    ((cdA > EPSILON && cdB < -EPSILON) || (cdA < -EPSILON && cdB > EPSILON))
  ) {
    return true;
  }

  return (
    pointOnSegment(c, a, b) ||
    pointOnSegment(d, a, b) ||
    pointOnSegment(a, c, d) ||
    pointOnSegment(b, c, d)
  );
}

function pointInPolygon(point: Point, polygon: Point[]): boolean {
  let inside = false;
  for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
    const current = polygon[i];
    const previous = polygon[j];

    if (pointOnSegment(point, previous, current)) return true;

    if (current[1] > point[1] !== previous[1] > point[1]) {
      const x =
        ((previous[0] - current[0]) * (point[1] - current[1])) / (previous[1] - current[1]) +
        current[0];
      if (point[0] < x) inside = !inside;
    }
  }
  return inside;
}

export function polygonsOverlap(a: Point[], b: Point[]): boolean {
  if (a.length < 3 || b.length < 3) return false;

  if (a.some((point) => pointInPolygon(point, b))) return true;
  if (b.some((point) => pointInPolygon(point, a))) return true;

  for (let i = 0; i < a.length; i++) {
    const aNext = (i + 1) % a.length;
    for (let j = 0; j < b.length; j++) {
      const bNext = (j + 1) % b.length;
      if (segmentsIntersect(a[i], a[aNext], b[j], b[bNext])) return true;
    }
  }

  return false;
}

export function hasMatchingHexagonAtResolution(
  hexagons: HexagonData[],
  resolution: number
): boolean {
  return hexagons.some((hexagon) => hexagon.count > 0 && getResolution(hexagon.h3) === resolution);
}

export function findOverlappingSelectableHexagon(
  previousH3: string,
  hexagons: HexagonData[],
  resolution: number
): HexagonData | null {
  const previousBoundary = cellBoundary(previousH3);
  const [previousLat, previousLng] = cellToLatLng(previousH3);
  let best: HexagonData | null = null;
  let bestDistance = Infinity;

  for (const hexagon of hexagons) {
    if (getResolution(hexagon.h3) !== resolution) continue;
    if (!polygonsOverlap(previousBoundary, cellBoundary(hexagon.h3))) continue;

    const distance = (hexagon.lat - previousLat) ** 2 + (hexagon.lon - previousLng) ** 2;
    const hasMatches = hexagon.count > 0;
    const bestHasMatches = (best?.count ?? 0) > 0;
    if (
      !best ||
      (hasMatches && !bestHasMatches) ||
      (hasMatches === bestHasMatches &&
        (distance < bestDistance - EPSILON ||
          (Math.abs(distance - bestDistance) <= EPSILON &&
            (hexagon.count > best.count ||
              (hexagon.count === best.count && hexagon.h3.localeCompare(best.h3) < 0)))))
    ) {
      best = hexagon;
      bestDistance = distance;
    }
  }

  return best;
}
