import * as THREE from "three";
import { describeHoverLabel, getSelectionGroup } from "./viewerSelection";
import { ACTIVE_SYSTEM_DETAIL_SCALE } from "./viewerConstants";
import { DISPLAY_UNITS_PER_KILOMETER, DISPLAY_UNITS_PER_LIGHT_YEAR, KILOMETERS_PER_AU, formatAdaptiveDistanceFromKilometers, formatSystemDistance } from "./viewerMath";
import type { HoverLabelState, MarqueeState } from "./viewerHudState";
import type { Selectable, SelectionGroup, WorldState, PovLevel } from "./viewerTypes";

export interface HoverPickResult {
  selection: Selectable;
  object: THREE.Object3D;
  /** Which camera was used for this pick (for distance calculation) */
  camera: THREE.Camera;
}

function pickOneCamera(
  renderer: THREE.WebGLRenderer,
  raycaster: THREE.Raycaster,
  mouse: THREE.Vector2,
  camera: THREE.Camera,
  selectableTargets: Map<THREE.Object3D, Selectable>,
  clientX: number,
  clientY: number,
): HoverPickResult | undefined {
  const bounds = renderer.domElement.getBoundingClientRect();
  mouse.x = ((clientX - bounds.left) / bounds.width) * 2 - 1;
  mouse.y = -(((clientY - bounds.top) / bounds.height) * 2 - 1);
  raycaster.setFromCamera(mouse, camera);
  const hit = raycaster.intersectObjects([...selectableTargets.keys()], false)[0];
  const selection = hit ? selectableTargets.get(hit.object) : undefined;
  return hit && selection
    ? { selection, object: hit.object, camera }
    : undefined;
}

export function pickSelectableAtClientPosition(
  renderer: THREE.WebGLRenderer,
  raycaster: THREE.Raycaster,
  mouse: THREE.Vector2,
  galaxyCamera: THREE.Camera,
  galaxySelectableTargets: Map<THREE.Object3D, Selectable>,
  systemCamera: THREE.Camera,
  systemSelectableTargets: Map<THREE.Object3D, Selectable>,
  clientX: number,
  clientY: number,
) {
  const hit = pickSelectableHitAtClientPosition(renderer, raycaster, mouse, galaxyCamera, galaxySelectableTargets, systemCamera, systemSelectableTargets, clientX, clientY);
  return hit?.selection;
}

export function pickSelectableHitAtClientPosition(
  renderer: THREE.WebGLRenderer,
  raycaster: THREE.Raycaster,
  mouse: THREE.Vector2,
  galaxyCamera: THREE.Camera,
  galaxySelectableTargets: Map<THREE.Object3D, Selectable>,
  systemCamera: THREE.Camera,
  systemSelectableTargets: Map<THREE.Object3D, Selectable>,
  clientX: number,
  clientY: number,
): HoverPickResult | undefined {
  // Try system camera first (higher priority when in a system)
  const systemHit = pickOneCamera(renderer, raycaster, mouse, systemCamera, systemSelectableTargets, clientX, clientY);
  if (systemHit) {
    return systemHit;
  }
  return pickOneCamera(renderer, raycaster, mouse, galaxyCamera, galaxySelectableTargets, clientX, clientY);
}

export function updateHoverLabel(params: {
  dragMode?: string;
  hoverState: HoverLabelState;
  hoverLabelEl: HTMLDivElement;
  hoverConnectorLineEl: SVGLineElement;
  hoverPick: HoverPickResult | undefined;
  activeSystemId?: string;
  povLevel: PovLevel;
  world?: WorldState;
  point: THREE.Vector2;
}) {
  const {
    dragMode,
    hoverState,
    hoverLabelEl,
    hoverConnectorLineEl,
    hoverPick,
    activeSystemId,
    povLevel,
    world,
    point,
  } = params;

  if (dragMode || !hoverPick) {
    hoverState.hidden = true;
    hoverState.connectorHidden = true;
    hoverLabelEl.hidden = true;
    hoverConnectorLineEl.setAttribute("hidden", "");
    return;
  }

  const { selection, object, camera } = hoverPick;
  const label = describeHoverLabel(world, selection);
  if (!label) {
    hoverState.hidden = true;
    hoverState.connectorHidden = true;
    hoverLabelEl.hidden = true;
    hoverConnectorLineEl.setAttribute("hidden", "");
    return;
  }

  const distance = formatHoverDistance(camera, object, selection, povLevel, activeSystemId);

  hoverState.hidden = false;
  hoverState.text = `${label}\n${distance}`;
  hoverState.x = point.x + 44;
  hoverState.y = point.y - 90;
  hoverLabelEl.hidden = false;
  hoverLabelEl.textContent = hoverState.text;
  hoverLabelEl.style.left = `${hoverState.x}px`;
  hoverLabelEl.style.top = `${hoverState.y}px`;

  const rect = hoverLabelEl.getBoundingClientRect();
  const svgRect = (hoverConnectorLineEl.ownerSVGElement as SVGSVGElement).getBoundingClientRect();
  hoverState.connectorHidden = false;
  hoverState.x1 = point.x;
  hoverState.y1 = point.y;
  hoverState.x2 = rect.left - svgRect.left;
  hoverState.y2 = rect.top - svgRect.top + rect.height / 2;
  hoverConnectorLineEl.removeAttribute("hidden");
  hoverConnectorLineEl.setAttribute("x1", String(hoverState.x1));
  hoverConnectorLineEl.setAttribute("y1", String(hoverState.y1));
  hoverConnectorLineEl.setAttribute("x2", String(hoverState.x2));
  hoverConnectorLineEl.setAttribute("y2", String(hoverState.y2));
}

