/* sat.thedarkelite.com - Cesium globe client
   - Static crisp background image behind Cesium (/assets/bg/space.jpg) with subtle drift
   - Cesium canvas made transparent (alpha) so CSS background shows through
   - Radar overlay (RainViewer) as an imagery layer (tiles)
   - Satellites via TLE -> SGP4 (satellite.js)
*/

const API = {
  groups: "/api/groups.php",
  tle: (group) => `/api/tle.php?group=${encodeURIComponent(group)}`
};

// RainViewer Weather Maps API endpoint (public)
const RADAR = {
  api: "https://api.rainviewer.com/public/weather-maps.json", // provides host + frames[1](https://www.rainviewer.com/api/weather-maps-api.html)
  size: 256,
  color: 2,            // universal blue (good default)
  options: "1_0",       // smooth=1, snow=0 (see docs)[1](https://www.rainviewer.com/api/weather-maps-api.html)
  opacity: 0.55,
  refreshMs: 5 * 60 * 1000
};

const state = {
  groups: [],
  group: "noaa",
  sats: [],
  satsFiltered: [],
  satrecs: new Map(),     // norad -> satrec
  entities: new Map(),    // norad -> Cesium.Entity
  selected: null,         // norad
  trackEntity: null,
  lastMeta: null,
  timer: null,

  // background drift
  drift: { enabled: true, raf: null },

  // radar
  radar: {
    layer: null,          // Cesium.ImageryLayer
    provider: null,       // Cesium.UrlTemplateImageryProvider
    framePath: null,      // current /v2/radar/##########
    lastUpdate: 0,
    timer: null
  }
};

const el = (id) => document.getElementById(id);

function fmt(n, d = 3) {
  if (n === null || n === undefined || Number.isNaN(n)) return "—";
  return Number(n).toFixed(d);
}

function setWarn(msg) {
  const chip = el("chipWarn");
  if (!chip) return;
  if (!msg) {
    chip.style.display = "none";
    chip.textContent = "";
    return;
  }
  chip.style.display = "inline-flex";
  chip.textContent = msg;
}

function metaString(meta) {
  if (!meta) return "—";
  const parts = [];
  if (meta.groupName) parts.push(meta.groupName);
  if (meta.source) parts.push(meta.source);
  if (meta.cached !== undefined) parts.push(meta.cached ? "cached" : "fresh");
  if (meta.fetchedAtUtc) parts.push(`fetched ${meta.fetchedAtUtc} UTC`);
  if (meta.cacheTtlSeconds) parts.push(`TTL ${Math.round(meta.cacheTtlSeconds / 60)}m`);
  if (meta.count !== undefined) parts.push(`${meta.count} sats`);
  return parts.join(" • ");
}

/* ---------- Cesium init ---------- */

const viewer = new Cesium.Viewer("cesiumContainer", {
  animation: false,
  timeline: false,
  baseLayerPicker: false,
  homeButton: false,
  geocoder: false,
  sceneModePicker: false,
  navigationHelpButton: false,
  fullscreenButton: false,
  infoBox: false,
  selectionIndicator: false,
  shouldAnimate: false,

  // KEY: make the canvas support transparency so CSS background shows through
  contextOptions: {
    webgl: { alpha: true }
  }
});

// --- BASEMAP (labeled, dark) ---
// Using CARTO dark_all with proper "@2x" support via customTags.
// CARTO documents optional @2x tiles as "{scale}" in URL pattern.[5](https://cdnjs.com/libraries/leaflet)
viewer.imageryLayers.removeAll();
const basemap = viewer.imageryLayers.addImageryProvider(
  new Cesium.UrlTemplateImageryProvider({
    url: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{scale}.png",
    subdomains: ["a", "b", "c", "d"],
    maximumLevel: 20,
    credit: "© OpenStreetMap contributors © CARTO",
    customTags: {
      scale: () => (window.devicePixelRatio > 1 ? "@2x" : "")
    }
  })
); // UrlTemplateImageryProvider supports URL templates + customTags[4](https://usradioguy.com/wxtoimg-kepler-fix/)

