/**
 * Hub landing globe — orthographic Canvas2D (globe3d=1).
 * Grid: hub SVG ellipses (E–W) + meridian paths (N–S) traced on sphere, then Y-rotated.
 * Land: ortho hub disk on front face; mercator-flat only on back wrap (avoids front squeeze).
 */
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.04;
const RASTER_SCALE = 2;
const AXIS_STEPS = 160;

/** hub-logo-fragment-globe-grid-overlay.svg — E–W parallels. */
const HUB_EW_PARALLELS = [
  { cx: 720, cy: 560, rx: 318, ry: 11, bright: true },
  { cx: 720, cy: 392, rx: 260, ry: 39, bright: false },
  { cx: 720, cy: 728, rx: 260, ry: 39, bright: false },
];

/** Same SVG — N–S meridians (line + quadratic beziers). */
const HUB_NS_MERIDIANS = [
  { prime: true, points: sampleLinePoints(720, 248, 720, 872, AXIS_STEPS) },
  { prime: false, points: sampleQuadPoints(620, 248, 358, 560, 620, 872, AXIS_STEPS) },
  { prime: false, points: sampleQuadPoints(820, 248, 1082, 560, 820, 872, AXIS_STEPS) },
];

function sampleLinePoints(x0, y0, x1, y1, steps) {
  const pts = [];
  for (let i = 0; i <= steps; i++) {
    const t = i / steps;
    pts.push({ x: x0 + (x1 - x0) * t, y: y0 + (y1 - y0) * t });
  }
  return pts;
}

function sampleQuadPoints(x0, y0, cx, cy, x1, y1, steps) {
  const pts = [];
  for (let i = 0; i <= steps; i++) {
    const t = i / steps;
    const u = 1 - t;
    pts.push({
      x: u * u * x0 + 2 * u * t * cx + t * t * x1,
      y: u * u * y0 + 2 * u * t * cy + t * t * y1,
    });
  }
  return pts;
}

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,
  };
}

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

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

/** Hub disk pixel → unit-sphere body (front hemisphere, matches globe3d=0). */
function hubPixelToBody(hpX, hpY) {
  const texX = (hpX - GLOBE_VIEW.cx) / GLOBE_VIEW.r;
  const texY = (GLOBE_VIEW.cy - hpY) / GLOBE_VIEW.r;
  const rr = texX * texX + texY * texY;
  if (rr > 1.001) {
    return null;
  }
  return {
    x: texX,
    y: -texY,
    z: Math.sqrt(Math.max(0, 1 - rr)),
  };
}

function hubPointsToBodyRing(points) {
  const out = [];
  for (let i = 0; i < points.length; i++) {
    const body = hubPixelToBody(points[i].x, points[i].y);
    if (body) {
      out.push(body);
    }
  }
  return out;
}

function ellipseBodyRing(ellipse, steps) {
  const out = [];
  for (let i = 0; i <= steps; i++) {
    const a = (i / steps) * Math.PI * 2;
    const body = hubPixelToBody(
      ellipse.cx + ellipse.rx * Math.cos(a),
      ellipse.cy + ellipse.ry * Math.sin(a)
    );
    if (body) {
      out.push(body);
    }
  }
  return out;
}

function parallelStyle(bright, scale, z) {
  const t = Math.max(0, Math.min(1, (z - FRONT_Z) / (0.95 - FRONT_Z)));
  if (t <= 0) {
    return null;
  }
  const alpha = bright ? 0.55 + 0.45 * t : 0.35 + 0.5 * t;
  const rgb = bright ? '219,231,246' : '231,236,243';
  return {
    color: 'rgba(' + rgb + ',' + alpha.toFixed(3) + ')',
    width: Math.max(bright ? 3 : 2, scale * (bright ? 0.009 : 0.006)) * (0.5 + 0.5 * t),
  };
}

function meridianStyle(prime, scale, z) {
  const t = Math.max(0, Math.min(1, (z - FRONT_Z) / (0.95 - FRONT_Z)));
  if (t <= 0) {
    return null;
  }
  const alpha = prime ? 0.5 + 0.48 * t : 0.3 + 0.45 * t;
  return {
    color: 'rgba(238,245,255,' + alpha.toFixed(3) + ')',
    width: Math.max(prime ? 3 : 2.5, scale * (prime ? 0.0095 : 0.008)) * (0.45 + 0.55 * t),
  };
}

function strokeBodyRing(ctx, cx, cy, r, rotY, bodies, styleAt) {
  let run = [];

  function flush() {
    if (run.length < 2) {
      run = [];
      return;
    }
    let zSum = 0;
    for (let i = 0; i < run.length; i++) {
      zSum += run[i].z;
    }
    const style = styleAt(zSum / run.length);
    if (!style) {
      run = [];
      return;
    }
    ctx.beginPath();
    ctx.moveTo(run[0].sx, run[0].sy);
    for (let i = 1; i < run.length; i++) {
      ctx.lineTo(run[i].sx, run[i].sy);
    }
    ctx.strokeStyle = style.color;
    ctx.lineWidth = style.width;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    ctx.stroke();
    run = [];
  }

  for (let i = 0; i < bodies.length; i++) {
    const view = bodyToView(bodies[i], rotY);
    if (view.z <= FRONT_Z) {
      flush();
      continue;
    }
    run.push({
      sx: cx + view.x * r,
      sy: cy - view.y * r,
      z: view.z,
    });
  }
  flush();
}

