import * as THREE from "three";
import {
  applyCelestialDeltas as applyCelestialDeltaUpdates,
  applyClaimDeltas as applyClaimDeltaUpdates,
  applyConstructionSiteDeltas as applyConstructionSiteDeltaUpdates,
  applyNodeDeltas as applyNodeDeltaUpdates,
  applyShipDeltas as applyShipDeltaUpdates,
  applyStationDeltas as applyStationDeltaUpdates,
  rebuildSystems as rebuildSystemScene,
  syncCelestials as syncCelestialScene,
  syncClaims as syncClaimScene,
  syncConstructionSites as syncConstructionSiteScene,
  syncNodes as syncNodeScene,
  syncShips as syncShipScene,
  syncStations as syncStationScene,
} from "./viewerSceneSync";
import {
  createClaimMesh,
  createConstructionSiteMesh,
  createNodeMesh,
  createResourceDepositMesh,
  createShipMesh,
  createShipTacticalIcon,
  createStationMesh,
  createTacticalIcon,
} from "./viewerSceneFactory";
import {
  deriveNodeOrbital,
  deriveOrbitalFromLocalPosition,
  resolveOrbitalAnchor,
  resolvePointPosition,
} from "./viewerWorldPresentation";
import {
  createCirclePoints,
  shipLength,
  shipPresentationColor,
  shipSize,
  celestialColor,
} from "./viewerSceneAppearance";
import type {
  CelestialDelta,
  CelestialSnapshot,
  ClaimDelta,
  ClaimSnapshot,
  ConstructionSiteDelta,
  ConstructionSiteSnapshot,
  ResourceNodeDelta,
  ResourceNodeSnapshot,
  ShipDelta,
  ShipSnapshot,
  StationDelta,
  StationSnapshot,
  SystemSnapshot,
} from "./contracts";
import type { OrbitalAnchor, Selectable } from "./viewerTypes";
import { rawObject, registerSelectableTarget } from "./viewerScenePrimitives";

export interface ViewerSceneDataContext {
  documentRef: Document;
  getWorldOrbitalTimeSeconds: () => number | undefined;
  getOrbitalSimulationSpeed: () => number;
  getWorldSeed: () => number;
  getWorldTimeSyncMs: () => number;
  getWorldPresentationContext: () => any;
  getActiveSystemId: () => string | undefined;
  galaxySystemGroup: THREE.Group;
  systemScene: THREE.Scene;
  celestialGroup: THREE.Group;
  nodeGroup: THREE.Group;
  stationGroup: THREE.Group;
  claimGroup: THREE.Group;
  constructionSiteGroup: THREE.Group;
  shipGroup: THREE.Group;
  localNodeGroup: THREE.Group;
  localStationGroup: THREE.Group;
  localClaimGroup: THREE.Group;
  localConstructionSiteGroup: THREE.Group;
  localShipGroup: THREE.Group;
  galaxySelectableTargets: Map<THREE.Object3D, Selectable>;
  systemSelectableTargets: Map<THREE.Object3D, Selectable>;
  localSelectableTargets: Map<THREE.Object3D, Selectable>;
  systemVisuals: Map<any, any>;
  planetVisuals: any[];
  celestialVisuals: Map<any, any>;
  nodeVisuals: Map<any, any>;
  stationVisuals: Map<any, any>;
  claimVisuals: Map<any, any>;
  constructionSiteVisuals: Map<any, any>;
  shipVisuals: Map<any, any>;
  localNodeVisuals: Map<any, any>;
  localStationVisuals: Map<any, any>;
  localClaimVisuals: Map<any, any>;
  localConstructionSiteVisuals: Map<any, any>;
  localShipVisuals: Map<any, any>;
}

export class ViewerSceneDataController {
  private activeSystemRootInScene: THREE.Object3D | undefined;

  constructor(private readonly context: ViewerSceneDataContext) {}

  rebuildSystems(systems: SystemSnapshot[]) {
    this.activeSystemRootInScene = undefined;
    rebuildSystemScene(this.createSceneSyncContext(), systems);
    // Re-activate the current active system if any
    const activeId = this.context.getActiveSystemId();
    if (activeId) {
      this.activateSystemRoot(activeId);
    }
  }