function formatHoverDistance(
  camera: THREE.Camera,
  object: THREE.Object3D,
  selection: Selectable,
  povLevel: PovLevel,
  activeSystemId?: string,
) {
  const worldPosition = object.getWorldPosition(new THREE.Vector3());
  const displayDistance = camera.position.distanceTo(worldPosition);

  if (selection.kind === "system") {
    return formatAdaptiveDistanceFromKilometers((displayDistance / DISPLAY_UNITS_PER_LIGHT_YEAR) * 9.4607e12);
  }

  const inActiveSystem = selection.kind === "planet"
    ? selection.systemId === activeSystemId
    : selection.kind === "ship"
      || selection.kind === "station"
      || selection.kind === "node"
      || selection.kind === "celestial"
      || selection.kind === "claim"
      || selection.kind === "construction-site";

  if (inActiveSystem && activeSystemId) {
    const kilometers = displayDistance / (DISPLAY_UNITS_PER_KILOMETER * ACTIVE_SYSTEM_DETAIL_SCALE);
    return povLevel === "system"
      ? formatSystemDistance(kilometers / KILOMETERS_PER_AU)
      : formatAdaptiveDistanceFromKilometers(kilometers);
  }

  return formatAdaptiveDistanceFromKilometers(displayDistance / DISPLAY_UNITS_PER_KILOMETER);
}

export function updateMarqueeBox(
  marqueeState: MarqueeState,
  marqueeEl: HTMLDivElement,
  dragStart: THREE.Vector2,
  dragLast: THREE.Vector2,
) {
  const minX = Math.min(dragStart.x, dragLast.x);
  const minY = Math.min(dragStart.y, dragLast.y);
  const maxX = Math.max(dragStart.x, dragLast.x);
  const maxY = Math.max(dragStart.y, dragLast.y);
  marqueeState.visible = true;
  marqueeState.x = minX;
  marqueeState.y = minY;
  marqueeState.width = maxX - minX;
  marqueeState.height = maxY - minY;
  marqueeEl.style.left = `${minX}px`;
  marqueeEl.style.top = `${minY}px`;
  marqueeEl.style.width = `${maxX - minX}px`;
  marqueeEl.style.height = `${maxY - minY}px`;
}

export function hideMarqueeBox(marqueeState: MarqueeState, marqueeEl: HTMLDivElement) {
  marqueeState.visible = false;
  marqueeState.width = 0;
  marqueeState.height = 0;
  marqueeEl.style.display = "none";
  marqueeEl.style.width = "0";
  marqueeEl.style.height = "0";
}

export function completeMarqueeSelection(params: {
  renderer: THREE.WebGLRenderer;
  systemCamera: THREE.Camera;
  dragStart: THREE.Vector2;
  dragLast: THREE.Vector2;
  systemSelectableTargets: Map<THREE.Object3D, Selectable>;
}) {
  const {
    renderer,
    systemCamera,
    dragStart,
    dragLast,
    systemSelectableTargets,
  } = params;

  const bounds = renderer.domElement.getBoundingClientRect();
  const minX = Math.min(dragStart.x, dragLast.x);
  const minY = Math.min(dragStart.y, dragLast.y);
  const maxX = Math.max(dragStart.x, dragLast.x);
  const maxY = Math.max(dragStart.y, dragLast.y);
  const grouped = new Map<SelectionGroup, Selectable[]>();

  for (const [object, selectable] of systemSelectableTargets.entries()) {
    if (object instanceof THREE.Sprite && !object.visible) {
      continue;
    }
    if (!object.visible) {
      continue;
    }

    const worldPosition = new THREE.Vector3();
    object.getWorldPosition(worldPosition);
    worldPosition.project(systemCamera);
    const screenX = ((worldPosition.x + 1) * 0.5) * bounds.width;
    const screenY = ((1 - worldPosition.y) * 0.5) * bounds.height;
    if (screenX < minX || screenX > maxX || screenY < minY || screenY > maxY) {
      continue;
    }

    const group = getSelectionGroup(selectable);
    const list = grouped.get(group) ?? [];
    if (!list.some((entry) => JSON.stringify(entry) === JSON.stringify(selectable))) {
      list.push(selectable);
    }
    grouped.set(group, list);
  }

  return [...grouped.entries()]
    .sort((left, right) => right[1].length - left[1].length)[0]?.[1] ?? [];
}
