/**
 * Hub landing globe — orthographic Canvas2D (globe3d=1).
 * Bakes globe3d=0 stack to equirect maps once; runtime always samples by lon/lat
 * after inverse Y-rotation (rotation-invariant, full sphere wrap).
 */
export const GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 322, rim: 330 };

const AUTO_SPIN_RAD_S = 0.035;
const DRAG_ROTATION_PER_PX = 0.004;
const OCEAN_FALLBACK = { r: 22, g: 38, b: 60 };
const FRONT_Z = 0.02;
const RASTER_SCALE = 2;
const EQ_W = 2048;
const EQ_H = 1024;
const GRID_PARALLELS = [0, 0.521, -0.521];
const GRID_MERIDIAN_SPACING = Math.PI / 6;
const GRID_LINE_RAD = 0.022;

function hubAsset(rel, build) {
  const link = document.querySelector('link[href*="hub.css"]');
  const base = link ? (link.getAttribute('href') || '').replace(/[^/]*$/, '') : '';
  const sep = rel.indexOf('?') >= 0 ? '&' : '?';
  return base + rel + sep + 'v=' + build;
}

export function globeCoverRect() {
  const vw = window.innerWidth;
  const vh = window.innerHeight;
  const scale = Math.max(vw / GLOBE_VIEW.imgW, vh / GLOBE_VIEW.imgH);
  const drawW = GLOBE_VIEW.imgW * scale;
  const drawH = GLOBE_VIEW.imgH * scale;
  const offX = (vw - drawW) / 2;
  const offY = (vh - drawH) / 2;
  const left = offX + (GLOBE_VIEW.cx - GLOBE_VIEW.r) * scale;
  const top = offY + (GLOBE_VIEW.cy - GLOBE_VIEW.r) * scale;
  const size = GLOBE_VIEW.r * 2 * scale;
  return {
    left,
    top,
    width: size,
    height: size,
    scale,
    cx: GLOBE_VIEW.cx * scale + offX,
    cy: GLOBE_VIEW.cy * scale + offY,
    r: GLOBE_VIEW.r * scale,
    rim: GLOBE_VIEW.rim * scale,
  };
}

function loadImage(url) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = () => reject(new Error('image load failed: ' + url));
    img.src = url;
  });
}

function rasterizeImage(img, w, h, scale) {
  const s = scale || 1;
  const rw = Math.max(1, Math.round(w * s));
  const rh = Math.max(1, Math.round(h * s));
  const c = document.createElement('canvas');
  c.width = rw;
  c.height = rh;
  const ctx = c.getContext('2d', { willReadFrequently: true });
  ctx.imageSmoothingEnabled = true;
  ctx.imageSmoothingQuality = 'high';
  ctx.drawImage(img, 0, 0, rw, rh);
  return { data: ctx.getImageData(0, 0, rw, rh).data, w: rw, h: rh };
}

function sampleImageDataBilinear(imgData, w, h, fx, fy) {
  const x = Math.max(0, Math.min(w - 1.001, fx));
  const y = Math.max(0, Math.min(h - 1.001, fy));
  const x0 = x | 0;
  const y0 = y | 0;
  const x1 = Math.min(w - 1, x0 + 1);
  const y1 = Math.min(h - 1, y0 + 1);
  const tx = x - x0;
  const ty = y - y0;
  const i00 = (y0 * w + x0) * 4;
  const i10 = (y0 * w + x1) * 4;
  const i01 = (y1 * w + x0) * 4;
  const i11 = (y1 * w + x1) * 4;
  const out = [0, 0, 0, 0];
  for (let c = 0; c < 4; c++) {
    out[c] = Math.round(
      imgData[i00 + c] * (1 - tx) * (1 - ty) +
        imgData[i10 + c] * tx * (1 - ty) +
        imgData[i01 + c] * (1 - tx) * ty +
        imgData[i11 + c] * tx * ty
    );
  }
  return out;
}

function inverseRotateY(nx, ny, nz, rotY) {
  const cosR = Math.cos(rotY);
  const sinR = Math.sin(rotY);
  return {
    x: nx * cosR - nz * sinR,
    y: ny,
    z: nx * sinR + nz * cosR,
  };
}

