import * as THREE from "three";
import { MOON_RENDER_SCALE } from "./viewerConstants";
import type {
  ShipSnapshot,
  PlanetSnapshot,
  MoonSnapshot,
  Vector3Dto,
  WorldSnapshot,
} from "./contracts";
import type {
  OrbitalAnchor,
  WorldState,
  PovLevel,
} from "./viewerTypes";
import type { ZoomBlend } from "./viewerConstants";

export const KILOMETERS_PER_AU = 149_597_870.7;
export const DISPLAY_UNITS_PER_KILOMETER = 0.0000015;
export const DISPLAY_UNITS_PER_LIGHT_YEAR = 2600;

export function formatInventory(entries: { itemId: string; amount: number }[]): string {
  if (entries.length === 0) {
    return "empty";
  }

  return entries
    .map((entry) => `${entry.itemId} ${entry.amount.toFixed(0)}`)
    .join("<br>");
}

export function inventoryAmount(entries: { itemId: string; amount: number }[], itemId: string): number {
  return entries.find((entry) => entry.itemId === itemId)?.amount ?? 0;
}

export function formatVector(vector: Vector3Dto): string {
  return `${vector.x.toFixed(1)}, ${vector.y.toFixed(1)}, ${vector.z.toFixed(1)}`;
}

function formatNumber(value: number, fractionDigits: number) {
  return new Intl.NumberFormat("en-US", {
    minimumFractionDigits: fractionDigits,
    maximumFractionDigits: fractionDigits,
  }).format(value);
}

export function formatLocalDistance(value: number): string {
  return `${formatNumber(value, 0)} km`;
}

export function formatSystemDistance(value: number): string {
  return `${formatNumber(value, 2)} AU`;
}

export function formatGalaxyDistance(value: number): string {
  return `${formatNumber(value, 2)} ly`;
}

export function formatAdaptiveDistanceFromKilometers(kilometers: number): string {
  const absoluteKilometers = Math.max(0, kilometers);
  const meters = absoluteKilometers * 1000;
  const astronomicalUnits = absoluteKilometers / KILOMETERS_PER_AU;
  const lightYears = absoluteKilometers / (KILOMETERS_PER_AU * 63_241.077);

  if (lightYears >= 0.1) {
    return `${formatNumber(lightYears, 2)} ly`;
  }

  if (astronomicalUnits >= 0.1) {
    return `${formatNumber(astronomicalUnits, astronomicalUnits >= 10 ? 1 : 3)} AU`;
  }

  if (absoluteKilometers >= 1) {
    return `${formatNumber(absoluteKilometers, absoluteKilometers >= 100 ? 0 : 2)} km`;
  }

  return `${formatNumber(meters, meters >= 100 ? 0 : 1)} m`;
}

export function formatShipSpeed(ship: ShipSnapshot): string {
  const speed = Math.max(0, ship.travelSpeed);
  const unit = ship.travelSpeedUnit;
  if (unit === "ly/s") {
    return `${formatNumber(speed, 3)} ly/s`;
  }
  if (unit === "AU/s") {
    return `${formatNumber(speed, 4)} AU/s`;
  }
  if (speed >= 1000) {
    return `${formatNumber(speed / 1000, 2)} km/s`;
  }
  return `${formatNumber(speed, 0)} m/s`;
}

export function formatBytes(bytes: number): string {
  if (bytes >= 1024 * 1024) {
    return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
  }
  if (bytes >= 1024) {
    return `${(bytes / 1024).toFixed(1)} KB`;
  }
  return `${Math.round(bytes)} B`;
}

export function smoothBand(value: number, start: number, end: number): number {
  const t = THREE.MathUtils.clamp((value - start) / Math.max(end - start, 1), 0, 1);
  return t * t * (3 - (2 * t));
}

export function computeZoomBlend(distance: number): ZoomBlend {
  const localToSystem = smoothBand(distance, 1200, 5200);
  const systemToUniverse = smoothBand(distance, 9000, 22000);

  return {
    localWeight: 1 - localToSystem,
    systemWeight: Math.min(localToSystem, 1 - systemToUniverse),
    galaxyWeight: systemToUniverse,
  };
}

export function classifyPovLevel(distance: number): PovLevel {
  const blend = computeZoomBlend(distance);
  if (blend.localWeight >= blend.systemWeight && blend.localWeight >= blend.galaxyWeight) {
    return "local";
  }
  if (blend.systemWeight >= blend.galaxyWeight) {
    return "system";
  }
  return "galaxy";
}

export function toThreeVector(vector: Vector3Dto): THREE.Vector3 {
  return new THREE.Vector3(vector.x, vector.y, vector.z);
}

export function scaleGalaxyScalar(lightYears: number): number {
  return lightYears * DISPLAY_UNITS_PER_LIGHT_YEAR;
}

export function scaleLocalScalar(kilometers: number): number {
  return kilometers * DISPLAY_UNITS_PER_KILOMETER;
}

