// FIT → GPX. Garmin/Wahoo .fit files use a binary container; we walk the
// records and pull lat/lon/altitude/timestamp from "record" messages
// (global message #20) and emit a GPX 1.1 file.
//
// FIT file format reference: https://developer.garmin.com/fit/protocol/
// We deliberately implement just the subset needed for GPS activity files —
// no CRC validation, no developer fields, no compressed timestamps.

(function () {
  const SEMI_TO_DEG = 180 / Math.pow(2, 31);
  // FIT timestamps are seconds since 1989-12-31 UTC.
  const FIT_EPOCH = Date.UTC(1989, 11, 31, 0, 0, 0) / 1000;

  // base_type id → { size, reader, invalid }
  const BASE = {
    0x00: { size: 1, read: (dv, o) => dv.getUint8(o), invalid: 0xFF },
    0x01: { size: 1, read: (dv, o) => dv.getInt8(o), invalid: 0x7F },
    0x02: { size: 1, read: (dv, o) => dv.getUint8(o), invalid: 0xFF },
    0x83: { size: 2, read: (dv, o, le) => dv.getInt16(o, le), invalid: 0x7FFF },
    0x84: { size: 2, read: (dv, o, le) => dv.getUint16(o, le), invalid: 0xFFFF },
    0x85: { size: 4, read: (dv, o, le) => dv.getInt32(o, le), invalid: 0x7FFFFFFF },
    0x86: { size: 4, read: (dv, o, le) => dv.getUint32(o, le), invalid: 0xFFFFFFFF },
    0x07: { size: 1, read: (dv, o) => dv.getUint8(o), invalid: 0x00 }, // strings handled specially
    0x88: { size: 4, read: (dv, o, le) => dv.getFloat32(o, le), invalid: NaN },
    0x89: { size: 8, read: (dv, o, le) => dv.getFloat64(o, le), invalid: NaN },
    0x0A: { size: 1, read: (dv, o) => dv.getUint8(o), invalid: 0x00 },
    0x8B: { size: 2, read: (dv, o, le) => dv.getUint16(o, le), invalid: 0x0000 },
    0x8C: { size: 4, read: (dv, o, le) => dv.getUint32(o, le), invalid: 0x00000000 },
    0x0D: { size: 1, read: (dv, o) => dv.getUint8(o), invalid: 0xFF },
  };

  function parseFIT(buffer) {
    const dv = new DataView(buffer);
    if (buffer.byteLength < 14) throw new Error('File too small to be FIT');
    const headerSize = dv.getUint8(0);
    const dataSize = dv.getUint32(4, true);
    const sig = String.fromCharCode(dv.getUint8(8), dv.getUint8(9), dv.getUint8(10), dv.getUint8(11));
    if (sig !== '.FIT') throw new Error('Not a FIT file (bad signature)');
    const definitions = {};   // localType → { globalType, fields: [{num,size,baseType}], le }
    let pos = headerSize;
    const end = headerSize + dataSize;
    const points = [];

    while (pos < end) {
      const recHeader = dv.getUint8(pos++);
      // We don't support compressed timestamp records (top bit set)
      if (recHeader & 0x80) {
        // Compressed-timestamp data record. Skip safely by reading according to
        // its definition (low 5 bits = local type).
        const localType = (recHeader >> 5) & 0x03;
        const def = definitions[localType];
        if (!def) throw new Error('Compressed record without definition');
        pos += def.totalSize;
        continue;
      }
      const isDef = (recHeader & 0x40) !== 0;
      const localType = recHeader & 0x0F;

      if (isDef) {
        // Skip reserved byte
        pos += 1;
        const arch = dv.getUint8(pos++);
        const le = arch === 0;
        const globalType = dv.getUint16(pos, le); pos += 2;
        const fieldCount = dv.getUint8(pos++);
        const fields = [];
        let totalSize = 0;
        for (let i = 0; i < fieldCount; i++) {
          const num = dv.getUint8(pos++);
          const size = dv.getUint8(pos++);
          const baseType = dv.getUint8(pos++);
          fields.push({ num, size, baseType });
          totalSize += size;
        }
        // Developer fields (not used here, but advance pos if present).
        if (recHeader & 0x20) {
          const devCount = dv.getUint8(pos++);
          for (let i = 0; i < devCount; i++) {
            pos += 2; // num + size
            pos += 1; // dev_data_index
            // We can't decode them; they don't appear in stock Records.
          }
        }
        definitions[localType] = { globalType, fields, le, totalSize };
      } else {
        const def = definitions[localType];
        if (!def) throw new Error('Data record without prior definition');
        const start = pos;
        const fieldsRead = {};
        for (const f of def.fields) {
          const baseType = BASE[f.baseType];
          if (!baseType) { pos += f.size; continue; }
          // Multi-element field: read the first one, ignore the rest.
          let value = null;
          try {
            value = baseType.read(dv, pos, def.le);
            // sentinel "invalid" → null
            if (Number.isFinite(baseType.invalid) && value === baseType.invalid) value = null;
          } catch { value = null; }
          fieldsRead[f.num] = value;
          pos += f.size;
        }

        if (def.globalType === 20) {
          const lat = fieldsRead[0] != null ? fieldsRead[0] * SEMI_TO_DEG : null;
          const lon = fieldsRead[1] != null ? fieldsRead[1] * SEMI_TO_DEG : null;
          let ele = null;
          if (fieldsRead[78] != null) ele = fieldsRead[78] / 5 - 500;
          else if (fieldsRead[2] != null) ele = fieldsRead[2] / 5 - 500;
          let timeIso = null;
          if (fieldsRead[253] != null) timeIso = new Date((FIT_EPOCH + fieldsRead[253]) * 1000).toISOString();
          if (Number.isFinite(lat) && Number.isFinite(lon) && Math.abs(lat) <= 90 && Math.abs(lon) <= 180) {
            points.push({ lat, lon, ele: Number.isFinite(ele) ? ele : null, time: timeIso });
          }
        }
        // Defensive: if pos didn't move forward we'd loop; ensure progress.
        if (pos === start) pos += def.totalSize || 1;
      }
    }
    return points;
  }

  window.MMFitParse = parseFIT;
})();