// Minor “ops” tuning
basemap.brightness = 0.90;
basemap.contrast = 1.15;
basemap.saturation = 0.95;
basemap.gamma = 1.05;

// ---- CLEAN CRISP BACKGROUND IMAGE MODE ----
// Disable Cesium skybox entirely (prevents blurry/noisy stars). SkyBox can be assigned/removed.[6](https://github.com/CesiumGS/cesium/releases)
viewer.scene.skyBox = null;
viewer.scene.skyAtmosphere.show = false;
viewer.scene.fog.enabled = false;

// Make Cesium background transparent so the CSS image is visible
viewer.scene.backgroundColor = Cesium.Color.TRANSPARENT;

viewer.scene.globe.depthTestAgainstTerrain = false;

// Start camera over North America
viewer.camera.setView({
  destination: Cesium.Cartesian3.fromDegrees(-98.5, 38.5, 12_500_000)
});

function applyLightingToggle() {
  const t = el("toggleLighting");
  viewer.scene.globe.enableLighting = !!(t && t.checked);
}

/* ---------- Background drift (subtle parallax) ---------- */
function startBackgroundDrift() {
  if (!state.drift.enabled) return;

  const container = document.getElementById("cesiumContainer");
  if (!container) return;

  const maxShiftPx = 28;
  const smooth = 0.10;
  let curX = 50;
  let curY = 50;

  function clamp(v, min, max) { return Math.min(max, Math.max(min, v)); }

  function tick() {
    const heading = viewer.camera.heading || 0;
    const pitch = viewer.camera.pitch || 0;

    let hn = ((heading + Math.PI) % (Math.PI * 2)) - Math.PI;

    const targetXpx = (hn / Math.PI) * maxShiftPx;
    const targetYpx = (pitch / (Math.PI / 2)) * (maxShiftPx * 0.6);

    const w = container.clientWidth || 1;
    const h = container.clientHeight || 1;

    const targetX = 50 + (targetXpx / w) * 100;
    const targetY = 50 + (targetYpx / h) * 100;

    curX = curX + (targetX - curX) * smooth;
    curY = curY + (targetY - curY) * smooth;

    curX = clamp(curX, 45, 55);
    curY = clamp(curY, 46, 54);

    container.style.backgroundPosition = `${curX.toFixed(2)}% ${curY.toFixed(2)}%`;

    state.drift.raf = requestAnimationFrame(tick);
  }

  if (state.drift.raf) cancelAnimationFrame(state.drift.raf);
  state.drift.raf = requestAnimationFrame(tick);
}

/* ---------- Radar overlay (RainViewer tiles) ---------- */
/*
  RainViewer provides:
  - JSON listing frames, plus "host" and "path" fields
  - Tile URL format: {host}{path}/{size}/{z}/{x}/{y}/{color}/{options}.png[1](https://www.rainviewer.com/api/weather-maps-api.html)
  - Public API limitations include max zoom 7 in examples/docs[3](https://github.com/rainviewer/rainviewer-api-example/blob/master/README.md)[1](https://www.rainviewer.com/api/weather-maps-api.html)
*/
async function updateRadarLayer(force = false) {
  const now = Date.now();
  if (!force && now - state.radar.lastUpdate < RADAR.refreshMs) return;

  state.radar.lastUpdate = now;

  let data;
  try {
    const res = await fetch(RADAR.api, { cache: "no-store" });
    if (!res.ok) throw new Error(`RainViewer HTTP ${res.status}`);
    data = await res.json();
  } catch (e) {
    // don’t kill app if radar fails
    console.warn("Radar fetch failed:", e);
    return;
  }

  const host = data.host;
  const past = data?.radar?.past;
  if (!host || !Array.isArray(past) || past.length === 0) return;

  // Use latest past frame (stable)
  const latest = past[past.length - 1];
  const path = latest.path; // like "/v2/radar/##########"[1](https://www.rainviewer.com/api/weather-maps-api.html)
  if (!path) return;

  // If frame didn’t change, nothing to do
  if (state.radar.framePath === path && state.radar.layer) return;

  state.radar.framePath = path;

  // Build tile template
  const url =
    `${host}${path}/${RADAR.size}/{z}/{x}/{y}/${RADAR.color}/${RADAR.options}.png`;

  const provider = new Cesium.UrlTemplateImageryProvider({
    url,
    maximumLevel: 7, // public max zoom per RainViewer docs/examples[1](https://www.rainviewer.com/api/weather-maps-api.html)[3](https://github.com/rainviewer/rainviewer-api-example/blob/master/README.md)
    credit: "Radar: RainViewer"
  }); // UrlTemplateImageryProvider supports tile templates[4](https://usradioguy.com/wxtoimg-kepler-fix/)

  // Remove old layer if exists
  if (state.radar.layer) {
    viewer.imageryLayers.remove(state.radar.layer, true);
    state.radar.layer = null;
  }

  state.radar.provider = provider;
  state.radar.layer = viewer.imageryLayers.addImageryProvider(provider);

  // Put radar above basemap
  // (it is added last so it’s already on top; keep explicit ordering safe)
  state.radar.layer.alpha = RADAR.opacity;
}

