import { normalizeLon } from "./mapProjection";

const DEG = Math.PI / 180;
const RAD = 180 / Math.PI;
const EARTH_RADIUS_KM = 6371;
const EARTH_MU_KM3_S2 = 398600.4418;
const SIDEREAL_DAY_SECONDS = 86164;
const DEFAULT_ALTITUDE_KM = 550;
const DEFAULT_TARGET_THRESHOLD_DEG = 6.5;
const TARGET_SCAN_STEP_MS = 20 * 1000;
const STATION_SCAN_STEP_MS = 30 * 1000;
const MIN_WINDOW_MS = 40 * 1000;

export type GroundPoint = {
  lat: number;
  lon: number;
};

export type GroundStationPoint = GroundPoint & {
  id: string;
};

export type PassOrbit = {
  spacecraftId?: string;
  altitudeKm?: number;
};

export type PassRange = {
  fromMs: number;
  toMs: number;
};

export type PassWindow = {
  startMs: number;
  endMs: number;
  quality: number;
};

export function subPoint(orbit: PassOrbit, timeMs: number): GroundPoint {
  const params = orbitParams(orbit);
  const dtSeconds = timeMs / 1000;
  const argument = params.phaseRad + 2 * Math.PI * (dtSeconds / params.periodSeconds);
  const lat = Math.asin(Math.sin(params.inclinationRad) * Math.sin(argument)) * RAD;
  const relativeLon = Math.atan2(Math.cos(params.inclinationRad) * Math.sin(argument), Math.cos(argument));
  const lon = normalizeLon((params.nodeRad + relativeLon) * RAD - (360 / SIDEREAL_DAY_SECONDS) * dtSeconds);

  return { lat, lon };
}

export function targetPasses(orbit: PassOrbit, target: GroundPoint, range: PassRange): PassWindow[] {
  return scanWindows(orbit, target, range, DEFAULT_TARGET_THRESHOLD_DEG, TARGET_SCAN_STEP_MS);
}

export function stationPasses(orbit: PassOrbit, station: GroundPoint, range: PassRange, minElevDeg = 5): PassWindow[] {
  const thresholdDeg = accessAngleDeg(orbit.altitudeKm ?? DEFAULT_ALTITUDE_KM, minElevDeg);
  return scanWindows(orbit, station, range, thresholdDeg, STATION_SCAN_STEP_MS);
}

export function nearestStation(point: GroundPoint, stations: GroundStationPoint[]): GroundStationPoint | undefined {
  let bestStation: GroundStationPoint | undefined;
  let bestDistance = Number.POSITIVE_INFINITY;

  for (const station of stations) {
    const distance = greatCircleDistanceDeg(point, station);
    if (distance < bestDistance) {
      bestDistance = distance;
      bestStation = station;
    }
  }

  return bestStation;
}

export type OrbitPassInfo = {
  index: number;
  startMs: number;
  endMs: number;
  revolution?: number;
};

export function orbitPeriodMs(orbit: PassOrbit): number {
  return orbitParams(orbit).periodSeconds * 1000;
}

export function orbitPassList(orbit: PassOrbit, range: PassRange): OrbitPassInfo[] {
  if (range.toMs <= range.fromMs) return [];
  const periodMs = orbitPeriodMs(orbit);
  const passes: OrbitPassInfo[] = [];
  let t = range.fromMs;
  let index = 1;
  while (t < range.toMs) {
    passes.push({ index, startMs: t, endMs: Math.min(t + periodMs, range.toMs) });
    t += periodMs;
    index++;
  }
  return passes;
}

