import { BoundingBox } from "./BoundingBox.js";

export const LassoSelectionMode = Object.freeze({
  ANY_POINT: "any-point",
  ENTIRE_STROKE: "entire-stroke"
});

export const getShapePoints = (stroke) => {
  const points = Array.isArray(stroke?.points) ? stroke.points : [];
  const start = {
    x: Number.isFinite(stroke?.startX) ? stroke.startX : points[0]?.x,
    y: Number.isFinite(stroke?.startY) ? stroke.startY : points[0]?.y
  };
  const end = {
    x: Number.isFinite(stroke?.endX) ? stroke.endX : points[1]?.x,
    y: Number.isFinite(stroke?.endY) ? stroke.endY : points[1]?.y
  };

  if (stroke?.tool === "line") {
    return [start, end].filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y));
  }

  const x = Number.isFinite(stroke?.x) ? stroke.x : start.x;
  const y = Number.isFinite(stroke?.y) ? stroke.y : start.y;
  const width = Number.isFinite(stroke?.width) ? stroke.width : end.x - start.x;
  const height = Number.isFinite(stroke?.height) ? stroke.height : end.y - start.y;

  return [
    { x, y },
    { x: x + width, y },
    { x: x + width, y: y + height },
    { x, y: y + height }
  ].filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y));
};

export const getStrokeGeometryPoints = (stroke) => {
  if (!stroke) {
    return [];
  }

  if (stroke.tool === "line" || stroke.tool === "rectangle" || stroke.tool === "square") {
    return getShapePoints(stroke);
  }

  return Array.isArray(stroke.points) ? stroke.points : [];
};

export const getStrokeBoundingBox = (stroke, padding = 0) => {
  const box = BoundingBox.fromPoints(getStrokeGeometryPoints(stroke));
  return padding > 0 ? BoundingBox.inflate(box, padding) : box;
};

export const pointInPolygon = (point, polygon) => {
  if (!point || !Array.isArray(polygon) || polygon.length < 3) {
    return false;
  }

  let inside = false;
  for (let current = 0, previous = polygon.length - 1; current < polygon.length; previous = current, current += 1) {
    const a = polygon[current];
    const b = polygon[previous];
    const intersects = ((a.y > point.y) !== (b.y > point.y))
      && point.x < ((b.x - a.x) * (point.y - a.y)) / ((b.y - a.y) || Number.EPSILON) + a.x;
    if (intersects) {
      inside = !inside;
    }
  }

  return inside;
};

export const strokeMatchesPolygon = (stroke, polygon, mode = LassoSelectionMode.ANY_POINT) => {
  const points = getStrokeGeometryPoints(stroke);
  if (points.length === 0) {
    return false;
  }

  if (mode === LassoSelectionMode.ENTIRE_STROKE) {
    return points.every((point) => pointInPolygon(point, polygon));
  }

  return points.some((point) => pointInPolygon(point, polygon));
};

export class StrokeSpatialIndex {
  constructor({ cellSize = 512, getBounds = getStrokeBoundingBox } = {}) {
    this.cellSize = cellSize;
    this.getBounds = getBounds;
    this.cells = new Map();
    this.entries = new Map();
  }

  rebuild(strokes = []) {
    this.cells.clear();
    this.entries.clear();
    strokes.forEach((stroke) => this.insert(stroke));
  }

  insert(stroke) {
    const bounds = this.getBounds(stroke, 8);
    if (!BoundingBox.isValid(bounds)) {
      return;
    }

    const id = stroke.id;
    const entry = { id, stroke, bounds };
    this.entries.set(id, entry);
    this.#cellKeys(bounds).forEach((key) => {
      if (!this.cells.has(key)) {
        this.cells.set(key, new Set());
      }
      this.cells.get(key).add(id);
    });
  }

  query(bounds) {
    if (!BoundingBox.isValid(bounds)) {
      return [];
    }

    const ids = new Set();
    this.#cellKeys(bounds).forEach((key) => {
      this.cells.get(key)?.forEach((id) => ids.add(id));
    });

    return Array.from(ids)
      .map((id) => this.entries.get(id))
      .filter((entry) => entry && BoundingBox.intersects(entry.bounds, bounds));
  }

  #cellKeys(bounds) {
    const minCol = Math.floor(bounds.minX / this.cellSize);
    const maxCol = Math.floor(bounds.maxX / this.cellSize);
    const minRow = Math.floor(bounds.minY / this.cellSize);
    const maxRow = Math.floor(bounds.maxY / this.cellSize);
    const keys = [];

    for (let row = minRow; row <= maxRow; row += 1) {
      for (let col = minCol; col <= maxCol; col += 1) {
        keys.push(`${col}:${row}`);
      }
    }

    return keys;
  }
}