  syncCelestials(celestials: CelestialSnapshot[]) {
    syncCelestialScene(this.createSceneSyncContext(), celestials, this.context.getActiveSystemId());
  }

  syncNodes(nodes: ResourceNodeSnapshot[]) {
    syncNodeScene(this.createSceneSyncContext(), nodes, this.context.getActiveSystemId());
  }

  syncStations(stations: StationSnapshot[]) {
    syncStationScene(this.createSceneSyncContext(), stations, this.context.getActiveSystemId());
  }

  syncClaims(claims: ClaimSnapshot[]) {
    syncClaimScene(this.createSceneSyncContext(), claims, this.context.getActiveSystemId());
  }

  syncConstructionSites(sites: ConstructionSiteSnapshot[]) {
    syncConstructionSiteScene(this.createSceneSyncContext(), sites, this.context.getActiveSystemId());
  }

  syncShips(ships: ShipSnapshot[], tickIntervalMs: number) {
    syncShipScene(this.createSceneSyncContext(), ships, tickIntervalMs, this.context.getActiveSystemId());
  }

  applyCelestialDeltas(celestials: CelestialDelta[]) {
    applyCelestialDeltaUpdates(this.createSceneSyncContext(), celestials, this.context.getActiveSystemId());
  }

  applyNodeDeltas(nodes: ResourceNodeDelta[]) {
    applyNodeDeltaUpdates(this.createSceneSyncContext(), nodes, this.context.getActiveSystemId());
  }

  applyStationDeltas(stations: StationDelta[]) {
    applyStationDeltaUpdates(this.createSceneSyncContext(), stations, this.context.getActiveSystemId());
  }

  applyClaimDeltas(claims: ClaimDelta[]) {
    applyClaimDeltaUpdates(this.createSceneSyncContext(), claims, this.context.getActiveSystemId());
  }

  applyConstructionSiteDeltas(sites: ConstructionSiteDelta[]) {
    applyConstructionSiteDeltaUpdates(this.createSceneSyncContext(), sites, this.context.getActiveSystemId());
  }

  applyShipDeltas(ships: ShipDelta[], tickIntervalMs: number) {
    applyShipDeltaUpdates(this.createSceneSyncContext(), ships, tickIntervalMs, this.context.getActiveSystemId());
  }

