/* MapView — imperative Leaflet map with glowing div-icon markers + uncertainty
   zones. All cameras are mobile units (locations approximate), coloured by region.
   Props:
     cameras     [{id,lat,lng,hue,road,suburb,region}] (only mappable ones)
     hoveredId, selectedId, onSelect(id), onHover(id)
     theme       'midnight' | 'charcoal' | 'streets'
     flyTo       {center,zoom,key}
*/
const ESRI_DARK = 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Base/MapServer/tile/{z}/{y}/{x}';
const ESRI_DARK_REF = 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Reference/MapServer/tile/{z}/{y}/{x}';
const OSM = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';

// mobile cameras: locations are approximate, so a single generous uncertainty zone
const ZONE_RADIUS = 340;

const TILE_THEMES = {
  midnight: { base: ESRI_DARK, labels: ESRI_DARK_REF, sub: false, maxNative: 16, attribution: 'Tiles &copy; Esri', bg: '#0e0c0b' },
  charcoal: { base: ESRI_DARK, labels: null,          sub: false, maxNative: 16, attribution: 'Tiles &copy; Esri', bg: '#14110f' },
  streets:  { base: OSM,       labels: null,          sub: true,  maxNative: 19, attribution: '&copy; OpenStreetMap', bg: '#1a1714' },
};

function hueColor(hue) {
  return {
    core: `oklch(0.72 0.19 ${hue})`,
    glow: `oklch(0.72 0.19 ${hue} / 0.55)`,
  };
}

function markerHTML(cam, state) {
  const c = hueColor(cam.hue);
  return `<span class="mk mk-glow ${state}" style="--core:${c.core};--glow:${c.glow}">
    <span class="mk-ring"></span><span class="mk-core"></span></span>`;
}