/** Hub SVG grid: E–W ellipses + N–S meridian paths (matches globe3d=0 artwork geometry). */
function drawHubGrid(ctx, cx, cy, r, rotY, layoutScale) {
  ctx.save();
  ctx.beginPath();
  ctx.arc(cx, cy, r, 0, Math.PI * 2);
  ctx.clip();

  HUB_EW_PARALLELS.forEach(function (ellipse) {
    strokeBodyRing(ctx, cx, cy, r, rotY, ellipseBodyRing(ellipse, AXIS_STEPS), function (z) {
      return parallelStyle(ellipse.bright, layoutScale, z);
    });
  });

  HUB_NS_MERIDIANS.forEach(function (mer) {
    strokeBodyRing(ctx, cx, cy, r, rotY, hubPointsToBodyRing(mer.points), function (z) {
      return meridianStyle(mer.prime, layoutScale, z);
    });
  });

  ctx.restore();
}

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 texLonLat(tex) {
  return {
    lon: Math.atan2(tex.x, tex.z),
    lat: Math.asin(Math.max(-1, Math.min(1, tex.y))),
  };
}

function sampleMercFlat(mercFlat, lon, lat) {
  const u = ((lon + Math.PI) / (2 * Math.PI)) * (mercFlat.w - 1);
  const v = (0.5 - lat / Math.PI) * (mercFlat.h - 1);
  return sampleImageDataBilinear(mercFlat.data, mercFlat.w, mercFlat.h, u, v);
}

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

/** Light hub grid strokes baked into hub-logo-fragment-globe.svg (vector grid drawn separately). */
function isGridPixel(r, g, b, a) {
  if (a < 12) {
    return false;
  }
  const lum = r * 0.299 + g * 0.587 + b * 0.114;
  if (lum < 125) {
    return false;
  }
  if (g < 155 || b < 155) {
    return false;
  }
  return Math.max(r, g, b) - Math.min(r, g, b) < 85;
}

function sampleOceanRgba(globe, tex) {
  if (tex.z <= FRONT_Z) {
    return [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255];
  }
  const s = sampleHub(globe, tex);
  if (s[3] > 8 && !isGridPixel(s[0], s[1], s[2], s[3])) {
    return [s[0], s[1], s[2], 255];
  }
  return [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255];
}

function sampleLandRgba(continents, mercFlat, tex) {
  if (tex.z > FRONT_Z) {
    const ortho = sampleHub(continents, tex);
    if (ortho[3] > 6) {
      return ortho;
    }
    return null;
  }
  if (tex.z > -FRONT_Z) {
    return null;
  }
  const ll = texLonLat(tex);
  const flat = sampleMercFlat(mercFlat, ll.lon, ll.lat);
  if (isLandPixel(flat[0], flat[1], flat[2], flat[3])) {
    return [flat[0], flat[1], flat[2], Math.min(255, flat[3])];
  }
  return null;
}

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 tex = bodyToTex(inverseRotateY(nx, ny, nz, rotY));
      const rgba = fn(tex);
      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;
  }
  const { out, x0, y0 } = layer;
  const w = out.width;
  const h = out.height;
  const src = out.data;

  ctx.save();
  ctx.beginPath();
  ctx.arc(cx, cy, r, 0, Math.PI * 2);
  ctx.clip();

  const existing = ctx.getImageData(x0, y0, w, h);
  const dst = existing.data;
  for (let i = 0; i < src.length; i += 4) {
    const sa = src[i + 3];
    if (sa === 0) {
      continue;
    }
    if (sa === 255) {
      dst[i] = src[i];
      dst[i + 1] = src[i + 1];
      dst[i + 2] = src[i + 2];
      dst[i + 3] = 255;
      continue;
    }
    const a = sa / 255;
    dst[i] = Math.round(src[i] * a + dst[i] * (1 - a));
    dst[i + 1] = Math.round(src[i + 1] * a + dst[i + 1] * (1 - a));
    dst[i + 2] = Math.round(src[i + 2] * a + dst[i + 2] * (1 - a));
    dst[i + 3] = 255;
  }
  ctx.putImageData(existing, x0, y0);
  ctx.restore();
}

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),
    mercatorFlat: hubAsset('assets/hub-logo-continents-mercator-flat.svg', build),
  };

  let globeImg;
  let continentsImg;
  let mercatorFlatImg;
  try {
    [globeImg, continentsImg, mercatorFlatImg] = await Promise.all([
      loadImage(urls.globe),
      loadImage(urls.continents),
      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 mercW = (mercatorFlatImg.naturalWidth || 800) * 2;
  const mercH = (mercatorFlatImg.naturalHeight || 519) * 2;
  const mercFlat = rasterizeImage(mercatorFlatImg, mercW, mercH, 1);

  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;
    const layoutScale = rect.scale * dpr;

    ctx.clearRect(0, 0, px, px);
    blitLayer(
      ctx,
      cx,
      cy,
      r,
      fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) {
        return sampleOceanRgba(globe, tex);
      })
    );
    blitLayer(
      ctx,
      cx,
      cy,
      r,
      fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) {
        const l = sampleLandRgba(continents, mercFlat, tex);
        return l || [0, 0, 0, 0];
      })
    );
    drawHubGrid(ctx, cx, cy, r, rotation, layoutScale);
    drawRim(ctx, globeImg, cx, cy, r, rim, layoutScale);
    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, layoutScale * 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';
  stage.dataset.globeAxis = 'ew-ns-grid';

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