// GPX shared parser + projection helpers used by gpx-viewer, gpx-merger,
// route-elevation, gpx-trimmer, gpx-to-geojson.

(function () {
  const R = 6371000; // earth radius in metres

  // Web-Mercator projection: lat/lon → unitless x/y (km-ish), scaled to map box.
  function mercator(lat, lon) {
    const x = (lon + 180) / 360;
    const sinLat = Math.sin((lat * Math.PI) / 180);
    const y = 0.5 - Math.log((1 + sinLat) / (1 - sinLat)) / (4 * Math.PI);
    return { x, y };
  }

  // Great-circle distance between two lat/lon points (haversine, metres).
  function haversine(lat1, lon1, lat2, lon2) {
    const toRad = Math.PI / 180;
    const dLat = (lat2 - lat1) * toRad;
    const dLon = (lon2 - lon1) * toRad;
    const a = Math.sin(dLat / 2) ** 2 +
              Math.cos(lat1 * toRad) * Math.cos(lat2 * toRad) * Math.sin(dLon / 2) ** 2;
    return 2 * R * Math.asin(Math.min(1, Math.sqrt(a)));
  }

  // Parse a GPX text string into an array of tracks. Each track is an array of
  // segments; each segment is an array of points {lat, lon, ele, time}.
  function parseGPX(text) {
    const doc = new DOMParser().parseFromString(text, 'application/xml');
    if (doc.getElementsByTagName('parsererror').length) {
      throw new Error('Invalid GPX file');
    }
    const tracks = [];
    const trkNodes = doc.getElementsByTagName('trk');
    for (const trk of trkNodes) {
      const name = trk.getElementsByTagName('name')[0]?.textContent || 'Track';
      const segments = [];
      const segNodes = trk.getElementsByTagName('trkseg');
      for (const seg of segNodes) {
        const pts = [];
        const ptNodes = seg.getElementsByTagName('trkpt');
        for (const p of ptNodes) {
          const lat = Number(p.getAttribute('lat'));
          const lon = Number(p.getAttribute('lon'));
          const ele = Number(p.getElementsByTagName('ele')[0]?.textContent || NaN);
          const time = p.getElementsByTagName('time')[0]?.textContent || null;
          if (Number.isFinite(lat) && Number.isFinite(lon)) {
            pts.push({ lat, lon, ele: Number.isFinite(ele) ? ele : null, time });
          }
        }
        if (pts.length) segments.push(pts);
      }
      if (segments.length) tracks.push({ name, segments });
    }
    // Also handle routes (rte/rtept).
    const rteNodes = doc.getElementsByTagName('rte');
    for (const rte of rteNodes) {
      const name = rte.getElementsByTagName('name')[0]?.textContent || 'Route';
      const pts = [];
      const ptNodes = rte.getElementsByTagName('rtept');
      for (const p of ptNodes) {
        const lat = Number(p.getAttribute('lat'));
        const lon = Number(p.getAttribute('lon'));
        const ele = Number(p.getElementsByTagName('ele')[0]?.textContent || NaN);
        if (Number.isFinite(lat) && Number.isFinite(lon)) {
          pts.push({ lat, lon, ele: Number.isFinite(ele) ? ele : null });
        }
      }
      if (pts.length) tracks.push({ name, segments: [pts] });
    }
    return tracks;
  }

  function flattenPoints(tracks) {
    const out = [];
    for (const t of tracks) for (const s of t.segments) for (const p of s) out.push(p);
    return out;
  }

  // Compute distance, elevation gain/loss, duration for a list of points.
  function computeStats(points) {
    let dist = 0, gain = 0, loss = 0;
    let minEle = Infinity, maxEle = -Infinity;
    let firstTime = null, lastTime = null;
    for (let i = 0; i < points.length; i++) {
      const p = points[i];
      if (i > 0) {
        dist += haversine(points[i - 1].lat, points[i - 1].lon, p.lat, p.lon);
        if (Number.isFinite(p.ele) && Number.isFinite(points[i - 1].ele)) {
          const dEle = p.ele - points[i - 1].ele;
          if (dEle > 0) gain += dEle; else loss -= dEle;
        }
      }
      if (Number.isFinite(p.ele)) {
        if (p.ele < minEle) minEle = p.ele;
        if (p.ele > maxEle) maxEle = p.ele;
      }
      if (p.time) {
        if (!firstTime) firstTime = new Date(p.time);
        lastTime = new Date(p.time);
      }
    }
    const duration = firstTime && lastTime ? (lastTime - firstTime) / 1000 : 0;
    return {
      distanceM: dist,
      gainM: gain,
      lossM: loss,
      minEle: minEle === Infinity ? null : minEle,
      maxEle: maxEle === -Infinity ? null : maxEle,
      durationS: duration,
      pointCount: points.length,
    };
  }

  // Bounding box in lat/lon.
  function boundsOf(points) {
    let minLat = Infinity, maxLat = -Infinity, 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;
    }
    return { minLat, maxLat, minLon, maxLon };
  }

  // Project a list of points to SVG x/y inside [0..w] × [0..h], preserving aspect.
  function projectToSVG(points, w, h, pad = 12) {
    if (!points.length) return [];
    const merc = points.map((p) => mercator(p.lat, p.lon));
    let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
    for (const m of merc) {
      if (m.x < minX) minX = m.x; if (m.x > maxX) maxX = m.x;
      if (m.y < minY) minY = m.y; if (m.y > maxY) maxY = m.y;
    }
    const dx = maxX - minX || 1e-9;
    const dy = maxY - minY || 1e-9;
    const scale = Math.min((w - pad * 2) / dx, (h - pad * 2) / dy);
    const offX = (w - dx * scale) / 2;
    const offY = (h - dy * scale) / 2;
    return merc.map((m) => ({
      x: offX + (m.x - minX) * scale,
      y: offY + (m.y - minY) * scale,
    }));
  }

  // Build GPX text from a list of tracks (each {name, segments: [[points]]}).
  function buildGPX(tracks, creator = 'MiniMagics') {
    const esc = (s) => String(s).replace(/[<>&'"]/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', "'": '&apos;', '"': '&quot;' }[c]));
    const lines = [];
    lines.push('<?xml version="1.0" encoding="UTF-8"?>');
    lines.push(`<gpx version="1.1" creator="${esc(creator)}" xmlns="http://www.topografix.com/GPX/1/1">`);
    for (const trk of tracks) {
      lines.push('  <trk>');
      lines.push(`    <name>${esc(trk.name || 'Track')}</name>`);
      for (const seg of trk.segments) {
        lines.push('    <trkseg>');
        for (const p of seg) {
          lines.push(`      <trkpt lat="${p.lat}" lon="${p.lon}">`);
          if (Number.isFinite(p.ele) && p.ele !== null) lines.push(`        <ele>${p.ele}</ele>`);
          if (p.time) lines.push(`        <time>${esc(p.time)}</time>`);
          lines.push('      </trkpt>');
        }
        lines.push('    </trkseg>');
      }
      lines.push('  </trk>');
    }
    lines.push('</gpx>');
    return lines.join('\n');
  }

  // SVG polyline-only map renderer. Returns React JSX.
  function GpxMap({ points, height = 320, color = '#16a34a', startEnd = true, title }) {
    if (!points || points.length === 0) {
      return (
        <div className="sport-empty" style={{ minHeight: height, display: 'grid', placeItems: 'center' }}>
          No track to display.
        </div>
      );
    }
    const w = 600;
    const projected = projectToSVG(points, w, height, 16);
    const d = projected.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ');
    const start = projected[0];
    const end = projected[projected.length - 1];
    return (
      <div className="gpx-svg-wrap">
        {title && <div className="gpx-svg-title">{title}</div>}
        <svg viewBox={`0 0 ${w} ${height}`} preserveAspectRatio="xMidYMid meet" className="gpx-svg">
          <defs>
            <pattern id="gpxgrid" width="40" height="40" patternUnits="userSpaceOnUse">
              <path d="M 40 0 L 0 0 0 40" fill="none" stroke="currentColor" strokeOpacity=".08" strokeWidth=".5" />
            </pattern>
          </defs>
          <rect width={w} height={height} fill="url(#gpxgrid)" />
          <path d={d} fill="none" stroke={color} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
          {startEnd && start && (
            <>
              <circle cx={start.x} cy={start.y} r="6" fill="#22c55e" stroke="#fff" strokeWidth="2" />
              <circle cx={end.x} cy={end.y} r="6" fill="#dc2626" stroke="#fff" strokeWidth="2" />
            </>
          )}
        </svg>
      </div>
    );
  }

  // SVG elevation chart.
  function ElevationChart({ points, height = 180, color = '#0e9488', title }) {
    if (!points || points.length < 2) return null;
    const eles = points.map((p) => p.ele).filter((e) => Number.isFinite(e));
    if (eles.length === 0) return (
      <div className="sport-empty" style={{ minHeight: height, display: 'grid', placeItems: 'center' }}>
        No elevation data in this track.
      </div>
    );
    const w = 600;
    const minE = Math.min(...eles);
    const maxE = Math.max(...eles);
    const dE = Math.max(1, maxE - minE);
    // Cumulative distance along the line.
    const cum = [0];
    for (let i = 1; i < points.length; i++) {
      cum.push(cum[i - 1] + haversine(points[i - 1].lat, points[i - 1].lon, points[i].lat, points[i].lon));
    }
    const totalD = cum[cum.length - 1] || 1;
    const pad = 24;
    const xs = cum.map((d) => pad + (d / totalD) * (w - pad * 2));
    const ys = points.map((p) => Number.isFinite(p.ele)
      ? height - pad / 2 - ((p.ele - minE) / dE) * (height - pad * 1.5)
      : null);
    const path = points.reduce((acc, _, i) => {
      if (ys[i] == null) return acc;
      return acc + `${acc ? ' L' : 'M'}${xs[i].toFixed(1)},${ys[i].toFixed(1)}`;
    }, '');
    const fillPath = path + ` L${xs[points.length - 1].toFixed(1)},${(height - 2).toFixed(1)} L${xs[0].toFixed(1)},${(height - 2).toFixed(1)} Z`;
    return (
      <div className="gpx-svg-wrap">
        {title && <div className="gpx-svg-title">{title}</div>}
        <svg viewBox={`0 0 ${w} ${height}`} className="gpx-svg" preserveAspectRatio="none">
          <path d={fillPath} fill={color} fillOpacity="0.18" />
          <path d={path} fill="none" stroke={color} strokeWidth="2" />
          <text x={pad} y={14} fontSize="11" fill="currentColor" opacity="0.6">{maxE.toFixed(0)} m</text>
          <text x={pad} y={height - 4} fontSize="11" fill="currentColor" opacity="0.6">{minE.toFixed(0)} m</text>
          <text x={w - pad} y={height - 4} textAnchor="end" fontSize="11" fill="currentColor" opacity="0.6">{(totalD / 1000).toFixed(2)} km</text>
        </svg>
      </div>
    );
  }

  function StatTile({ label, value, sub }) {
    return (
      <div className="gpx-stat">
        <div className="gpx-stat-l">{label}</div>
        <div className="gpx-stat-v">{value}</div>
        {sub && <div className="gpx-stat-s">{sub}</div>}
      </div>
    );
  }

  window.MMGpx = {
    haversine, mercator, parseGPX, buildGPX, computeStats, boundsOf,
    flattenPoints, projectToSVG,
    GpxMap, ElevationChart, StatTile,
  };
})();