/** Hub texture coords: N-up artwork on orthographic disk (matches globe3d=0). */
function texFromLonLat(lon, lat) {
  const cosLat = Math.cos(lat);
  return {
    x: cosLat * Math.sin(lon),
    y: Math.sin(lat),
    z: cosLat * Math.cos(lon),
  };
}

function bodyToTex(body) {
  return { x: body.x, y: -body.y, z: body.z };
}

function bodyLonLat(body) {
  const tex = bodyToTex(body);
  return {
    lon: Math.atan2(tex.x, tex.z),
    lat: Math.asin(Math.max(-1, Math.min(1, tex.y))),
  };
}

function eqUV(lon, lat, w, h) {
  return {
    x: ((lon + Math.PI) / (2 * Math.PI)) * (w - 1),
    y: (0.5 - lat / Math.PI) * (h - 1),
  };
}

function sampleEq(map, lon, lat) {
  const uv = eqUV(lon, lat, map.w, map.h);
  return sampleImageDataBilinear(map.data, map.w, map.h, uv.x, uv.y);
}

function sampleHub(layer, tex) {
  const hp = {
    x: GLOBE_VIEW.cx + tex.x * GLOBE_VIEW.r,
    y: GLOBE_VIEW.cy - tex.y * GLOBE_VIEW.r,
  };
  const sx = (hp.x / GLOBE_VIEW.imgW) * (layer.w - 1);
  const sy = (hp.y / GLOBE_VIEW.imgH) * (layer.h - 1);
  return sampleImageDataBilinear(layer.data, layer.w, layer.h, sx, sy);
}

function sampleMercFlat(mercFlat, lon, lat) {
  const uv = eqUV(lon, lat, mercFlat.w, mercFlat.h);
  return sampleImageDataBilinear(mercFlat.data, mercFlat.w, mercFlat.h, uv.x, uv.y);
}

function isLandPixel(r, g, b, a) {
  return a > 10 && r > 100 && g < 140 && b < 140;
}

function isGridPixel(r, g, b, a) {
  return a > 8 && r + g + b > 260;
}

function analyticGridRgba(lon, lat) {
  const wrapped = ((lon + Math.PI) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI);
  const cell = wrapped % GRID_MERIDIAN_SPACING;
  const merDist = Math.min(cell, GRID_MERIDIAN_SPACING - cell);
  const onMeridian = merDist < GRID_LINE_RAD;
  let onParallel = false;
  let bright = false;
  for (let i = 0; i < GRID_PARALLELS.length; i++) {
    const targetLat = Math.asin(Math.max(-1, Math.min(1, GRID_PARALLELS[i])));
    if (Math.abs(lat - targetLat) < GRID_LINE_RAD) {
      onParallel = true;
      bright = i === 0;
    }
  }
  if (!onMeridian && !onParallel) {
    return [0, 0, 0, 0];
  }
  if (onMeridian) {
    const prime = Math.abs(lon) < GRID_LINE_RAD * 1.5 || Math.abs(Math.abs(lon) - Math.PI) < GRID_LINE_RAD * 1.5;
    return prime ? [238, 245, 255, 235] : [238, 245, 255, 185];
  }
  return bright ? [219, 231, 246, 210] : [231, 236, 243, 155];
}

function bakeOceanEquator(globe) {
  const out = new Uint8ClampedArray(EQ_W * EQ_H * 4);
  for (let j = 0; j < EQ_H; j++) {
    for (let i = 0; i < EQ_W; i++) {
      const lon = (i / EQ_W) * 2 * Math.PI - Math.PI;
      const lat = Math.PI * 0.5 - (j / EQ_H) * Math.PI;
      const tex = texFromLonLat(lon, lat);
      const p = (j * EQ_W + i) * 4;
      let rgba = [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255];

      if (tex.z > FRONT_Z) {
        const s = sampleHub(globe, tex);
        if (!isGridPixel(s[0], s[1], s[2], s[3]) && s[3] > 8) {
          rgba = [s[0], s[1], s[2], 255];
        }
      }

      out[p] = rgba[0];
      out[p + 1] = rgba[1];
      out[p + 2] = rgba[2];
      out[p + 3] = rgba[3];
    }
  }
  return { data: out, w: EQ_W, h: EQ_H };
}

