'use strict';

// ── NATS AIXM 5.1 XML parser Web Worker ──────────────────────────────────────
// Runs off the main thread to avoid freezing the UI on the 72 MB file.
// Accepts: { buffer: ArrayBuffer }
// Returns: { type:'progress', percent } or { type:'done', geojson } or { type:'error', message }

self.onmessage = function (e) {
  try {
    const text = new TextDecoder('utf-8').decode(e.data.buffer);
    const geojson = parseAIXM(text);
    self.postMessage({ type: 'done', geojson });
  } catch (err) {
    self.postMessage({ type: 'error', message: err.message });
  }
};

// ── Airspace types relevant to drone operations ───────────────────────────────
// FIR excluded: it is one giant polygon covering all UK airspace — useless on a drone map.
const RELEVANT_TYPES = new Set([
  'R',          // Restricted
  'P',          // Prohibited
  'CTR',        // Control zone (immediately around aerodromes)
  'CTA',        // Control area
  'TMA',        // Terminal manoeuvring area
  'RAS',        // Radar Advisory Service area
  'OTHER:TMZ',  // Transponder Mandatory Zone
  // D / D_OTHER (Danger) excluded — UK data contains enormous small-arms ranges
  // that cover huge swathes of the country and swamp the map. Danger areas are
  // covered separately via the user-loaded GeoJSON layer.
]);

// ── Main parse function ───────────────────────────────────────────────────────
function parseAIXM(text) {
  const features = [];

  // Slice the document into hasMember chunks — avoids running complex regex
  // over the full 72 MB string all at once.
  const members = text.split('</message:hasMember>');
  const total = members.length;

  for (let i = 0; i < members.length; i++) {
    const member = members[i];

    // Progress every 250 chunks
    if (i % 250 === 0) {
      self.postMessage({ type: 'progress', percent: Math.round((i / total) * 100) });
    }

    // Fast bail-out: only process members that contain an Airspace element
    if (!member.includes('<aixm:Airspace ')) continue;

    // Extract the Airspace element from within this member
    const start = member.indexOf('<aixm:Airspace ');
    if (start === -1) continue;

    // Only process BASELINE timeslices
    if (!member.includes('<aixm:interpretation>BASELINE</aixm:interpretation>')) continue;

    const block = member.substring(start);

    // ── Properties ──────────────────────────────────────────────────────────
    // type is within AirspaceTimeSlice; take the first match
    const typeM = block.match(/<aixm:type>([^<]+)<\/aixm:type>/);
    if (!typeM) continue;
    const type = typeM[1].trim();
    if (!RELEVANT_TYPES.has(type)) continue;

    const name       = first(block, /<aixm:name>([^<]+)<\/aixm:name>/);
    const designator = first(block, /<aixm:designator>([^<]+)<\/aixm:designator>/);
    const localType  = first(block, /<aixm:localType>([^<]+)<\/aixm:localType>/);
    const upperLimit = first(block, /<aixm:upperLimit[^>]*>([^<]+)<\/aixm:upperLimit>/);
    const upperUom   = first(block, /<aixm:upperLimit uom="([^"]+)"/);
    const upperRef   = first(block, /<aixm:upperLimitReference>([^<]+)<\/aixm:upperLimitReference>/);
    const lowerLimit = first(block, /<aixm:lowerLimit[^>]*>([^<]+)<\/aixm:lowerLimit>/);
    const lowerUom   = first(block, /<aixm:lowerLimit uom="([^"]+)"/);
    const lowerRef   = first(block, /<aixm:lowerLimitReference>([^<]+)<\/aixm:lowerLimitReference>/);

    // ── Geometry ─────────────────────────────────────────────────────────────
    const geometry = parseGeometry(block);
    if (!geometry) continue;

    features.push({
      type: 'Feature',
      properties: { name, designator, localType, type, upperLimit, upperUom, upperRef, lowerLimit, lowerUom, lowerRef },
      geometry,
    });
  }

  self.postMessage({ type: 'progress', percent: 100 });
  return { type: 'FeatureCollection', features };
}

// ── Geometry parser ───────────────────────────────────────────────────────────
function parseGeometry(block) {
  // Find the horizontal projection Surface
  const sStart = block.indexOf('<aixm:Surface ');
  if (sStart === -1) return null;
  const sEnd = block.indexOf('</aixm:Surface>', sStart);
  if (sEnd === -1) return null;
  const surface = block.substring(sStart, sEnd + 15);

  const ring = [];

  // Walk through each curveMember
  const cmRe = /<gml:curveMember[^>]*>([\s\S]*?)<\/gml:curveMember>/g;
  let cm;
  while ((cm = cmRe.exec(surface)) !== null) {
    const seg = cm[1];
    appendSegment(seg, ring);
  }

  if (ring.length < 3) return null;

  // Close the ring
  const first0 = ring[0], last = ring[ring.length - 1];
  if (first0[0] !== last[0] || first0[1] !== last[1]) ring.push([...first0]);

  return { type: 'Polygon', coordinates: [ring] };
}