  refreshLocalLayer(world: any, focusedAnchorId?: string) {
    this.context.localNodeGroup.clear();
    this.context.localStationGroup.clear();
    this.context.localClaimGroup.clear();
    this.context.localConstructionSiteGroup.clear();
    this.context.localShipGroup.clear();
    this.context.localSelectableTargets.clear();
    this.context.localNodeVisuals.clear();
    this.context.localStationVisuals.clear();
    this.context.localClaimVisuals.clear();
    this.context.localConstructionSiteVisuals.clear();
    this.context.localShipVisuals.clear();

    if (!world || !focusedAnchorId) {
      return;
    }

    const activeSystemId = this.context.getActiveSystemId();
    const inFocusedSystem = (systemId: string) => !activeSystemId || systemId === activeSystemId;

    for (const node of world.nodes.values()) {
      if (node.anchorId !== focusedAnchorId || !inFocusedSystem(node.systemId)) {
        continue;
      }

      const mesh = createNodeMesh(node);
      const icon = createTacticalIcon(this.context.documentRef, node.sourceKind === "gas-cloud" ? "#7fd6ff" : "#d2b07a", 100);
      const localPosition = new THREE.Vector3(0, 0, 0);
      mesh.setPosition(localPosition);
      icon.setPosition(localPosition);
      this.context.localNodeVisuals.set(node.id, {
        systemId: node.systemId,
        anchorId: node.anchorId,
        mesh,
        icon,
        sourceKind: node.sourceKind,
        anchor: { kind: "star" as const },
        localPosition,
        orbitRadius: 0,
        orbitPhase: 0,
        orbitInclination: 0,
      });
      this.context.localNodeGroup.add(rawObject(mesh), rawObject(icon));
      for (const deposit of node.deposits) {
        const depositMesh = createResourceDepositMesh(deposit, node);
        this.context.localNodeGroup.add(rawObject(depositMesh));
      }
      registerSelectableTarget(this.context.localSelectableTargets, mesh, { kind: "node", id: node.id });
      registerSelectableTarget(this.context.localSelectableTargets, icon, { kind: "node", id: node.id });
    }

    for (const station of world.stations.values()) {
      if (station.anchorId !== focusedAnchorId || !inFocusedSystem(station.systemId)) {
        continue;
      }

      const mesh = createStationMesh(station);
      const icon = createTacticalIcon(this.context.documentRef, station.color, 130);
      const localPosition = new THREE.Vector3(station.localPosition.x, station.localPosition.y, station.localPosition.z);
      mesh.setPosition(localPosition);
      icon.setPosition(localPosition);
      this.context.localStationVisuals.set(station.id, {
        id: station.id,
        systemId: station.systemId,
        anchorId: station.anchorId,
        mesh,
        icon,
        anchor: { kind: "star" as const },
        orbitRadius: 0,
        orbitPhase: 0,
        orbitInclination: 0,
        localPosition,
      });
      this.context.localStationGroup.add(rawObject(mesh), rawObject(icon));
      registerSelectableTarget(this.context.localSelectableTargets, mesh, { kind: "station", id: station.id });
      registerSelectableTarget(this.context.localSelectableTargets, icon, { kind: "station", id: station.id });
    }

    for (const claim of world.claims.values()) {
      if (claim.anchorId !== focusedAnchorId || !inFocusedSystem(claim.systemId)) {
        continue;
      }

      const mesh = createClaimMesh(claim);
      const icon = createTacticalIcon(this.context.documentRef, "#ff5b5b", 90);
      const localPosition = new THREE.Vector3(0, 0, 0);
      mesh.setPosition(localPosition);
      icon.setPosition(localPosition);
      this.context.localClaimVisuals.set(claim.id, {
        id: claim.id,
        anchorId: claim.anchorId,
        systemId: claim.systemId,
        mesh,
        icon,
        localPosition,
      });
      this.context.localClaimGroup.add(rawObject(mesh), rawObject(icon));
      registerSelectableTarget(this.context.localSelectableTargets, mesh, { kind: "claim", id: claim.id });
      registerSelectableTarget(this.context.localSelectableTargets, icon, { kind: "claim", id: claim.id });
    }

    for (const site of world.constructionSites.values()) {
      if (site.anchorId !== focusedAnchorId || !inFocusedSystem(site.systemId)) {
        continue;
      }

      const mesh = createConstructionSiteMesh(site);
      const icon = createTacticalIcon(this.context.documentRef, "#9df29c", 90);
      const localPosition = new THREE.Vector3(0, 0, 0);
      mesh.setPosition(localPosition);
      icon.setPosition(localPosition);
      this.context.localConstructionSiteVisuals.set(site.id, {
        id: site.id,
        anchorId: site.anchorId,
        systemId: site.systemId,
        mesh,
        icon,
        localPosition,
      });
      this.context.localConstructionSiteGroup.add(rawObject(mesh), rawObject(icon));
      registerSelectableTarget(this.context.localSelectableTargets, mesh, { kind: "construction-site", id: site.id });
      registerSelectableTarget(this.context.localSelectableTargets, icon, { kind: "construction-site", id: site.id });
    }

    for (const ship of world.ships.values()) {
      const shipAnchorId = ship.spatialState.currentAnchorId ?? ship.anchorId;
      if (shipAnchorId !== focusedAnchorId || !inFocusedSystem(ship.systemId) || ship.spatialState.spaceLayer !== "local-space") {
        continue;
      }

      const shipColor = shipPresentationColor(ship);
      const mesh = createShipMesh(ship, shipSize(ship), shipLength(ship), shipColor);
      const icon = createShipTacticalIcon(this.context.documentRef, shipColor, 78);
      const localPosition = new THREE.Vector3(ship.localPosition.x, ship.localPosition.y, ship.localPosition.z);
      mesh.setPosition(localPosition);
      icon.setPosition(localPosition);
      icon.setColor(shipColor);
      this.context.localShipVisuals.set(ship.id, {
        systemId: ship.systemId,
        anchorId: shipAnchorId,
        mesh,
        icon,
        iconBaseScale: 78,
        startPosition: localPosition.clone(),
        authoritativePosition: localPosition.clone(),
        targetPosition: new THREE.Vector3(ship.targetLocalPosition.x, ship.targetLocalPosition.y, ship.targetLocalPosition.z),
        velocity: new THREE.Vector3(ship.localVelocity.x, ship.localVelocity.y, ship.localVelocity.z),
        receivedAtMs: performance.now(),
        blendDurationMs: Math.max(world.tickIntervalMs ?? 80, 80),
      });
      this.context.localShipGroup.add(rawObject(mesh), rawObject(icon));
      registerSelectableTarget(this.context.localSelectableTargets, mesh, { kind: "ship", id: ship.id });
      registerSelectableTarget(this.context.localSelectableTargets, icon, { kind: "ship", id: ship.id });
    }
  }

