/**
 * WebGL coverage layer using texture-based value interpolation.
 *
 * Simple approach (like CloudRF surface raster):
 * 1. Create texture where each pixel = one grid cell's RSRP value
 * 2. GPU's GL_LINEAR filtering interpolates between adjacent cells
 * 3. Fragment shader maps interpolated value to color gradient
 */

import { useEffect, useRef, useMemo, useCallback } from 'react';
import { useMap } from 'react-leaflet';

export interface CoveragePoint {
  lat: number;
  lon: number;
  rsrp: number;
}

interface WebGLCoverageLayerProps {
  points: CoveragePoint[];
  opacity: number;
  minRsrp?: number;
  maxRsrp?: number;
  visible: boolean;
  onWebGLFailed?: () => void;
}

const VERTEX_SHADER = `
  attribute vec2 a_position;
  varying vec2 v_uv;

  void main() {
    gl_Position = vec4(a_position, 0.0, 1.0);
    // Map position to UV, flip Y
    v_uv = vec2((a_position.x + 1.0) * 0.5, 1.0 - (a_position.y + 1.0) * 0.5);
  }
`;

// Fragment shader with smoothstep interpolation for C2 continuity
// This removes visible grid edges with minimal performance cost
const FRAGMENT_SHADER = `
  precision mediump float;

  uniform sampler2D u_coverage;
  uniform vec2 u_textureSize;

  varying vec2 v_uv;

  // Quintic Hermite smoothstep - gives C2 continuity (smooth 2nd derivatives)
  // This removes visible "seams" between grid cells
  vec4 textureSmooth(sampler2D tex, vec2 uv, vec2 texSize) {
    vec2 p = uv * texSize + 0.5;
    vec2 i = floor(p);
    vec2 f = p - i;
    // Quintic hermite curve: f³(6f² - 15f + 10)
    f = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
    return texture2D(tex, (i + f - 0.5) / texSize);
  }

  // RSRP to color gradient (red -> orange -> yellow -> green -> cyan)
  // Applied AFTER interpolation for clean gradients
  vec3 rsrpToColor(float t) {
    // t: 0 = weak (red), 1 = strong (cyan)
    if (t < 0.25) return mix(vec3(1.0, 0.0, 0.0), vec3(1.0, 0.5, 0.0), t / 0.25);
    if (t < 0.5) return mix(vec3(1.0, 0.5, 0.0), vec3(1.0, 1.0, 0.0), (t - 0.25) / 0.25);
    if (t < 0.75) return mix(vec3(1.0, 1.0, 0.0), vec3(0.0, 1.0, 0.0), (t - 0.5) / 0.25);
    return mix(vec3(0.0, 1.0, 0.0), vec3(0.0, 1.0, 1.0), (t - 0.75) / 0.25);
  }

  void main() {
    // 1. Sample with smoothstep interpolation (RAW RSRP value)
    vec4 texel = textureSmooth(u_coverage, v_uv, u_textureSize);

    // 2. Alpha channel indicates coverage presence
    if (texel.a < 0.1) discard;

    // 3. Apply colormap AFTER interpolation (critical for clean gradients)
    float rsrp = texel.r;
    vec3 color = rsrpToColor(rsrp);

    // 4. Smooth boundary fading
    float boundaryAlpha = smoothstep(0.01, 0.05, rsrp);

    gl_FragColor = vec4(color, boundaryAlpha * 0.85);
  }
`;

function compileShader(gl: WebGLRenderingContext, source: string, type: number): WebGLShader | null {
  const shader = gl.createShader(type);
  if (!shader) return null;
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.error('Shader error:', gl.getShaderInfoLog(shader));
    gl.deleteShader(shader);
    return null;
  }
  return shader;
}

function createProgram(gl: WebGLRenderingContext): WebGLProgram | null {
  const vs = compileShader(gl, VERTEX_SHADER, gl.VERTEX_SHADER);
  const fs = compileShader(gl, FRAGMENT_SHADER, gl.FRAGMENT_SHADER);
  if (!vs || !fs) return null;

  const program = gl.createProgram();
  if (!program) return null;
  gl.attachShader(program, vs);
  gl.attachShader(program, fs);
  gl.linkProgram(program);

  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    console.error('Program error:', gl.getProgramInfoLog(program));
    return null;
  }
  return program;
}

interface GridInfo {
  width: number;
  height: number;
  minLat: number;
  maxLat: number;
  minLon: number;
  maxLon: number;
  latStep: number;
  lonStep: number;
}