function startRadarAutoRefresh() {
  // Load immediately, then every 5 min
  updateRadarLayer(true);
  if (state.radar.timer) clearInterval(state.radar.timer);
  state.radar.timer = setInterval(() => updateRadarLayer(false), RADAR.refreshMs);
}

/* ---------- Click picking ---------- */
const clickHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
clickHandler.setInputAction((movement) => {
  const picked = viewer.scene.pick(movement.position);
  if (Cesium.defined(picked) && picked.id && picked.id.id) {
    selectSatellite(String(picked.id.id));
  }
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

/* ---------- Data + propagation ---------- */

async function loadGroups() {
  const res = await fetch(API.groups, { cache: "no-store" });
  if (!res.ok) throw new Error(`groups.php HTTP ${res.status}`);
  const data = await res.json();
  state.groups = data.groups || [];
  return state.groups;
}

function fillGroupSelect() {
  const sel = el("groupSelect");
  sel.innerHTML = "";
  for (const g of state.groups) {
    const opt = document.createElement("option");
    opt.value = g.id;
    opt.textContent = g.name;
    if (g.id === state.group) opt.selected = true;
    sel.appendChild(opt);
  }
}

async function loadTLEs(group) {
  setWarn("");
  el("statMeta").textContent = "Loading…";

  const res = await fetch(API.tle(group), { cache: "no-store" });
  const txt = await res.text();
  if (!res.ok) throw new Error(`tle.php HTTP ${res.status} ${txt}`);
  const data = JSON.parse(txt);

  state.lastMeta = data.meta || null;

  state.sats = (data.satellites || []).map((s) => ({
    name: s.name,
    norad: String(s.norad),
    line1: s.line1,
    line2: s.line2,
    epoch: s.epoch || null
  }));

  state.satrecs.clear();
  for (const s of state.sats) {
    try {
      const satrec = satellite.twoline2satrec(s.line1, s.line2);
      state.satrecs.set(s.norad, satrec);
    } catch (_) {}
  }

  el("statLoaded").textContent = String(state.sats.length);
  el("statMeta").textContent = metaString(state.lastMeta);

  clearSelection();
  applyFilterAndRender();
}

function filterSats() {
  const q = (el("searchBox").value || "").trim().toLowerCase();
  const max = Math.max(50, Math.min(5000, parseInt(el("maxSats").value || "800", 10)));

  let arr = state.sats;

  if (q) {
    arr = arr.filter(
      (s) => s.name.toLowerCase().includes(q) || s.norad.toLowerCase() === q
    );
  }

  arr = arr.slice(0, max);

  state.satsFiltered = arr;
  el("statShown").textContent = String(arr.length);
}

function computeLatLonAltKm(norad, date) {
  const satrec = state.satrecs.get(norad);
  if (!satrec) return null;

  const pv = satellite.propagate(satrec, date);
  if (!pv.position) return null;

  const gmst = satellite.gstime(date);
  const gd = satellite.eciToGeodetic(pv.position, gmst);

  const lat = satellite.degreesLat(gd.latitude);
  const lon = satellite.degreesLong(gd.longitude);
  const altKm = gd.height;

  let speed = null;
  if (pv.velocity) {
    const v = pv.velocity;
    speed = Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
  }

  return { lat, lon, altKm, speed };
}

/* ---------- Entities ---------- */

function clearEntities() {
  for (const ent of state.entities.values()) viewer.entities.remove(ent);
  state.entities.clear();

  if (state.trackEntity) {
    viewer.entities.remove(state.trackEntity);
    state.trackEntity = null;
  }
}

function makeSatEntity(sat) {
  return viewer.entities.add({
    id: sat.norad,
    name: sat.name,
    position: Cesium.Cartesian3.fromDegrees(0, 0, 500000),
    point: {
      pixelSize: 7,
      color: Cesium.Color.fromCssColorString("#9b8cff"),
      outlineColor: Cesium.Color.fromCssColorString("#000000"),
      outlineWidth: 2,
      disableDepthTestDistance: Number.POSITIVE_INFINITY
    }
  });
}

function applyFilterAndRender() {
  filterSats();
  clearEntities();

  for (const sat of state.satsFiltered) {
    const ent = makeSatEntity(sat);
    state.entities.set(sat.norad, ent);
  }

  tick();
}

function selectSatellite(norad) {
  state.selected = norad;

  const sat = state.sats.find((x) => x.norad === norad) || null;
  el("statSelected").textContent = sat ? `${sat.name} (${sat.norad})` : "—";

  for (const [id, ent] of state.entities.entries()) {
    const active = id === norad;

    if (ent.point) {
      ent.point.pixelSize = active ? 11 : 7;
      ent.point.color = active
        ? Cesium.Color.fromCssColorString("#22c55e")
        : Cesium.Color.fromCssColorString("#9b8cff");
      ent.point.outlineColor = active
        ? Cesium.Color.fromCssColorString("#ffffff")
        : Cesium.Color.fromCssColorString("#000000");
      ent.point.outlineWidth = 2;
    }

    if (el("toggleLabels").checked && active && sat) {
      ent.label = new Cesium.LabelGraphics({
        text: `${sat.name} (${sat.norad})`,
        font: "14px ui-sans-serif",
        fillColor: Cesium.Color.WHITE,
        outlineColor: Cesium.Color.BLACK,
        outlineWidth: 3,
        style: Cesium.LabelStyle.FILL_AND_OUTLINE,
        pixelOffset: new Cesium.Cartesian2(12, -12),
        disableDepthTestDistance: Number.POSITIVE_INFINITY
      });
    } else {
      ent.label = undefined;
    }
  }

  if (!sat) {
    el("detail").style.display = "none";
    if (state.trackEntity) {
      viewer.entities.remove(state.trackEntity);
      state.trackEntity = null;
    }
    return;
  }

  el("detailTitle").textContent = sat.name;
  el("detailNorad").textContent = sat.norad;
  el("detailTle").textContent = `${sat.line1}\n${sat.line2}`;
  el("detailEpoch").textContent = sat.epoch || "—";
  el("detail").style.display = "block";

  const pos = computeLatLonAltKm(norad, new Date());
  if (pos) {
    const height = Math.max(1_500_000, pos.altKm * 1000 * 6);
    viewer.camera.flyTo({
      destination: Cesium.Cartesian3.fromDegrees(pos.lon, pos.lat, height),
      duration: 0.6
    });
  }

  drawTrackIfEnabled();
}

function clearSelection() {
  state.selected = null;
  el("statSelected").textContent = "—";
  el("detail").style.display = "none";

  for (const ent of state.entities.values()) {
    if (ent.point) {
      ent.point.pixelSize = 7;
      ent.point.color = Cesium.Color.fromCssColorString("#9b8cff");
      ent.point.outlineColor = Cesium.Color.fromCssColorString("#000000");
      ent.point.outlineWidth = 2;
    }
    ent.label = undefined;
  }

  if (state.trackEntity) {
    viewer.entities.remove(state.trackEntity);
    state.trackEntity = null;
  }
}

function drawTrackIfEnabled() {
  if (!el("toggleTracks").checked) {
    if (state.trackEntity) {
      viewer.entities.remove(state.trackEntity);
      state.trackEntity = null;
    }
    return;
  }
  if (!state.selected) return;

  const norad = state.selected;
  const now = new Date();

  const positions = [];
  for (let t = -10 * 60; t <= 80 * 60; t += 30) {
    const d = new Date(now.getTime() + t * 1000);
    const p = computeLatLonAltKm(norad, d);
    if (!p) continue;
    positions.push(Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.altKm * 1000));
  }

  if (state.trackEntity) {
    viewer.entities.remove(state.trackEntity);
    state.trackEntity = null;
  }

  state.trackEntity = viewer.entities.add({
    id: "__track__",
    polyline: {
      positions,
      width: 2,
      material: new Cesium.PolylineGlowMaterialProperty({
        glowPower: 0.12,
        color: Cesium.Color.fromCssColorString("#22c55e")
      })
    }
  });
}

/* ---------- Tick loop ---------- */

function tick() {
  const now = new Date();
  el("chipTime").textContent = `UTC ${now.toISOString().slice(11, 19)}`;

  viewer.clock.currentTime = Cesium.JulianDate.fromDate(now);

  let selectedPos = null;

  for (const sat of state.satsFiltered) {
    const p = computeLatLonAltKm(sat.norad, now);
    if (!p) continue;

    const ent = state.entities.get(sat.norad);
    if (ent) ent.position = Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.altKm * 1000);

    if (state.selected === sat.norad) selectedPos = p;
  }

  if (selectedPos) {
    el("detailLat").textContent = fmt(selectedPos.lat, 4);
    el("detailLon").textContent = fmt(selectedPos.lon, 4);
    el("detailAlt").textContent = fmt(selectedPos.altKm, 2);
    el("detailSpeed").textContent = fmt(selectedPos.speed, 3);
  }

  if (state.selected && el("toggleTracks").checked) {
    const sec = now.getUTCSeconds();
    if (sec % 5 === 0) drawTrackIfEnabled();
  }
}