  /**
   * Called when the active system changes. Swaps which system's root is in systemScene
   * and updates visibility of all system-filtered objects.
   */
  onActiveSystemChanged(oldSystemId: string | undefined, newSystemId: string | undefined) {
    // Remove old system's root from systemScene
    if (this.activeSystemRootInScene) {
      this.context.systemScene.remove(this.activeSystemRootInScene);
      this.activeSystemRootInScene = undefined;
    }

    // Add new system's root to systemScene
    if (newSystemId) {
      this.activateSystemRoot(newSystemId);
    }

    // Update visibility of all system-filtered objects
    this.updateSystemObjectVisibility(newSystemId);
  }

  private activateSystemRoot(systemId: string) {
    const visual = this.context.systemVisuals.get(systemId);
    if (!visual) {
      return;
    }
    const threeObj = rawObject(visual.systemRoot);
    this.context.systemScene.add(threeObj);
    this.activeSystemRootInScene = threeObj;
  }

  private updateSystemObjectVisibility(activeSystemId: string | undefined) {
    for (const visual of this.context.celestialVisuals.values()) {
      const isActive = visual.systemId === activeSystemId;
      visual.mesh.setVisible(isActive);
      visual.icon.setVisible(isActive);
    }
    for (const visual of this.context.nodeVisuals.values()) {
      const isActive = visual.systemId === activeSystemId;
      visual.mesh.setVisible(isActive);
      visual.icon.setVisible(isActive);
    }
    for (const visual of this.context.stationVisuals.values()) {
      const isActive = visual.systemId === activeSystemId;
      visual.mesh.setVisible(isActive);
      visual.icon.setVisible(isActive);
    }
    for (const visual of this.context.claimVisuals.values()) {
      const isActive = visual.systemId === activeSystemId;
      visual.mesh.setVisible(isActive);
      visual.icon.setVisible(isActive);
    }
    for (const visual of this.context.constructionSiteVisuals.values()) {
      const isActive = visual.systemId === activeSystemId;
      visual.mesh.setVisible(isActive);
      visual.icon.setVisible(isActive);
    }
    for (const visual of this.context.shipVisuals.values()) {
      const isActive = visual.systemId === activeSystemId;
      visual.mesh.setVisible(isActive);
      visual.icon.setVisible(isActive);
    }
  }

