(() => {
  /**
   * map_server grayscale editing — bake walls/floors and brush-erase on floor-plan raster.
   * Values match ROS map_server convention (see map-occupancy-canvas.js).
   */
  const GRAY = {
    free: 254,
    occupied: 0,
    unknown: 205,
  };

  const DEFAULT_LINE_WIDTH = 3;
  const DEFAULT_BRUSH_RADIUS = 10;

  function ensureCanvas(canvas, width, height) {
    if (!canvas) return null;
    if (canvas.width !== width || canvas.height !== height) {
      canvas.width = width;
      canvas.height = height;
    }
    return canvas.getContext("2d");
  }

  function copyCanvas(src, dst) {
    if (!src?.width || !dst) return false;
    const ctx = ensureCanvas(dst, src.width, src.height);
    if (!ctx) return false;
    ctx.drawImage(src, 0, 0);
    return true;
  }

  /**
   * Composite = base scan + floor polygons (free) + wall polylines (occupied).
   * @param {HTMLCanvasElement} baseCanvas
   * @param {HTMLCanvasElement} outCanvas
   * @param {object[]} zones
   */
  function rebakeComposite(baseCanvas, outCanvas, zones, mapMeta = null) {
    if (!baseCanvas?.width || !outCanvas) return false;
    copyCanvas(baseCanvas, outCanvas);
    const list = Array.isArray(zones) ? zones : [];
    const linePx = (z) => window.MapObjects?.zoneLineWidthPx(z, mapMeta) ?? DEFAULT_LINE_WIDTH;
    list.filter((z) => z?.type === "floor").forEach((z) => bakeFloor(outCanvas, z.points));
    list.filter((z) => z?.type === "wall").forEach((z) => bakeWall(outCanvas, z.points, linePx(z)));
    return true;
  }

  /** Load PNG pixels into editable grayscale source canvas. */
  function initSourceFromImage(sourceCanvas, imageEl) {
    if (!sourceCanvas || !imageEl?.naturalWidth) return false;
    const w = imageEl.naturalWidth;
    const h = imageEl.naturalHeight;
    const ctx = ensureCanvas(sourceCanvas, w, h);
    if (!ctx) return false;
    ctx.drawImage(imageEl, 0, 0, w, h);
    return true;
  }

  function cloneRaster(sourceCanvas) {
    if (!sourceCanvas?.width) return null;
    const ctx = sourceCanvas.getContext("2d");
    if (!ctx) return null;
    return ctx.getImageData(0, 0, sourceCanvas.width, sourceCanvas.height);
  }

  function restoreRaster(sourceCanvas, imageData) {
    if (!sourceCanvas || !imageData) return false;
    const ctx = ensureCanvas(sourceCanvas, imageData.width, imageData.height);
    if (!ctx) return false;
    ctx.putImageData(imageData, 0, 0);
    return true;
  }

  function grayRgb(gray) {
    const g = Math.max(0, Math.min(255, gray | 0));
    return `rgb(${g},${g},${g})`;
  }

  /** Paint display canvas from grayscale source using map_server thresholds. */
  function renderDisplayFromSource(displayCanvas, sourceCanvas, meta, occModule) {
    const occ = occModule || window.MapOccupancyCanvas;
    if (!occ?.renderFromImage || !displayCanvas || !sourceCanvas?.width) return false;
    return occ.renderFromImage(displayCanvas, sourceCanvas, meta || {});
  }

  function strokePolyline(ctx, points, gray, lineWidth = DEFAULT_LINE_WIDTH) {
    if (!ctx || !points || points.length < 2) return;
    ctx.save();
    ctx.strokeStyle = grayRgb(gray);
    ctx.lineWidth = lineWidth;
    ctx.lineCap = "round";
    ctx.lineJoin = "round";
    ctx.beginPath();
    points.forEach((p, i) => {
      if (i === 0) ctx.moveTo(p.x, p.y);
      else ctx.lineTo(p.x, p.y);
    });
    ctx.stroke();
    ctx.restore();
  }

  function fillPolygon(ctx, points, gray) {
    if (!ctx || !points || points.length < 3) return;
    ctx.save();
    ctx.fillStyle = grayRgb(gray);
    ctx.beginPath();
    points.forEach((p, i) => {
      if (i === 0) ctx.moveTo(p.x, p.y);
      else ctx.lineTo(p.x, p.y);
    });
    ctx.closePath();
    ctx.fill();
    ctx.restore();
  }

  /** Bake wall polyline as occupied pixels. */
  function bakeWall(sourceCanvas, points, lineWidth = DEFAULT_LINE_WIDTH) {
    const ctx = sourceCanvas?.getContext("2d");
    if (!ctx) return false;
    strokePolyline(ctx, points, GRAY.occupied, lineWidth);
    return true;
  }

  /** Bake floor polygon as free walkable pixels. */
  function bakeFloor(sourceCanvas, points) {
    const ctx = sourceCanvas?.getContext("2d");
    if (!ctx) return false;
    fillPolygon(ctx, points, GRAY.free);
    return true;
  }

  /** Remove baked wall by painting free along its path. */
  function unbakeWall(sourceCanvas, points, lineWidth = DEFAULT_LINE_WIDTH + 2) {
    const ctx = sourceCanvas?.getContext("2d");
    if (!ctx) return false;
    strokePolyline(ctx, points, GRAY.free, lineWidth);
    return true;
  }

  function bakeZone(sourceCanvas, zone, mapMeta = null) {
    if (!zone?.points?.length) return false;
    if (zone.type === "wall") {
      const px = window.MapObjects?.zoneLineWidthPx(zone, mapMeta) ?? DEFAULT_LINE_WIDTH;
      return bakeWall(sourceCanvas, zone.points, px);
    }
    if (zone.type === "floor") return bakeFloor(sourceCanvas, zone.points);
    return false;
  }

  function unbakeZone(sourceCanvas, zone, mapMeta = null) {
    if (!zone?.points?.length) return false;
    if (zone.type === "wall") {
      const px = window.MapObjects?.zoneLineWidthPx(zone, mapMeta) ?? DEFAULT_LINE_WIDTH;
      return unbakeWall(sourceCanvas, zone.points, px + 2);
    }
    if (zone.type === "floor") {
      const ctx = sourceCanvas?.getContext("2d");
      if (!ctx) return false;
      fillPolygon(ctx, zone.points, GRAY.unknown);
      return true;
    }
    return false;
  }

  function paintBrush(sourceCanvas, x, y, layer, radius = DEFAULT_BRUSH_RADIUS) {
    const ctx = sourceCanvas?.getContext("2d");
    if (!ctx) return false;
    const gray = layer === "floor" ? GRAY.unknown : GRAY.free;
    ctx.save();
    ctx.fillStyle = grayRgb(gray);
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
    return true;
  }

  /** Erase pixels inside axis-aligned rectangle on base layer. */
  function eraseRect(sourceCanvas, x0, y0, x1, y1, layer) {
    const ctx = sourceCanvas?.getContext("2d");
    if (!ctx) return false;
    const gray = layer === "floor" ? GRAY.unknown : GRAY.free;
    const x = Math.min(x0, x1);
    const y = Math.min(y0, y1);
    const w = Math.abs(x1 - x0);
    const h = Math.abs(y1 - y0);
    if (w < 1 || h < 1) return false;
    ctx.save();
    ctx.fillStyle = grayRgb(gray);
    ctx.fillRect(x, y, w, h);
    ctx.restore();
    return true;
  }

  /** Interpolated brush stroke between two image-space points. */
  function paintBrushStroke(sourceCanvas, x0, y0, x1, y1, layer, radius = DEFAULT_BRUSH_RADIUS) {
    const dist = Math.hypot(x1 - x0, y1 - y0);
    const step = Math.max(1, radius * 0.5);
    const n = Math.max(1, Math.ceil(dist / step));
    for (let i = 0; i <= n; i++) {
      const t = i / n;
      paintBrush(sourceCanvas, x0 + (x1 - x0) * t, y0 + (y1 - y0) * t, layer, radius);
    }
  }

  function exportPngBlob(sourceCanvas) {
    return new Promise((resolve, reject) => {
      if (!sourceCanvas?.width) {
        reject(new Error("no source canvas"));
        return;
      }
      sourceCanvas.toBlob(
        (blob) => {
          if (blob) resolve(blob);
          else reject(new Error("export failed"));
        },
        "image/png",
      );
    });
  }

  window.MapOccupancyEdit = {
    GRAY,
    DEFAULT_LINE_WIDTH,
    DEFAULT_BRUSH_RADIUS,
    initSourceFromImage,
    cloneRaster,
    restoreRaster,
    copyCanvas,
    rebakeComposite,
    renderDisplayFromSource,
    bakeWall,
    bakeFloor,
    bakeZone,
    unbakeZone,
    paintBrush,
    paintBrushStroke,
    eraseRect,
    exportPngBlob,
  };
})();
