/* ==========================================================================
   SVG charts.

   Mark specs follow the house data-viz rules: 2px lines with round joins, area
   washes at 10% opacity, >=8px end markers carrying a 2px surface ring, solid
   hairline gridlines, selective direct labels (endpoint only), a legend
   whenever there are two or more series, and a crosshair + tooltip on hover.
   Text always wears text tokens, never the series colour.
   ========================================================================== */

import { el, clear } from './ui.js';
import { seriesColor, cssVar } from './core.js';

const NS = 'http://www.w3.org/2000/svg';

function svgEl(tag, attrs = {}) {
  const node = document.createElementNS(NS, tag);
  for (const [key, value] of Object.entries(attrs)) {
    if (value !== null && value !== undefined) node.setAttribute(key, value);
  }
  return node;
}

function niceTicks(min, max, count = 4) {
  if (!Number.isFinite(min) || !Number.isFinite(max)) return [0, 1];
  if (min === max) { min -= 0.5; max += 0.5; }
  const span = max - min;
  const raw = span / count;
  const magnitude = Math.pow(10, Math.floor(Math.log10(raw)));
  const normalised = raw / magnitude;
  const step = (normalised >= 5 ? 10 : normalised >= 2 ? 5 : normalised >= 1 ? 2 : 1) * magnitude;
  const ticks = [];
  for (let t = Math.ceil(min / step) * step; t <= max + step * 0.001; t += step) {
    ticks.push(Number(t.toFixed(10)));
  }
  return ticks.length >= 2 ? ticks : [min, max];
}

const fmt = (v, precision) => Number(v).toLocaleString(undefined, {
  minimumFractionDigits: precision, maximumFractionDigits: precision,
});

/* -- Sparkline ------------------------------------------------------------ */

/**
 * A bare trend line for a stat tile. No axes, no legend - the tile's label and
 * value carry the meaning; this only shows shape.
 */
export function sparkline(points, { width = 200, height = 30, colorIndex = 0, fill = true } = {}) {
  const svg = svgEl('svg', {
    class: 'chart', viewBox: `0 0 ${width} ${height}`,
    preserveAspectRatio: 'none', height,
  });
  svg.style.width = '100%';

  const values = (points || []).map((p) => (Array.isArray(p) ? p[1] : p)).filter(Number.isFinite);
  if (values.length < 2) return svg;

  const min = Math.min(...values);
  const max = Math.max(...values);
  const span = (max - min) || 1;
  const pad = 3;
  const x = (i) => (i / (values.length - 1)) * width;
  const y = (v) => height - pad - ((v - min) / span) * (height - pad * 2);

  const line = values.map((v, i) => `${i ? 'L' : 'M'}${x(i).toFixed(2)},${y(v).toFixed(2)}`).join('');
  const color = seriesColor(colorIndex);

  if (fill) {
    svg.appendChild(svgEl('path', {
      class: 'series-area', d: `${line}L${width},${height}L0,${height}Z`, fill: color,
    }));
  }
  svg.appendChild(svgEl('path', { class: 'series-line', d: line, stroke: color }));
  svg.appendChild(svgEl('circle', {
    class: 'end-dot', cx: width, cy: y(values.at(-1)), r: 2.5, fill: color,
  }));
  return svg;
}

/* -- Time series ---------------------------------------------------------- */

/**
 * series: [{ key, label, points: [[ts, value], ...], colorIndex, unit }]
 * Returns a container element with an .update(series) method.
 */