export function groundTrack(orbit: PassOrbit, range: PassRange, maxPoints = 240): GroundPoint[][] {
  if (range.toMs <= range.fromMs) {
    return [];
  }

  const spanMs = range.toMs - range.fromMs;
  const stepMs = Math.max(60 * 1000, Math.ceil(spanMs / Math.max(2, maxPoints)));
  const segments: GroundPoint[][] = [];
  let current: GroundPoint[] = [];
  let previousLon: number | undefined;

  for (let timeMs = range.fromMs; timeMs <= range.toMs; timeMs += stepMs) {
    const point = subPoint(orbit, timeMs);
    if (previousLon !== undefined && Math.abs(point.lon - previousLon) > 180) {
      if (current.length > 1) {
        segments.push(current);
      }
      current = [];
    }
    current.push(point);
    previousLon = point.lon;
  }

  const endPoint = subPoint(orbit, range.toMs);
  if (current.length === 0 || current[current.length - 1].lat !== endPoint.lat || current[current.length - 1].lon !== endPoint.lon) {
    current.push(endPoint);
  }
  if (current.length > 1) {
    segments.push(current);
  }

  return segments;
}

export function greatCircleDistanceDeg(first: GroundPoint, second: GroundPoint): number {
  const firstLat = first.lat * DEG;
  const secondLat = second.lat * DEG;
  const deltaLon = (second.lon - first.lon) * DEG;
  const cosine =
    Math.sin(firstLat) * Math.sin(secondLat) +
    Math.cos(firstLat) * Math.cos(secondLat) * Math.cos(deltaLon);

  return Math.acos(Math.max(-1, Math.min(1, cosine))) * RAD;
}

export function accessAngleDeg(altitudeKm: number, minElevDeg: number): number {
  const minElevRad = minElevDeg * DEG;
  const ratio = EARTH_RADIUS_KM / (EARTH_RADIUS_KM + altitudeKm);
  return (Math.acos(ratio * Math.cos(minElevRad)) - minElevRad) * RAD;
}

function scanWindows(
  orbit: PassOrbit,
  target: GroundPoint,
  range: PassRange,
  thresholdDeg: number,
  stepMs: number
): PassWindow[] {
  if (range.toMs <= range.fromMs) {
    return [];
  }

  const windows: PassWindow[] = [];
  let inside = false;
  let startMs = range.fromMs;
  let bestMargin = 0;

  for (let timeMs = range.fromMs; timeMs <= range.toMs; timeMs += stepMs) {
    const distance = greatCircleDistanceDeg(subPoint(orbit, timeMs), target);
    const visible = distance <= thresholdDeg;

    if (visible && !inside) {
      inside = true;
      startMs = timeMs;
      bestMargin = thresholdDeg - distance;
    } else if (visible) {
      bestMargin = Math.max(bestMargin, thresholdDeg - distance);
    } else if (inside) {
      windows.push(toWindow(startMs, timeMs, bestMargin, thresholdDeg));
      inside = false;
    }
  }

  if (inside) {
    windows.push(toWindow(startMs, range.toMs, bestMargin, thresholdDeg));
  }

  return windows.filter((window) => window.endMs - window.startMs >= MIN_WINDOW_MS);
}

function toWindow(startMs: number, endMs: number, bestMargin: number, thresholdDeg: number): PassWindow {
  return {
    startMs,
    endMs,
    quality: Math.max(0, Math.min(1, bestMargin / thresholdDeg))
  };
}

function orbitParams(orbit: PassOrbit): {
  altitudeKm: number;
  periodSeconds: number;
  inclinationRad: number;
  nodeRad: number;
  phaseRad: number;
} {
  const altitudeKm = orbit.altitudeKm ?? DEFAULT_ALTITUDE_KM;
  const semiMajorAxis = EARTH_RADIUS_KM + altitudeKm;
  const periodSeconds = 2 * Math.PI * Math.sqrt((semiMajorAxis * semiMajorAxis * semiMajorAxis) / EARTH_MU_KM3_S2);
  const key = orbit.spacecraftId ?? "default-spacecraft";

  return {
    altitudeKm,
    periodSeconds,
    inclinationRad: (97.4 + hash01(key, 7) * 1.6) * DEG,
    nodeRad: hash01(key, 3) * 360 * DEG,
    phaseRad: hash01(key, 11) * 2 * Math.PI
  };
}

function hash01(value: string, salt: number): number {
  let hash = 2166136261 ^ salt;
  for (let index = 0; index < value.length; index += 1) {
    hash ^= value.charCodeAt(index);
    hash = Math.imul(hash, 16777619);
  }
  return ((hash >>> 0) % 100000) / 100000;
}
