(() => {
  /**
   * MiR-style map coordinate model (3 layers):
   *
   * 1. View space  — screen pixels in the viewport; pan + zoom (UI only).
   * 2. Image space — floor plan pixels (1 px image = 1 px on sheet; 20 px/m at res 0.05).
   * 3. World space — map coordinates in metres + yaw (robot, positions).
   *
   *   view  --scale+translate-->  image  --resolution+origin-->  world
   */

  function meta(map) {
    return {
      resolution: Number(map?.resolution) || 0.05,
      originX: Number(map?.origin_x) || 0,
      originY: Number(map?.origin_y) || 0,
      originYaw: Number(map?.origin_yaw) || 0,
      width: Number(map?.width) || 0,
      height: Number(map?.height) || 0,
    };
  }

  function pixelsPerMeter(map) {
    const res = meta(map).resolution;
    return res > 0 ? 1 / res : 20;
  }

  /** Centimetres → image pixels (uses map resolution m/px). */
  function cmToPixels(cm, map) {
    const res = meta(map).resolution;
    if (!Number.isFinite(Number(cm)) || res <= 0) return 0;
    return Number(cm) / 100 / res;
  }

  /** Image pixels → centimetres. */
  function pixelsToCm(px, map) {
    const res = meta(map).resolution;
    if (!Number.isFinite(Number(px)) || res <= 0) return 0;
    return Number(px) * res * 100;
  }

  function imageSize(map, imageEl) {
    const w = imageEl?.naturalWidth || meta(map).width || 0;
    const h = imageEl?.naturalHeight || meta(map).height || 0;
    return { width: w, height: h };
  }

  /** --- View space (layer 1) --- */

  function createView(scale = 1, panX = 0, panY = 0) {
    return { scale, panX, panY };
  }

  function applyViewTransform(el, view) {
    if (!el || !view) return;
    el.style.transform = `translate(${view.panX}px, ${view.panY}px) scale(${view.scale})`;
  }

  function fitViewToImage(viewportW, viewportH, imageW, imageH, pad = 48) {
    if (!imageW || !imageH) {
      return createView(1, Math.max(40, pad), Math.max(40, pad));
    }
    const scale = Math.min((viewportW - pad) / imageW, (viewportH - pad) / imageH, 4);
    const s = Math.max(0.1, scale);
    return {
      scale: s,
      panX: (viewportW - imageW * s) / 2,
      panY: (viewportH - imageH * s) / 2,
    };
  }

  function centerViewOnImage(viewportW, viewportH, imageW, imageH, view) {
    const s = view?.scale || 1;
    return {
      scale: s,
      panX: Math.max(40, (viewportW - imageW * s) / 2),
      panY: Math.max(40, (viewportH - imageH * s) / 2),
    };
  }

  /** Viewport-local px → image px (inverse of translate+scale). */
  function viewportToImage(view, vx, vy) {
    const s = view.scale || 1;
    return {
      x: (vx - view.panX) / s,
      y: (vy - view.panY) / s,
    };
  }

  /** Image px → viewport-local px. */
  function imageToViewport(view, ix, iy) {
    return {
      x: view.panX + ix * view.scale,
      y: view.panY + iy * view.scale,
    };
  }

  /** Zoom toward a viewport anchor; keeps image point under cursor fixed. */
  function zoomViewAt(view, anchorVx, anchorVy, factor, minScale = 0.1, maxScale = 8) {
    const img = viewportToImage(view, anchorVx, anchorVy);
    const nextScale = Math.min(maxScale, Math.max(minScale, view.scale * factor));
    return {
      scale: nextScale,
      panX: anchorVx - img.x * nextScale,
      panY: anchorVy - img.y * nextScale,
    };
  }

  /** --- Image space (layer 2) --- */

  function gridSteps(map) {
    const ppm = pixelsPerMeter(map);
    return {
      minor: Math.max(1, Math.round(ppm)),
      major: Math.max(1, Math.round(ppm * 5)),
    };
  }

  function clampImagePoint(px, py, imageW, imageH) {
    if (px < 0 || py < 0 || px > imageW || py > imageH) return null;
    return { x: px, y: py };
  }

  /** Screen/client coords → image px using the transformed sheet rect. */
  function clientToImage(clientX, clientY, sheetRect, imageW, imageH) {
    if (!sheetRect?.width || !sheetRect?.height || !imageW || !imageH) return null;
    const px = ((clientX - sheetRect.left) / sheetRect.width) * imageW;
    const py = ((clientY - sheetRect.top) / sheetRect.height) * imageH;
    return clampImagePoint(px, py, imageW, imageH);
  }

  /** --- World space (layer 3) — ROS map_server convention --- */

  function worldToPixel(map, imgW, imgH, wx, wy) {
    const { resolution, originX, originY } = meta(map);
    return {
      x: (wx - originX) / resolution,
      y: imgH - (wy - originY) / resolution,
    };
  }

  function pixelToWorld(map, imgW, imgH, px, py) {
    const { resolution, originX, originY } = meta(map);
    return {
      x: originX + px * resolution,
      y: originY + (imgH - py) * resolution,
    };
  }

  /** Viewport pointer → world (chains view→image→world). */
  function clientToWorld(map, clientX, clientY, sheetRect, imageW, imageH) {
    const img = clientToImage(clientX, clientY, sheetRect, imageW, imageH);
    if (!img) return null;
    return pixelToWorld(map, imageW, imageH, img.x, img.y);
  }

  window.MapGeo = {
    meta,
    pixelsPerMeter,
    cmToPixels,
    pixelsToCm,
    imageSize,
    createView,
    applyViewTransform,
    fitViewToImage,
    centerViewOnImage,
    viewportToImage,
    imageToViewport,
    zoomViewAt,
    gridSteps,
    clientToImage,
    clientToWorld,
    worldToPixel,
    pixelToWorld,
  };
})();
