// ── Ballistic Calculator — point-mass trajectory engine ──────────────────────
// No server dependency. All computation is done in-browser.

// ── Drag tables  [Mach, Cd_ref]  ─────────────────────────────────────────────
// Cd values for the reference projectile (G7 or G1).
// Used in: a_drag = (rho/rho_std) * Cd(M) * v² / (2 * BC_SI)
// BC_SI (kg/m²) = BC_lb_in2 * 703.07

const DRAG_G7 = [
  [0.00,0.1198],[0.05,0.1197],[0.10,0.1196],[0.20,0.1194],[0.30,0.1194],
  [0.40,0.1194],[0.50,0.1194],[0.60,0.1193],[0.70,0.1194],[0.80,0.1193],
  [0.83,0.1193],[0.86,0.1194],[0.88,0.1194],[0.90,0.1194],[0.92,0.1198],
  [0.95,0.1210],[0.975,0.1215],[1.00,0.1212],[1.025,0.1206],[1.05,0.1202],
  [1.10,0.1208],[1.15,0.1220],[1.20,0.1240],[1.30,0.1262],[1.40,0.1280],
  [1.50,0.1283],[1.60,0.1290],[1.80,0.1297],[2.00,0.1298],[2.20,0.1295],
  [2.50,0.1290],[3.00,0.1280],[4.00,0.1245],[5.00,0.1210],
];

const DRAG_G1 = [
  [0.00,0.2629],[0.05,0.2558],[0.10,0.2568],[0.20,0.2581],[0.30,0.2578],
  [0.40,0.2561],[0.50,0.2537],[0.60,0.2528],[0.65,0.2591],[0.70,0.2668],
  [0.75,0.2760],[0.80,0.2983],[0.825,0.3163],[0.85,0.3285],[0.875,0.3449],
  [0.90,0.3680],[0.925,0.4000],[0.95,0.4258],[0.975,0.4465],[1.00,0.4581],
  [1.025,0.4547],[1.05,0.4420],[1.10,0.4152],[1.20,0.3736],[1.30,0.3449],
  [1.40,0.3231],[1.50,0.3064],[1.60,0.2927],[1.80,0.2722],[2.00,0.2573],
  [2.50,0.2313],[3.00,0.2144],[4.00,0.1875],[5.00,0.1695],
];

const G         = 9.80665;   // m/s²
const RHO_STD   = 1.2250;    // ICAO std sea-level density, kg/m³
const BC_CONV   = 703.0696;  // lb/in² → kg/m²
const SUBSONIC  = 340;       // m/s threshold for subsonic display

// ── Physics helpers ───────────────────────────────────────────────────────────

function dragCd(mach, table) {
  if (mach <= table[0][0])                  return table[0][1];
  if (mach >= table[table.length - 1][0])   return table[table.length - 1][1];
  for (let i = 1; i < table.length; i++) {
    if (mach <= table[i][0]) {
      const [m0, c0] = table[i - 1], [m1, c1] = table[i];
      return c0 + (c1 - c0) * (mach - m0) / (m1 - m0);
    }
  }
  return table[table.length - 1][1];
}

// Air density (kg/m³) — accounts for temp, pressure and humidity.
function airDensity(temp_c, press_hpa, humid_pct) {
  const T    = temp_c + 273.15;
  const P    = press_hpa * 100;
  const Psat = 610.78 * Math.exp(17.27 * temp_c / (temp_c + 237.3));
  const Pv   = (humid_pct / 100) * Psat;
  return (P - Pv) / (287.058 * T) + Pv / (461.495 * T);
}

// Pressure at altitude h (m) via ISA lapse rate.
function pressureAtAltitude(h) {
  return 1013.25 * Math.pow(1 - 0.0065 * h / 288.15, 5.2561);
}

// Speed of sound (m/s) at temperature.
function speedOfSound(temp_c) {
  return 331.3 * Math.sqrt(1 + temp_c / 273.15);
}

// ── Trajectory integration (Euler, dt = 0.5 ms) ──────────────────────────────
//
// Coordinate system:
//   x  — range (forward)
//   y  — vertical (up = positive)
//   z  — lateral (right = positive)
//
// wind_cross_ms: effective lateral wind component.
//   positive  = wind FROM the right → bullet drifts LEFT (z becomes negative)
//
// Returns array of { x, y, z, v, t } sampled every step_m meters of range.

function integrate(v0, theta, bc_si, rho, sound, dragTable, scopeH_m, wind_cross_ms, step_m, max_m) {
  const dt = 0.0005;
  let x = 0, y = -scopeH_m, z = 0;
  let vx = v0 * Math.cos(theta), vy = v0 * Math.sin(theta), vz = 0;
  let t = 0;
  let nextRecord = 0;
  const pts = [];

  while (x <= max_m + step_m * 0.5 && t < 15) {
    if (x >= nextRecord - 1e-6) {
      pts.push({ x: nextRecord, y, z, v: Math.hypot(vx, vy), t });
      nextRecord += step_m;
    }
    const vb   = Math.hypot(vx, vy);
    const mach = vb / sound;
    const cd   = dragCd(mach, dragTable);
    const k    = (rho / RHO_STD) * cd / (2 * bc_si);

    // Drag decelerates bullet along its velocity vector; wind creates lateral drag.
    // az = -k*vb*(vz + wind_cross_ms)  [wind from right → bullet pushed left]
    const ax = -k * vb * vx;
    const ay = -k * vb * vy - G;
    const az = -k * vb * (vz + wind_cross_ms);

    vx += ax * dt;  vy += ay * dt;  vz += az * dt;
    x  += vx * dt;  y  += vy * dt;  z  += vz * dt;
    t  += dt;

    if (vb < 50) break;
  }
  return pts;
}

