/**
 * Custom Canvas tile renderer for geographic-scale RF coverage heatmap.
 *
 * Each coverage point is rendered with a fixed geographic radius (default 400m),
 * which means the same RSRP always maps to the same color regardless of zoom.
 *
 * Features:
 *   - True geographic scale (400m = 400m at every zoom)
 *   - Gaussian falloff for smooth point edges
 *   - LRU tile cache (configurable size)
 *   - Performance monitoring (dev mode)
 *   - "Max wins" blending — overlapping points show the strongest signal
 */

import {
  metersToPixels,
  latLonToTilePixel,
  getTileBounds,
} from '@/utils/geographicScale.ts';
import {
  normalizeRSRP,
  valueToColorRGB,
  gaussianWeight,
} from '@/utils/colorGradient.ts';
import { logger } from '@/utils/logger.ts';

export interface HeatmapPoint {
  lat: number;
  lon: number;
  rsrp: number;
  siteId: string;
}

export class HeatmapTileRenderer {
  private tileSize = 256;
  private radiusMeters: number;
  private rsrpThreshold: number;

  // LRU cache: key → canvas
  private cache = new Map<string, ImageData>();
  private maxCacheSize: number;

  // Points fingerprint for cache invalidation
  private pointsHash = '';

  constructor(radiusMeters = 400, maxCacheSize = 150, rsrpThreshold = -100) {
    this.radiusMeters = radiusMeters;
    this.maxCacheSize = maxCacheSize;
    this.rsrpThreshold = rsrpThreshold;
  }

  /** Update the geographic radius of each coverage point. */
  setRadiusMeters(r: number): void {
    if (r !== this.radiusMeters) {
      this.radiusMeters = r;
      this.clearCache();
    }
  }

  /** Update the RSRP threshold — points below this are not rendered. */
  setRsrpThreshold(threshold: number): void {
    if (threshold !== this.rsrpThreshold) {
      this.rsrpThreshold = threshold;
      this.clearCache();
    }
  }

  /** Call when points change to invalidate cache. */
  setPointsHash(hash: string): void {
    if (hash !== this.pointsHash) {
      this.pointsHash = hash;
      this.clearCache();
    }
  }

  clearCache(): void {
    this.cache.clear();
  }

  /**
   * Render a single 256×256 tile.
   *
   * Returns true if anything was drawn, false for empty tiles.
   */
  renderTile(
    canvas: HTMLCanvasElement,
    points: HeatmapPoint[],
    tileX: number,
    tileY: number,
    zoom: number
  ): boolean {
    const ctx = canvas.getContext('2d');
    if (!ctx) return false;

    canvas.width = this.tileSize;
    canvas.height = this.tileSize;
    ctx.clearRect(0, 0, this.tileSize, this.tileSize);

    // Check cache
    const cacheKey = `${tileX}:${tileY}:${zoom}`;
    const cached = this.cache.get(cacheKey);
    if (cached) {
      ctx.putImageData(cached, 0, 0);
      return true;
    }

    // Tile geographic bounds
    const [[latMin, lonMin], [latMax, lonMax]] = getTileBounds(
      tileX,
      tileY,
      zoom
    );

    // Buffer in degrees for the radius so edge points outside the tile
    // still contribute their gaussian tail inside the tile
    const bufferDeg = (this.radiusMeters / 111_000) * 2;

    // Filter relevant points — geographic bounds AND RSRP threshold
    const relevant = points.filter(
      (p) =>
        p.rsrp >= this.rsrpThreshold &&
        p.lat >= latMin - bufferDeg &&
        p.lat <= latMax + bufferDeg &&
        p.lon >= lonMin - bufferDeg &&
        p.lon <= lonMax + bufferDeg
    );

    if (relevant.length === 0) return false;

    const t0 = import.meta.env.DEV ? performance.now() : 0;

    // Accumulation buffer: per-pixel best (max) normalized RSRP weighted by gaussian
    const size = this.tileSize;
    const buf = new Float32Array(size * size); // max weighted intensity per pixel

    for (const pt of relevant) {
      const [cx, cy] = latLonToTilePixel(pt.lat, pt.lon, tileX, tileY, zoom);
      const rPx = metersToPixels(this.radiusMeters, pt.lat, zoom);
      const normVal = normalizeRSRP(pt.rsrp);

      // Bounding box in pixels (clamped to tile)
      const x0 = Math.max(0, Math.floor(cx - rPx));
      const x1 = Math.min(size, Math.ceil(cx + rPx));
      const y0 = Math.max(0, Math.floor(cy - rPx));
      const y1 = Math.min(size, Math.ceil(cy + rPx));

      if (x0 >= size || x1 <= 0 || y0 >= size || y1 <= 0) continue;

      const rPxSq = rPx * rPx;

      for (let y = y0; y < y1; y++) {
        const dy = y - cy;
        const dySq = dy * dy;
        const rowOff = y * size;

        for (let x = x0; x < x1; x++) {
          const dx = x - cx;
          const distSq = dx * dx + dySq;

          if (distSq > rPxSq) continue;

          const dist = Math.sqrt(distSq);
          const weight = gaussianWeight(dist, rPx);
          const intensity = normVal * weight;

          const idx = rowOff + x;
          // "Max wins" — strongest signal wins at each pixel
          if (intensity > buf[idx]) {
            buf[idx] = intensity;
          }
        }
      }
    }

    // Render to ImageData
    const imageData = ctx.createImageData(size, size);
    const data = imageData.data;

    for (let i = 0; i < buf.length; i++) {
      const val = buf[i];
      if (val <= 0) continue;

      const clamped = Math.min(1, val);
      const [r, g, b] = valueToColorRGB(clamped);

      // Alpha: full opacity for strong signals, fade at weak edges
      const alpha = Math.min(255, Math.round(clamped * 300));

      const off = i * 4;
      data[off] = r;
      data[off + 1] = g;
      data[off + 2] = b;
      data[off + 3] = alpha;
    }

    ctx.putImageData(imageData, 0, 0);

    // Cache the rendered tile
    this.cacheStore(cacheKey, imageData);

    // Dev perf log (slow tiles only)
    const ms = performance.now() - t0;
    if (ms > 50) {
      logger.log(
        `Tile ${tileX},${tileY} z${zoom}: ${ms.toFixed(1)}ms (${relevant.length} pts)`
      );
    }

    return true;
  }

  private cacheStore(key: string, data: ImageData): void {
    // Evict oldest if full
    if (this.cache.size >= this.maxCacheSize) {
      const firstKey = this.cache.keys().next().value;
      if (firstKey !== undefined) {
        this.cache.delete(firstKey);
      }
    }
    this.cache.set(key, data);
  }
}
