const DEG_TO_RAD = Math.PI / 180;
const J2000 = 2451545.0;

const ELEMENTS = {
  mercury: [0.38709927, 0.00000037, 0.20563593, 0.00001906, 7.00497902, -0.00594749, 252.25032350, 149472.67411175, 77.45779628, 0.16047689, 48.33076593, -0.12534081],
  venus: [0.72333566, 0.00000390, 0.00677672, -0.00004107, 3.39467605, -0.00078890, 181.97909950, 58517.81538729, 131.60246718, 0.00268329, 76.67984255, -0.27769418],
  earth: [1.00000261, 0.00000562, 0.01671123, -0.00004392, -0.00001531, -0.01294668, 100.46457166, 35999.37244981, 102.93768193, 0.32327364, 0, 0],
  mars: [1.52371034, 0.00001847, 0.09339410, 0.00007882, 1.84969142, -0.00813131, -4.55343205, 19140.30268499, -23.94362959, 0.44441088, 49.55953891, -0.29257343],
  jupiter: [5.20288700, -0.00011607, 0.04838624, -0.00013253, 1.30439695, -0.00183714, 34.39644051, 3034.74612775, 14.72847983, 0.21252668, 100.47390909, 0.20469106],
  saturn: [9.53667594, -0.00125060, 0.05386179, -0.00050991, 2.48599187, 0.00193609, 49.95424423, 1222.49362201, 92.59887831, -0.41897216, 113.66242448, -0.28867794],
  uranus: [19.18916464, -0.00196176, 0.04725744, -0.00004397, 0.77263783, -0.00242939, 313.23810451, 428.48202785, 170.95427630, 0.40805281, 74.01692503, 0.04240589],
  neptune: [30.06992276, 0.00026291, 0.00859048, 0.00005105, 1.77004347, 0.00035372, -55.12002969, 218.45945325, 44.96476227, -0.32241464, 131.78422574, -0.00508664]
};

const ORBITAL_PERIOD_DAYS = {
  mercury: 87.969,
  venus: 224.701,
  earth: 365.256,
  mars: 686.98,
  jupiter: 4332.589,
  saturn: 10759.22,
  uranus: 30685.4,
  neptune: 60190
};

export function julianDate(date = new Date()) {
  return date.getTime() / 86400000 + 2440587.5;
}

export function approximatePlanetPosition(bodyId, date = new Date()) {
  const element = ELEMENTS[bodyId];
  if (!element) {
    return { x: 0, y: 0, z: 0 };
  }

  const jd = julianDate(date);
  const t = (jd - J2000) / 36525;
  const [a0, aRate, e0, eRate, i0, iRate, l0, lRate, peri0, periRate, node0, nodeRate] = element;
  const a = a0 + aRate * t;
  const e = e0 + eRate * t;
  const inclination = normalizeDegrees(i0 + iRate * t) * DEG_TO_RAD;
  const meanLongitude = normalizeDegrees(l0 + lRate * t);
  const longitudeOfPerihelion = normalizeDegrees(peri0 + periRate * t);
  const longitudeOfAscendingNode = normalizeDegrees(node0 + nodeRate * t) * DEG_TO_RAD;
  const argumentOfPerihelion = (longitudeOfPerihelion * DEG_TO_RAD) - longitudeOfAscendingNode;
  const meanAnomaly = normalizeDegrees(meanLongitude - longitudeOfPerihelion) * DEG_TO_RAD;
  const eccentricAnomaly = solveKepler(meanAnomaly, e);
  const xv = a * (Math.cos(eccentricAnomaly) - e);
  const yv = a * Math.sqrt(1 - e * e) * Math.sin(eccentricAnomaly);
  const trueAnomaly = Math.atan2(yv, xv);
  const radius = Math.sqrt(xv * xv + yv * yv);
  const angle = argumentOfPerihelion + trueAnomaly;

  return {
    x: radius * (Math.cos(longitudeOfAscendingNode) * Math.cos(angle) - Math.sin(longitudeOfAscendingNode) * Math.sin(angle) * Math.cos(inclination)),
    y: radius * (Math.sin(longitudeOfAscendingNode) * Math.cos(angle) + Math.cos(longitudeOfAscendingNode) * Math.sin(angle) * Math.cos(inclination)),
    z: radius * (Math.sin(angle) * Math.sin(inclination))
  };
}

export function approximateSolarSystem(date = new Date()) {
  return Object.keys(ELEMENTS).map(id => ({
    id,
    heliocentricAu: approximatePlanetPosition(id, date)
  }));
}

export function orbitSamples(bodyId, date = new Date(), count = 720) {
  const element = elementsForDate(bodyId, date);
  if (!element) {
    return [];
  }

  const samples = [];
  const sampleCount = Math.max(64, count);
  for (let index = 0; index <= sampleCount; index += 1) {
    samples.push(positionFromEccentricAnomaly(element, (index / sampleCount) * Math.PI * 2));
  }
  return samples;
}

export function orbitalPeriodDays(bodyId) {
  return ORBITAL_PERIOD_DAYS[bodyId] ?? 365.256;
}

function elementsForDate(bodyId, date) {
  const element = ELEMENTS[bodyId];
  if (!element) {
    return null;
  }

  const jd = julianDate(date);
  const t = (jd - J2000) / 36525;
  const [a0, aRate, e0, eRate, i0, iRate, , , peri0, periRate, node0, nodeRate] = element;
  const node = normalizeDegrees(node0 + nodeRate * t) * DEG_TO_RAD;
  const perihelion = normalizeDegrees(peri0 + periRate * t) * DEG_TO_RAD;
  return {
    a: a0 + aRate * t,
    e: e0 + eRate * t,
    inclination: normalizeDegrees(i0 + iRate * t) * DEG_TO_RAD,
    node,
    argumentOfPerihelion: perihelion - node
  };
}

function positionFromEccentricAnomaly(element, eccentricAnomaly) {
  const xv = element.a * (Math.cos(eccentricAnomaly) - element.e);
  const yv = element.a * Math.sqrt(1 - element.e * element.e) * Math.sin(eccentricAnomaly);
  const cosNode = Math.cos(element.node);
  const sinNode = Math.sin(element.node);
  const cosPeri = Math.cos(element.argumentOfPerihelion);
  const sinPeri = Math.sin(element.argumentOfPerihelion);
  const cosInclination = Math.cos(element.inclination);
  const sinInclination = Math.sin(element.inclination);

  return {
    x: (cosNode * cosPeri - sinNode * sinPeri * cosInclination) * xv +
      (-cosNode * sinPeri - sinNode * cosPeri * cosInclination) * yv,
    y: (sinNode * cosPeri + cosNode * sinPeri * cosInclination) * xv +
      (-sinNode * sinPeri + cosNode * cosPeri * cosInclination) * yv,
    z: (sinPeri * sinInclination) * xv + (cosPeri * sinInclination) * yv
  };
}

function solveKepler(meanAnomaly, eccentricity) {
  let eccentricAnomaly = eccentricity < 0.8 ? meanAnomaly : Math.PI;
  for (let index = 0; index < 12; index += 1) {
    const delta = (eccentricAnomaly - eccentricity * Math.sin(eccentricAnomaly) - meanAnomaly) /
      (1 - eccentricity * Math.cos(eccentricAnomaly));
    eccentricAnomaly -= delta;
    if (Math.abs(delta) < 1e-10) {
      break;
    }
  }
  return eccentricAnomaly;
}

function normalizeDegrees(value) {
  return ((value % 360) + 360) % 360;
}