function detectGrid(points: CoveragePoint[]): GridInfo | null {
  if (points.length < 4) return null;

  // Calculate bounds directly from points (no rounding)
  let minLat = Infinity, maxLat = -Infinity;
  let minLon = Infinity, maxLon = -Infinity;

  for (const p of points) {
    if (p.lat < minLat) minLat = p.lat;
    if (p.lat > maxLat) maxLat = p.lat;
    if (p.lon < minLon) minLon = p.lon;
    if (p.lon > maxLon) maxLon = p.lon;
  }

  // Find grid step by looking at sorted unique coordinates
  const lats = new Set<number>();
  const lons = new Set<number>();
  for (const p of points) {
    lats.add(Math.round(p.lat * 1000000) / 1000000); // 6 decimal places
    lons.add(Math.round(p.lon * 1000000) / 1000000);
  }

  const sortedLats = Array.from(lats).sort((a, b) => a - b);
  const sortedLons = Array.from(lons).sort((a, b) => a - b);

  // Calculate step from median difference between adjacent points
  const latDiffs: number[] = [];
  const lonDiffs: number[] = [];
  for (let i = 1; i < sortedLats.length; i++) {
    latDiffs.push(sortedLats[i] - sortedLats[i-1]);
  }
  for (let i = 1; i < sortedLons.length; i++) {
    lonDiffs.push(sortedLons[i] - sortedLons[i-1]);
  }

  latDiffs.sort((a, b) => a - b);
  lonDiffs.sort((a, b) => a - b);

  const latStep = latDiffs[Math.floor(latDiffs.length / 2)] || (maxLat - minLat) / 10;
  const lonStep = lonDiffs[Math.floor(lonDiffs.length / 2)] || (maxLon - minLon) / 10;

  // Calculate grid dimensions from actual extent and step
  const width = Math.max(2, Math.round((maxLon - minLon) / lonStep) + 1);
  const height = Math.max(2, Math.round((maxLat - minLat) / latStep) + 1);

  return {
    width,
    height,
    minLat,
    maxLat,
    minLon,
    maxLon,
    latStep,
    lonStep,
  };
}

interface TextureResult {
  texture: WebGLTexture;
  width: number;
  height: number;
}

function createCoverageTexture(
  gl: WebGLRenderingContext,
  points: CoveragePoint[],
  grid: GridInfo,
  minRsrp: number,
  maxRsrp: number
): TextureResult | null {
  const { width, height, minLat, maxLat, minLon, maxLon } = grid;

  const latRange = maxLat - minLat;
  const lonRange = maxLon - minLon;
  const rsrpRange = maxRsrp - minRsrp;

  // Step 1: Create sparse grid with actual point positions
  // Store normalized RSRP value (0-1) at each grid cell that has data
  const sparseGrid = new Map<number, number>(); // key = gy * width + gx, value = normalized RSRP

  for (const p of points) {
    const gx = Math.round((p.lon - minLon) / lonRange * (width - 1));
    const gy = Math.round((p.lat - minLat) / latRange * (height - 1));

    if (gx >= 0 && gx < width && gy >= 0 && gy < height) {
      const normalized = Math.max(0, Math.min(1, (p.rsrp - minRsrp) / rsrpRange));
      const key = gy * width + gx;
      // Keep the stronger signal if multiple points map to same cell
      if (!sparseGrid.has(key) || sparseGrid.get(key)! < normalized) {
        sparseGrid.set(key, normalized);
      }
    }
  }

  // Step 2: For each empty cell, find nearest filled cell using expanding search
  // This fills the circular coverage area properly
  const data = new Uint8Array(width * height * 4);
  const maxSearchRadius = Math.max(width, height); // Max distance to search
  let filledCount = 0;

  for (let gy = 0; gy < height; gy++) {
    for (let gx = 0; gx < width; gx++) {
      const key = gy * width + gx;

      if (sparseGrid.has(key)) {
        // Cell has actual data
        const value = Math.round(sparseGrid.get(key)! * 255);
        const idx = key * 4;
        data[idx] = value;
        data[idx + 1] = 0;
        data[idx + 2] = 0;
        data[idx + 3] = 255;
        filledCount++;
      } else {
        // Find nearest cell with data using expanding square search
        let found = false;
        let nearestValue = 0;
        let nearestDistSq = Infinity;

        // Search in expanding radius
        for (let r = 1; r <= maxSearchRadius && !found; r++) {
          // Check cells at distance r (square perimeter)
          for (let dy = -r; dy <= r && !found; dy++) {
            for (let dx = -r; dx <= r; dx++) {
              // Only check perimeter cells (optimization)
              if (Math.abs(dx) !== r && Math.abs(dy) !== r) continue;

              const nx = gx + dx;
              const ny = gy + dy;
              if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;

              const nkey = ny * width + nx;
              if (sparseGrid.has(nkey)) {
                const distSq = dx * dx + dy * dy;
                if (distSq < nearestDistSq) {
                  nearestDistSq = distSq;
                  nearestValue = sparseGrid.get(nkey)!;
                }
              }
            }
          }
          // If we found something at this radius, use it (nearest neighbor)
          if (nearestDistSq < Infinity) {
            found = true;
          }
        }

        if (found) {
          // Fill with nearest neighbor value
          // Apply distance-based alpha fade for smooth edges
          const dist = Math.sqrt(nearestDistSq);
          const maxDist = 3; // Fade out over 3 cells
          const alpha = dist <= maxDist ? 255 : Math.max(0, 255 - (dist - maxDist) * 50);

          const value = Math.round(nearestValue * 255);
          const idx = key * 4;
          data[idx] = value;
          data[idx + 1] = 0;
          data[idx + 2] = 0;
          data[idx + 3] = Math.round(alpha);
          filledCount++;
        }
        // If not found, leave as transparent (alpha = 0)
      }
    }
  }

  console.log('[WebGL] Texture created (nearest-neighbor filled):', {
    textureSize: `${width}x${height}`,
    originalPoints: sparseGrid.size,
    filledCells: filledCount,
    totalCells: width * height,
    fillPercent: (filledCount / (width * height) * 100).toFixed(1) + '%'
  });

  const texture = gl.createTexture();
  if (!texture) return null;

  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);

  // LINEAR filtering for smooth interpolation between filled cells
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

  return { texture, width, height };
}