export function timeSeries(series, {
  height = 200, precision = 2, unit = '', yMin = null, yMax = null,
  zeroLine = false, showLegend = null, directLabel = true,
} = {}) {
  const host = el('div', { style: { position: 'relative' } });
  const svg = svgEl('svg', { class: 'chart', height, preserveAspectRatio: 'none' });
  svg.style.width = '100%';
  host.appendChild(svg);

  const tip = el('div.chart-tip');
  document.body.appendChild(tip);
  let currentSeries = series;
  let hover = null;

  const cleanup = new MutationObserver(() => {
    if (!host.isConnected) { tip.remove(); cleanup.disconnect(); }
  });
  cleanup.observe(document.body, { childList: true, subtree: true });

  function render() {
    const rect = host.getBoundingClientRect();
    const width = Math.max(220, rect.width || 480);
    const padL = 44, padR = directLabel ? 52 : 12, padT = 10, padB = 22;
    const plotW = width - padL - padR;
    const plotH = height - padT - padB;

    clear(svg);
    svg.setAttribute('viewBox', `0 0 ${width} ${height}`);

    const active = currentSeries.filter((s) => (s.points || []).length >= 2);
    if (!active.length) {
      svg.appendChild(svgEl('text', {
        x: width / 2, y: height / 2, 'text-anchor': 'middle', class: 'tick-text',
      })).textContent = 'Waiting for data…';
      return;
    }

    const allValues = active.flatMap((s) => s.points.map((p) => p[1])).filter(Number.isFinite);
    const allTimes = active.flatMap((s) => s.points.map((p) => p[0]));
    let lo = yMin !== null ? yMin : Math.min(...allValues);
    let hi = yMax !== null ? yMax : Math.max(...allValues);
    if (lo === hi) { lo -= 0.5; hi += 0.5; }
    const headroom = (hi - lo) * 0.12;
    if (yMin === null) lo -= headroom;
    if (yMax === null) hi += headroom;
    if (zeroLine) { lo = Math.min(lo, 0); hi = Math.max(hi, 0); }

    const t0 = Math.min(...allTimes), t1 = Math.max(...allTimes);
    const tSpan = (t1 - t0) || 1;
    const X = (t) => padL + ((t - t0) / tSpan) * plotW;
    const Y = (v) => padT + plotH - ((v - lo) / (hi - lo)) * plotH;

    // Gridlines - solid hairlines, one step off the surface, recessive.
    for (const tick of niceTicks(lo, hi, 4)) {
      if (tick < lo || tick > hi) continue;
      const y = Y(tick);
      svg.appendChild(svgEl('line', { class: 'grid-line', x1: padL, x2: padL + plotW, y1: y, y2: y }));
      const label = svgEl('text', { class: 'tick-text', x: padL - 7, y: y + 3.5, 'text-anchor': 'end' });
      label.textContent = fmt(tick, precision);
      svg.appendChild(label);
    }

    if (zeroLine && lo < 0 && hi > 0) {
      svg.appendChild(svgEl('line', { class: 'axis-line', x1: padL, x2: padL + plotW, y1: Y(0), y2: Y(0) }));
    }

    svg.appendChild(svgEl('line', {
      class: 'axis-line', x1: padL, x2: padL + plotW, y1: padT + plotH, y2: padT + plotH,
    }));

    const surface = cssVar('--surface') || '#141619';

    // When series converge their end-labels overlap. Nudging them apart would
    // detach each label from its line and read as noise, so drop direct labels
    // for this render and let the legend and tooltip carry identity instead.
    const endYs = active.map((s) => Y(s.points.at(-1)[1])).sort((a, b) => a - b);
    const labelsCollide = endYs.some((y, i) => i > 0 && Math.abs(y - endYs[i - 1]) < 13);
    const showEndLabels = directLabel && !labelsCollide;

    active.forEach((s, i) => {
      const color = seriesColor(s.colorIndex ?? i);
      const path = s.points
        .map((p, idx) => `${idx ? 'L' : 'M'}${X(p[0]).toFixed(2)},${Y(p[1]).toFixed(2)}`)
        .join('');

      if (active.length === 1) {
        svg.appendChild(svgEl('path', {
          class: 'series-area', fill: color,
          d: `${path}L${X(t1)},${padT + plotH}L${X(t0)},${padT + plotH}Z`,
        }));
      }
      svg.appendChild(svgEl('path', { class: 'series-line', d: path, stroke: color }));

      // End marker: >=8px with a 2px surface ring so overlaps stay legible.
      const last = s.points.at(-1);
      svg.appendChild(svgEl('circle', {
        class: 'end-dot', cx: X(last[0]), cy: Y(last[1]), r: 4, fill: color, stroke: surface,
      }));

      // Direct-label the endpoint only - never a number on every point.
      if (showEndLabels) {
        const label = svgEl('text', {
          class: 'end-label', x: X(last[0]) + 9, y: Y(last[1]) + 3.5,
        });
        label.textContent = `${fmt(last[1], precision)}${s.unit ?? unit}`;
        svg.appendChild(label);
      }
    });

    if (hover !== null) {
      const x = padL + hover * plotW;
      svg.appendChild(svgEl('line', { class: 'crosshair', x1: x, x2: x, y1: padT, y2: padT + plotH }));
    }

    // Hover layer sits above everything and is the full plot height, so the hit
    // target is far bigger than the marks.
    const overlay = svgEl('rect', {
      x: padL, y: padT, width: plotW, height: plotH, fill: 'transparent', style: 'cursor:crosshair',
    });
    overlay.addEventListener('pointermove', (event) => {
      const bounds = svg.getBoundingClientRect();
      const scale = width / bounds.width;
      const px = (event.clientX - bounds.left) * scale;
      hover = Math.max(0, Math.min(1, (px - padL) / plotW));
      const t = t0 + hover * tSpan;

      const rows = active.map((s, i) => {
        let best = s.points[0], bestGap = Infinity;
        for (const p of s.points) {
          const gap = Math.abs(p[0] - t);
          if (gap < bestGap) { bestGap = gap; best = p; }
        }
        return { label: s.label, value: best[1], unit: s.unit ?? unit, color: seriesColor(s.colorIndex ?? i) };
      });

      tip.innerHTML = '';
      tip.appendChild(el('div.tip-time', { text: new Date(t * 1000).toLocaleTimeString([], { hour12: false }) }));
      for (const row of rows) {
        tip.appendChild(el('div.tip-row', {},
          el('span', { style: { display: 'inline-flex', alignItems: 'center', gap: '6px' } },
            el('span', { style: { width: '10px', height: '2.5px', borderRadius: '2px', background: row.color } }),
            el('span', { text: row.label, style: { color: 'var(--text-2)' } }),
          ),
          el('b', { text: `${fmt(row.value, precision)}${row.unit}` }),
        ));
      }
      tip.dataset.show = 'true';
      const tipBox = tip.getBoundingClientRect();
      tip.style.left = `${Math.min(window.innerWidth - tipBox.width - 10, event.clientX + 14)}px`;
      tip.style.top = `${Math.max(10, event.clientY - tipBox.height - 12)}px`;
      render();
    });
    overlay.addEventListener('pointerleave', () => {
      hover = null; tip.dataset.show = 'false'; render();
    });
    svg.appendChild(overlay);
  }

  // A legend is always present for two or more series - identity never rests on
  // colour alone. One series needs none; the card title already names it.
  const legendVisible = showLegend ?? (series.length >= 2);
  if (legendVisible) {
    const legend = el('div.legend');
    series.forEach((s, i) => {
      legend.appendChild(el('div.legend-item', {},
        el('span.legend-key', { style: { background: seriesColor(s.colorIndex ?? i) } }),
        el('span', { text: s.label }),
      ));
    });
    host.appendChild(legend);
  }

  host.update = (next) => { currentSeries = next; render(); };
  host.redraw = render;

  requestAnimationFrame(render);
  const observer = new ResizeObserver(() => render());
  observer.observe(host);

  return host;
}

