import * as THREE from "three";
import {
  pickSelectableHitAtClientPosition,
  pickSelectableAtClientPosition,
  updateHoverLabel,
} from "./viewerInteraction";
import {
  applyKeyboardControl,
  cycleStatsOverlayMode,
  toggleCameraMode,
  navigateFromWheel,
} from "./viewerControls";
import { MAX_CAMERA_DISTANCE, MIN_CAMERA_DISTANCE, NAV_DISTANCE_PLANET_ORBIT } from "./viewerConstants";
import { ViewerHistoryWindowController } from "./viewerHistoryWindowController";
import type { StatsOverlayMode, ViewerHudState } from "./viewerHudState";
import type {
  CameraMode,
  DragMode,
  Selectable,
  WorldState,
  PovLevel,
} from "./viewerTypes";
import type { ViewerOrderContextMenuTarget } from "./ui/stores/viewerOrderContextMenu";

export interface ViewerInteractionContext {
  renderer: THREE.WebGLRenderer;
  raycaster: THREE.Raycaster;
  mouse: THREE.Vector2;
  galaxyCamera: THREE.PerspectiveCamera;
  systemCamera: THREE.PerspectiveCamera;
  galaxySelectableTargets: Map<THREE.Object3D, Selectable>;
  systemSelectableTargets: Map<THREE.Object3D, Selectable>;
  hoverLabelEl: HTMLDivElement;
  hoverConnectorLineEl: SVGLineElement;
  marqueeEl: HTMLDivElement;
  hudState: ViewerHudState;
  keyState: Set<string>;
  getWorld: () => WorldState | undefined;
  getActiveSystemId: () => string | undefined;
  getPovLevel: () => PovLevel;
  getSelectedItems: () => Selectable[];
  setSelectedItems: (items: Selectable[]) => void;
  getDragMode: () => DragMode | undefined;
  setDragMode: (mode: DragMode | undefined) => void;
  getDragPointerId: () => number | undefined;
  setDragPointerId: (pointerId: number | undefined) => void;
  dragStart: THREE.Vector2;
  dragLast: THREE.Vector2;
  getMarqueeActive: () => boolean;
  setMarqueeActive: (value: boolean) => void;
  getSuppressClickSelection: () => boolean;
  setSuppressClickSelection: (value: boolean) => void;
  getDesiredDistance: () => number;
  setDesiredDistance: (value: number) => void;
  getCameraMode: () => CameraMode;
  setCameraMode: (value: CameraMode) => void;
  getCameraTargetShipId: () => string | undefined;
  setCameraTargetShipId: (value: string | undefined) => void;
  getFollowCameraPosition: () => THREE.Vector3;
  getFollowCameraFocus: () => THREE.Vector3;
  screenPointFromClient: (clientX: number, clientY: number) => THREE.Vector2;
  applyPanDelta: (delta: THREE.Vector2) => void;
  syncFollowStateFromSelection: () => void;
  updatePanels: () => void;
  focusOnSelection: (selection: Selectable) => void;
  updateGamePanel: (mode: string) => void;
  openOrderContextMenu: (x: number, y: number, target: ViewerOrderContextMenuTarget) => void;
  closeOrderContextMenu: () => void;
  getStatsOverlayMode: () => StatsOverlayMode;
  setStatsOverlayMode: (mode: StatsOverlayMode) => void;
  refreshStatsOverlay: () => void;
  historyController: ViewerHistoryWindowController;
}

export class ViewerInteractionController {
  private readonly activePointers = new Map<number, THREE.Vector2>();
  private pinchStartDistance?: number;
  private pinchStartZoom?: number;
  private pinchLastCenter?: THREE.Vector2;

  constructor(private readonly context: ViewerInteractionContext) {}

  readonly onPointerDown = (event: PointerEvent) => {
    if (event.button !== 0) {
      return;
    }

    const point = this.context.screenPointFromClient(event.clientX, event.clientY);
    this.activePointers.set(event.pointerId, point);
    this.context.renderer.domElement.setPointerCapture(event.pointerId);

    if (this.activePointers.size >= 2) {
      const gesture = this.getPinchGesture();
      if (!gesture) {
        return;
      }

      this.context.setSuppressClickSelection(true);
      this.context.setDragMode("pinch");
      this.context.setDragPointerId(event.pointerId);
      this.pinchStartDistance = gesture.distance;
      this.pinchStartZoom = this.context.getDesiredDistance();
      this.pinchLastCenter = gesture.center;
      return;
    }

    this.context.setDragMode("pan");
    this.context.setDragPointerId(event.pointerId);
    this.context.dragStart.copy(point);
    this.context.dragLast.copy(point);
  };