function MapView(props) {
  const { cameras, zones, exact, hoveredId, selectedId, onSelect, onHover, theme, flyTo, fitKey } = props;
  const elRef = React.useRef(null);
  const mapRef = React.useRef(null);
  const tileRef = React.useRef(null);
  const labelRef = React.useRef(null);
  const layerRef = React.useRef(null);
  const circleLayerRef = React.useRef(null);
  const zoneLayerRef = React.useRef(null);
  const markersRef = React.useRef({});
  const circlesRef = React.useRef({});
  const [ready, setReady] = React.useState(false);

  // init once
  React.useEffect(() => {
    if (!window.L || !elRef.current) return;
    const map = window.L.map(elRef.current, {
      center: window.MAP_DEFAULT.center, zoom: window.MAP_DEFAULT.zoom,
      maxZoom: 19, zoomControl: false, attributionControl: true,
    });
    map.attributionControl.setPrefix('');  // drop the "Leaflet" link, keep a small tile credit
    window.L.control.zoom({ position: 'bottomright' }).addTo(map);
    mapRef.current = map;
    // stacking order (bottom → top): zone highlights, uncertainty circles, markers
    zoneLayerRef.current = window.L.layerGroup().addTo(map);
    circleLayerRef.current = window.L.layerGroup().addTo(map);
    layerRef.current = window.L.layerGroup().addTo(map);
    setReady(true);
    setTimeout(() => map.invalidateSize(), 200);
    // Recompute size whenever the *visible* viewport changes. On mobile the
    // address bar collapsing/expanding and rotation don't always fire a window
    // 'resize', but visualViewport + orientationchange do — without this the map
    // can leave a strip uncovered when the chrome retracts.
    const onResize = () => map.invalidateSize();
    window.addEventListener('resize', onResize);
    window.addEventListener('orientationchange', onResize);
    window.addEventListener('load', onResize);
    // pageshow + visibilitychange fire when a standalone PWA is launched/resumed
    // from the background — that's exactly when iOS finally settles the safe-area
    // layout, so re-measure then to paint the full screen.
    window.addEventListener('pageshow', onResize);
    document.addEventListener('visibilitychange', () => { if (!document.hidden) onResize(); });
    const vv = window.visualViewport;
    if (vv) vv.addEventListener('resize', onResize);
    let ro = null;
    if (window.ResizeObserver) {
      ro = new ResizeObserver(() => map.invalidateSize());
      ro.observe(elRef.current);
    }
    // Re-measure across several frames after first paint — the standalone layout
    // (and lvh resolution) can settle a beat after the map initialises.
    const timers = [120, 350, 700, 1200, 2000, 3000].map((ms) => setTimeout(onResize, ms));
    return () => {
      window.removeEventListener('resize', onResize);
      window.removeEventListener('orientationchange', onResize);
      window.removeEventListener('load', onResize);
      window.removeEventListener('pageshow', onResize);
      if (vv) vv.removeEventListener('resize', onResize);
      if (ro) ro.disconnect();
      timers.forEach(clearTimeout);
      map.remove(); mapRef.current = null;
    };
  }, []);

  // tiles / theme
  React.useEffect(() => {
    if (!ready || !mapRef.current) return;
    const t = TILE_THEMES[theme] || TILE_THEMES.midnight;
    if (tileRef.current) { tileRef.current.remove(); tileRef.current = null; }
    if (labelRef.current) { labelRef.current.remove(); labelRef.current = null; }
    const opts = { attribution: t.attribution, maxZoom: 19, maxNativeZoom: t.maxNative };
    if (t.sub) opts.subdomains = 'abc';
    tileRef.current = window.L.tileLayer(t.base, opts).addTo(mapRef.current);
    tileRef.current.setZIndex(1);
    if (t.labels) {
      labelRef.current = window.L.tileLayer(t.labels, { maxZoom: 19, maxNativeZoom: t.maxNative }).addTo(mapRef.current);
      labelRef.current.setZIndex(2);
    }
    if (elRef.current) {
      elRef.current.style.background = t.bg;
      elRef.current.classList.remove('theme-midnight', 'theme-charcoal', 'theme-streets');
      elRef.current.classList.add('theme-' + (TILE_THEMES[theme] ? theme : 'midnight'));
    }
  }, [ready, theme]);

  // rebuild markers + zones when cameras change
  React.useEffect(() => {
    if (!ready || !layerRef.current) return;
    layerRef.current.clearLayers();
    circleLayerRef.current.clearLayers();
    markersRef.current = {};
    circlesRef.current = {};
    const pts = [];
    cameras.forEach((cam, i) => {
      const col = hueColor(cam.hue);
      // Mobile cameras are approximate → draw an uncertainty zone. Fixed cameras
      // have exact coordinates, so no circle (would be misleading).
      if (!exact) {
        const circle = window.L.circle([cam.lat, cam.lng], {
          radius: ZONE_RADIUS, interactive: false,
          color: col.core, weight: 1.25, opacity: 0.55,
          fillColor: col.core, fillOpacity: 0.1, className: 'zone',
        });
        circle.addTo(circleLayerRef.current);
        circlesRef.current[cam.id] = circle;
      }
      const state = cam.id === selectedId ? 'selected' : (cam.id === hoveredId ? 'hovered' : 'idle');
      const icon = window.L.divIcon({
        className: 'mk-wrap',
        html: `<span style="animation-delay:${(i % 12) * 0.12}s" class="mk-anim">${markerHTML(cam, state)}</span>`,
        iconSize: [34, 34], iconAnchor: [17, 17],
      });
      const mk = window.L.marker([cam.lat, cam.lng], { icon, riseOnHover: true });
      mk.on('click', () => onSelect(cam.id));
      mk.on('mouseover', () => onHover(cam.id));
      mk.on('mouseout', () => onHover(null));
      mk.bindPopup(`<div class="pop-road">${cam.road}</div><div class="pop-sub">${cam.suburb} · ${cam.region}</div>`);
      mk.addTo(layerRef.current);
      markersRef.current[cam.id] = mk;
      pts.push([cam.lat, cam.lng]);
    });
  }, [ready, cameras, exact]);

  // draw average-speed (point-to-point) zone highlights along the road
  React.useEffect(() => {
    if (!ready || !zoneLayerRef.current) return;
    zoneLayerRef.current.clearLayers();
    const zs = zones || [];
    const col = hueColor(250); // average-zone blue
    zs.forEach((z) => {
      // only draw zones we actually traced along the road (no misleading straight lines)
      const path = (z.path && z.path.length >= 3) ? z.path : null;
      if (!path) return;
      // wide soft glow underlay + crisp core line, both hugging the road
      window.L.polyline(path, { color: col.core, weight: 12, opacity: 0.2,
        lineCap: 'round', lineJoin: 'round', interactive: false, className: 'avg-zone-glow' }).addTo(zoneLayerRef.current);
      const line = window.L.polyline(path, { color: col.core, weight: 4, opacity: 0.92,
        lineCap: 'round', lineJoin: 'round', dashArray: '1 9', className: 'avg-zone-line' });
      line.bindPopup(`<div class="pop-road">Average speed zone</div><div class="pop-sub">${z.road} · ${z.fromName} ↔ ${z.toName}</div>`);
      line.addTo(zoneLayerRef.current);
    });
  }, [ready, zones]);

  // fit the map to all current cameras whenever the day changes (fitKey bumps).
  // Driven by an explicit signal rather than the marker rebuild, so a lingering
  // flyTo from a previous selection can't leave the map zoomed into one zone.
  React.useEffect(() => {
    if (!ready || !mapRef.current) return;
    const pts = cameras.filter((c) => c.lat != null).map((c) => [c.lat, c.lng]);
    if (pts.length) mapRef.current.fitBounds(pts, { padding: [60, 60], maxZoom: 13 });
  }, [ready, fitKey]);

  // update marker + zone state without rebuild
  React.useEffect(() => {
    if (!ready) return;
    cameras.forEach((cam) => {
      const mk = markersRef.current[cam.id];
      const state = cam.id === selectedId ? 'selected' : (cam.id === hoveredId ? 'hovered' : 'idle');
      if (mk) {
        const el = mk.getElement();
        const span = el && el.querySelector('.mk');
        if (span) { span.classList.remove('idle', 'hovered', 'selected'); span.classList.add(state); }
      }
      const circle = circlesRef.current[cam.id];
      if (circle) {
        const active = state !== 'idle';
        circle.setStyle({ fillOpacity: active ? 0.22 : 0.1, weight: active ? 2 : 1.25, opacity: active ? 0.85 : 0.55 });
      }
    });
  }, [hoveredId, selectedId, ready, cameras]);

  // fly to selected / external request
  React.useEffect(() => {
    if (!ready || !mapRef.current || !flyTo) return;
    mapRef.current.flyTo(flyTo.center, flyTo.zoom, { duration: 0.9, easeLinearity: 0.25 });
  }, [ready, flyTo && flyTo.key]);

  return React.createElement('div', { ref: elRef, className: 'map-el' });
}

window.MapView = MapView;