export function scaleGalaxyVector(vector: THREE.Vector3): THREE.Vector3 {
  return vector.clone().multiplyScalar(DISPLAY_UNITS_PER_LIGHT_YEAR);
}

export function toDisplayGalaxyVector(vector: Vector3Dto): THREE.Vector3 {
  return scaleGalaxyVector(toThreeVector(vector));
}

export function scaleLocalVector(vector: THREE.Vector3): THREE.Vector3 {
  return vector.clone().multiplyScalar(DISPLAY_UNITS_PER_KILOMETER);
}

export function currentWorldTimeSeconds(world: WorldState | undefined, worldTimeSyncMs: number): number {
  if (!world) {
    return 0;
  }

  const elapsedMs = performance.now() - worldTimeSyncMs;
  return world.orbitalTimeSeconds + ((elapsedMs / 1000) * world.orbitalSimulation.simulatedSecondsPerRealSecond);
}

export function hashUnit(seed: number, value: string): number {
  let hash = seed;
  for (let index = 0; index < value.length; index += 1) {
    hash = ((hash << 5) - hash) + value.charCodeAt(index);
    hash |= 0;
  }

  return (hash >>> 0) / 0xffffffff;
}

export function computePlanetLocalPosition(planet: PlanetSnapshot, timeSeconds: number, phaseOverrideDegrees?: number): THREE.Vector3 {
  const eccentricity = THREE.MathUtils.clamp(planet.orbitEccentricity, 0, 0.85);
  const meanAnomaly = THREE.MathUtils.degToRad(phaseOverrideDegrees ?? planet.orbitPhaseAtEpoch) + (timeSeconds * planet.orbitSpeed);
  const eccentricAnomaly = meanAnomaly
    + (eccentricity * Math.sin(meanAnomaly))
    + (0.5 * eccentricity * eccentricity * Math.sin(2 * meanAnomaly));
  const semiMajorAxis = planet.orbitRadius * KILOMETERS_PER_AU;
  const semiMinorAxis = semiMajorAxis * Math.sqrt(Math.max(1 - (eccentricity * eccentricity), 0.05));
  const local = new THREE.Vector3(
    semiMajorAxis * (Math.cos(eccentricAnomaly) - eccentricity),
    0,
    semiMinorAxis * Math.sin(eccentricAnomaly),
  );

  local.applyAxisAngle(new THREE.Vector3(0, 1, 0), THREE.MathUtils.degToRad(planet.orbitArgumentOfPeriapsis));
  local.applyAxisAngle(new THREE.Vector3(1, 0, 0), THREE.MathUtils.degToRad(planet.orbitInclination));
  local.applyAxisAngle(new THREE.Vector3(0, 1, 0), THREE.MathUtils.degToRad(planet.orbitLongitudeOfAscendingNode));
  return local;
}

export function computeMoonLocalPosition(moon: MoonSnapshot, timeSeconds: number): THREE.Vector3 {
  const angle = THREE.MathUtils.degToRad(moon.orbitPhaseAtEpoch) + (timeSeconds * moon.orbitSpeed);
  const local = new THREE.Vector3(
    Math.cos(angle) * moon.orbitRadius,
    0,
    Math.sin(angle) * moon.orbitRadius,
  );
  local.applyAxisAngle(new THREE.Vector3(1, 0, 0), THREE.MathUtils.degToRad(moon.orbitInclination));
  local.applyAxisAngle(new THREE.Vector3(0, 1, 0), THREE.MathUtils.degToRad(moon.orbitLongitudeOfAscendingNode));
  return local;
}

export function celestialRenderRadius(size: number, scale: number, minRadius: number, exponent = 1): number {
  return Math.max(minRadius, Math.pow(Math.max(size, 0.1), exponent) * scale);
}

export function computeMoonRenderRadius(moon: MoonSnapshot): number {
  return celestialRenderRadius(moon.size, 0.00011, 0.025, 0.62);
}

export function starHaloOpacity(starKind: string): number {
  if (starKind.includes("neutron")) {
    return 0.22;
  }
  if (starKind.includes("white-dwarf")) {
    return 0.18;
  }
  if (starKind.includes("brown-dwarf")) {
    return 0.1;
  }
  return 0.14;
}

export function resolveOrbitalAnchorPosition(
  world: WorldState | undefined,
  systemId: string,
  anchor: OrbitalAnchor,
  timeSeconds: number,
): THREE.Vector3 {
  if (!world || anchor.kind === "star") {
    return new THREE.Vector3();
  }

  const system = world.systems.get(systemId);
  const planet = system?.planets[anchor.planetIndex];
  if (!system || !planet) {
    return new THREE.Vector3();
  }

  const planetPosition = computePlanetLocalPosition(planet, timeSeconds);
  if (anchor.kind === "planet") {
    return planetPosition;
  }

  const moon = planet.moons[anchor.moonIndex];
  return planetPosition.add(computeMoonLocalPosition(moon, timeSeconds));
}