  readonly onPointerMove = (event: PointerEvent) => {
    this.updateHoverLabel(event);

    const point = this.context.screenPointFromClient(event.clientX, event.clientY);
    if (this.activePointers.has(event.pointerId)) {
      this.activePointers.set(event.pointerId, point);
    }

    if (this.context.getDragPointerId() !== event.pointerId || !this.context.getDragMode()) {
      return;
    }

    if (this.context.getDragMode() === "pinch") {
      const gesture = this.getPinchGesture();
      if (!gesture || !this.pinchStartDistance || !this.pinchStartZoom || !this.pinchLastCenter) {
        return;
      }

      const zoomRatio = THREE.MathUtils.clamp(gesture.distance / this.pinchStartDistance, 0.25, 4);
      this.context.setDesiredDistance(THREE.MathUtils.clamp(
        this.pinchStartZoom / zoomRatio,
        MIN_CAMERA_DISTANCE,
        MAX_CAMERA_DISTANCE,
      ));
      const centerDelta = gesture.center.clone().sub(this.pinchLastCenter);
      this.pinchLastCenter = gesture.center;
      this.context.applyPanDelta(centerDelta);
      return;
    }

    const delta = point.clone().sub(this.context.dragLast);
    const dragDistance = point.distanceTo(this.context.dragStart);
    if (dragDistance > 6) {
      this.context.setSuppressClickSelection(true);
    }
    this.context.dragLast.copy(point);
    this.context.applyPanDelta(delta);
  };

  readonly onPointerUp = (event: PointerEvent) => {
    if (this.context.renderer.domElement.hasPointerCapture(event.pointerId)) {
      this.context.renderer.domElement.releasePointerCapture(event.pointerId);
    }
    this.activePointers.delete(event.pointerId);

    if (this.activePointers.size >= 2) {
      const gesture = this.getPinchGesture();
      if (gesture) {
        this.context.setDragMode("pinch");
        this.pinchStartDistance = gesture.distance;
        this.pinchStartZoom = this.context.getDesiredDistance();
        this.pinchLastCenter = gesture.center;
      }
      return;
    }

    const remainingPointer = this.activePointers.entries().next();
    if (!remainingPointer.done) {
      const [pointerId, point] = remainingPointer.value;
      this.context.setDragMode("pan");
      this.context.setDragPointerId(pointerId);
      this.context.dragStart.copy(point);
      this.context.dragLast.copy(point);
    } else {
      this.context.setDragMode(undefined);
      this.context.setDragPointerId(undefined);
    }

    this.pinchStartDistance = undefined;
    this.pinchStartZoom = undefined;
    this.pinchLastCenter = undefined;
  };

  readonly onClick = (event: MouseEvent) => {
    this.context.closeOrderContextMenu();
    if (this.context.getSuppressClickSelection()) {
      this.context.setSuppressClickSelection(false);
      return;
    }

    const picked = this.pickSelectableAtClientPosition(event.clientX, event.clientY);
    this.context.setSelectedItems(picked ? [picked] : []);
    if (picked && this.shouldFocusSelectionOnClick(picked)) {
      this.context.focusOnSelection(picked);
    }
    this.context.syncFollowStateFromSelection();
    this.context.updatePanels();
  };

  readonly onContextMenu = (event: MouseEvent) => {
    event.preventDefault();
    this.context.closeOrderContextMenu();

    const picked = this.pickSelectableAtClientPosition(event.clientX, event.clientY);
    if (!picked) {
      return;
    }

    const target = this.buildOrderContextTarget(picked);
    if (!target) {
      return;
    }

    this.context.openOrderContextMenu(event.clientX, event.clientY, target);
  };