// Binary-search for the elevation angle (rad) that gives y = 0 at zero_m.
function findZeroAngle(v0, zero_m, bc_si, rho, sound, dragTable, scopeH_m, wind_cross_ms) {
  let lo = -0.01, hi = 0.15;
  for (let i = 0; i < 60; i++) {
    const mid = (lo + hi) / 2;
    const pts = integrate(v0, mid, bc_si, rho, sound, dragTable, scopeH_m, wind_cross_ms, zero_m, zero_m);
    const y   = pts.length ? pts[pts.length - 1].y : -999;
    if (y > 0) hi = mid; else lo = mid;
  }
  return (lo + hi) / 2;
}

// ── Angular conversion helpers ────────────────────────────────────────────────

function moaFromMm(dist_m, mm)  { return dist_m > 0 ? mm / (dist_m * 0.29089) : null; }
function mradFromMm(dist_m, mm) { return dist_m > 0 ? mm / (dist_m * 0.1)     : null; }
function angFromMm(dist_m, mm, unit) {
  return unit === 'MOA' ? moaFromMm(dist_m, mm) : mradFromMm(dist_m, mm);
}

function toMeters(val, unit) { return unit === 'yd' ? val * 0.9144 : val; }

function fmt(n, dec, showSign = false) {
  if (n === null || n === undefined || isNaN(n)) return '—';
  const s = Math.abs(n).toFixed(dec);
  if (showSign) return (n >= 0 ? '+' : '−') + s;
  return s;
}

// ── Main entry point ──────────────────────────────────────────────────────────

function calculate() {
  const alertEl = document.getElementById('calcAlert');
  alertEl.classList.add('d-none');

  // Read + validate inputs
  const v0Raw      = parseFloat(document.getElementById('v0').value);
  const v0Unit     = document.getElementById('v0Unit').value;
  const bcRaw      = parseFloat(document.getElementById('bc').value);
  const bcModel    = document.getElementById('dragModel').value;
  const wtGr       = parseFloat(document.getElementById('bulletWeight').value) || null;
  const zeroRaw    = parseFloat(document.getElementById('zeroDist').value);
  const zeroUnit   = document.getElementById('zeroUnit').value;
  const scopeHMm   = parseFloat(document.getElementById('scopeHeight').value) || 50;
  let   tempRaw    = parseFloat(document.getElementById('temp').value);
  const tempUnit   = document.getElementById('tempUnit').value;
  const press      = parseFloat(document.getElementById('pressure').value) || 1013.25;
  const humid      = parseFloat(document.getElementById('humidity').value) || 50;
  const altM       = parseFloat(document.getElementById('altitude').value) || 0;
  const windSpdRaw = parseFloat(document.getElementById('windSpeed').value) || 0;
  const windUnit   = document.getElementById('windUnit').value;
  const windDirDeg = parseFloat(document.getElementById('windDir').value) || 0;
  const outUnit    = document.getElementById('outputUnit').value;
  const distFrom   = parseFloat(document.getElementById('distFrom').value) || 0;
  const distTo     = parseFloat(document.getElementById('distTo').value)   || 800;
  const distStep   = parseFloat(document.getElementById('distStep').value)  || 25;

  if (!v0Raw || v0Raw <= 0)   return showAlert('Muzzle velocity is required and must be > 0.');
  if (!bcRaw || bcRaw <= 0)   return showAlert('Ballistic coefficient is required and must be > 0.');
  if (!zeroRaw || zeroRaw<=0) return showAlert('Zero distance is required and must be > 0.');
  if (distTo <= distFrom)     return showAlert('"To" must be greater than "From".');
  if (distTo > 3000)          return showAlert('Maximum distance is 3000 m/yd.');

  // Convert to SI
  const v0     = v0Unit === 'fps' ? v0Raw * 0.3048 : v0Raw;
  const zero_m = toMeters(zeroRaw, zeroUnit);
  const max_m  = toMeters(distTo,  zeroUnit);
  const from_m = toMeters(distFrom, zeroUnit);
  const step_m = toMeters(distStep, zeroUnit);
  const scopeH = scopeHMm / 1000;

  if (tempUnit === 'f') tempRaw = (tempRaw - 32) * 5 / 9;

  // Wind: convert speed to m/s, compute crosswind component
  let windMs = windSpdRaw;
  if (windUnit === 'kph') windMs /= 3.6;
  else if (windUnit === 'mph') windMs *= 0.44704;
  // Positive windCross = wind FROM the right → bullet drifts left (z < 0)
  const windCross = windMs * Math.sin(windDirDeg * Math.PI / 180);

  // Atmosphere
  const pressEff = (altM > 0 && press === 1013.25) ? pressureAtAltitude(altM) : press;
  const rho   = airDensity(tempRaw, pressEff, humid);
  const sound = speedOfSound(tempRaw);
  const bc_si = bcRaw * BC_CONV;
  const table = bcModel === 'G7' ? DRAG_G7 : DRAG_G1;

  // Find zero elevation angle, then compute full trajectory
  const theta = findZeroAngle(v0, zero_m, bc_si, rho, sound, table, scopeH, windCross);
  const pts   = integrate(v0, theta, bc_si, rho, sound, table, scopeH, windCross, step_m, max_m + step_m);
  const shown = pts.filter(p => p.x >= from_m - 0.1 && p.x <= max_m + 0.1);

  if (!shown.length) return showAlert('No trajectory points in the requested range.');

  renderResults(shown, { zero_m, zeroUnit, v0Raw, v0Unit, outUnit, wtGr, rho, sound });
}

