<script lang="ts">
  /**
   * The hero element: the route, with the rider's position on it.
   *
   * For a GPX or terrain profile this is a real elevation trace. For a
   * waveform profile there is no elevation, so it plots the profile's own
   * channel instead — same chart, same marker, honest label (FR-9.7).
   *
   * The ridden part is drawn bright and filled; what is still to come is dim.
   * At a glance, from a metre away, that is the one thing a rider wants: how
   * much of this is left, and does it go up.
   */
  import { onMount } from 'svelte';
  import uPlot from 'uplot';
  import 'uplot/dist/uPlot.min.css';
  import { axis, observeSize, positionMarker } from '../lib/uplot';
  import type { ProfileView } from '../lib/types';

  interface Props {
    profile: ProfileView | null;
    positionX: number;
    revision: number;
  }

  let { profile, positionX, revision }: Props = $props();

  let host = $state<HTMLDivElement | null>(null);
  let plot: uPlot | null = null;
  let disposeSize: (() => void) | null = null;
  let currentX = 0;

  /** Elevation when we have it, otherwise the profile's own channel. */
  const source = $derived.by(() => {
    if (!profile) return null;
    const pairs = profile.elevation ?? profile.series;
    if (!pairs || pairs.length < 2) return null;
    return { pairs, isElevation: profile.elevation != null };
  });

  function build(): void {
    if (!host || !source) return;
    plot?.destroy();
    plot = null;

    const xs = source.pairs.map((p) => p[0]);
    const ys = source.pairs.map((p) => p[1]);
    const isMetres = profile?.xUnit === 'metres';

    const opts: uPlot.Options = {
      width: host.clientWidth || 800,
      height: host.clientHeight || 260,
      padding: [12, 8, 0, 0],
      cursor: { show: false },
      legend: { show: false },
      scales: { x: { time: false } },
      axes: [
        axis({
          values: (_u, splits) =>
            splits.map((v) => (isMetres ? `${(v / 1000).toFixed(1)}` : formatMinutes(v))),
        }),
        axis({
          side: 3,
          size: 46,
          values: (_u, splits) => splits.map((v) => v.toFixed(0)),
        }),
      ],
      series: [
        {},
        {
          // Everything still to come.
          stroke: '#2b4d5e',
          width: 1,
          fill: 'rgba(24, 58, 76, 0.55)',
          points: { show: false },
        },
        {
          // Ridden so far.
          stroke: '#45d0ff',
          width: 2,
          fill: 'rgba(69, 208, 255, 0.22)',
          points: { show: false },
        },
      ],
      plugins: [positionMarker(() => currentX, '#ffffff')],
    };

    plot = new uPlot(opts, [xs, ys, ys.map(() => null)] as unknown as uPlot.AlignedData, host);
    disposeSize?.();
    disposeSize = observeSize(host, (w, h) => plot?.setSize({ width: w, height: h }));
    update();
  }

  function formatMinutes(v: number): string {
    const m = Math.round(v / 60);
    return m >= 60 ? `${Math.floor(m / 60)}h${String(m % 60).padStart(2, '0')}` : `${m}m`;
  }

  /** Split the trace at the current position; only the split array changes. */
  function update(): void {
    if (!plot || !source) return;
    currentX = positionX;
    const xs = source.pairs;
    const ridden: (number | null)[] = new Array(xs.length);
    for (let i = 0; i < xs.length; i++) {
      ridden[i] = xs[i][0] <= positionX ? xs[i][1] : null;
    }
    // Carry one point past the split so the bright fill meets the marker.
    const idx = ridden.findIndex((v) => v === null);
    if (idx > 0) ridden[idx] = xs[idx][1];
    plot.setData(
      [xs.map((p) => p[0]), xs.map((p) => p[1]), ridden] as unknown as uPlot.AlignedData,
      true,
    );
  }

  onMount(() => () => {
    disposeSize?.();
    plot?.destroy();
  });

  // Rebuild only when the route itself changes; otherwise just re-split, which
  // is a single array walk rather than a chart teardown.
  let builtFor: string | null = null;
  $effect(() => {
    const key = host && profile ? `${profile.name}|${profile.source}|${profile.totalX}` : null;
    if (key === builtFor) return;
    builtFor = key;
    if (key) {
      build();
    } else {
      plot?.destroy();
      plot = null;
    }
  });

  $effect(() => {
    revision;
    positionX;
    update();
  });
</script>

<div class="route">
  <div class="canvas" bind:this={host}></div>
  {#if !source}
    <div class="empty">
      <span class="label">No route loaded</span>
      <p>Load a GPX or a YAML profile to see the terrain ahead.</p>
    </div>
  {/if}
  {#if source && !source.isElevation}
    <span class="overlay label">
      {profile?.channel} profile — no elevation
    </span>
  {/if}
</div>

<style>
  .route {
    position: relative;
    width: 100%;
    height: 100%;
    min-height: 0;
  }

  .canvas {
    width: 100%;
    height: 100%;
  }

  .empty {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    color: var(--ink-dim);
  }

  .empty p {
    margin: 0;
    font-size: 0.95rem;
    color: var(--ink-faint);
  }

  .overlay {
    position: absolute;
    top: 0.25rem;
    right: 0.5rem;
    color: var(--ink-faint);
  }
</style>
