// map.jsx — Leaflet satellite map
// Leaflet + ESRI World Imagery tiles (free, no API key).
// Loaded after leaflet.js CDN script in index.html.

const DUBAI_NODES = [
  { en: "Jumeirah",     ar: "جميرا",    lat: 25.2130, lng: 55.2530, tier: 3, lbl: "left" },
  { en: "Al Barsha",    ar: "البرشاء",  lat: 25.1126, lng: 55.1962, tier: 2, lbl: "bottom" },
  { en: "Zabeel",       ar: "زعبيل",    lat: 25.2280, lng: 55.3050, tier: 3, lbl: "left" },
  { en: "Nad Al Sheba", ar: "ند الشبا", lat: 25.1657, lng: 55.3300, tier: 1, lbl: "bottom" },
  { en: "Mushrif",      ar: "مشرف",     lat: 25.2150, lng: 55.4300, tier: 3, lbl: "bottom", minor: true },
  { en: "Al Tawar",     ar: "الطوار",   lat: 25.2637, lng: 55.3810, tier: 3, lbl: "left",   minor: true },
  { en: "Muhaisnah",    ar: "محيصنة",   lat: 25.2800, lng: 55.4100, tier: 3, lbl: "left",   minor: true },
  { en: "Nad Al Hamar", ar: "ند الحمر", lat: 25.1930, lng: 55.3760, tier: 3, lbl: "left",   minor: true },
  { en: "Al Awir",      ar: "العوير",   lat: 25.1830, lng: 55.5400, tier: 3, lbl: "bottom", minor: true },
  { en: "Madinat Hind", ar: "مدينة هند", lat: 25.0150, lng: 55.3750, tier: 3, lbl: "bottom", minor: true },
  { en: "Al Mizhar",    ar: "المزهر",   lat: 25.2370, lng: 55.4400, tier: 2, lbl: "left" },
  { en: "Al Khawaneej", ar: "الخوانيج", lat: 25.2560, lng: 55.4750, tier: 1, lbl: "top" },
  { en: "Wadi Al Amardi", ar: "وادي العمردي", lat: 25.2093, lng: 55.4879, tier: 3, lbl: "right" },
];