function showAlert(msg) {
  const el = document.getElementById('calcAlert');
  el.textContent = msg;
  el.classList.remove('d-none');
}

// ── Render trajectory table ───────────────────────────────────────────────────

function renderResults(pts, { zero_m, zeroUnit, v0Raw, v0Unit, outUnit, wtGr, rho, sound }) {
  document.getElementById('resultsPlaceholder').classList.add('d-none');
  document.getElementById('resultsSection').classList.remove('d-none');

  const distLabel = zeroUnit === 'yd' ? 'yd' : 'm';
  const velLabel  = v0Unit   === 'fps' ? 'fps' : 'm/s';
  const uLabel    = outUnit;

  document.getElementById('trajHead').innerHTML = [
    `<th>${distLabel}</th>`,
    `<th>${velLabel}</th>`,
    wtGr !== null ? '<th>E (J)</th>' : '',
    '<th>TOF (s)</th>',
    `<th>Drop (mm)</th>`,
    `<th>Drop (${uLabel})</th>`,
    `<th>Wind (mm)</th>`,
    `<th>Wind (${uLabel})</th>`,
  ].join('');

  // Find first subsonic distance
  const subPt = pts.find(p => p.v < SUBSONIC);

  const rows = pts.map(p => {
    const distDisp = zeroUnit === 'yd'
      ? (p.x / 0.9144).toFixed(0)
      : p.x.toFixed(0);
    const vel      = v0Unit === 'fps' ? p.v / 0.3048 : p.v;
    const isZero   = Math.abs(p.x - zero_m) < 0.5;
    const isSub    = p.v < SUBSONIC;

    // Drop: y is negative when bullet is below LOS.
    // We show how much bullet is BELOW (positive = drop below), correction sign = hold up (+).
    const dropMm   = p.y * 1000;        // negative = below LOS
    const dropCorr = angFromMm(p.x, -dropMm, outUnit); // positive = aim up / dial up

    // Wind: z is negative when bullet drifts left (wind from right).
    const windMm   = p.z * 1000;
    const windAbs  = Math.abs(windMm);
    const windDir  = windMm < -0.5 ? 'L' : windMm > 0.5 ? 'R' : '';
    const windCorr = windAbs > 0.5 ? angFromMm(p.x, windAbs, outUnit) : null;

    // Energy
    let eTd = '';
    if (wtGr !== null) {
      const mass_kg = wtGr / 15432.4;
      const ke      = 0.5 * mass_kg * p.v * p.v;
      eTd = `<td>${ke.toFixed(0)}</td>`;
    }

    return `<tr class="${isZero ? 'zero-row' : ''}${isSub ? ' subsonic' : ''}">
      <td>${distDisp}${isZero ? ' ★' : ''}</td>
      <td>${vel.toFixed(0)}</td>
      ${eTd}
      <td>${p.t.toFixed(3)}</td>
      <td>${fmt(dropMm, 0, true)}</td>
      <td>${fmt(dropCorr, 2, true)}</td>
      <td>${windAbs < 0.5 ? '—' : fmt(windAbs, 0) + ' ' + windDir}</td>
      <td>${windAbs < 0.5 || windCorr === null ? '—' : fmt(windCorr, 2) + ' ' + windDir}</td>
    </tr>`;
  });

  document.getElementById('trajBody').innerHTML = rows.join('');

  // Summary line
  document.getElementById('resultsSummary').innerHTML =
    `ρ = ${rho.toFixed(4)} kg/m³ | Speed of sound = ${sound.toFixed(1)} m/s` +
    (subPt ? ` | Subsonic at ~${subPt.x.toFixed(0)} m` : '');
}

// ── Keep distance step unit label in sync ────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
  const zeroUnitSel = document.getElementById('zeroUnit');
  const stepLabel   = document.getElementById('distStepUnit');
  if (zeroUnitSel && stepLabel) {
    zeroUnitSel.addEventListener('change', () => {
      stepLabel.textContent = zeroUnitSel.value;
    });
  }
});