function appendSegment(seg, ring) {
  if (seg.includes('<gml:CircleByCenterPoint')) {
    // ── Full circle ──────────────────────────────────────────────────────────
    const posM = seg.match(/<gml:pos[^>]*>([\d.\-]+)\s+([\d.\-]+)<\/gml:pos>/);
    const radM = seg.match(/<gml:radius[^>]*>([\d.\-]+)<\/gml:radius>/);
    const uomM = seg.match(/<gml:radius uom="([^"]+)"/);
    if (!posM || !radM) return;
    const lat = parseFloat(posM[1]), lon = parseFloat(posM[2]);
    const km = toKm(parseFloat(radM[1]), uomM ? uomM[1] : '[nmi_i]');
    ring.push(...circleCoords(lon, lat, km, 64));

  } else if (seg.includes('<gml:ArcByCenterPoint')) {
    // ── Arc segment ──────────────────────────────────────────────────────────
    // GML angles: counterclockwise from east (math convention).
    // Bearing = (90 - mathAngle + 360) % 360
    const posM   = seg.match(/<gml:pos[^>]*>([\d.\-]+)\s+([\d.\-]+)<\/gml:pos>/);
    const radM   = seg.match(/<gml:radius[^>]*>([\d.\-]+)<\/gml:radius>/);
    const uomM   = seg.match(/<gml:radius uom="([^"]+)"/);
    const startM = seg.match(/<gml:startAngle[^>]*>([\d.\-]+)<\/gml:startAngle>/);
    const endM   = seg.match(/<gml:endAngle[^>]*>([\d.\-]+)<\/gml:endAngle>/);
    if (!posM || !radM || !startM || !endM) return;
    const lat = parseFloat(posM[1]), lon = parseFloat(posM[2]);
    const km  = toKm(parseFloat(radM[1]), uomM ? uomM[1] : '[nmi_i]');
    const sa  = parseFloat(startM[1]), ea = parseFloat(endM[1]);
    ring.push(...arcCoords(lon, lat, km, sa, ea, 32));

  } else {
    // ── GeodesicString / LineStringSegment ───────────────────────────────────
    // gml:posList: flat sequence "lat1 lon1 lat2 lon2 ..."
    const plM = seg.match(/<gml:posList[^>]*>([\s\S]*?)<\/gml:posList>/);
    if (plM) {
      const vals = plM[1].trim().split(/\s+/);
      for (let i = 0; i + 1 < vals.length; i += 2) {
        ring.push([parseFloat(vals[i + 1]), parseFloat(vals[i])]);
      }
    } else {
      // Individual gml:pos elements inside pointProperty/Point
      const posRe = /<gml:pos[^>]*>([\d.\-]+)\s+([\d.\-]+)<\/gml:pos>/g;
      let pt;
      while ((pt = posRe.exec(seg)) !== null) {
        ring.push([parseFloat(pt[2]), parseFloat(pt[1])]); // flip to [lon, lat]
      }
    }
  }
}

// ── Geometry utilities ────────────────────────────────────────────────────────
function toKm(value, uom) {
  if (uom === '[nmi_i]') return value * 1.852;
  if (uom === 'M')       return value / 1000;
  if (uom === 'KM')      return value;
  return value * 1.852; // assume nmi
}

// Great-circle destination point given start (lat/lon in degrees), bearing (deg CW from N), distance (km)
function destPoint(lat, lon, bearingDeg, distKm) {
  const R  = 6371;
  const d  = distKm / R;
  const b  = bearingDeg * Math.PI / 180;
  const φ1 = lat * Math.PI / 180;
  const λ1 = lon * Math.PI / 180;
  const φ2 = Math.asin(Math.sin(φ1) * Math.cos(d) + Math.cos(φ1) * Math.sin(d) * Math.cos(b));
  const λ2 = λ1 + Math.atan2(Math.sin(b) * Math.sin(d) * Math.cos(φ1), Math.cos(d) - Math.sin(φ1) * Math.sin(φ2));
  return [λ2 * 180 / Math.PI, φ2 * 180 / Math.PI]; // [lon, lat] for GeoJSON
}

function circleCoords(lon, lat, radiusKm, steps) {
  const pts = [];
  for (let i = 0; i <= steps; i++) {
    pts.push(destPoint(lat, lon, (i / steps) * 360, radiusKm));
  }
  return pts;
}

function arcCoords(lon, lat, radiusKm, startMathDeg, endMathDeg, steps) {
  // GML CCW-from-east → sweep counterclockwise; always positive
  let sweep = endMathDeg - startMathDeg;
  if (sweep <= 0) sweep += 360;

  const pts = [];
  for (let i = 0; i <= steps; i++) {
    const mathAngle = startMathDeg + (sweep * i / steps);
    const bearing   = ((90 - mathAngle) % 360 + 360) % 360; // CW from N
    pts.push(destPoint(lat, lon, bearing, radiusKm));
  }
  return pts;
}

// ── Regex helper ──────────────────────────────────────────────────────────────
function first(text, re) {
  const m = text.match(re);
  return m ? m[1].trim() : '';
}
