/* global React, WeatherData, Astronomy, fmtNum */
const { useEffect: _useEffEx, useRef: _useRefEx } = React;

// =========================================================
// SunWidget — real astronomy from lat/lon + date
// =========================================================
// Moon phase rendered as the lit portion atop a shadow disk. Phase ∈ [0,1],
// where 0 = new, 0.25 = first quarter (right half lit), 0.5 = full, 0.75 =
// last quarter (left half lit). The lit shape is bounded by one half of the
// moon's limb and one half of the terminator ellipse — sweep flags pick the
// correct side / bulge so all 8 phases render accurately with a single path.
function MoonIcon({ phase, size = 24 }) {
  const r = size / 2 - 1;
  const cx = size / 2, cy = size / 2;
  // Illuminated fraction is the natural quantity for the terminator width:
  // when illum=0.5 the terminator collapses to a vertical line (rxTerm=0),
  // at illum=0 or 1 it coincides with the moon's limb (rxTerm=r).
  const illum = (1 - Math.cos(2 * Math.PI * phase)) / 2;
  const rxTerm = r * Math.abs(1 - 2 * illum);
  const litRight = phase < 0.5;   // waxing → right limb lit; waning → left
  const crescent = illum < 0.5;
  // outerSweep: which side of the moon's limb forms the lit boundary.
  // termSweep:  which side the terminator ellipse bulges. For a crescent it
  // must retrace the SAME side as the lit limb (so lit area → 0 at new moon,
  // where rxTerm → r and the terminator coincides with the limb); for a
  // gibbous it bulges the OPPOSITE way (so the disk fills to full moon).
  const outerSweep = litRight ? 1 : 0;
  const termSweep  = litRight
    ? (crescent ? 0 : 1)
    : (crescent ? 1 : 0);
  const litPath = `M ${cx} ${cy - r}
                   A ${r}      ${r} 0 0 ${outerSweep} ${cx} ${cy + r}
                   A ${rxTerm} ${r} 0 0 ${termSweep}  ${cx} ${cy - r} Z`;
  return (
    <svg viewBox={`0 0 ${size} ${size}`} width={size} height={size} aria-hidden>
      <circle cx={cx} cy={cy} r={r} fill="#1c2438"/>
      <path d={litPath} fill="#e8e8f0"/>
      <circle cx={cx} cy={cy} r={r} fill="none" stroke="rgba(255,255,255,0.25)" strokeWidth="0.5"/>
    </svg>
  );
}

// Den a noc: one chart of how high the Sun (and the Moon) stand above the
// horizon over today, 0–24 h. Daylight and civil twilight are tinted, the
// horizon is the reference line, „teď“ sits on both curves. Below it the
// numbers people look for: sunrise / sunset with dawn / dusk, day length and
// its change since yesterday, then the Moon (phase, rise / set, next full or
// new moon). Drawn in real pixels (useBox); hover = ChartHover layer.
const SUN_REFRACT = 0.833;   // sun curve crosses 0 exactly at sunrise / sunset
const czDays = (n) => (n === 1 ? 'den' : n >= 2 && n <= 4 ? 'dny' : 'dní');
function inDays(target, now) {
  const a = new Date(now); a.setHours(0, 0, 0, 0);
  const b = new Date(target); b.setHours(0, 0, 0, 0);
  const n = Math.round((b - a) / 86400000);
  return n <= 0 ? 'dnes' : n === 1 ? 'zítra' : `za ${n} ${czDays(n)}`;
}

