/* Control - mode switching, arbitration, driving, preset motions. */

import { store, socket, post, command, toast, num, confirmDialog } from '../core.js';
import {
  el, card, pageHead, button, note, badge, table, range, keyHint, kv, setChildren,
} from '../ui.js';

export default {
  id: 'control',
  label: 'Control',
  icon: 'control',

  async render() {
    const spec = store.spec;
    const root = el('div.stack');

    root.appendChild(pageHead(
      'Control',
      'Motion mode, drive input and preset motions. Keep the physical remote controller within reach — it outranks this dashboard.',
    ));

    /* ==================================================================
       Motion mode
       ================================================================== */

    const modeTiles = el('div.stack');
    const modeButtons = new Map();

    for (const group of spec?.mode_groups || []) {
      const inGroup = (spec?.modes || []).filter((m) => m.group === group.key);
      if (!inGroup.length) continue;

      const grid = el('div.tile-grid');
      for (const mode of inGroup) {
        const tile = el('button.tile', {
          type: 'button',
          'aria-pressed': 'false',
          title: mode.desc,
          onclick: () => setMode(mode),
        },
          el('div.row.between', {},
            el('span.tile-name', { text: mode.label }),
            mode.danger ? badge('risk', 'critical') : null,
          ),
          el('span.tile-meta', { text: `${mode.id} · ${mode.value}` }),
          el('span', {
            text: mode.desc,
            style: { fontSize: '11px', color: 'var(--text-3)', lineHeight: '1.4' },
          }),
        );
        modeButtons.set(mode.id, tile);
        grid.appendChild(tile);
      }

      modeTiles.appendChild(el('div', {},
        el('div', {
          text: group.label,
          style: { fontSize: '11px', textTransform: 'uppercase', letterSpacing: '.07em',
                   color: 'var(--text-3)', fontWeight: '600', margin: '2px 0 8px' },
        }),
        grid,
      ));
    }

    async function setMode(mode) {
      if (mode.danger) {
        const releases = ['PASSIVE_DEFAULT', 'ZERO_TORQUE_DEFAULT'].includes(mode.id);
        const ok = await confirmDialog(
          `Switch to ${mode.label}?`,
          releases
            ? 'This removes all joint holding force. A free-standing robot will collapse. '
              + 'Only continue if the robot is supported or already seated.'
            : `${mode.desc} Make sure the area around the robot is clear before continuing.`,
          { confirmLabel: `Yes, switch to ${mode.label}` },
        );
        if (!ok) return;
      }
      await command('/api/mode', { mode: mode.id, confirmed: true },
                    { successTitle: `Mode: ${mode.label}` });
    }

    const modeCard = card('Motion mode', {
      sub: `SetMcAction · ${(spec?.modes || []).length} modes`,
    },
      modeTiles,
      el('div', { style: { marginTop: '12px' } },
        note('Stable stand is the prerequisite for preset motions and for walking. '
           + 'Joint control mode is required before direct joint commands take effect.', 'info'),
      ),
    );

    /* ==================================================================
       Input source arbitration
       ================================================================== */

    const dash = spec?.dashboard_input_source || { name: 'x2_dashboard', priority: 30, timeout: 1000 };
    const sourceStatus = el('div');

    const registerBtn = button('Announce the dashboard again', async () => {
      await command('/api/input-source', dash, { successTitle: 'Dashboard announced' });
    }, { size: 'sm' });

    const arbitrationRows = [
      ...(spec?.builtin_input_sources || []),
      { name: dash.name, priority: dash.priority, timeout: dash.timeout, desc: 'This dashboard' },
    ].sort((a, b) => b.priority - a.priority);

    const sourceCard = card('Who is allowed to drive', { sub: 'Control arbitration' },
      el('div.stack', {},
        sourceStatus,

        el('p', {
          style: { margin: '0', fontSize: '13px', color: 'var(--text-2)', lineHeight: '1.6' },
          text: 'Several things can send movement commands to this robot at once — the handheld '
              + 'remote, the mobile app, VR teleoperation, and this dashboard. The robot listens '
              + 'to whichever one is actively sending and has the highest priority.',
        }),
        el('p', {
          style: { margin: '0', fontSize: '13px', color: 'var(--text-2)', lineHeight: '1.6' },
          text: 'The robot ignores commands from a sender it has never heard of, so the dashboard '
              + 'introduces itself automatically as soon as it connects. That is all "announcing" '
              + 'means — it does not take control away from anyone. You only need the button below '
              + 'if the robot restarts and forgets.',
        }),

        table(
          [
            { key: 'name', label: 'Sender' },
            { key: 'priority', label: 'Priority', align: 'right' },
            { key: 'desc', label: 'What it is' },
          ],
          arbitrationRows,
        ),

        note(`This dashboard sits at priority ${dash.priority} on purpose — below the handheld `
           + 'remote (80) and the mobile app (60). If someone picks up the remote while you are '
           + 'driving, the remote wins immediately.', 'info'),

        el('div.row', {}, registerBtn),
      ),
    );

    root.appendChild(el('div.grid.cols-2', {}, modeCard, sourceCard));

    /* ==================================================================
       Drive
       ================================================================== */

    const limits = spec?.velocity_limits || {
      forward: { max: 0.8 }, lateral: { max: 0.7 }, angular: { max: 0.8 },
    };
    const thresholds = spec?.velocity_thresholds || {};

    let scale = 0.5;
    let vector = { forward: 0, lateral: 0, angular: 0 };
    let strafeMode = false;

    const knob = el('div.joystick-knob');
    const pad = el('div.joystick', { dataset: { active: 'false' } },
      el('div.joystick-ring'),
      el('span.joystick-label.n', { text: 'fwd' }),
      el('span.joystick-label.s', { text: 'back' }),
      el('span.joystick-label.w', { text: 'left' }),
      el('span.joystick-label.e', { text: 'right' }),
      knob,
    );

    const readoutHost = el('div');
    const strafeToggle = el('div.segmented');
    for (const [label, value] of [['Turn', false], ['Strafe', true]]) {
      const btn = el('button', { type: 'button', text: label, 'aria-pressed': String(value === strafeMode) });
      btn.addEventListener('click', () => {
        strafeMode = value;
        for (const b of strafeToggle.children) b.setAttribute('aria-pressed', String(b === btn));
        setVector(0, 0);
      });
      strafeToggle.appendChild(btn);
    }

    const scaleRange = range({
      min: 0.1, max: 1, step: 0.05, value: scale, precision: 2,
      onInput: (v) => { scale = v; },
    });

    function setVector(nx, ny) {
      // nx / ny are -1..1 in pad space; y is inverted so up is forward.
      const forward = -ny * (limits.forward.max ?? 0.8) * scale;
      const secondary = nx * ((strafeMode ? limits.lateral.max : limits.angular.max) ?? 0.7) * scale;

      vector = {
        forward: Number(forward.toFixed(3)),
        lateral: strafeMode ? Number(secondary.toFixed(3)) : 0,
        // A joystick pushed right should turn right, i.e. clockwise, which is a
        // negative yaw rate under the documented counter-clockwise-positive sign.
        angular: strafeMode ? 0 : Number((-secondary).toFixed(3)),
      };

      knob.style.transform = `translate(${nx * 62}px, ${ny * 62}px)`;
      socket.sendThrottled('velocity', vector);
      paintReadout();
    }

    function paintReadout() {
      const s = store.state;
      const measured = s?.velocity || {};
      const belowThreshold = (axis, value) => {
        const limit = thresholds[axis];
        return limit !== undefined && Math.abs(value) > 0 && Math.abs(value) < limit;
      };

      readoutHost.replaceChildren(
        kv([
          ['Forward cmd', `${num(vector.forward, 2)} m/s`],
          ['Lateral cmd', `${num(vector.lateral, 2)} m/s`],
          ['Yaw cmd', `${num(vector.angular, 2)} rad/s`],
          ['Forward now', `${num(measured.forward, 2)} m/s`],
          ['Yaw now', `${num(measured.angular, 2)} rad/s`],
        ]),
      );

      const warnings = [];
      if (belowThreshold('forward', vector.forward)) {
        warnings.push(`Forward command is below the ${thresholds.forward} m/s activation threshold — the robot will not start moving.`);
      }
      if (belowThreshold('angular', vector.angular)) {
        warnings.push(`Yaw command is below the ${thresholds.angular} rad/s activation threshold.`);
      }
      if (belowThreshold('lateral', vector.lateral)) {
        warnings.push(`Lateral command is below the ${thresholds.lateral} m/s activation threshold.`);
      }
      for (const text of warnings) readoutHost.appendChild(note(text, 'warning', '⚠'));
    }

    /* Pointer drive */
    let pointerId = null;

    function padPoint(event) {
      const rect = pad.getBoundingClientRect();
      const cx = rect.left + rect.width / 2;
      const cy = rect.top + rect.height / 2;
      const radius = rect.width / 2 - 28;
      let dx = (event.clientX - cx) / radius;
      let dy = (event.clientY - cy) / radius;
      const magnitude = Math.hypot(dx, dy);
      if (magnitude > 1) { dx /= magnitude; dy /= magnitude; }
      return [dx, dy];
    }

    pad.addEventListener('pointerdown', (event) => {
      pointerId = event.pointerId;
      pad.setPointerCapture(pointerId);
      pad.dataset.active = 'true';
      setVector(...padPoint(event));
    });
    pad.addEventListener('pointermove', (event) => {
      if (event.pointerId !== pointerId) return;
      setVector(...padPoint(event));
    });
    const releasePad = (event) => {
      if (event.pointerId !== pointerId) return;
      pointerId = null;
      pad.dataset.active = 'false';
      setVector(0, 0);
    };
    pad.addEventListener('pointerup', releasePad);
    pad.addEventListener('pointercancel', releasePad);

    /* Keyboard drive */
    const held = new Set();
    const KEYS = {
      KeyW: 'up', ArrowUp: 'up',
      KeyS: 'down', ArrowDown: 'down',
      KeyA: 'left', ArrowLeft: 'left',
      KeyD: 'right', ArrowRight: 'right',
    };

    function applyKeys() {
      const nx = (held.has('right') ? 1 : 0) - (held.has('left') ? 1 : 0);
      const ny = (held.has('down') ? 1 : 0) - (held.has('up') ? 1 : 0);
      setVector(nx, ny);
    }

    const onKeyDown = (event) => {
      if (['INPUT', 'TEXTAREA', 'SELECT'].includes(event.target.tagName)) return;
      const dir = KEYS[event.code];
      if (!dir) return;
      event.preventDefault();
      if (held.has(dir)) return;
      held.add(dir);
      applyKeys();
    };
    const onKeyUp = (event) => {
      const dir = KEYS[event.code];
      if (!dir) return;
      held.delete(dir);
      applyKeys();
    };
    // A lost window must not leave the robot walking.
    const onBlur = () => { held.clear(); setVector(0, 0); };

    document.addEventListener('keydown', onKeyDown);
    document.addEventListener('keyup', onKeyUp);
    window.addEventListener('blur', onBlur);

    const driveCard = card('Drive', { sub: 'McLocomotionVelocity · /aima/mc/locomotion/velocity' },
      el('div.joystick-wrap', {},
        pad,
        el('div.joystick-readout', {},
          el('div.row.between', {},
            el('span', { text: 'Horizontal axis', style: { fontSize: '12px', color: 'var(--text-2)' } }),
            strafeToggle,
          ),
          el('div', {},
            el('div', { text: 'Speed scale', style: { fontSize: '12px', color: 'var(--text-2)', marginBottom: '4px' } }),
            scaleRange,
          ),
          readoutHost,
        ),
      ),
      el('div.stack', { style: { marginTop: '14px', gap: '8px' } },
        keyHint(['W', 'A', 'S', 'D'], 'or arrow keys to drive'),
        keyHint(['Space'], 'emergency stop — zeroes velocity immediately'),
        note('Velocity is republished continuously while you hold the stick. If this browser stops '
           + 'sending for half a second the server zeroes the command automatically.', 'info'),
      ),
    );

    root.appendChild(driveCard);

    /* ==================================================================
       Preset motions
       ================================================================== */

    const presetHost = el('div.stack');
    const groups = spec?.preset_groups || [];
    const presets = spec?.presets || [];

    for (const group of groups) {
      const inGroup = presets.filter((p) => p.group === group.key);
      if (!inGroup.length) continue;

      const grid = el('div.tile-grid');
      for (const preset of inGroup) {
        grid.appendChild(el('button.tile', {
          type: 'button',
          onclick: async (event) => {
            const node = event.currentTarget;
            node.disabled = true;
            await command('/api/preset', { key: preset.key }, { successTitle: preset.label });
            setTimeout(() => { node.disabled = false; }, 900);
          },
        },
          el('span.tile-name', { text: preset.label }),
          el('span.tile-meta', { text: `${preset.side} · ${preset.enum}` }),
          el('span.tile-meta', {
            text: `motion ${preset.motion} · area ${preset.area}`,
            style: { opacity: '.7' },
          }),
        ));
      }

      presetHost.appendChild(el('div', {},
        el('div', {
          text: group.label,
          style: { fontSize: '11px', textTransform: 'uppercase', letterSpacing: '.07em',
                   color: 'var(--text-3)', fontWeight: '600', margin: '2px 0 8px' },
        }),
        grid,
      ));
    }

    root.appendChild(card('Preset motions', { sub: `SetMcPresetMotion · ${presets.length} actions` },
      note('All preset motions require Stable stand mode and clear space around the robot.', 'warning', '⚠'),
      el('div', { style: { marginTop: '12px' } }, presetHost),
    ));

    /* ==================================================================
       Live binding
       ================================================================== */

    function paintState() {
      const s = store.state;
      if (!s) return;

      for (const [id, tile] of modeButtons) {
        tile.setAttribute('aria-pressed', String(id === s.mode));
      }

      const registered = s.source_registered;
      const winner = s.input_source;
      setChildren(sourceStatus,
        el('div.row', {},
          badge(registered ? 'Dashboard can drive' : 'Not announced yet',
                registered ? 'good' : 'warning'),
          winner && winner !== dash.name
            ? badge(`${winner} is driving`, 'accent')
            : null,
        ),
        registered ? null : el('div', { style: { marginTop: '8px' } },
          note('The robot has not acknowledged the dashboard yet. This normally clears within a '
             + 'second or two of connecting; if it does not, use the button below.', 'warning', '⚠'),
        ),
      );

      const driveable = (spec?.driveable_modes || []).includes(s.mode);
      const canDrive = driveable && registered;
      pad.style.opacity = canDrive ? '1' : '.45';
      pad.style.pointerEvents = canDrive ? 'auto' : 'none';
      pad.title = canDrive ? ''
        : !driveable ? `Mode ${s.mode} does not accept velocity — switch to Stable stand or Walk`
        : 'Register an input source to drive';
    }

    paintState();
    paintReadout();

    let last = 0;
    const unsubscribe = store.on('state', () => {
      const now = Date.now();
      if (now - last < 400) return;
      last = now;
      paintState();
      paintReadout();
    });

    return {
      node: root,
      dispose: () => {
        unsubscribe();
        document.removeEventListener('keydown', onKeyDown);
        document.removeEventListener('keyup', onKeyUp);
        window.removeEventListener('blur', onBlur);
        // Leaving the tab must not leave a velocity command standing.
        socket.send('velocity', { forward: 0, lateral: 0, angular: 0 });
      },
    };
  },
};
