import { BoundingBox } from "./BoundingBox.js";
import { ClipboardManager } from "./ClipboardManager.js";
import {
  LassoSelectionMode,
  StrokeSpatialIndex,
  getStrokeBoundingBox,
  pointInPolygon,
  strokeMatchesPolygon
} from "./HitTest.js";
import { SelectionRenderer } from "./SelectionRenderer.js";
import { TransformManager } from "./TransformManager.js";

export class SelectionManager {
  constructor({
    canvas,
    inputCanvas = canvas,
    context,
    toolbar,
    getPoint,
    getStrokes,
    setStrokes,
    drawStroke,
    renderInk,
    requestSave,
    idFactory,
    onSelectionChange,
    mode = LassoSelectionMode.ANY_POINT
  }) {
    this.canvas = canvas;
    this.inputCanvas = inputCanvas;
    this.toolbar = toolbar;
    this.getPoint = getPoint;
    this.getStrokes = getStrokes;
    this.setStrokes = setStrokes;
    this.renderInk = renderInk;
    this.requestSave = requestSave;
    this.onSelectionChange = onSelectionChange || (() => {});
    this.mode = mode;
    this.renderer = new SelectionRenderer({ canvas, context, drawStroke });
    this.transforms = new TransformManager();
    this.clipboard = new ClipboardManager({ idFactory });
    this.index = new StrokeSpatialIndex({ getBounds: getStrokeBoundingBox });
    this.indexDirty = true;
    this.selectedStrokeIds = new Set();
    this.selectedBounds = null;
    this.lassoPoints = [];
    this.dragState = null;
  }

  resizeToElement(element) {
    this.renderer.resizeToElement(element);
    this.renderSelection();
  }

  invalidateIndex() {
    this.indexDirty = true;
  }

  clear({ render = true } = {}) {
    this.selectedStrokeIds.clear();
    this.selectedBounds = null;
    this.onSelectionChange(this.selectedStrokeIds, []);
    this.hideToolbar();
    if (render) {
      this.renderer.clear();
    }
  }

  hasSelection() {
    return this.selectedStrokeIds.size > 0;
  }

  selectedStrokes() {
    const ids = this.selectedStrokeIds;
    return this.getStrokes().filter((stroke) => ids.has(stroke.id));
  }

  startPointer(event) {
    const point = this.getPoint(event, this.inputCanvas);
    if (this.hasSelection() && this.selectedBounds && BoundingBox.containsPoint(this.selectedBounds, point)) {
      this.dragState = {
        pointerId: event.pointerId,
        start: point,
        lastDx: 0,
        lastDy: 0,
        bounds: { ...this.selectedBounds },
        strokes: this.selectedStrokes()
      };
      this.inputCanvas.setPointerCapture(event.pointerId);
      return true;
    }

    this.clear({ render: false });
    this.lassoPoints = [point];
    this.inputCanvas.setPointerCapture(event.pointerId);
    this.renderer.renderLasso(this.lassoPoints);
    return true;
  }