  readonly onOpsStripClick = (event: MouseEvent) => {
    const target = event.target;
    if (!(target instanceof HTMLElement)) {
      return;
    }

    const historyButton = target.closest<HTMLElement>("[data-history-ship-id]");
    const historyShipId = historyButton?.dataset.historyShipId;
    if (historyShipId) {
      this.context.historyController.openHistoryWindow({ kind: "ship", id: historyShipId });
      return;
    }

    const shipCard = target.closest<HTMLElement>("[data-ship-id]");
    const shipId = shipCard?.dataset.shipId;
    if (shipId) {
      this.context.setSelectedItems([{ kind: "ship", id: shipId }]);
      this.context.syncFollowStateFromSelection();
      this.context.updatePanels();
      return;
    }

    const stationCard = target.closest<HTMLElement>("[data-station-id]");
    const stationId = stationCard?.dataset.stationId;
    if (stationId) {
      this.context.setSelectedItems([{ kind: "station", id: stationId }]);
      this.context.syncFollowStateFromSelection();
      this.context.updatePanels();
    }
  };

  readonly onOpsStripDoubleClick = (event: MouseEvent) => {
    const target = event.target;
    if (!(target instanceof HTMLElement)) {
      return;
    }

    if (target.closest("[data-history-ship-id]")) {
      return;
    }

    const shipCard = target.closest<HTMLElement>("[data-ship-id]");
    const shipId = shipCard?.dataset.shipId;
    if (shipId) {
      this.context.setSelectedItems([{ kind: "ship", id: shipId }]);
      this.context.syncFollowStateFromSelection();
      this.context.focusOnSelection({ kind: "ship", id: shipId });
      this.toggleCameraMode("tactical");
      this.context.updatePanels();
      this.context.updateGamePanel("Live");
      return;
    }

    const stationCard = target.closest<HTMLElement>("[data-station-id]");
    const stationId = stationCard?.dataset.stationId;
    if (stationId) {
      this.context.setSelectedItems([{ kind: "station", id: stationId }]);
      this.context.syncFollowStateFromSelection();
      this.toggleCameraMode("tactical");
      this.context.focusOnSelection({ kind: "station", id: stationId });
      this.context.updatePanels();
      this.context.updateGamePanel("Live");
    }
  };

  readonly onHistoryLayerClick = (event: MouseEvent) => this.context.historyController.onHistoryLayerClick(event);

  readonly onHistoryLayerPointerDown = (event: PointerEvent) => this.context.historyController.onHistoryLayerPointerDown(event);

  readonly onHistoryWindowPointerMove = (event: PointerEvent) => this.context.historyController.onHistoryWindowPointerMove(event);

  readonly onHistoryWindowPointerUp = (event: PointerEvent) => this.context.historyController.onHistoryWindowPointerUp(event);

  readonly onDoubleClick = () => {
    const selectedItems = this.context.getSelectedItems();
    if (selectedItems.length !== 1) {
      return;
    }

    const selection = selectedItems[0];
    this.context.focusOnSelection(selection);
    this.context.syncFollowStateFromSelection();
    if (selection.kind === "planet") {
      this.context.setDesiredDistance(NAV_DISTANCE_PLANET_ORBIT);
      this.context.updateGamePanel("Live");
      return;
    }

    if (selection.kind === "ship") {
      this.toggleCameraMode("tactical");
      this.context.updatePanels();
      this.context.updateGamePanel("Live");
      return;
    }
  };

  readonly onWheel = (event: WheelEvent) => {
    event.preventDefault();
    this.context.setDesiredDistance(navigateFromWheel(this.context.getDesiredDistance(), event.deltaY));
    this.context.updateGamePanel("Live");
  };

  readonly onKeyDown = (event: KeyboardEvent) => {
    if (event.repeat) {
      return;
    }

    const key = event.key.toLowerCase();
    if (key === "f10") {
      event.preventDefault();
      this.context.setStatsOverlayMode(cycleStatsOverlayMode(this.context.getStatsOverlayMode()));
      this.context.refreshStatsOverlay();
      return;
    }

    const controlState = applyKeyboardControl({
      keyState: this.context.keyState,
      cameraMode: this.context.getCameraMode(),
      desiredDistance: this.context.getDesiredDistance(),
      key,
    });
    this.context.setCameraMode(controlState.cameraMode);
    this.context.setDesiredDistance(controlState.desiredDistance);
    if (key === "c") {
      this.toggleCameraMode();
    }
    this.context.updateGamePanel("Live");
  };

  readonly onKeyUp = (event: KeyboardEvent) => {
    this.context.keyState.delete(event.key.toLowerCase());
  };