  createWorldPresentationContext(overrides: {
    world: any;
    activeSystemId?: string;
    focusedAnchorId?: string;
    cameraMode: any;
    povLevel: any;
    orbitYaw: number;
    systemCamera: THREE.PerspectiveCamera;
    systemAnchor: THREE.Vector3;
    toDisplayLocalPosition: (localPosition: THREE.Vector3) => THREE.Vector3;
    setShellReticleOpacity: (sprite: any, opacity: number) => void;
  }) {
    return {
      world: overrides.world,
      worldTimeSyncMs: this.context.getWorldTimeSyncMs(),
      worldSeed: this.context.getWorldSeed(),
      activeSystemId: overrides.activeSystemId,
      focusedAnchorId: overrides.focusedAnchorId,
      cameraMode: overrides.cameraMode,
      povLevel: overrides.povLevel,
      orbitYaw: overrides.orbitYaw,
      camera: overrides.systemCamera,
      systemAnchor: overrides.systemAnchor,
      shipVisuals: this.context.shipVisuals,
      localShipVisuals: this.context.localShipVisuals,
      nodeVisuals: this.context.nodeVisuals,
      localNodeVisuals: this.context.localNodeVisuals,
      celestialVisuals: this.context.celestialVisuals,
      stationVisuals: this.context.stationVisuals,
      localStationVisuals: this.context.localStationVisuals,
      claimVisuals: this.context.claimVisuals,
      localClaimVisuals: this.context.localClaimVisuals,
      constructionSiteVisuals: this.context.constructionSiteVisuals,
      localConstructionSiteVisuals: this.context.localConstructionSiteVisuals,
      systemVisuals: this.context.systemVisuals,
      systemSummaryVisuals: new Map(),
      toDisplayLocalPosition: overrides.toDisplayLocalPosition,
      updateSystemDetailVisibility: () => {},
      setShellReticleOpacity: overrides.setShellReticleOpacity,
    };
  }

  private createSceneSyncContext() {
    return {
      documentRef: this.context.documentRef,
      worldOrbitalTimeSeconds: this.context.getWorldOrbitalTimeSeconds(),
      orbitalSimulationSpeed: this.context.getOrbitalSimulationSpeed(),
      worldSeed: this.context.getWorldSeed(),
      worldTimeSyncMs: this.context.getWorldTimeSyncMs(),
      galaxySystemGroup: this.context.galaxySystemGroup,
      celestialGroup: this.context.celestialGroup,
      nodeGroup: this.context.nodeGroup,
      stationGroup: this.context.stationGroup,
      claimGroup: this.context.claimGroup,
      constructionSiteGroup: this.context.constructionSiteGroup,
      shipGroup: this.context.shipGroup,
      localNodeGroup: this.context.localNodeGroup,
      localStationGroup: this.context.localStationGroup,
      localClaimGroup: this.context.localClaimGroup,
      localConstructionSiteGroup: this.context.localConstructionSiteGroup,
      localShipGroup: this.context.localShipGroup,
      galaxySelectableTargets: this.context.galaxySelectableTargets,
      systemSelectableTargets: this.context.systemSelectableTargets,
      localSelectableTargets: this.context.localSelectableTargets,
      systemVisuals: this.context.systemVisuals,
      planetVisuals: this.context.planetVisuals,
      celestialVisuals: this.context.celestialVisuals,
      nodeVisuals: this.context.nodeVisuals,
      stationVisuals: this.context.stationVisuals,
      claimVisuals: this.context.claimVisuals,
      constructionSiteVisuals: this.context.constructionSiteVisuals,
      shipVisuals: this.context.shipVisuals,
      localNodeVisuals: this.context.localNodeVisuals,
      localStationVisuals: this.context.localStationVisuals,
      localClaimVisuals: this.context.localClaimVisuals,
      localConstructionSiteVisuals: this.context.localConstructionSiteVisuals,
      localShipVisuals: this.context.localShipVisuals,
      shipSize,
      shipLength,
      shipPresentationColor,
      celestialColor,
      createCirclePoints,
      resolvePointPosition: (systemId: string, celestialId?: string | null, anchorId?: string | null) => resolvePointPosition(this.context.getWorldPresentationContext(), systemId, celestialId, anchorId),
      resolveOrbitalAnchor: (systemId: string, localPosition: THREE.Vector3) => resolveOrbitalAnchor(this.context.getWorldPresentationContext(), systemId, localPosition),
      deriveNodeOrbital: (node: ResourceNodeSnapshot | ResourceNodeDelta, anchor: OrbitalAnchor) => deriveNodeOrbital(this.context.getWorldPresentationContext(), node, anchor),
      deriveOrbitalFromLocalPosition: (localPosition: THREE.Vector3, systemId: string, anchor: OrbitalAnchor) => deriveOrbitalFromLocalPosition(this.context.getWorldPresentationContext(), localPosition, systemId, anchor),
    };
  }
}