function bakeGridEquator(globe, gridOverlay) {
  const out = new Uint8ClampedArray(EQ_W * EQ_H * 4);
  for (let j = 0; j < EQ_H; j++) {
    for (let i = 0; i < EQ_W; i++) {
      const lon = (i / EQ_W) * 2 * Math.PI - Math.PI;
      const lat = Math.PI * 0.5 - (j / EQ_H) * Math.PI;
      const tex = texFromLonLat(lon, lat);
      const p = (j * EQ_W + i) * 4;
      let rgba = analyticGridRgba(lon, lat);

      if (tex.z > FRONT_Z) {
        const overlay = sampleHub(gridOverlay, tex);
        if (isGridPixel(overlay[0], overlay[1], overlay[2], overlay[3])) {
          rgba = overlay;
        } else {
          const base = sampleHub(globe, tex);
          if (isGridPixel(base[0], base[1], base[2], base[3])) {
            rgba = base;
          }
        }
      }

      out[p] = rgba[0];
      out[p + 1] = rgba[1];
      out[p + 2] = rgba[2];
      out[p + 3] = rgba[3];
    }
  }
  return { data: out, w: EQ_W, h: EQ_H };
}

function bakeLandEquator(continents, mercFlat) {
  const out = new Uint8ClampedArray(EQ_W * EQ_H * 4);
  for (let j = 0; j < EQ_H; j++) {
    for (let i = 0; i < EQ_W; i++) {
      const lon = (i / EQ_W) * 2 * Math.PI - Math.PI;
      const lat = Math.PI * 0.5 - (j / EQ_H) * Math.PI;
      const tex = texFromLonLat(lon, lat);
      const p = (j * EQ_W + i) * 4;
      let rgba = [0, 0, 0, 0];

      if (tex.z > FRONT_Z) {
        const ortho = sampleHub(continents, tex);
        if (ortho[3] > 6) {
          rgba = [ortho[0], ortho[1], ortho[2], ortho[3]];
        }
      }

      if (rgba[3] < 12) {
        const flat = sampleMercFlat(mercFlat, lon, lat);
        if (isLandPixel(flat[0], flat[1], flat[2], flat[3])) {
          rgba = [flat[0], flat[1], flat[2], Math.min(255, flat[3])];
        }
      }

      out[p] = rgba[0];
      out[p + 1] = rgba[1];
      out[p + 2] = rgba[2];
      out[p + 3] = rgba[3];
    }
  }
  return { data: out, w: EQ_W, h: EQ_H };
}

function fillDiskRegion(ctx, cx, cy, r, rotY, fn) {
  const r2 = r * r;
  const x0 = Math.max(0, Math.floor(cx - r));
  const y0 = Math.max(0, Math.floor(cy - r));
  const x1 = Math.min(ctx.canvas.width - 1, Math.ceil(cx + r));
  const y1 = Math.min(ctx.canvas.height - 1, Math.ceil(cy + r));
  const w = x1 - x0 + 1;
  const h = y1 - y0 + 1;
  if (w <= 0 || h <= 0) return null;

  const out = ctx.createImageData(w, h);
  const px = out.data;
  let p = 0;

  for (let dy = y0; dy <= y1; dy++) {
    for (let dx = x0; dx <= x1; dx++) {
      const ox = dx - cx;
      const oy = dy - cy;
      if (ox * ox + oy * oy > r2) {
        px[p++] = 0;
        px[p++] = 0;
        px[p++] = 0;
        px[p++] = 0;
        continue;
      }
      const nx = ox / r;
      const ny = oy / r;
      const nz = Math.sqrt(Math.max(0, 1 - nx * nx - ny * ny));
      const body = inverseRotateY(nx, ny, nz, rotY);
      const rgba = fn(body);
      px[p++] = rgba[0];
      px[p++] = rgba[1];
      px[p++] = rgba[2];
      px[p++] = rgba[3] !== undefined ? rgba[3] : 255;
    }
  }

  return { out, x0, y0 };
}

function blitLayer(ctx, cx, cy, r, layer) {
  if (!layer) return;
  ctx.save();
  ctx.beginPath();
  ctx.arc(cx, cy, r, 0, Math.PI * 2);
  ctx.clip();
  ctx.putImageData(layer.out, layer.x0, layer.y0);
  ctx.restore();
}