const DubaiMap = ({ lang }) => {
  const isAr = lang === "ar";
  const outerRef  = React.useRef(null); // .satmap — gets is-in for pin animations
  const mapDivRef = React.useRef(null); // inner div Leaflet renders into
  const mapRef    = React.useRef(null);
  const markersRef = React.useRef([]);
  const langRef   = React.useRef(lang);
  langRef.current = lang;

  const placeMarkers = React.useCallback((map, ar) => {
    markersRef.current.forEach(m => m.remove());
    markersRef.current = [];
    DUBAI_NODES.forEach((node) => {
      const name = ar ? node.ar : node.en;
      const icon = L.divIcon({
        className: `spin spin--t${node.tier} spin--${node.lbl}${node.minor ? " spin--minor" : ""}`,
        html: `<span class="spin__ring" aria-hidden="true"></span>` +
              `<span class="spin__ring" aria-hidden="true"></span>` +
              `<span class="spin__ring" aria-hidden="true"></span>` +
              `<span class="spin__dot"  aria-hidden="true"></span>` +
              (node.lbl === "none" ? "" : `<span class="spin__name">${name}</span>`),
        iconSize:   [0, 0],
        iconAnchor: [0, 0],
      });
      const marker = L.marker([node.lat, node.lng], { icon, interactive: false });
      marker.addTo(map);
      markersRef.current.push(marker);
    });
  }, []);

  // Initialise map once on mount
  React.useEffect(() => {
    const el = mapDivRef.current;
    if (!el || mapRef.current) return;

    const NODE_BOUNDS = L.latLngBounds(DUBAI_NODES.map(n => [n.lat, n.lng]));
    // Padding is (x, y). Desktop: generous horizontal room — side labels extend
    // ~110px past their point. Phones: much tighter, or fitBounds zooms out so
    // far the city becomes a sliver inside the frame.
    const fitOpts = () => {
      // 512 = container width at the 560px viewport CSS breakpoint (48px gutters),
      // so JS padding and the CSS portrait aspect-ratio always flip together.
      const narrow = (el.clientWidth || window.innerWidth) <= 512;
      return { padding: narrow ? [34, 30] : [120, 40], animate: false, maxZoom: 12 };
    };

    // One-finger dragging on touch devices traps page scroll inside the map —
    // disable it there (pinch-zoom via touchZoom still works). Desktop keeps drag.
    const isTouch = L.Browser.mobile;
    const map = L.map(el, {
      scrollWheelZoom: false,
      dragging: !isTouch,
      tap: false,
      zoomSnap: 1,       // integer zooms only — fractional zoom creates tile seams
      minZoom: 9,        // phones fit the city at z10; keep one step out available
      zoomControl: true,
      attributionControl: true,
    });
    map.fitBounds(NODE_BOUNDS, fitOpts());
    map.attributionControl.setPrefix(false);

    // Toggle .is-zoomed-out on the wrapper: hides minor/overlapping markers
    // at the fitted overview zoom, reveals them when the user zooms in.
    const updateZoomClass = () => {
      const z = map.getZoom();
      outerRef.current?.classList.toggle("is-zoomed-out", z < 12);
    };
    map.on("zoomend", updateZoomClass);
    updateZoomClass(); // set correct class immediately after fitBounds

    // Satellite imagery base layer. detectRetina is off — retina mode loads 2×
    // tiles and scales them down, which causes visible tile-seam grid lines.
    L.tileLayer(
      "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
      {
        maxNativeZoom: 19,
        maxZoom: 20,
        detectRetina: false,
        attribution: "Esri",
      }
    ).addTo(map);

    mapRef.current = map;

    placeMarkers(map, langRef.current === "ar");

    // --- Robust sizing -------------------------------------------------------
    // Leaflet reads the container's pixel size when it initialises. If that
    // happens while the section is hidden or mid-layout (preloader, reveal
    // animations, web-font swap, image reflow) it locks onto the wrong
    // dimensions: tiles fill only part of the frame, the rest shows the bare
    // tile grid, and every marker collapses onto a single point. A
    // ResizeObserver recomputes the size whenever the container actually has
    // dimensions, and we recentre the first time it measures a real size so the
    // view is correct regardless of when the map became visible.
    const fixSize = () => {
      if (!el.clientWidth || !el.clientHeight) return;
      map.invalidateSize({ pan: false, animate: false });
      map.fitBounds(NODE_BOUNDS, fitOpts());
    };

    let ro;
    if ("ResizeObserver" in window) {
      ro = new ResizeObserver(() => fixSize());
      ro.observe(el);
    }
    const raf = requestAnimationFrame(fixSize);

    // Reveal pins when map scrolls into view
    const outer = outerRef.current;
    let io;
    if (outer && "IntersectionObserver" in window) {
      io = new IntersectionObserver(
        (entries) => {
          entries.forEach((e) => {
            if (e.isIntersecting) {
              fixSize();
              outer.classList.add("is-in");
              io.disconnect();
            }
          });
        },
        { threshold: 0.2 }
      );
      io.observe(outer);
    } else if (outer) {
      outer.classList.add("is-in");
    }

    return () => {
      cancelAnimationFrame(raf);
      if (io) io.disconnect();
      if (ro) ro.disconnect();
      map.remove();
      mapRef.current = null;
    };
  }, []);

  // Re-place markers on language change
  React.useEffect(() => {
    if (mapRef.current) placeMarkers(mapRef.current, isAr);
  }, [lang]);

  const names = DUBAI_NODES.map((n) => (isAr ? n.ar : n.en)).join(isAr ? "، " : ", ");
  const ariaLabel = isAr
    ? "خريطة لمواقع مشاريعنا في دبي: " + names
    : "Map of our project locations across Dubai: " + names;

  return (
    <div className="satmap" ref={outerRef} role="region" aria-label={ariaLabel}>
      <div className="satmap__frame">
        <div className="satmap__leaflet" ref={mapDivRef} />
      </div>
    </div>
  );
};

window.DubaiMap = DubaiMap;
