(() => {
  /**
   * RViz-style occupancy grid renderer for map view (Canvas 2D).
   *
   * ROS map_server / nav_msgs/OccupancyGrid:
   *   0   = free
   *   100 = occupied
   *  -1   = unknown
   *
   * Grid data is row-major with index 0 at the bottom-left cell (world +Y up).
   * Canvas pixels use top-left origin — Y is flipped when painting.
   */

  const DEFAULT_PALETTE = {
    free: [254, 254, 254, 255],
    occupied: [0, 0, 0, 255],
    unknown: [180, 180, 180, 255],
  };

  const DEFAULT_THRESHOLDS = {
    occupied_thresh: 0.65,
    free_thresh: 0.196,
    negate: 0,
  };

  function metaThresholds(meta) {
    return {
      occupied_thresh: Number(meta?.occupied_thresh ?? DEFAULT_THRESHOLDS.occupied_thresh),
      free_thresh: Number(meta?.free_thresh ?? DEFAULT_THRESHOLDS.free_thresh),
      negate: Number(meta?.negate ?? DEFAULT_THRESHOLDS.negate),
    };
  }

  /** Trinary mode (Nav2/map_server): darkness or lightness vs thresholds in [0, 1]. */
  function grayToOccValue(gray, meta) {
    const { occupied_thresh, free_thresh, negate } = metaThresholds(meta);
    const lightness = gray / 255;
    const probability = negate ? lightness : 1 - lightness;
    if (probability > occupied_thresh) return 100;
    if (probability < free_thresh) return 0;
    return -1;
  }

  function occToRgba(value, palette) {
    if (value < 0) return palette.unknown;
    if (value >= 100) return palette.occupied;
    if (value === 0) return palette.free;
    return value > 50 ? palette.occupied : palette.free;
  }

  function paletteFrom(opts) {
    const p = opts?.palette || {};
    return {
      free: p.free || DEFAULT_PALETTE.free,
      occupied: p.occupied || DEFAULT_PALETTE.occupied,
      unknown: p.unknown || DEFAULT_PALETTE.unknown,
    };
  }

  function ensureCanvasSize(canvas, width, height) {
    if (canvas.width !== width || canvas.height !== height) {
      canvas.width = width;
      canvas.height = height;
    }
  }

  function decodeGridData(data) {
    if (data instanceof Int8Array || data instanceof Uint8Array) return data;
    if (Array.isArray(data)) return data;
    if (typeof data === "string" && data.length) {
      try {
        const binary = atob(data);
        const out = new Int8Array(binary.length);
        for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
        return out;
      } catch {
        return [];
      }
    }
    return [];
  }

  /**
   * Paint ROS occupancy grid onto canvas.
   * @param {HTMLCanvasElement} canvas
   * @param {{ width: number, height: number, data: number[]|Int8Array|string }} grid
   * @param {{ palette?: object }} [opts]
   */
  function renderGrid(canvas, grid, opts = {}) {
    if (!canvas || !grid) return false;
    const width = Number(grid.width) | 0;
    const height = Number(grid.height) | 0;
    if (!width || !height) return false;

    const data = decodeGridData(grid.data);
    if (!data.length) return false;

    ensureCanvasSize(canvas, width, height);
    const ctx = canvas.getContext("2d");
    if (!ctx) return false;

    const palette = paletteFrom(opts);
    const imageData = ctx.createImageData(width, height);
    const px = imageData.data;

    for (let row = 0; row < height; row++) {
      const srcRow = height - 1 - row;
      for (let col = 0; col < width; col++) {
        const idx = srcRow * width + col;
        const value = idx < data.length ? data[idx] : -1;
        const rgba = occToRgba(value, palette);
        const dst = (row * width + col) * 4;
        px[dst] = rgba[0];
        px[dst + 1] = rgba[1];
        px[dst + 2] = rgba[2];
        px[dst + 3] = rgba[3];
      }
    }

    ctx.putImageData(imageData, 0, 0);
    return true;
  }

  /**
   * Convert map_server grayscale image to RViz-style occupancy colors.
   * @param {HTMLCanvasElement} canvas
   * @param {CanvasImageSource} source
   * @param {object} [meta] — negate, occupied_thresh, free_thresh
   * @param {{ palette?: object }} [opts]
   */
  function renderFromImage(canvas, source, meta = {}, opts = {}) {
    if (!canvas || !source) return false;
    const width = source.naturalWidth || source.videoWidth || source.width;
    const height = source.naturalHeight || source.videoHeight || source.height;
    if (!width || !height) return false;

    ensureCanvasSize(canvas, width, height);
    const ctx = canvas.getContext("2d");
    if (!ctx) return false;

    const scratch = document.createElement("canvas");
    scratch.width = width;
    scratch.height = height;
    const sctx = scratch.getContext("2d");
    if (!sctx) return false;

    sctx.drawImage(source, 0, 0, width, height);
    const sampled = sctx.getImageData(0, 0, width, height);
    const src = sampled.data;
    const palette = paletteFrom(opts);
    const imageData = ctx.createImageData(width, height);
    const dst = imageData.data;
    const thresholds = metaThresholds(meta);

    for (let i = 0, p = 0; p < width * height; p++, i += 4) {
      const gray = src[i];
      const occ = grayToOccValue(gray, thresholds);
      const rgba = occToRgba(occ, palette);
      dst[i] = rgba[0];
      dst[i + 1] = rgba[1];
      dst[i + 2] = rgba[2];
      dst[i + 3] = 255;
    }

    ctx.putImageData(imageData, 0, 0);
    return true;
  }

  window.MapOccupancyCanvas = {
    DEFAULT_PALETTE,
    DEFAULT_THRESHOLDS,
    grayToOccValue,
    occToRgba,
    decodeGridData,
    renderGrid,
    renderFromImage,
  };
})();