/* -- Horizontal bars ------------------------------------------------------ */

/**
 * rows: [{ label, value, min, max, tone }]
 * Used for joint positions - a diverging bar around a zero centre reads better
 * than a table of numbers when you are looking for the joint that is off.
 */
export function bars(rows, { height = 20, precision = 2, unit = '', diverging = true } = {}) {
  const host = el('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } });

  for (const row of rows) {
    const min = row.min ?? -1;
    const max = row.max ?? 1;
    const span = (max - min) || 1;
    const value = Math.max(min, Math.min(max, row.value ?? 0));

    const zeroFrac = diverging && min < 0 && max > 0 ? (0 - min) / span : 0;
    const valueFrac = (value - min) / span;
    const left = Math.min(zeroFrac, valueFrac) * 100;
    const width = Math.abs(valueFrac - zeroFrac) * 100;

    const track = el('div', {
      style: {
        position: 'relative', height: `${height - 8}px`, borderRadius: '3px',
        background: 'var(--surface-3)', flex: '1', minWidth: '60px', overflow: 'hidden',
      },
    },
      // 4px rounded data-end, square where it meets the baseline.
      el('div', {
        style: {
          position: 'absolute', left: `${left}%`, width: `${Math.max(width, 0.6)}%`,
          top: '0', bottom: '0',
          background: row.tone === 'critical' ? 'var(--critical)'
            : row.tone === 'warning' ? 'var(--warning)' : seriesColor(row.colorIndex ?? 0),
          borderRadius: valueFrac >= zeroFrac ? '0 4px 4px 0' : '4px 0 0 4px',
        },
      }),
      zeroFrac > 0 ? el('div', {
        style: {
          position: 'absolute', left: `${zeroFrac * 100}%`, top: '0', bottom: '0',
          width: '1px', background: 'var(--axis)',
        },
      }) : null,
    );

    host.appendChild(el('div', { style: { display: 'flex', alignItems: 'center', gap: '10px' } },
      el('span', {
        text: row.label,
        style: { fontSize: '11.5px', color: 'var(--text-2)', minWidth: '118px',
                 whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
      }),
      track,
      el('span', {
        text: `${Number(row.value ?? 0).toFixed(precision)}${unit}`,
        style: { fontSize: '11px', fontFamily: 'var(--mono)', color: 'var(--text-2)',
                 minWidth: '58px', textAlign: 'right', fontVariantNumeric: 'tabular-nums' },
      }),
    ));
  }

  return host;
}

/* -- Attitude indicator --------------------------------------------------- */

/** Artificial horizon from IMU roll and pitch - the one place a dial beats a number. */
export function attitude(roll = 0, pitch = 0, { size = 150 } = {}) {
  const svg = svgEl('svg', { class: 'chart', viewBox: '0 0 200 200', width: size, height: size });
  const clipId = `att-clip-${Math.random().toString(36).slice(2, 8)}`;

  const defs = svgEl('defs');
  const clip = svgEl('clipPath', { id: clipId });
  clip.appendChild(svgEl('circle', { cx: 100, cy: 100, r: 84 }));
  defs.appendChild(clip);
  svg.appendChild(defs);

  const group = svgEl('g', { 'clip-path': `url(#${clipId})` });
  const pitchPx = Math.max(-70, Math.min(70, pitch * (180 / Math.PI) * 2));
  const inner = svgEl('g', {
    transform: `rotate(${-roll * (180 / Math.PI)} 100 100) translate(0 ${pitchPx})`,
  });

  inner.appendChild(svgEl('rect', { x: -60, y: -80, width: 320, height: 180, fill: 'var(--surface-3)' }));
  inner.appendChild(svgEl('rect', { x: -60, y: 100, width: 320, height: 200, fill: 'var(--surface-hi)' }));
  inner.appendChild(svgEl('line', { x1: -60, x2: 260, y1: 100, y2: 100,
                                    stroke: seriesColor(0), 'stroke-width': 2 }));
  for (const offset of [-40, -20, 20, 40]) {
    const wide = Math.abs(offset) === 40;
    inner.appendChild(svgEl('line', {
      x1: 100 - (wide ? 26 : 16), x2: 100 + (wide ? 26 : 16),
      y1: 100 + offset, y2: 100 + offset,
      stroke: 'var(--text-3)', 'stroke-width': 1,
    }));
  }
  group.appendChild(inner);
  svg.appendChild(group);

  svg.appendChild(svgEl('circle', { cx: 100, cy: 100, r: 84, fill: 'none',
                                    stroke: 'var(--border)', 'stroke-width': 1.5 }));
  // Fixed aircraft reference.
  svg.appendChild(svgEl('path', {
    d: 'M62 100 h22 l8 8 l8 -8 h22', fill: 'none',
    stroke: 'var(--text)', 'stroke-width': 2.5, 'stroke-linejoin': 'round', 'stroke-linecap': 'round',
  }));
  svg.appendChild(svgEl('path', {
    d: 'M100 16 l-7 12 h14 z', fill: 'var(--text-2)',
  }));

  return svg;
}

/* -- Compass / heading ---------------------------------------------------- */

export function compass(yaw = 0, { size = 150 } = {}) {
  const svg = svgEl('svg', { class: 'chart', viewBox: '0 0 200 200', width: size, height: size });
  svg.appendChild(svgEl('circle', { cx: 100, cy: 100, r: 84, fill: 'var(--surface-2)',
                                    stroke: 'var(--border)', 'stroke-width': 1.5 }));

  const dial = svgEl('g', { transform: `rotate(${-yaw * (180 / Math.PI)} 100 100)` });
  for (let deg = 0; deg < 360; deg += 15) {
    const major = deg % 45 === 0;
    const rad = (deg - 90) * Math.PI / 180;
    const r1 = major ? 66 : 74;
    dial.appendChild(svgEl('line', {
      x1: 100 + Math.cos(rad) * r1, y1: 100 + Math.sin(rad) * r1,
      x2: 100 + Math.cos(rad) * 80, y2: 100 + Math.sin(rad) * 80,
      stroke: major ? 'var(--text-3)' : 'var(--border)', 'stroke-width': major ? 1.5 : 1,
    }));
  }
  for (const [label, deg] of [['N', 0], ['E', 90], ['S', 180], ['W', 270]]) {
    const rad = (deg - 90) * Math.PI / 180;
    const text = svgEl('text', {
      x: 100 + Math.cos(rad) * 52, y: 100 + Math.sin(rad) * 52 + 4,
      'text-anchor': 'middle', class: 'tick-text',
      style: 'font-size:11px;font-weight:600',
    });
    text.textContent = label;
    dial.appendChild(text);
  }
  svg.appendChild(dial);

  svg.appendChild(svgEl('path', {
    d: 'M100 34 L110 100 L100 92 L90 100 Z', fill: seriesColor(0),
  }));
  svg.appendChild(svgEl('circle', { cx: 100, cy: 100, r: 4, fill: 'var(--text-2)' }));

  const heading = svgEl('text', {
    x: 100, y: 132, 'text-anchor': 'middle', class: 'tick-text',
    style: 'font-size:15px;font-weight:600;fill:var(--text)',
  });
  heading.textContent = `${(((yaw * 180 / Math.PI) % 360 + 360) % 360).toFixed(0)}°`;
  svg.appendChild(heading);

  return svg;
}