/* ---------- UI wiring ---------- */

function wireUI() {
  el("groupSelect").addEventListener("change", async (e) => {
    state.group = e.target.value;
    clearSelection();
    await loadTLEs(state.group);
  });

  el("btnReload").addEventListener("click", async () => {
    clearSelection();
    await loadTLEs(state.group);
  });

  el("btnClear").addEventListener("click", () => clearSelection());
  el("detailClose").addEventListener("click", () => clearSelection());

  el("searchBox").addEventListener("input", () => {
    clearSelection();
    applyFilterAndRender();
  });

  el("maxSats").addEventListener("change", () => {
    clearSelection();
    applyFilterAndRender();
  });

  el("toggleTracks").addEventListener("change", () => drawTrackIfEnabled());

  el("toggleLabels").addEventListener("change", () => {
    if (state.selected) selectSatellite(state.selected);
  });

  el("toggleLighting").addEventListener("change", () => applyLightingToggle());
}

async function boot() {
  try {
    wireUI();
    applyLightingToggle();

    await loadGroups();
    fillGroupSelect();

    await loadTLEs(state.group);

    startBackgroundDrift();

    // Start radar overlay updater
    startRadarAutoRefresh();

    if (state.timer) clearInterval(state.timer);
    state.timer = setInterval(tick, 1000);
    el("chipRate").textContent = "Update: 1s";
  } catch (err) {
    console.error(err);
    setWarn("Load failed (see console)");
    el("statMeta").textContent = String(err.message || err);
  }
}

boot();