function drawOceanLayer(ctx, cx, cy, r, rotY, oceanEq) {
  const layer = fillDiskRegion(ctx, cx, cy, r, rotY, function (body) {
    const ll = bodyLonLat(body);
    return sampleEq(oceanEq, ll.lon, ll.lat);
  });
  blitLayer(ctx, cx, cy, r, layer);
}

function drawGridLayer(ctx, cx, cy, r, rotY, gridEq) {
  const layer = fillDiskRegion(ctx, cx, cy, r, rotY, function (body) {
    const ll = bodyLonLat(body);
    const g = sampleEq(gridEq, ll.lon, ll.lat);
    if (g[3] < 8) {
      return [0, 0, 0, 0];
    }
    return g;
  });
  blitLayer(ctx, cx, cy, r, layer);
}

function drawLandLayer(ctx, cx, cy, r, rotY, landEq) {
  const layer = fillDiskRegion(ctx, cx, cy, r, rotY, function (body) {
    const ll = bodyLonLat(body);
    const l = sampleEq(landEq, ll.lon, ll.lat);
    if (l[3] < 8) {
      return [0, 0, 0, 0];
    }
    return l;
  });
  blitLayer(ctx, cx, cy, r, layer);
}

function drawRim(ctx, globeImg, cx, cy, r, rim, layoutScale) {
  const drawW = GLOBE_VIEW.imgW * layoutScale;
  const drawH = GLOBE_VIEW.imgH * layoutScale;
  const dx = cx - GLOBE_VIEW.cx * layoutScale;
  const dy = cy - GLOBE_VIEW.cy * layoutScale;

  ctx.save();
  ctx.beginPath();
  ctx.arc(cx, cy, rim, 0, Math.PI * 2);
  ctx.arc(cx, cy, r, 0, Math.PI * 2, true);
  ctx.clip();
  ctx.drawImage(globeImg, 0, 0, GLOBE_VIEW.imgW, GLOBE_VIEW.imgH, dx, dy, drawW, drawH);
  ctx.restore();
}