window.TOOL_HANDLERS['fit-to-gpx'] = function FitToGpxTool() {
  const G = window.MMGpx;
  const S = window.MMSports;
  const [filename, setFilename] = React.useState('');
  const [points, setPoints] = React.useState(null);
  const [error, setError] = React.useState('');

  const onFile = async (f) => {
    if (!f) return;
    setError('');
    try {
      const buf = await f.arrayBuffer();
      const pts = window.MMFitParse(buf);
      if (!pts.length) throw new Error('No GPS points found in this FIT file. Indoor activities have no coordinates.');
      setPoints(pts); setFilename(f.name);
    } catch (e) { setError(e.message); setPoints(null); }
  };

  const download = () => {
    if (!points) return;
    const xml = G.buildGPX([{ name: filename.replace(/\.fit$/i, '') || 'Activity', segments: [points] }]);
    const blob = new Blob([xml], { type: 'application/gpx+xml' });
    window.downloadBlob(blob, (filename.replace(/\.fit$/i, '') || 'activity') + '.gpx');
  };

  if (!points) {
    return (
      <div className="mini-tool sport-tool">
        <S.SportsToolHeader title="FIT → GPX" sub="Convert Garmin or Wahoo .fit files to GPX." icon="swap" accent="#2563eb" />
        {error && <div style={{ marginBottom: 12 }}><window.ToolError error={error} onRetry={() => setError('')} /></div>}
        <window.Dropzone onFile={onFile} title="Drop a .fit file" hint="Garmin / Wahoo / Polar binary export" accept=".fit,application/octet-stream" />
        <div className="sport-helper" style={{ marginTop: 16 }}>
          We decode the FIT binary in your browser — your activity stays on this device.
        </div>
      </div>
    );
  }

  const stats = G.computeStats(points);
  return (
    <div className="mini-tool sport-tool">
      <S.SportsToolHeader title="FIT → GPX" sub={filename} icon="swap" accent="#2563eb" />
      <G.GpxMap points={points} height={260} title="Decoded route" />
      <div className="gpx-stats">
        <G.StatTile label="Distance" value={`${(stats.distanceM / 1000).toFixed(2)} km`} />
        <G.StatTile label="Points" value={points.length.toLocaleString()} />
        {stats.gainM > 0 && <G.StatTile label="Elev gain" value={`${stats.gainM.toFixed(0)} m`} />}
        {stats.durationS > 0 && <G.StatTile label="Duration" value={S.formatHMS(stats.durationS, { alwaysHours: true })} />}
      </div>
      <div className="cmp-actions">
        <button className="btn btn-secondary" onClick={() => setPoints(null)}>Another file</button>
        <button className="btn btn-primary" onClick={download}>
          <window.Icon name="download" size={16} /> Download .gpx
        </button>
      </div>
    </div>
  );
};
