<script lang="ts">
  import { onMount } from 'svelte';
  import { app } from './lib/app.svelte';
  import { api, inTauri, type ControllerInput } from './lib/bridge';
  import ConnectionScreen from './components/ConnectionScreen.svelte';
  import HelpOverlay from './components/HelpOverlay.svelte';
  import ProfileDrawer from './components/ProfileDrawer.svelte';
  import RideScreen from './components/RideScreen.svelte';
  import SummaryScreen from './components/SummaryScreen.svelte';
  import Toasts from './components/Toasts.svelte';

  let ready = $state(false);
  let fatal = $state<string | null>(null);

  onMount(async () => {
    if (!inTauri) {
      fatal = 'Not running inside the Tauri shell — start the app with `cargo tauri dev`.';
      return;
    }
    try {
      await app.init({ onControllerInput });
      ready = true;
    } catch (e) {
      fatal = String(e);
    }
  });

  /**
   * Keyboard control (FR-3.19). Handled once, here, and dispatched straight to
   * Rust — the frontend never applies a step itself, it only asks.
   */
  function onKey(e: KeyboardEvent) {
    if (e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLInputElement) return;
    if (e.metaKey || e.ctrlKey || e.altKey) return;

    const step = e.shiftKey ? 2 : 0.5;
    const run = (fn: () => Promise<unknown>) => {
      e.preventDefault();
      app.run(fn);
    };

    // The summary screen has its own two keys, and swallows the ride controls:
    // nudging the gradient of a ride that has ended is meaningless, and space
    // would silently start a new one out from under the summary.
    if (app.screen === 'summary') {
      switch (e.key) {
        case 's':
        case 'S':
          e.preventDefault();
          app.saveFit();
          return;
        case 'n':
        case 'N':
          e.preventDefault();
          app.newRide();
          return;
        case 'd':
        case 'D':
          app.screen = 'connect';
          return;
        case '?':
          app.showHelp = !app.showHelp;
          return;
        case 'Escape':
          app.showHelp = false;
          return;
        default:
          return;
      }
    }

    switch (e.key) {
      case 'ArrowUp':
        return run(() => api.nudgeGradient(step));
      case 'ArrowDown':
        return run(() => api.nudgeGradient(-step));
      case '0':
        return run(() => api.resetGradient());
      case ' ':
        return run(() => api.togglePause());
      case 'm':
      case 'M':
        return run(() => api.cycleMode());
      case 'l':
      case 'L':
        return run(() => api.markLap());
      // Shifting is the primary control, so it gets the obvious keys and the
      // paddles. `[` / `]` keep the mode's own target for the modes that have
      // one — a gear is not a substitute for an ERG wattage.
      case '+':
      case '=':
        return run(() => api.shiftGear(1));
      case '-':
      case '_':
        return run(() => api.shiftGear(-1));
      case ']':
        return run(() => bumpTarget(1));
      case '[':
        return run(() => bumpTarget(-1));
      case 'p':
      case 'P':
        e.preventDefault();
        app.showProfiles = !app.showProfiles;
        return;
      case 'd':
      case 'D':
        app.screen = 'connect';
        return;
      case 'r':
      case 'R':
        app.goToRide();
        return;
      case '?':
        app.showHelp = !app.showHelp;
        return;
      case 'Escape':
        app.showHelp = false;
        app.showProfiles = false;
        return;
    }
  }

  /** `[` / `]` adjust whichever target the active mode actually uses. */
  async function bumpTarget(dir: number): Promise<unknown> {
    const ride = app.ride;
    if (!ride) return;
    if (ride.mode === 'Erg') return api.setPower(ride.powerTargetW + dir * 10);
    if (ride.mode === 'Resistance') return api.setResistance(ride.resistanceLevel + dir * 2);
    return api.nudgeGradient(dir * 0.5);
  }

  /**
   * Zwift Click input. The paddles shift the virtual gear and the D-pad drives
   * the UI.
   *
   * Every button routes to an intent the keyboard already has, rather than to a
   * second implementation — that is the whole point of doing this here instead
   * of in Rust. If a shortcut changes, the controller follows it for free.
   *
   * The one exception is the paddles, and it is deliberate: shifting is applied
   * in Rust (`spawn_controller_loop`) so it keeps working with the window
   * unfocused or minimised. They are therefore absent from the switch below.
   * Handling them here as well would shift twice per press — and the version of
   * this file that also nudged the gradient from them is exactly why virtual
   * shifting appeared not to work.
   *
   * Only press edges act. The Rust side already filters the pod's ~10 Hz repeat
   * while a button is held, so acting on releases too would double every shift.
   */
  function onControllerInput(input: ControllerInput) {
    if (!input.pressed) return;
    const run = (fn: () => Promise<unknown>) => app.run(fn);

    // As with the keyboard: on the summary the ride controls are inert, and the
    // face buttons carry that screen's own two actions instead. Leaving `a` on
    // toggle-pause here would restart the ride the rider just finished.
    if (app.screen === 'summary') {
      switch (input.button) {
        case 'a':
          app.saveFit();
          return;
        case 'b':
          app.newRide();
          return;
        case 'left':
          app.screen = 'connect';
          return;
        default:
          return;
      }
    }

    switch (input.button) {
      // 'plus' / 'minus' are handled in Rust — see above.

      // D-pad: gradient on the vertical axis, screens on the horizontal.
      case 'up':
        return run(() => api.nudgeGradient(0.5));
      case 'down':
        return run(() => api.nudgeGradient(-0.5));
      case 'left':
        app.screen = 'connect';
        return;
      case 'right':
        app.goToRide();
        return;

      // Face buttons mirror the existing single-key shortcuts.
      case 'a':
        return run(() => api.togglePause());
      case 'b':
        return run(() => api.markLap());
      // 'y' is shift-down, applied in Rust beside the paddles — see
      // `spawn_controller_loop`. Handling it here as well would shift twice per
      // press. Mode cycling keeps the 'm' key.
      case 'z':
        app.showProfiles = !app.showProfiles;
        return;
    }
  }

</script>

<svelte:window on:keydown={onKey} />

<main>
  {#if fatal}
    <div class="fatal">
      <h1>BikeControl could not start</h1>
      <p>{fatal}</p>
    </div>
  {:else if !ready}
    <div class="boot"><span class="label">Starting…</span></div>
  {:else if app.screen === 'summary'}
    <SummaryScreen />
  {:else if app.screen === 'ride'}
    <RideScreen />
  {:else}
    <ConnectionScreen />
  {/if}

  {#if app.showProfiles}<ProfileDrawer />{/if}
  {#if app.showHelp}<HelpOverlay />{/if}
  <Toasts />
</main>

<style>
  main {
    height: 100%;
    min-height: 0;
  }

  .boot,
  .fatal {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.6rem;
    height: 100%;
    text-align: center;
    padding: 2rem;
  }

  .fatal h1 {
    margin: 0;
    font-size: 1.4rem;
    font-weight: 300;
  }

  .fatal p {
    margin: 0;
    color: var(--ink-dim);
    max-width: 40rem;
  }
</style>