export async function mountHubGlobe(stage, opts) {
  const build = opts.build || 'earth3d';
  const reducedMotion = !!opts.reducedMotion;

  const canvas = document.createElement('canvas');
  canvas.className = 'hub-globe-canvas';
  canvas.setAttribute('aria-hidden', 'true');
  stage.appendChild(canvas);

  const urls = {
    globe: hubAsset('assets/hub-logo-fragment-globe.svg', build),
    continents: hubAsset('assets/hub-logo-continents-mercator-globe.svg', build),
    gridOverlay: hubAsset('assets/hub-logo-fragment-globe-grid-overlay.svg', build),
    mercatorFlat: hubAsset('assets/hub-logo-continents-mercator-flat.svg', build),
  };

  let globeImg;
  let continentsImg;
  let gridOverlayImg;
  let mercatorFlatImg;
  try {
    [globeImg, continentsImg, gridOverlayImg, mercatorFlatImg] = await Promise.all([
      loadImage(urls.globe),
      loadImage(urls.continents),
      loadImage(urls.gridOverlay),
      loadImage(urls.mercatorFlat),
    ]);
  } catch (e) {
    console.warn('HubGlobe: asset load failed', e);
    canvas.remove();
    return null;
  }

  const globe = rasterizeImage(globeImg, GLOBE_VIEW.imgW, GLOBE_VIEW.imgH, RASTER_SCALE);
  const continents = rasterizeImage(continentsImg, GLOBE_VIEW.imgW, GLOBE_VIEW.imgH, RASTER_SCALE);
  const gridOverlay = rasterizeImage(gridOverlayImg, GLOBE_VIEW.imgW, GLOBE_VIEW.imgH, RASTER_SCALE);
  const mercW = (mercatorFlatImg.naturalWidth || 800) * 2;
  const mercH = (mercatorFlatImg.naturalHeight || 519) * 2;
  const mercFlat = rasterizeImage(mercatorFlatImg, mercW, mercH, 1);

  const oceanEq = bakeOceanEquator(globe);
  const gridEq = bakeGridEquator(globe, gridOverlay);
  const landEq = bakeLandEquator(continents, mercFlat);

  let rotation = typeof opts.initialRotationY === 'number' ? opts.initialRotationY : 0;
  let angularVelocity = reducedMotion ? 0 : AUTO_SPIN_RAD_S;
  let dragging = false;
  let lastPointerX = 0;
  let lastPointerTime = 0;
  let raf = 0;
  let lastFrame = performance.now();

  const ctx = canvas.getContext('2d', { willReadFrequently: true });

  const resize = () => {
    const rect = globeCoverRect();
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    const px = Math.max(64, Math.floor(rect.width * dpr));
    canvas.style.left = rect.left + 'px';
    canvas.style.top = rect.top + 'px';
    canvas.style.width = rect.width + 'px';
    canvas.style.height = rect.height + 'px';
    canvas.width = px;
    canvas.height = px;
    return { rect, dpr, px };
  };

  let layout = resize();
  window.addEventListener('resize', () => {
    layout = resize();
  });

  const render = () => {
    const { rect, dpr, px } = layout;
    const cx = (rect.cx - rect.left) * dpr;
    const cy = (rect.cy - rect.top) * dpr;
    const r = rect.r * dpr;
    const rim = rect.rim * dpr;

    ctx.clearRect(0, 0, px, px);
    drawOceanLayer(ctx, cx, cy, r, rotation, oceanEq);
    drawGridLayer(ctx, cx, cy, r, rotation, gridEq);
    drawLandLayer(ctx, cx, cy, r, rotation, landEq);
    drawRim(ctx, globeImg, cx, cy, r, rim, rect.scale * dpr);
    ctx.save();
    ctx.beginPath();
    ctx.arc(cx, cy, r, 0, Math.PI * 2);
    ctx.strokeStyle = 'rgba(10,14,20,0.55)';
    ctx.lineWidth = Math.max(2, rect.scale * dpr * 0.006);
    ctx.stroke();
    ctx.restore();
  };

  const onPointerDown = (ev) => {
    if (ev.button !== 0) return;
    dragging = true;
    lastPointerX = ev.clientX;
    lastPointerTime = performance.now();
    canvas.setPointerCapture(ev.pointerId);
    canvas.classList.add('is-dragging');
    ev.preventDefault();
  };

  const onPointerMove = (ev) => {
    if (!dragging) return;
    const now = performance.now();
    const dx = ev.clientX - lastPointerX;
    rotation += dx * DRAG_ROTATION_PER_PX;
    if (now > lastPointerTime) {
      const inst = (dx * DRAG_ROTATION_PER_PX) / ((now - lastPointerTime) / 1000);
      angularVelocity = Math.max(-2.5, Math.min(2.5, inst));
    }
    lastPointerX = ev.clientX;
    lastPointerTime = now;
    ev.preventDefault();
  };

  const endDrag = (ev) => {
    if (!dragging) return;
    dragging = false;
    canvas.classList.remove('is-dragging');
    try {
      canvas.releasePointerCapture(ev.pointerId);
    } catch (ignored) {}
  };

  canvas.addEventListener('pointerdown', onPointerDown);
  canvas.addEventListener('pointermove', onPointerMove);
  canvas.addEventListener('pointerup', endDrag);
  canvas.addEventListener('pointercancel', endDrag);

  const tick = (now) => {
    raf = requestAnimationFrame(tick);
    const dt = Math.min(0.05, (now - lastFrame) / 1000);
    lastFrame = now;

    if (!dragging) {
      if (!reducedMotion) {
        const target = AUTO_SPIN_RAD_S;
        angularVelocity += (target - angularVelocity) * 1.2 * dt;
        if (Math.abs(angularVelocity - target) < 0.004) angularVelocity = target;
      } else {
        angularVelocity *= Math.exp(-4 * dt);
      }
      rotation += angularVelocity * dt;
    }

    render();
  };

  raf = requestAnimationFrame(tick);
  stage.dataset.globe3d = 'active';

  return {
    destroy() {
      cancelAnimationFrame(raf);
      window.removeEventListener('resize', resize);
      canvas.remove();
      delete stage.dataset.globe3d;
    },
  };
}