window.SunWidget = function SunWidget({ coords, config = {} }) {
  const c = coords || Astronomy.getCoords();
  const showMoon = config.showMoon !== false;
  const showCountdown = config.showCountdown !== false;
  const plotRef = _useRefEx(null);
  const box = window.useBox(plotRef);
  // the „teď“ marker and the countdown move every minute
  const [, setTick] = React.useState(0);
  _useEffEx(() => { const t = setInterval(() => setTick(x => x + 1), 60000); return () => clearInterval(t); }, []);

  const now = new Date();
  const dayStart = new Date(now); dayStart.setHours(0, 0, 0, 0);
  const dayEnd = new Date(dayStart); dayEnd.setDate(dayEnd.getDate() + 1);   // 23 / 25 h on DST days
  const t0 = dayStart.getTime(), t1 = dayEnd.getTime();

  // Today's curves, every 10 min (computed once a day).
  const curve = React.useMemo(() => {
    const out = [];
    for (let t = t0; t <= t1; t += 600000) {
      const d = new Date(t);
      out.push({ t, sun: Astronomy.sunAltitude(d, c.lat, c.lon) + SUN_REFRACT, moon: Astronomy.moonAltitude(d, c.lat, c.lon) });
    }
    return out;
  }, [t0, c.lat, c.lon]);

  const { sunrise, sunset, polar } = Astronomy.sunTimes(now, c.lat, c.lon);
  const civil = Astronomy.sunTimes(now, c.lat, c.lon, 96);
  const sunNow = Astronomy.sunAltitude(now, c.lat, c.lon) + SUN_REFRACT;
  const moonNow = Astronomy.moonAltitude(now, c.lat, c.lon);
  const isDay = sunNow > 0;
  const light = isDay ? 'den' : sunNow > -6 + SUN_REFRACT ? 'šero' : 'noc';

  const dayLen = sunrise && sunset ? Math.round((sunset - sunrise) / 60000) : null;
  const ySun = Astronomy.sunTimes(new Date(now.getTime() - 86400000), c.lat, c.lon);
  const yLen = ySun.sunrise && ySun.sunset ? Math.round((ySun.sunset - ySun.sunrise) / 60000) : null;
  const lenDiff = dayLen != null && yLen != null ? dayLen - yLen : null;

  // Next sun event: before sunrise → sunrise, by day → sunset, after → tomorrow.
  const tomorrowSun = Astronomy.sunTimes(new Date(now.getTime() + 86400000), c.lat, c.lon);
  let nextKind = null, nextAt = null;
  if (sunrise && now < sunrise) { nextKind = 'sunrise'; nextAt = sunrise; }
  else if (sunset && now < sunset) { nextKind = 'sunset'; nextAt = sunset; }
  else if (tomorrowSun.sunrise) { nextKind = 'sunrise'; nextAt = tomorrowSun.sunrise; }
  const mins = nextAt ? Math.round((nextAt - now) / 60000) : null;
  const nextTxt = nextAt == null ? null
    : `${nextKind === 'sunrise' ? 'východ' : 'západ'} ${showCountdown && mins >= 1
        ? `za ${Astronomy.fmtMinutes(mins)}`
        : `${nextAt.getDate() !== now.getDate() ? 'zítra ' : 'v '}${Astronomy.fmtTime(nextAt)}`}`;

  const phase = Astronomy.moonPhase(now);
  const illum = Astronomy.illumFraction(phase);
  const { moonrise, moonset } = Astronomy.moonTimes(now, c.lat, c.lon);
  const mp = Astronomy.nextMoonPhases(now);
  const nextMoon = mp.full < mp.new ? { name: 'úplněk', at: mp.full } : { name: 'nov', at: mp.new };
  const phaseLabel = Astronomy.phaseName(phase);

  let svg = null;
  if (box && box.w > 80 && box.h > 50) {
    const W = box.w, H = box.h;
    const fs = window.fontScale();
    const padL = 4, padR = 4, padT = Math.round(16 * fs), padB = Math.round(18 * fs);
    const pW = W - padL - padR, pH = H - padT - padB;
    const x = (t) => padL + ((t - t0) / (t1 - t0)) * pW;
    const maxSun = Math.max(...curve.map(p => p.sun));
    const maxMoon = showMoon ? Math.max(...curve.map(p => p.moon)) : -90;
    const lo = -20, hi = Math.max(20, Math.max(maxSun, maxMoon) + 8);
    const y = (v) => padT + ((hi - v) / (hi - lo)) * pH;
    const yH = y(0);
    const pts = (k) => curve.map(p => [x(p.t), y(p[k])]);
    const line = (a) => a.map((p, i) => `${i ? 'L' : 'M'}${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(' ');
    const sunPts = pts('sun');
    const sunArea = `${line(curve.map(p => [x(p.t), y(Math.max(0, p.sun))]))} L ${x(t1)} ${yH} L ${x(t0)} ${yH} Z`;
    // daylight / twilight bands from the curve itself (works for polar days too)
    const bands = [];
    const cls = (v) => (v > 0 ? 'day' : v > -6 + SUN_REFRACT ? 'twi' : null);
    curve.forEach((p, i) => {
      if (!i) return;
      const k = cls((p.sun + curve[i - 1].sun) / 2);
      const last = bands[bands.length - 1];
      if (last && last.k === k && last.b === i - 1) last.b = i;
      else bands.push({ k, a: i - 1, b: i });
    });
    const peakI = curve.findIndex(p => p.sun === maxSun);
    const peak = curve[peakI];
    const nowX = x(now.getTime());
    const hours = [6, 12, 18].map(h => { const d = new Date(dayStart); d.setHours(h); return d.getTime(); });
    const vis = (v) => v >= lo;
    svg = (
      <>
        <svg width={W} height={H} viewBox={`0 0 ${W} ${H}`} className="sw-svg">
          <defs>
            <linearGradient id="swSunFill" x1="0" x2="0" y1={y(Math.max(maxSun, 1))} y2={yH} gradientUnits="userSpaceOnUse">
              <stop offset="0" stopColor="var(--sw-sun)" stopOpacity=".38" />
              <stop offset="1" stopColor="var(--sw-sun)" stopOpacity=".04" />
            </linearGradient>
            <clipPath id="swAbove"><rect x="0" y="0" width={W} height={yH} /></clipPath>
            <clipPath id="swBelow"><rect x="0" y={yH} width={W} height={Math.max(0, padT + pH - yH)} /></clipPath>
            <clipPath id="swPlot"><rect x={padL} y={padT - 12} width={pW} height={pH + 12} /></clipPath>
          </defs>
          {bands.filter(b => b.k).map((b, i) => (
            <rect key={i} x={x(curve[b.a].t)} y={padT} width={x(curve[b.b].t) - x(curve[b.a].t)} height={pH} className={`sw-band ${b.k}`} />
          ))}
          {hours.map(t => <line key={t} x1={x(t)} x2={x(t)} y1={padT} y2={padT + pH} className="sw-grid" />)}
          <line x1={padL} x2={W - padR} y1={yH} y2={yH} className="sw-horizon" />
          <text x={W - padR - 2} y={yH + 12} textAnchor="end" className="sw-axis">obzor</text>
          <g clipPath="url(#swPlot)">
            {maxSun > 0 && <path d={sunArea} fill="url(#swSunFill)" />}
            {showMoon && <>
              <path d={line(pts('moon'))} className="sw-moon-line" clipPath="url(#swAbove)" />
              <path d={line(pts('moon'))} className="sw-moon-line under" clipPath="url(#swBelow)" />
            </>}
            <path d={line(sunPts)} className="sw-sun-line" clipPath="url(#swAbove)" />
            <path d={line(sunPts)} className="sw-sun-line under" clipPath="url(#swBelow)" />
          </g>
          {maxSun > 0 && peak && (
            <text x={Math.max(padL + 24, Math.min(W - padR - 24, x(peak.t)))} y={y(maxSun) - 6} textAnchor="middle" className="sw-peak">
              {fmtNum(maxSun, 0)}°
            </text>
          )}
          <line x1={nowX} x2={nowX} y1={padT - 2} y2={padT + pH} className="sw-now" />
          {showMoon && vis(moonNow) && (
            <g transform={`translate(${nowX - 8} ${y(moonNow) - 8})`} className="sw-moon-dot"><MoonIcon phase={phase} size={16} /></g>
          )}
          {vis(sunNow) && <circle cx={nowX} cy={y(sunNow)} r="5.5" className={`sw-sun-dot ${isDay ? '' : 'under'}`} />}
          <text x={padL} y={H - 4} className="sw-axis">0</text>
          {hours.map(t => <text key={t} x={x(t)} y={H - 4} textAnchor="middle" className="sw-axis">{new Date(t).getHours()}</text>)}
          <text x={W - padR} y={H - 4} textAnchor="end" className="sw-axis">24</text>
        </svg>
        <window.ChartHover W={W} H={H} top={padT} bottom={padT + pH}
          xs={curve.map(p => x(p.t))}
          marks={(i) => [
            vis(curve[i].sun) && { y: y(curve[i].sun), color: 'var(--sw-sun)' },
            showMoon && vis(curve[i].moon) && { y: y(curve[i].moon), color: 'var(--sw-moon)' },
          ]}
          tip={(i) => {
            const p = curve[i];
            const l = p.sun > 0 ? 'den' : p.sun > -6 + SUN_REFRACT ? 'šero' : 'noc';
            const alt = (v) => (v > 0 ? `${fmtNum(v, 0)}° nad obzorem` : 'pod obzorem');
            return (<>
              <div className="tip-time">{window.fmtTipTime(p.t)}<small>{l}</small></div>
              <div className="tip-row"><span className="k"><i className="tip-sw" style={{ background: 'var(--sw-sun)' }} />Slunce</span><strong>{alt(p.sun)}</strong></div>
              {showMoon && <div className="tip-row"><span className="k"><i className="tip-sw" style={{ background: 'var(--sw-moon)' }} />Měsíc</span><strong>{alt(p.moon)}</strong></div>}
            </>);
          }} />
      </>
    );
  }

  const hm = (min) => `${Math.floor(min / 60)} h ${String(min % 60).padStart(2, '0')} min`;
  return (
    <div className="sw">
      <div className="sw-head">
        <span className={`sw-state ${isDay ? 'day' : 'night'}`}>
          {isDay ? <i className="bi bi-sun-fill" aria-hidden="true"></i> : <MoonIcon phase={phase} size={16} />}
          <b>{light === 'den' ? 'Den' : light === 'šero' ? 'Šero' : 'Noc'}</b>
        </span>
        {nextTxt && <span className="sw-next">{nextTxt}</span>}
        <span className="sw-legend" aria-hidden="true">
          <span><i className="sw-key sun"></i>Slunce</span>
          {showMoon && <span><i className="sw-key moon"></i>Měsíc</span>}
        </span>
      </div>
      <div className="sw-plot" ref={plotRef}>{svg}</div>
      <div className="sw-stats">
        <div className={nextKind === 'sunrise' && nextAt === sunrise ? 'is-next' : ''}>
          <small>Východ</small><b>{Astronomy.fmtTime(sunrise)}</b>
          <span>svítání {Astronomy.fmtTime(civil.sunrise)}</span>
        </div>
        <div className={nextKind === 'sunset' ? 'is-next' : ''}>
          <small>Západ</small><b>{Astronomy.fmtTime(sunset)}</b>
          <span>soumrak {Astronomy.fmtTime(civil.sunset)}</span>
        </div>
        <div className="sw-len">
          <small>Délka dne</small><b>{dayLen != null ? hm(dayLen) : polar === 'day' ? 'polární den' : 'polární noc'}</b>
          <span>{lenDiff == null ? ' ' : lenDiff === 0 ? 'jako včera' : `${lenDiff > 0 ? '+' : '−'}${Math.abs(lenDiff)} min proti včerejšku`}</span>
        </div>
      </div>
      {showMoon && (
        <div className="sw-moon">
          <MoonIcon phase={phase} size={30} />
          <div className="sw-moon-main">
            <b>{phaseLabel.charAt(0).toUpperCase() + phaseLabel.slice(1)}</b>
            <span>{Math.round(illum * 100)} % osvětleno · {nextMoon.name} {inDays(nextMoon.at, now)}</span>
          </div>
          <div className="sw-moon-rs">
            <span title="Východ Měsíce"><i className="bi bi-arrow-up-short" aria-hidden="true"></i>{Astronomy.fmtTime(moonrise)}</span>
            <span title="Západ Měsíce"><i className="bi bi-arrow-down-short" aria-hidden="true"></i>{Astronomy.fmtTime(moonset)}</span>
          </div>
        </div>
      )}
    </div>
  );
};

// =========================================================
// AirWidget — real station data (UV, solar, rainfall, dew point)
// All fields fall back gracefully if station is unavailable.
// =========================================================
window.AirWidget = function AirWidget({ stationLatest, hourly }) {
  const h = hourly && hourly[0];
  const s = stationLatest || {};

  // UV — only show if station has it
  const uv = s.UVindex;
  const uvLabel = uv == null ? '—'
    : uv < 3 ? 'nízký' : uv < 6 ? 'střední' : uv < 8 ? 'vysoký' : uv < 11 ? 'velmi vysoký' : 'extrémní';

  // Solar irradiance
  const solar = s.SlunZareni;

  // Dew point — Magnus formula, needs T and RH (both from station preferred)
  const T = s.TeplotaVnejsi != null ? s.TeplotaVnejsi : (h ? h.temp : null);
  const RH = s.VlhkostVnejsi != null ? s.VlhkostVnejsi : (h ? h.humidity : null);
  let dew = null;
  if (T != null && RH != null && RH > 0) {
    const a = 17.625, b = 243.04;
    const alpha = Math.log(RH / 100) + (a * T) / (b + T);
    dew = (b * alpha) / (a - alpha);
  }

  // Cloud cover from forecast (station doesn't measure it)
  const cloud = h && h.cloud;

  return (
    <div className="kpi-grid">
      <div className="kpi">
        <div className="label"><i className="bi bi-brightness-high-fill"></i>UV index</div>
        <div className="value">{uv != null ? window.czNum(uv, 1) : '—'}{uv != null && <small> {uvLabel}</small>}</div>
        {uv == null && <div className="trend muted">stanice neměří</div>}
      </div>
      <div className="kpi">
        <div className="label"><i className="bi bi-sun"></i>Sluneční záření</div>
        <div className="value">{solar != null ? Math.round(solar) : '—'}<small> W/m²</small></div>
        {solar == null && <div className="trend muted">stanice neměří</div>}
      </div>
      <div className="kpi">
        <div className="label"><i className="bi bi-droplet"></i>Rosný bod</div>
        <div className="value">{dew != null ? window.czNum(dew, 1) : '—'}<small>°C</small></div>
        {RH != null && <div className="trend">{Math.round(RH)}% vlhkost</div>}
      </div>
      <div className="kpi">
        <div className="label"><i className="bi bi-cloud"></i>Oblačnost</div>
        <div className="value">{cloud != null ? Math.round(cloud) : '—'}<small>%</small></div>
        {cloud != null && <div className="trend">{cloud < 30 ? 'jasno' : cloud < 70 ? 'polojasno' : 'zataženo'}</div>}
      </div>
    </div>
  );
};

// =========================================================
// AlertsWidget — derived from forecast (real data)
// =========================================================
window.AlertsWidget = function AlertsWidget({ hourly }) {
  if (!hourly) return null;
  const rain = hourly.find(h => h.precip > 0.5);
  const gust = hourly.find(h => h.gust > 12);
  return (
    <div style={{display:'flex', flexDirection:'column', gap:8}}>
      {rain && (
        <div className="auto-info" style={{borderColor:'rgba(79,195,247,0.3)', background:'linear-gradient(135deg, rgba(79,195,247,0.10), transparent 70%)'}}>
          <i className="bi bi-cloud-rain-heavy" style={{color:'var(--rain)'}}></i>
          <div>
            <strong>Déšť za {Math.round((rain.time - new Date()) / 3600000)} h</strong> — {WeatherData.WD_SHORT[rain.time.getDay()]} {String(rain.time.getHours()).padStart(2,'0')}:00, {window.czNum(rain.precip, 1)} mm
          </div>
        </div>
      )}
      {gust && (
        <div className="auto-info" style={{borderColor:'rgba(255,167,38,0.3)', background:'linear-gradient(135deg, rgba(255,167,38,0.10), transparent 70%)'}}>
          <i className="bi bi-wind" style={{color:'var(--warn)'}}></i>
          <div>
            <strong>Silný vítr</strong> — nárazy až {(window.Units ? window.Units.wind(gust.gust) : gust.gust).toFixed(0)} {window.Units ? window.Units.windLabel() : 'm/s'} {WeatherData.WD_SHORT[gust.time.getDay()]} {String(gust.time.getHours()).padStart(2,'0')}:00
          </div>
        </div>
      )}
      {!rain && !gust && (
        <div className="auto-info" style={{borderColor:'rgba(93,232,181,0.22)'}}>
          <i className="bi bi-check2-circle" style={{color:'var(--good)'}}></i>
          <div>Žádná varování — klidné počasí v následujících 72 h.</div>
        </div>
      )}
    </div>
  );
};

// =========================================================
// WindWidget — side-by-side station vs forecast comparison, with an
// animated particle flow drifting across the whole widget background.
//
// Animace (převzato z pocasimeteo_MS, windflow.jsx): canvas s částicemi,
// které nesou krátkou stopu (trail) a driftují ve směru, kam vítr fouká.
// Theme-aware — barva = --accent. ZACHOVÁVÁ PRŮHLEDNOST WIDGETU: místo
// painting poloprůhledného pozadí (jako originál) každý frame "odmaže"
// zlomek alfa-kanálu přes globalCompositeOperation = 'destination-out',
// takže stopy mizí do průhledna a karta zůstává see-through.
// =========================================================

// Canvas particle flow. `speed` v m/s ovládá rychlost driftu, `direction`
// je VitrSmer (úhel, ZE kterého vítr fouká — particles tedy letí +180°).
// `mult` = uživatelský násobek rychlosti animace (windFlowSpeed).
window.WindFlow = function WindFlow({ speed, direction, mult = 1 }) {
  const canvasRef = _useRefEx(null);
  const wrapRef = _useRefEx(null);

  _useEffEx(() => {
    const canvas = canvasRef.current;
    const wrap = wrapRef.current;
    if (!canvas || !wrap) return;
    // NOTE: deliberately NOT gated on prefers-reduced-motion. The flow is an
    // opt-in product feature (toggle "Animované proudění" in Tweaks); honoring
    // the OS reduce-motion flag here is what made it vanish on Windows desktops
    // with "Animation effects" off while still showing on phones. Users who
    // want it off use the toggle.
    const ctx = canvas.getContext('2d');
    const dpr = window.devicePixelRatio || 1;
    let W = wrap.clientWidth, H = wrap.clientHeight;

    const resize = () => {
      W = wrap.clientWidth; H = wrap.clientHeight;
      if (W < 2 || H < 2) return;       // parent not laid out yet
      canvas.width = W * dpr; canvas.height = H * dpr;
      canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };
    resize();
    const ro = new ResizeObserver(resize);
    ro.observe(wrap);

    const TRAIL = 16;
    const N = Math.max(22, Math.floor((W || 200) / 6));
    const particles = [];
    for (let i = 0; i < N; i++) {
      const x = Math.random() * W, y = Math.random() * H;
      particles.push({
        x, y,
        trail: Array.from({ length: TRAIL }, () => [x, y]),
        life: Math.random() * 120 + 40,
        maxLife: 160,
        v: 0.6 + Math.random() * 0.5,
        size: 0.5 + Math.random() * 0.7,
      });
    }

    // Meteorologická konvence: direction = odkud vítr fouká. Pro flow chceme
    // "kam jde" → +180°.
    const dirRad = ((direction + 180) % 360) * Math.PI / 180;
    const dx = Math.sin(dirRad), dy = -Math.cos(dirRad);

    let alive = true;
    let t = 0;
    function frame() {
      if (!alive) return;
      t += 1;
      const styles = getComputedStyle(document.documentElement);
      const accent = styles.getPropertyValue('--accent').trim() || '#0ea5e9';

      // Fade-to-transparent: smaže ~12 % alfa každý frame (motion blur)
      // místo malování pozadí → widget zůstává průhledný.
      ctx.globalCompositeOperation = 'destination-out';
      ctx.fillStyle = 'rgba(0,0,0,0.12)';
      ctx.fillRect(0, 0, W, H);
      ctx.globalCompositeOperation = 'source-over';

      // Drift speed scales (near-)linearly with the real wind speed so calm
      // air visibly crawls and a gale clearly races. Small constant keeps it
      // from fully freezing in dead calm (a frozen canvas reads as broken).
      // `mult` is the optional global Tweaks fine-tune, not the main driver.
      const baseSpeed = (0.3 + speed * 0.24) * mult;
      const gustBoost = 1 + 0.18 * Math.sin(t * 0.04);

      ctx.lineCap = 'round';
      ctx.lineJoin = 'round';
      ctx.strokeStyle = accent;

      for (const p of particles) {
        const localSpeed = baseSpeed * p.v * gustBoost;
        const wobble = Math.sin(p.y * 0.012 + p.x * 0.008 + t * 0.02) * 0.18;
        p.trail.unshift([p.x, p.y]);
        if (p.trail.length > TRAIL) p.trail.pop();
        p.x += dx * localSpeed + (-dy) * wobble;
        p.y += dy * localSpeed + (dx) * wobble;
        p.life -= 1;

        if (p.life <= 0 || p.x < -10 || p.x > W + 10 || p.y < -10 || p.y > H + 10) {
          let nx = Math.random() * W - dx * W * 0.5;
          let ny = Math.random() * H - dy * H * 0.5;
          if (nx < 0) nx += W; if (ny < 0) ny += H;
          if (nx > W) nx -= W; if (ny > H) ny -= H;
          p.x = nx; p.y = ny;
          p.trail = Array.from({ length: TRAIL }, () => [nx, ny]);
          p.life = 100 + Math.random() * 80;
          p.maxLife = p.life;
        }

        const lifeAlpha = Math.min(1, p.life / 60, (p.maxLife - p.life) / 30);
        for (let k = 0; k < p.trail.length - 1; k++) {
          const [x1, y1] = p.trail[k];
          const [x2, y2] = p.trail[k + 1];
          const tNorm = k / (p.trail.length - 1);
          const a = (1 - tNorm) * 0.85 * lifeAlpha;
          if (a < 0.02) break;
          ctx.globalAlpha = a;
          ctx.lineWidth = (1 - tNorm * 0.6) * (p.size + 0.5);
          ctx.beginPath();
          ctx.moveTo(x1, y1);
          ctx.lineTo(x2, y2);
          ctx.stroke();
        }
      }
      ctx.globalAlpha = 1;
      raf = requestAnimationFrame(frame);
    }
    let raf = requestAnimationFrame(frame);
    return () => { alive = false; cancelAnimationFrame(raf); ro.disconnect(); };
  }, [speed, direction, mult]);

  return (
    <div ref={wrapRef} className="wind-flow-canvas-wrap" aria-hidden>
      <canvas ref={canvasRef} />
    </div>
  );
};

// Beaufort scale from raw m/s (station/forecast wind is always m/s before
// unit conversion). Czech names per ČHMÚ usage.
function beaufortOf(ms) {
  if (ms == null || isNaN(ms)) return null;
  const t = [
    [0.5, 0, 'Bezvětří'], [1.6, 1, 'Vánek'], [3.4, 2, 'Slabý vítr'],
    [5.5, 3, 'Mírný vítr'], [8.0, 4, 'Dosti čerstvý'], [10.8, 5, 'Čerstvý vítr'],
    [13.9, 6, 'Silný vítr'], [17.2, 7, 'Prudký vítr'], [20.8, 8, 'Bouřlivý vítr'],
    [24.5, 9, 'Vichřice'], [28.5, 10, 'Silná vichřice'], [32.7, 11, 'Mohutná vichřice'],
  ];
  for (const [lim, n, name] of t) if (ms < lim) return { n, name };
  return { n: 12, name: 'Orkán' };
}

// Wind shaft spanning the whole dial ("přes kříž") with one clear arrowhead.
// Points up before rotation; caller rotates by the bearing (+180 → flies
// downwind, same sense as the particle flow). Station = solid accent arrow;
// forecast = dashed shaft with a hollow head, so the two differ by shape too.
function WindArrow({ size, scale = 1, color = 'var(--accent)', ghost = false }) {
  const cx = size / 2, cy = size / 2, r = size / 2 - 9;
  const tip  = cy - r * 0.80 * scale;          // arrowhead point (downwind end)
  const tail = cy + r * 0.74 * scale;          // opposite end
  const ah   = Math.max(4, r * 0.15 * scale);  // arrowhead half-width
  const al   = Math.max(7, r * 0.24 * scale);  // arrowhead length
  const sw   = Math.max(1.75, size * 0.016);   // shaft thickness
  return (
    <g stroke={color} fill={ghost ? 'none' : color} strokeLinecap="round" strokeLinejoin="round">
      <line x1={cx} y1={tail} x2={cx} y2={tip + al * 0.55} strokeWidth={sw} strokeDasharray={ghost ? '4 4' : undefined}/>
      <path d={`M ${cx} ${tip} L ${cx - ah} ${tip + al} L ${cx + ah} ${tip + al} Z`}
            stroke={ghost ? color : 'none'} strokeWidth={ghost ? sw * 0.85 : undefined}/>
    </g>
  );
}

// Minimal wind rose — fine ticks (majors @45°), 8-point labels, station arrow
// on top of the forecast one.
function WindCompass({ direction, forecastDir, size = 150, calm = false }) {
  const cx = size / 2, cy = size / 2, r = size / 2 - 9;
  const dirs = [
    { l:'S', a:0 }, { l:'SV', a:45 }, { l:'V', a:90 }, { l:'JV', a:135 },
    { l:'J', a:180 }, { l:'JZ', a:225 }, { l:'Z', a:270 }, { l:'SZ', a:315 },
  ];
  const ticks = [];
  for (let i = 0; i < 36; i++) ticks.push(i * 10);
  const labelR = r - size * 0.10;
  return (
    <svg viewBox={`0 0 ${size} ${size}`} style={{ display: 'block' }}>
      <circle cx={cx} cy={cy} r={r} fill="none" stroke="var(--border)" strokeWidth="1" opacity="0.7"/>
      <circle cx={cx} cy={cy} r={r - size * 0.055} fill="none"
              stroke="var(--border)" strokeDasharray="1 6" opacity="0.3"/>
      {ticks.map((a, i) => {
        const major = a % 45 === 0;
        const med = a % 15 === 0 && !major;
        const len = major ? size * 0.075 : med ? size * 0.045 : size * 0.028;
        const rad = (a - 90) * Math.PI / 180;
        const x1 = cx + Math.cos(rad) * (r - 1);
        const y1 = cy + Math.sin(rad) * (r - 1);
        const x2 = cx + Math.cos(rad) * (r - len);
        const y2 = cy + Math.sin(rad) * (r - len);
        return <line key={i} x1={x1} y1={y1} x2={x2} y2={y2} stroke="var(--muted)"
                     strokeWidth={major ? 1.1 : 0.7} opacity={major ? 0.55 : med ? 0.3 : 0.16}/>;
      })}
      {dirs.map((d) => {
        const rad = (d.a - 90) * Math.PI / 180;
        const x = cx + Math.cos(rad) * labelR;
        const y = cy + Math.sin(rad) * labelR + size * 0.033;
        const cardinal = d.l.length === 1;
        return <text key={d.l} x={x} y={y} textAnchor="middle"
                     fontSize={cardinal ? size * 0.092 : size * 0.064}
                     fontWeight={cardinal ? 600 : 500}
                     letterSpacing="0.04em"
                     fill={d.l === 'S' ? 'var(--accent)' : 'var(--muted)'}
                     opacity={cardinal ? 0.9 : 0.55}
                     fontFamily="var(--mono)">{d.l}</text>;
      })}
      {forecastDir != null && (
        <g transform={`rotate(${forecastDir + 180} ${cx} ${cy})`}
           style={{ transition: 'transform .8s cubic-bezier(.4,0,.2,1)' }}>
          <WindArrow size={size} scale={0.92} color="var(--muted)" ghost/>
        </g>
      )}
      {/* in calm air the direction means nothing — the arrow only hints */}
      <g transform={`rotate(${direction + 180} ${cx} ${cy})`} opacity={calm ? 0.22 : 1}
         style={{ transition: 'transform .8s cubic-bezier(.4,0,.2,1)' }}>
        <WindArrow size={size} color="var(--accent)"/>
      </g>
      <circle cx={cx} cy={cy} r={size * 0.022} fill="var(--card-strong)" stroke="var(--accent)" strokeWidth="1.5"/>
    </svg>
  );
}

// Beaufort → colour of the chip (calm grey → green → amber → orange → red).
const BFT_COLOR = ['#94a3b8', '#94a3b8', '#22c55e', '#22c55e', '#14b8a6', '#eab308', '#f59e0b', '#f97316', '#ef4444', '#ef4444', '#dc2626', '#b91c1c', '#991b1b'];

// =========================================================
// Vítr — what the station measures now (speed, gusts, Beaufort, direction),
// the compass, and „Stanice × předpověď“: a one-line verdict, paired bars on
// one shared scale for wind and gusts with the difference, and the direction
// difference. The station side is the last hour (mean wind, max gust) — the
// forecast is an hourly value, so a single 5-min sample would be unfair.
// =========================================================
window.WindWidget = function WindWidget({ hourly, stationLatest, station, flow, flowSpeed, config = {} }) {
  const s = stationLatest;
  const h = hourly && hourly[0];
  const showCompass = config.showCompass !== false;
  const showCompare = config.showCompare !== false;

  // Live station readings (!= null lets a calm 0 through).
  const sSpeed = s?.Vitr        != null ? s.Vitr        : null;
  const sGust  = s?.VitrNarazy  != null ? s.VitrNarazy  : null;
  const sDeg   = s?.VitrSmer    != null ? s.VitrSmer    : null;
  // Forecast — this hour.
  const fSpeed = h?.wind    != null ? h.wind    : null;
  const fGust  = h?.gust    != null ? h.gust    : null;
  const fDeg   = h?.windDeg != null ? h.windDeg : null;

  // Station over the last hour (falls back to the latest sample).
  const cut = Date.now() - 3600e3;
  const recent = (station?.measurements || []).filter(m => m.time && m.time.getTime() >= cut);
  const nums = (k) => recent.map(m => m[k]).filter(v => v != null && !isNaN(v));
  const hw = nums('wind'), hg = nums('gust');
  const hourWind = hw.length ? hw.reduce((a, b) => a + b, 0) / hw.length : sSpeed;
  const hourGust = hg.length ? Math.max(...hg) : sGust;

  const headSpeed = sSpeed != null ? sSpeed : fSpeed;
  const headGust  = sGust  != null ? sGust  : fGust;
  const displayDeg   = sDeg != null ? sDeg : (fDeg != null ? fDeg : 0);
  const displaySpeed = headSpeed != null ? headSpeed : 0;
  const speedForFlow = Math.max(0.6, displaySpeed);
  const mult = Math.max(0.2, Math.min(3, flowSpeed || 1));
  const dirName = (d) => (window.windDir ? window.windDir(d) : '');
  const calm = headSpeed != null && headSpeed < 0.3;
  const bft = beaufortOf(displaySpeed);

  const wUnit = window.Units ? window.Units.windLabel() : 'm/s';
  const wConv = (v) => (v == null ? null : (window.Units ? window.Units.wind(v) : v));
  const f1 = (v) => (v == null ? '—' : fmtNum(wConv(v), 1));

  // ── comparison ──
  const hasStation = hourWind != null;
  const canCompare = showCompare && hasStation && fSpeed != null;
  const dW = canCompare ? hourWind - fSpeed : null;
  const dG = canCompare && hourGust != null && fGust != null ? hourGust - fGust : null;
  const lvl = (d, ok, warn) => (d == null ? '' : Math.abs(d) <= ok ? 'good' : Math.abs(d) <= warn ? 'warn' : 'bad');
  const dirCalm = hourWind != null && hourWind < 1;
  const angle = canCompare && sDeg != null && fDeg != null && !dirCalm
    ? Math.round(Math.min(Math.abs(sDeg - fDeg) % 360, 360 - (Math.abs(sDeg - fDeg) % 360))) : null;
  const scaleMax = Math.max(4, ...[hourWind, fSpeed, hourGust, fGust].filter(v => v != null)) * 1.12;
  // bar length on one scale, leaving room for the value written after the bar
  // (it must never run into the „rozdíl“ badge in narrow tiles)
  const pct = (v) => `calc((100% - 36px) * ${Math.max(0.02, Math.min(1, (v || 0) / scaleMax)).toFixed(4)})`;
  const sign = (d) => `${d > 0 ? '+' : d < 0 ? '−' : '±'}${fmtNum(Math.abs(wConv(d)), 1)}`;
  let verdict = null, vcls = '';
  if (canCompare) {
    vcls = lvl(dW, 1, 2.5);
    verdict = Math.abs(dW) <= 1 ? 'Vítr je v souladu s předpovědí'
      : dW > 0 ? `Fouká silněji, než předpověď čekala (o ${fmtNum(wConv(Math.abs(dW)), 1)} ${wUnit})`
      : `Fouká slaběji, než předpověď čekala (o ${fmtNum(wConv(Math.abs(dW)), 1)} ${wUnit})`;
    if (angle != null && angle > 60) verdict += ` · směr jiný o ${angle}°`;
  }
  const bar = (k, sv, fv, d, ok, warn) => (
    <div className="wv-row">
      <span className="wv-k">{k}</span>
      <div className="wv-bars">
        <div className="wv-bar wv-st"><i style={{ width: pct(sv) }}></i><b>{f1(sv)}</b></div>
        <div className="wv-bar wv-fc"><i style={{ width: pct(fv) }}></i><b>{f1(fv)}</b></div>
      </div>
      <span className={`wv-d ${lvl(d, ok, warn)}`}>{d == null ? '—' : Math.abs(d) <= ok ? 'v souladu' : sign(d)}</span>
    </div>
  );

  return (
    <div className="wind-widget wv">
      {flow !== false && (
        <div className="wind-flow-bg">
          <window.WindFlow speed={speedForFlow} direction={displayDeg} mult={mult}/>
        </div>
      )}
      <div className="wv-content">
        <div className="wv-top">
          <div className="wv-now">
            <div className="wv-label">
              <i className="bi bi-wind" aria-hidden="true"></i>Vítr
              {bft && <span className="wv-bft" style={{ '--bft': BFT_COLOR[bft.n] }} title="Beaufortova stupnice">{bft.n} Bft · {bft.name}</span>}
            </div>
            <div className="wv-speed">
              <span className="wv-num">{headSpeed != null ? f1(headSpeed) : '—'}</span>
              <span className="wv-unit">{wUnit}</span>
            </div>
            <div className="wv-sub">
              <span>nárazy <b>{f1(headGust)}</b></span>
              <span>{calm ? <b>bezvětří</b> : <>od <b>{dirName(displayDeg) || '—'}</b> {Math.round(displayDeg)}°</>}</span>
            </div>
          </div>
          {showCompass && (
            <div className="wv-compass">
              <WindCompass direction={displayDeg} forecastDir={showCompare ? fDeg : null} size={150} calm={calm}/>
              {showCompare && fDeg != null && (
                <div className="wv-legend" aria-hidden="true">
                  <span><i className="wv-key wv-key-st"></i>stanice</span>
                  <span><i className="wv-key wv-key-fc"></i>předpověď</span>
                </div>
              )}
            </div>
          )}
        </div>

        {showCompare && (canCompare ? (
          <div className="wv-cmp">
            <div className={`wv-verdict ${vcls}`}>
              <i className={`bi bi-${vcls === 'good' ? 'check-circle-fill' : 'exclamation-circle-fill'}`} aria-hidden="true"></i>
              <span>{verdict}</span>
            </div>
            <div className="wv-head" aria-hidden="true">
              <span></span>
              <span><i className="wv-key wv-key-st"></i>stanice <small>hodina</small><i className="wv-key wv-key-fc"></i>předpověď</span>
              <span>rozdíl</span>
            </div>
            {bar('Vítr', hourWind, fSpeed, dW, 1, 2.5)}
            {bar('Nárazy', hourGust, fGust, dG, 2, 4)}
            <div className="wv-row wv-dirrow">
              <span className="wv-k">Směr</span>
              <span className="wv-dirs">
                {dirCalm ? 'bezvětří — směr nehraje roli'
                  : <>{sDeg != null ? `${dirName(sDeg)} ${Math.round(sDeg)}°` : '—'} <span className="wv-vs">×</span> {fDeg != null ? `${dirName(fDeg)} ${Math.round(fDeg)}°` : '—'}</>}
              </span>
              <span className={`wv-d ${angle == null ? '' : angle <= 45 ? 'good' : angle <= 90 ? 'warn' : 'bad'}`}>{angle == null ? '—' : angle <= 45 ? 'v souladu' : `${angle}°`}</span>
            </div>
          </div>
        ) : (
          <div className="wv-cmp wv-cmp-empty">
            {fSpeed != null
              ? <>Předpověď na tuto hodinu: vítr <b>{f1(fSpeed)} {wUnit}</b>, nárazy <b>{f1(fGust)}</b>{fDeg != null ? <>, od <b>{dirName(fDeg)}</b></> : null}. Bez dat stanice se nedá porovnat.</>
              : 'Předpověď se načítá…'}
          </div>
        ))}
      </div>
    </div>
  );
};