  continuePointer(event) {
    if (this.dragState) {
      const point = this.getPoint(event, this.inputCanvas);
      const dx = point.x - this.dragState.start.x;
      const dy = point.y - this.dragState.start.y;
      this.dragState.lastDx = dx;
      this.dragState.lastDy = dy;
      this.renderer.renderDragPreview(this.dragState.strokes, dx, dy, this.dragState.bounds);
      this.positionToolbar(this.#translatedBounds(this.dragState.bounds, dx, dy));
      return true;
    }

    if (this.lassoPoints.length === 0) {
      return false;
    }

    const events = typeof event.getCoalescedEvents === "function" ? event.getCoalescedEvents() : [event];
    events.forEach((pointerEvent) => {
      const point = this.getPoint(pointerEvent, this.inputCanvas);
      const previous = this.lassoPoints[this.lassoPoints.length - 1];
      if (!previous || Math.hypot(point.x - previous.x, point.y - previous.y) >= 2) {
        this.lassoPoints.push(point);
      }
    });
    this.renderer.renderLasso(this.lassoPoints);
    return true;
  }

  stopPointer(event) {
    if (this.dragState) {
      const { pointerId, lastDx, lastDy, strokes } = this.dragState;
      if (event?.pointerId === pointerId && this.inputCanvas.hasPointerCapture(pointerId)) {
        this.inputCanvas.releasePointerCapture(pointerId);
      }
      this.transforms.translateStrokes(strokes, lastDx, lastDy);
      this.dragState = null;
      this.invalidateIndex();
      this.calculateSelectedBounds();
      this.renderInk();
      this.renderSelection();
      this.requestSave();
      return true;
    }

    if (this.lassoPoints.length === 0) {
      return false;
    }

    if (Number.isFinite(event?.pointerId) && this.inputCanvas.hasPointerCapture(event.pointerId)) {
      this.inputCanvas.releasePointerCapture(event.pointerId);
    }

    const polygon = this.#closedPolygon(this.lassoPoints);
    this.lassoPoints = [];
    this.renderer.stopLasso();
    this.selectByPolygon(polygon);
    return true;
  }

  selectByPolygon(polygon) {
    const polygonBounds = BoundingBox.fromPoints(polygon);
    if (!BoundingBox.isValid(polygonBounds) || polygon.length < 4) {
      this.clear();
      return [];
    }

    this.#ensureIndex();
    const selected = this.index.query(polygonBounds)
      .filter(({ bounds }) => BoundingBox.intersects(bounds, polygonBounds))
      .filter(({ stroke }) => strokeMatchesPolygon(stroke, polygon, this.mode));

    this.selectedStrokeIds = new Set(selected.map(({ stroke }) => stroke.id));
    this.calculateSelectedBounds();
    this.renderSelection();
    this.onSelectionChange(this.selectedStrokeIds, this.selectedStrokes());
    return this.selectedStrokes();
  }

  calculateSelectedBounds() {
    const bounds = BoundingBox.empty();
    this.selectedStrokes().forEach((stroke) => BoundingBox.includeBox(bounds, getStrokeBoundingBox(stroke, 4)));
    this.selectedBounds = BoundingBox.isValid(bounds) ? bounds : null;
    return this.selectedBounds;
  }

  renderSelection() {
    this.renderer.renderSelection({ bounds: this.selectedBounds });
    this.positionToolbar(this.selectedBounds);
  }

  copy() {
    return this.clipboard.copy(this.selectedStrokes());
  }

  cut() {
    if (!this.hasSelection()) {
      return 0;
    }
    const count = this.copy();
    this.deleteSelected();
    return count;
  }

  paste() {
    if (!this.clipboard.hasData()) {
      return [];
    }
    const pasted = this.clipboard.paste();
    const strokes = this.getStrokes();
    this.setStrokes([...strokes, ...pasted]);
    this.selectedStrokeIds = new Set(pasted.map((stroke) => stroke.id));
    this.invalidateIndex();
    this.calculateSelectedBounds();
    this.renderInk();
    this.renderSelection();
    this.requestSave();
    this.onSelectionChange(this.selectedStrokeIds, pasted);
    return pasted;
  }

  /*
  Future lasso action: duplicate selected strokes with a paste offset.
  duplicate() {
    const copied = this.copy();
    if (!copied) {
      return [];
    }
    return this.paste();
  }
  */

  // Kept active because Cut removes the original vector stroke objects after copying.
  deleteSelected() {
    if (!this.hasSelection()) {
      return 0;
    }
    const ids = this.selectedStrokeIds;
    const next = this.getStrokes().filter((stroke) => !ids.has(stroke.id));
    const removed = this.getStrokes().length - next.length;
    this.setStrokes(next);
    this.clear();
    this.invalidateIndex();
    this.renderInk();
    this.requestSave();
    return removed;
  }

  handleKeydown(event) {
    const target = event.target;
    const isEditable = target?.closest?.("[contenteditable='true'], input, textarea, select");
    if (isEditable) {
      return false;
    }

    const key = event.key.toLowerCase();
    if ((event.ctrlKey || event.metaKey) && key === "c" && this.hasSelection()) {
      event.preventDefault();
      this.copy();
      return true;
    }
    if ((event.ctrlKey || event.metaKey) && key === "x" && this.hasSelection()) {
      event.preventDefault();
      this.cut();
      return true;
    }
    if ((event.ctrlKey || event.metaKey) && key === "v") {
      event.preventDefault();
      this.paste();
      return true;
    }
    /*
    Future lasso shortcuts.
    if ((event.ctrlKey || event.metaKey) && key === "d" && this.hasSelection()) {
      event.preventDefault();
      this.duplicate();
      return true;
    }
    if ((event.key === "Delete" || event.key === "Backspace") && this.hasSelection()) {
      event.preventDefault();
      this.deleteSelected();
      return true;
    }
    */
    return false;
  }

  bindToolbar() {
    this.toolbar?.addEventListener("mousedown", (event) => event.preventDefault());
    this.toolbar?.addEventListener("click", (event) => {
      const button = event.target.closest("[data-selection-action]");
      if (!button) {
        return;
      }

      const action = button.dataset.selectionAction;
      if (action === "copy") this.copy();
      if (action === "cut") this.cut();
      if (action === "paste") this.paste();
      /*
      Future lasso toolbar actions.
      if (action === "duplicate") this.duplicate();
      if (action === "delete") this.deleteSelected();
      */
    });
  }

  positionToolbar(bounds) {
    if (!this.toolbar) {
      return;
    }

    if (!BoundingBox.isValid(bounds)) {
      this.hideToolbar();
      return;
    }

    this.toolbar.hidden = false;
    this.toolbar.style.left = `${Math.max(8, bounds.minX)}px`;
    this.toolbar.style.top = `${Math.max(8, bounds.minY - 48)}px`;
  }

  hideToolbar() {
    if (this.toolbar) {
      this.toolbar.hidden = true;
    }
  }

  #ensureIndex() {
    if (!this.indexDirty) {
      return;
    }

    const strokes = this.getStrokes();
    strokes.forEach((stroke, index) => {
      if (!stroke.id) {
        stroke.id = `stroke-existing-${index}-${Date.now()}`;
      }
    });
    this.index.rebuild(strokes);
    this.indexDirty = false;
  }

  #closedPolygon(points) {
    const polygon = [...points];
    const first = polygon[0];
    const last = polygon[polygon.length - 1];
    if (first && last && (first.x !== last.x || first.y !== last.y)) {
      polygon.push({ ...first });
    }
    return polygon;
  }

  #translatedBounds(bounds, dx, dy) {
    return {
      minX: bounds.minX + dx,
      minY: bounds.minY + dy,
      maxX: bounds.maxX + dx,
      maxY: bounds.maxY + dy
    };
  }
}

export { LassoSelectionMode, pointInPolygon };