  updateHoverLabel(event: PointerEvent) {
    updateHoverLabel({
      dragMode: this.context.getDragMode(),
      hoverState: this.context.hudState.hoverLabel,
      hoverLabelEl: this.context.hoverLabelEl,
      hoverConnectorLineEl: this.context.hoverConnectorLineEl,
      hoverPick: this.pickSelectableHitAtClientPosition(event.clientX, event.clientY),
      activeSystemId: this.context.getActiveSystemId(),
      povLevel: this.context.getPovLevel(),
      world: this.context.getWorld(),
      point: this.context.screenPointFromClient(event.clientX, event.clientY),
    });
  }

  refreshHistoryWindows() {
    this.context.historyController.refreshHistoryWindows();
  }

  openHistoryWindow(selection: Selectable) {
    this.context.historyController.openHistoryWindow(selection);
  }

  toggleCameraMode(forceMode?: CameraMode) {
    const nextState = toggleCameraMode({
      cameraMode: this.context.getCameraMode(),
      cameraTargetShipId: this.context.getCameraTargetShipId(),
      selectedItems: this.context.getSelectedItems(),
      desiredDistance: this.context.getDesiredDistance(),
      followCameraPosition: this.context.getFollowCameraPosition(),
      followCameraFocus: this.context.getFollowCameraFocus(),
      forceMode,
    });
    this.context.setCameraMode(nextState.cameraMode);
    this.context.setCameraTargetShipId(nextState.cameraTargetShipId);
    this.context.setDesiredDistance(nextState.desiredDistance);
  }

  private pickSelectableAtClientPosition(clientX: number, clientY: number) {
    return pickSelectableAtClientPosition(
      this.context.renderer,
      this.context.raycaster,
      this.context.mouse,
      this.context.galaxyCamera,
      this.context.galaxySelectableTargets,
      this.context.systemCamera,
      this.context.systemSelectableTargets,
      clientX,
      clientY,
    );
  }

  private pickSelectableHitAtClientPosition(clientX: number, clientY: number) {
    return pickSelectableHitAtClientPosition(
      this.context.renderer,
      this.context.raycaster,
      this.context.mouse,
      this.context.galaxyCamera,
      this.context.galaxySelectableTargets,
      this.context.systemCamera,
      this.context.systemSelectableTargets,
      clientX,
      clientY,
    );
  }

  private getPinchGesture() {
    const points = [...this.activePointers.values()];
    if (points.length < 2) {
      return null;
    }

    const [first, second] = points;
    return {
      center: first.clone().add(second).multiplyScalar(0.5),
      distance: first.distanceTo(second),
    };
  }

  private shouldFocusSelectionOnClick(selection: Selectable) {
    if (selection.kind === "planet") {
      return true;
    }

    return selection.kind === "system" && selection.id !== this.context.getActiveSystemId();
  }

  private buildOrderContextTarget(selection: Selectable): ViewerOrderContextMenuTarget | null {
    const world = this.context.getWorld();
    if (!world) {
      return null;
    }

    switch (selection.kind) {
      case "ship": {
        const ship = world.ships.get(selection.id);
        return ship ? {
          selection,
          label: ship.name,
          systemId: ship.systemId,
          targetPosition: ship.localPosition,
        } : null;
      }
      case "station": {
        const station = world.stations.get(selection.id);
        return station ? {
          selection,
          label: station.label,
          systemId: station.systemId,
          targetPosition: station.localPosition,
        } : null;
      }
      case "node": {
        const node = world.nodes.get(selection.id);
        return node ? {
          selection,
          label: node.itemId,
          systemId: node.systemId,
          itemId: node.itemId,
          targetPosition: node.localPosition,
        } : null;
      }
      case "celestial": {
        const celestial = world.celestials.get(selection.id);
        return celestial ? {
          selection,
          label: selection.id,
          systemId: celestial.systemId,
          targetPosition: celestial.orbitalAnchor,
        } : null;
      }
      case "construction-site": {
        const site = world.constructionSites.get(selection.id);
        return site ? {
          selection,
          label: site.blueprintId ?? site.targetDefinitionId ?? site.id,
          systemId: site.systemId,
        } : null;
      }
      case "system": {
        const system = world.systems.get(selection.id);
        return system ? {
          selection,
          label: system.label,
          systemId: system.id,
        } : null;
      }
      default:
        return null;
    }
  }
}
