/* Shared MapLibre GL utilities — loaded by map.html.twig, dailies.html.twig, home.html.twig */
(function (global) {
  var ACCENT      = '#2A8C73';
  var ACCENT_DIM  = '#155244';
  var MAP_STYLE   = 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json';

  /* Build a GeoJSON LineString feature */
  function lineFeature(coords) {
    return { type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: coords } };
  }

  /*
   * Catmull-Rom spline through waypoints → dense interpolated coords.
   * Produces a smooth curve that passes through every entry dot.
   * steps: interpolated points per segment (16 is plenty for daily entries).
   */
  function catmullRomSpline(coords, steps) {
    if (coords.length < 2) return coords;
    steps = steps || 16;
    var out = [];

    for (var i = 0; i < coords.length - 1; i++) {
      var p0 = coords[Math.max(i - 1, 0)];
      var p1 = coords[i];
      var p2 = coords[i + 1];
      var p3 = coords[Math.min(i + 2, coords.length - 1)];

      for (var s = 0; s < steps; s++) {
        var t  = s / steps;
        var t2 = t * t;
        var t3 = t2 * t;
        out.push([
          0.5 * ((2*p1[0]) + (-p0[0]+p2[0])*t + (2*p0[0]-5*p1[0]+4*p2[0]-p3[0])*t2 + (-p0[0]+3*p1[0]-3*p2[0]+p3[0])*t3),
          0.5 * ((2*p1[1]) + (-p0[1]+p2[1])*t + (2*p0[1]-5*p1[1]+4*p2[1]-p3[1])*t2 + (-p0[1]+3*p1[1]-3*p2[1]+p3[1])*t3)
        ]);
      }
    }
    out.push(coords[coords.length - 1]);
    return out;
  }

  /*
   * Progressively draw the journey line using a requestAnimationFrame loop.
   * splineCoords: dense interpolated coords from catmullRomSpline().
   */
  function animateJourneyLine(map, splineCoords, sourceId) {
    if (splineCoords.length < 2) return;

    var segDist = [0];
    for (var i = 1; i < splineCoords.length; i++) {
      var dx = splineCoords[i][0] - splineCoords[i - 1][0];
      var dy = splineCoords[i][1] - splineCoords[i - 1][1];
      segDist.push(segDist[i - 1] + Math.sqrt(dx * dx + dy * dy));
    }
    var totalDist = segDist[segDist.length - 1];
    var DURATION  = 5000;
    var startTime = performance.now();

    function frame(now) {
      if (!map.getSource(sourceId)) return;
      var t      = Math.min((now - startTime) / DURATION, 1);
      var eased  = 1 - Math.pow(1 - t, 3);
      var target = eased * totalDist;

      var animCoords = [splineCoords[0]];
      for (var j = 1; j < splineCoords.length; j++) {
        if (segDist[j] <= target) {
          animCoords.push(splineCoords[j]);
        } else {
          var frac = (target - segDist[j - 1]) / (segDist[j] - segDist[j - 1]);
          animCoords.push([
            splineCoords[j - 1][0] + (splineCoords[j][0] - splineCoords[j - 1][0]) * frac,
            splineCoords[j - 1][1] + (splineCoords[j][1] - splineCoords[j - 1][1]) * frac
          ]);
          break;
        }
      }

      map.getSource(sourceId).setData(lineFeature(animCoords));
      if (t < 1) requestAnimationFrame(frame);
    }

    requestAnimationFrame(frame);
  }

  /*
   * Add a journey line to a loaded map — dotted, subordinate style so GPX
   * tracks read as the primary route where they exist.
   * coords: [[lng, lat], ...] raw waypoints (daily entry positions).
   */
  function addJourneyLine(map, coords, sourceId) {
    if (coords.length < 2) return;

    var splineCoords = catmullRomSpline(coords, 16);

    map.addSource(sourceId, { type: 'geojson', data: lineFeature([splineCoords[0]]) });

    map.addLayer({
      id: sourceId + '-line', type: 'line', source: sourceId,
      layout: { 'line-join': 'round', 'line-cap': 'round' },
      paint: {
        'line-color':     ACCENT,
        'line-width':     2,
        'line-opacity':   0.45,
        'line-dasharray': [0, 2.5]
      }
    });

    var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reducedMotion) {
      map.getSource(sourceId).setData(lineFeature(splineCoords));
    } else {
      animateJourneyLine(map, splineCoords, sourceId);
    }
  }

  /*
   * Return a styled <div> element for a map marker dot.
   * isLatest: make it larger with a teal ring.
   */
  function createDotMarker(isLatest) {
    var el   = document.createElement('div');
    var size = isLatest ? 18 : 12;
    var bg   = isLatest ? ACCENT_DIM : ACCENT;
    var ring = isLatest ? ',0 0 0 4px rgba(42,140,115,0.25)' : '';
    el.style.cssText = [
      'width:' + size + 'px',
      'height:' + size + 'px',
      'background:' + bg,
      'border:2px solid #fff',
      'border-radius:50%',
      'box-shadow:0 1px 4px rgba(0,0,0,0.4)' + ring,
      'cursor:pointer'
    ].join(';');
    return el;
  }

  /* ── GPX connector algorithm ────────────────────────────────────────── */

  /* Haversine distance in km between two [lat, lng] points */
  function haversineKm(lat1, lng1, lat2, lng2) {
    var R    = 6371;
    var dLat = (lat2 - lat1) * Math.PI / 180;
    var dLng = (lng2 - lng1) * Math.PI / 180;
    var a    = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
               Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
               Math.sin(dLng / 2) * Math.sin(dLng / 2);
    return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  }

  /*
   * Extract trackpoints from a toGeoJSON output.
   * Returns [[lat, lng], ...] — latitude first (internal convention).
   * GeoJSON coordinates are [lng, lat]; we flip them here.
   */
  function extractTrackpoints(geojson) {
    var points = [];
    (geojson.features || []).forEach(function (feat) {
      var coords = [];
      if (feat.geometry.type === 'LineString') {
        coords = feat.geometry.coordinates;
      } else if (feat.geometry.type === 'MultiLineString') {
        feat.geometry.coordinates.forEach(function (line) {
          coords = coords.concat(line);
        });
      }
      coords.forEach(function (c) { points.push([c[1], c[0]]); }); // [lng,lat] → [lat,lng]
    });
    return points;
  }

  /*
   * Check whether a marker is within thresholdKm of any trackpoint in the array.
   * trackpoints: [[lat, lng], ...] (internal convention, latitude first).
   * Samples every 10th point for performance; always checks the last point.
   */
  function isNearTrack(markerLat, markerLng, trackpoints, thresholdKm) {
    if (!trackpoints || trackpoints.length === 0) return false;
    var degLat = thresholdKm / 111;
    var degLng = thresholdKm / (111 * Math.cos(markerLat * Math.PI / 180));
    for (var i = 0; i < trackpoints.length; i += 10) {
      var pt = trackpoints[i];
      if (Math.abs(pt[0] - markerLat) > degLat || Math.abs(pt[1] - markerLng) > degLng) continue;
      if (haversineKm(markerLat, markerLng, pt[0], pt[1]) <= thresholdKm) return true;
    }
    // Always check the last point (may be skipped by stride=10).
    // Note: per-point degree pre-filter in the loop is functionally equivalent
    // to a per-file bounding-box skip at this data scale.
    var last = trackpoints[trackpoints.length - 1];
    return haversineKm(markerLat, markerLng, last[0], last[1]) <= thresholdKm;
  }

  /*
   * Build journey line segments from entries and GPX trackpoints.
   *
   * entries:        [{lat, lng, force_connect}, ...] in chronological order
   * allTrackpoints: [ [[lat,lng],...], ... ] — one sub-array per GPX file
   * thresholdKm:    proximity radius (default 10)
   *
   * Returns array of segments, each segment being [[lng, lat], ...] in MapLibre
   * coordinate order. A segment with < 2 points is omitted.
   *
   * Rules:
   *   - No GPX files → all adjacent pairs connected (one segment)
   *   - GPX present, pair covered by same file → connector suppressed
   *   - GPX present, pair NOT covered by any single file → connector drawn
   *   - force_connect on arriving entry → always draw connector
   */
  function buildJourneySegments(entries, allTrackpoints, thresholdKm) {
    thresholdKm = thresholdKm || 10;
    var hasGpx   = allTrackpoints && allTrackpoints.length > 0;
    var segments = [];
    var current  = [];

    for (var i = 0; i < entries.length; i++) {
      var e      = entries[i];
      var lngLat = [parseFloat(e.lng), parseFloat(e.lat)]; // MapLibre: [lng, lat]

      if (i === 0) {
        current.push(lngLat);
        continue;
      }

      var prev    = entries[i - 1];
      var connect;

      if (!hasGpx || e.force_connect) {
        connect = true;
      } else {
        var pLat    = parseFloat(prev.lat);
        var pLng    = parseFloat(prev.lng);
        var cLat    = parseFloat(e.lat);
        var cLng    = parseFloat(e.lng);
        var covered = false;
        for (var f = 0; f < allTrackpoints.length; f++) {
          if (isNearTrack(pLat, pLng, allTrackpoints[f], thresholdKm) &&
              isNearTrack(cLat, cLng, allTrackpoints[f], thresholdKm)) {
            covered = true;
            break;
          }
        }
        connect = !covered;
      }

      if (connect) {
        current.push(lngLat);
      } else {
        if (current.length >= 2) segments.push(current);
        current = [lngLat]; // start new segment from this point
      }
    }

    if (current.length >= 2) segments.push(current);
    return segments;
  }

  /*
   * Draw journey segments — calls addJourneyLine once per segment.
   * baseSourceId: e.g. 'journey' → sources become 'journey-0', 'journey-1', ...
   * (single segment gets plain 'journey' for backwards compatibility).
   */
  function addJourneySegments(map, segments, baseSourceId) {
    segments.forEach(function (coords, i) {
      var sid = segments.length === 1 ? baseSourceId : baseSourceId + '-' + i;
      addJourneyLine(map, coords, sid);
    });
  }

  global.MapUtils = {
    MAP_STYLE:            MAP_STYLE,
    ACCENT:               ACCENT,
    addJourneyLine:       addJourneyLine,
    addJourneySegments:   addJourneySegments,
    buildJourneySegments: buildJourneySegments,
    extractTrackpoints:   extractTrackpoints,
    createDotMarker:      createDotMarker
  };
})(window);