export default function WebGLCoverageLayer({
  points,
  opacity,
  minRsrp = -130,
  maxRsrp = -50,
  visible,
  onWebGLFailed,
}: WebGLCoverageLayerProps) {
  const map = useMap();

  // Refs for WebGL resources
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const glRef = useRef<WebGLRenderingContext | null>(null);
  const programRef = useRef<WebGLProgram | null>(null);
  const textureRef = useRef<WebGLTexture | null>(null);
  const quadBufferRef = useRef<WebGLBuffer | null>(null);

  // Track what data the current texture was built from
  const lastPointsHashRef = useRef<string>('');
  const boundsRef = useRef<{ minLat: number; maxLat: number; minLon: number; maxLon: number } | null>(null);
  const textureSizeRef = useRef<{ width: number; height: number }>({ width: 1, height: 1 });

  // Stable ref for callback to avoid re-initialization
  const onWebGLFailedRef = useRef(onWebGLFailed);
  onWebGLFailedRef.current = onWebGLFailed;

  // Track if initialized to prevent re-runs
  const initializedRef = useRef(false);

  // Compute stable hash for points data
  const pointsHash = useMemo(() => {
    if (points.length === 0) return '';
    const first = points[0];
    const last = points[points.length - 1];
    return `${points.length}:${first.lat.toFixed(5)}:${last.lon.toFixed(5)}:${first.rsrp.toFixed(1)}`;
  }, [points]);

  // Render function - only draws, no resource creation
  const render = useCallback(() => {
    const canvas = canvasRef.current;
    const gl = glRef.current;
    const program = programRef.current;
    const texture = textureRef.current;
    const bounds = boundsRef.current;

    // DEBUG: Check what's missing if we can't render
    if (!canvas || !gl || !program || !texture || !bounds) {
      console.log('[WebGL] Render skipped - missing:', {
        canvas: !!canvas,
        gl: !!gl,
        program: !!program,
        texture: !!texture,
        bounds: !!bounds
      });
      return;
    }

    // Position canvas over coverage area
    const nw = map.latLngToLayerPoint([bounds.maxLat, bounds.minLon]);
    const se = map.latLngToLayerPoint([bounds.minLat, bounds.maxLon]);
    const width = Math.abs(se.x - nw.x);
    const height = Math.abs(se.y - nw.y);

    if (width < 1 || height < 1) return;

    canvas.style.transform = `translate(${nw.x}px, ${nw.y}px)`;
    canvas.style.width = `${width}px`;
    canvas.style.height = `${height}px`;

    // DEBUG: Log every reposition
    console.log('[WebGL] Canvas repositioned:', {
      transform: canvas.style.transform,
      width: canvas.style.width,
      height: canvas.style.height,
      zoom: map.getZoom()
    });

    // Get texture size for shader uniform
    const texSize = textureSizeRef.current;

    // Set canvas resolution
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    const canvasW = Math.min(Math.round(width * dpr), 2048);
    const canvasH = Math.min(Math.round(height * dpr), 2048);

    if (canvas.width !== canvasW || canvas.height !== canvasH) {
      canvas.width = canvasW;
      canvas.height = canvasH;
    }

    // Render
    gl.viewport(0, 0, canvasW, canvasH);
    gl.clearColor(0, 0, 0, 0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.useProgram(program);

    // Bind quad buffer
    gl.bindBuffer(gl.ARRAY_BUFFER, quadBufferRef.current);
    const posLoc = gl.getAttribLocation(program, 'a_position');
    gl.enableVertexAttribArray(posLoc);
    gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);

    // Bind texture
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.uniform1i(gl.getUniformLocation(program, 'u_coverage'), 0);

    // Set texture size uniform (texSize already defined above for blur)
    const textureSizeLocation = gl.getUniformLocation(program, 'u_textureSize');
    if (textureSizeLocation) {
      gl.uniform2f(textureSizeLocation, texSize.width, texSize.height);
    }

    // Draw
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

    gl.disableVertexAttribArray(posLoc);
  }, [map]);

  // Effect 1: Initialize WebGL (canvas, context, program, quad buffer) - runs ONCE
  useEffect(() => {
    if (!visible) return;

    // Skip if already initialized
    if (initializedRef.current && canvasRef.current && glRef.current) {
      return;
    }

    const pane = map.getPane('overlayPane');
    if (!pane) return;

    // Create canvas if needed
    if (!canvasRef.current) {
      // Remove any leftover canvas elements from previous sessions
      const existingCanvases = pane.querySelectorAll('canvas.webgl-coverage');
      existingCanvases.forEach(c => c.remove());
      console.log('[WebGL] Removed', existingCanvases.length, 'leftover canvas elements');

      const canvas = document.createElement('canvas');
      canvas.className = 'webgl-coverage'; // Add class for identification
      canvas.style.position = 'absolute';
      canvas.style.pointerEvents = 'none';
      canvas.style.transformOrigin = '0 0';
      pane.appendChild(canvas);
      canvasRef.current = canvas;
    }

    const canvas = canvasRef.current;

    // Initialize WebGL if needed
    if (!glRef.current) {
      const gl = canvas.getContext('webgl', { alpha: true, premultipliedAlpha: false });
      if (!gl) {
        console.error('[WebGL] WebGL not available');
        onWebGLFailedRef.current?.();
        return;
      }
      glRef.current = gl;
      gl.enable(gl.BLEND);
      gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
    }

    const gl = glRef.current;

    // Create program if needed
    if (!programRef.current) {
      const program = createProgram(gl);
      if (!program) {
        console.error('[WebGL] Failed to create program');
        onWebGLFailedRef.current?.();
        return;
      }
      programRef.current = program;
    }

    // Create quad buffer if needed
    if (!quadBufferRef.current) {
      const buf = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, buf);
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
        -1, -1,  1, -1,  -1, 1,  1, 1
      ]), gl.STATIC_DRAW);
      quadBufferRef.current = buf;
    }

    initializedRef.current = true;
    console.log('[WebGL] Initialized (should appear ONCE)');
  }, [visible, map]); // Removed onWebGLFailed - use ref instead

  // Effect 2: Create texture when points data changes
  useEffect(() => {
    if (!visible || points.length === 0 || !glRef.current) return;

    // Skip if same data
    if (pointsHash === lastPointsHashRef.current && textureRef.current) {
      return;
    }

    const gl = glRef.current;
    const grid = detectGrid(points);
    if (!grid) return;

    // Delete old texture
    if (textureRef.current) {
      gl.deleteTexture(textureRef.current);
      textureRef.current = null;
    }

    // Create new texture (returns texture + dimensions)
    const result = createCoverageTexture(gl, points, grid, minRsrp, maxRsrp);
    if (!result) {
      console.error('[WebGL] Failed to create texture');
      return;
    }

    textureRef.current = result.texture;
    lastPointsHashRef.current = pointsHash;

    // Store texture size for shader uniform
    textureSizeRef.current = { width: result.width, height: result.height };

    // Store bounds for rendering (with half-cell padding)
    const canvasBounds = {
      minLat: grid.minLat - grid.latStep / 2,
      maxLat: grid.maxLat + grid.latStep / 2,
      minLon: grid.minLon - grid.lonStep / 2,
      maxLon: grid.maxLon + grid.lonStep / 2,
    };
    boundsRef.current = canvasBounds;

    // FULL DEBUG: Compare data extent vs canvas bounds
    const lats = points.map(p => p.lat);
    const lons = points.map(p => p.lon);
    const dataMinLat = Math.min(...lats);
    const dataMaxLat = Math.max(...lats);
    const dataMinLon = Math.min(...lons);
    const dataMaxLon = Math.max(...lons);

    console.log('[WebGL] FULL DEBUG:', {
      // Data extent (actual points)
      dataMinLat: dataMinLat.toFixed(6),
      dataMaxLat: dataMaxLat.toFixed(6),
      dataMinLon: dataMinLon.toFixed(6),
      dataMaxLon: dataMaxLon.toFixed(6),
      dataLatRange: (dataMaxLat - dataMinLat).toFixed(6),
      dataLonRange: (dataMaxLon - dataMinLon).toFixed(6),

      // Grid detection result
      gridWidth: grid.width,
      gridHeight: grid.height,
      gridMinLat: grid.minLat.toFixed(6),
      gridMaxLat: grid.maxLat.toFixed(6),
      gridMinLon: grid.minLon.toFixed(6),
      gridMaxLon: grid.maxLon.toFixed(6),
      gridLatStep: grid.latStep.toFixed(6),
      gridLonStep: grid.lonStep.toFixed(6),

      // Texture size
      textureWidth: result.width,
      textureHeight: result.height,

      // Canvas bounds (what we use for rendering)
      canvasMinLat: canvasBounds.minLat.toFixed(6),
      canvasMaxLat: canvasBounds.maxLat.toFixed(6),
      canvasMinLon: canvasBounds.minLon.toFixed(6),
      canvasMaxLon: canvasBounds.maxLon.toFixed(6),
      canvasLatRange: (canvasBounds.maxLat - canvasBounds.minLat).toFixed(6),
      canvasLonRange: (canvasBounds.maxLon - canvasBounds.minLon).toFixed(6),

      // Comparison
      latCoveragePercent: ((canvasBounds.maxLat - canvasBounds.minLat) / (dataMaxLat - dataMinLat) * 100).toFixed(1) + '%',
      lonCoveragePercent: ((canvasBounds.maxLon - canvasBounds.minLon) / (dataMaxLon - dataMinLon) * 100).toFixed(1) + '%',

      // Expected
      expectedRange: '~0.18 degrees for 20km radius',
      pointCount: points.length
    });

    // Initial render
    render();
  }, [visible, points, pointsHash, minRsrp, maxRsrp, render]);

  // Effect 3: Set up map event listeners for re-rendering on move/zoom
  // Note: Set up listeners even without texture - render() will check for texture
  useEffect(() => {
    if (!visible) return;

    let frameId = 0;
    let moveCount = 0;
    const onMapChange = () => {
      moveCount++;
      if (moveCount <= 3 || moveCount % 10 === 0) {
        console.log('[WebGL] Map event #' + moveCount + ', triggering render');
      }
      cancelAnimationFrame(frameId);
      frameId = requestAnimationFrame(render);
    };

    map.on('move', onMapChange);
    map.on('zoom', onMapChange);
    map.on('resize', onMapChange);

    console.log('[WebGL] Map listeners attached');

    return () => {
      map.off('move', onMapChange);
      map.off('zoom', onMapChange);
      map.off('resize', onMapChange);
      cancelAnimationFrame(frameId);
      console.log('[WebGL] Map listeners detached');
    };
  }, [visible, map, render]);

  // Effect 4: Update opacity without recreating anything
  useEffect(() => {
    if (canvasRef.current) {
      canvasRef.current.style.opacity = String(opacity);
    }
  }, [opacity]);

  // Effect 5: Hide/show canvas based on visibility
  useEffect(() => {
    if (canvasRef.current) {
      canvasRef.current.style.display = visible ? 'block' : 'none';
    }
  }, [visible]);

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      const gl = glRef.current;
      if (gl) {
        if (textureRef.current) gl.deleteTexture(textureRef.current);
        if (quadBufferRef.current) gl.deleteBuffer(quadBufferRef.current);
        if (programRef.current) gl.deleteProgram(programRef.current);
      }
      if (canvasRef.current) {
        canvasRef.current.remove();
        canvasRef.current = null;
      }
      glRef.current = null;
      programRef.current = null;
      textureRef.current = null;
      quadBufferRef.current = null;
    };
  }, []);

  return null;
}
