/*!
 * SPDX-License-Identifier: AGPL-3.0-or-later
 * Copyright (C) 2026 PUDDY Inc.
 *
 * Part of STARMAP "Personal Sky". This file dynamically links the Swiss
 * Ephemeris (Copyright Astrodienst AG), compiled to WebAssembly by
 * swisseph-wasm (GPL-3.0-or-later, Copyright 2024 prolaxu), and therefore
 * forms a COMBINED WORK with it. It is free software: you may redistribute
 * and/or modify it under the GNU Affero General Public License v3.0 or (at
 * your option) any later version, with NO WARRANTY. See the GNU AGPL:
 * https://www.gnu.org/licenses/agpl-3.0.html
 *
 * The Swiss Ephemeris is used UNMODIFIED, under its free AGPL-3.0 option
 * (not the paid commercial license). Complete Corresponding Source for the
 * engine + this code: https://starmap.puddystudios.com/source.html
 */

/* STARMAP - Personal Sky compute worker (module worker).
 * Loads the Swiss Ephemeris WASM engine (vendor/sweph/, same 2.10.03 core as the
 * Python generator) plus OUR .se1 ephemeris files, sets the user's observer
 * coordinates, and root-finds the TOPOCENTRIC instant for each requested event.
 * Everything runs on the user's device - no coordinate ever leaves the browser.
 *
 * In:  { base, coords: {lat, lon, alt}, jobs: [{ id, type, utc, sign }] }
 * Out: { ok, results: [{ id, utc_topo }], ms }  |  { ok:false, error }
 */
'use strict';

const SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra',
  'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces'];
const TARGET = { new_moon: 0, solar_eclipse: 0, first_quarter: 90, full_moon: 180, lunar_eclipse: 180, last_quarter: -90 };
const FLAGS = 2 | 256 | 32768; // FLG_SWIEPH | FLG_SPEED | FLG_TOPOCTR
const SUN = 0, MOON = 1;

let enginePromise = null;

function initEngine(base) {
  if (enginePromise) return enginePromise;
  enginePromise = (async () => {
    const factory = (await import(base + 'vendor/sweph/swisseph.js')).default;
    const M = await factory({
      // bypass the package's 12MB .data preload - we inject our own ephemeris
      getPreloadedPackage: (_name, size) => new ArrayBuffer(size),
      locateFile: (f) => base + 'vendor/sweph/' + f,
    });
    M.FS.mkdir('/ephe');
    for (const f of ['seas_18.se1', 'semo_18.se1', 'sepl_18.se1']) {
      const buf = await (await fetch(base + 'data/ephe/' + f)).arrayBuffer();
      M.FS.writeFile('/ephe/' + f, new Uint8Array(buf));
    }
    M.ccall('swe_set_ephe_path', null, ['string'], ['/ephe']);
    const xx = M._malloc(48), err = M._malloc(256);
    function lon(jd, body) {
      const ret = M.ccall('swe_calc_ut', 'number',
        ['number', 'number', 'number', 'number', 'number'], [jd, body, FLAGS, xx, err]);
      if (ret < 0) throw new Error(M.UTF8ToString(err));
      return M.HEAPF64[xx >> 3];
    }
    return { M, lon };
  })();
  enginePromise = enginePromise.catch(function (err) { enginePromise = null; throw err; });
  return enginePromise;
}

const isoToJd = (iso) => Date.parse(iso) / 86400000 + 2440587.5;
function jdToIso(jd) {
  const d = new Date((jd - 2440587.5) * 86400000);
  return d.toISOString().replace(/\.(\d{3})\d*Z$/, '.$1Z');
}
const signed = (x) => ((x % 360) + 540) % 360 - 180;

function bisect(f, lo, hi) {
  let flo = f(lo);
  if ((flo < 0) === (f(hi) < 0)) return null; // no sign change in window
  for (let i = 0; i < 60; i++) {
    const mid = (lo + hi) / 2, fm = f(mid);
    if ((flo < 0) === (fm < 0)) { lo = mid; flo = fm; } else { hi = mid; }
  }
  return (lo + hi) / 2;
}

function solveJob(eng, job) {
  const seed = isoToJd(job.utc);
  let f;
  if (job.type === 'ingress') {
    const cusp = SIGNS.indexOf(job.sign) * 30;
    if (cusp < 0) return null;
    f = (jd) => signed(eng.lon(jd, MOON) - cusp);
  } else {
    const tv = TARGET[job.type];
    if (tv === undefined) return null;
    f = (jd) => signed((eng.lon(jd, MOON) - eng.lon(jd, SUN)) - tv);
  }
  // topo shift is bounded by lunar parallax (~0.95 deg ~= 0.08d of Moon motion);
  // +-0.15d covers it, widen once as a fallback
  return bisect(f, seed - 0.15, seed + 0.15) ?? bisect(f, seed - 0.3, seed + 0.3);
}

self.onmessage = async (ev) => {
  const { base, coords, jobs } = ev.data;
  const t0 = Date.now();
  try {
    const eng = await initEngine(base);
    eng.M.ccall('swe_set_topo', null, ['number', 'number', 'number'],
      [coords.lon, coords.lat, coords.alt || 0]);
    const results = [];
    for (const job of jobs) {
      const jd = solveJob(eng, job);
      if (jd !== null) results.push({ id: job.id, utc_topo: jdToIso(jd) });
    }
    self.postMessage({ ok: true, results, ms: Date.now() - t0 });
  } catch (e) {
    self.postMessage({ ok: false, error: String(e && e.message || e) });
  }
};
