/* Motion - joint-level control and end effectors. */

import {
  store, command, num, RAD2DEG, DEG2RAD,
} from '../core.js';
import {
  el, card, pageHead, button, note, segmented, range, table, badge, emptyState,
} from '../ui.js';
import { bars } from '../charts.js';

export default {
  id: 'motion',
  label: 'Motion',
  icon: 'motion',

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

    root.appendChild(pageHead(
      'Motion',
      'Drive joints and end effectors directly. Limits shown are the published X2 '
      + 'Ultra ranges; the server clamps anything outside them before publishing.',
    ));

    /* ==================================================================
       Joint control
       ================================================================== */

    const groups = spec?.joint_groups || [];
    let activeGroup = groups[0]?.key || 'arm';
    let controlMode = 'position';

    const sliders = new Map();     // joint name -> range control
    const targets = new Map();     // joint name -> radians
    const editorHost = el('div');
    const stateHost = el('div');

    const stiffness = range({ min: 0, max: 200, step: 1, value: 60, precision: 0, unit: ' N·m/rad' });
    const damping = range({ min: 0, max: 20, step: 0.5, value: 3, precision: 1, unit: ' N·m·s/rad' });

    function buildEditor() {
      const group = groups.find((g) => g.key === activeGroup);
      if (!group) { editorHost.replaceChildren(emptyState('Unknown joint group')); return; }

      sliders.clear();
      const rows = el('div.stack', { style: { gap: '10px' } });

      for (const joint of group.joints) {
        const fixed = joint.min_deg === joint.max_deg;
        const current = targets.get(joint.name) ?? 0;

        const control = range({
          min: joint.min_deg, max: joint.max_deg,
          step: fixed ? 1 : 0.5,
          value: current * RAD2DEG,
          precision: 1, unit: '°',
          onInput: (deg) => targets.set(joint.name, deg * DEG2RAD),
        });
        if (fixed) {
          control.querySelector('input').disabled = true;
          control.style.opacity = '.5';
        }
        sliders.set(joint.name, control);

        rows.appendChild(el('div', {},
          el('div.row.between', { style: { marginBottom: '3px' } },
            el('span', { text: joint.label, style: { fontSize: '12px', fontWeight: '550' } }),
            el('span', {
              text: fixed ? 'fixed' : `${joint.min_deg}° … ${joint.max_deg}°`,
              style: { fontSize: '10.5px', color: 'var(--text-3)', fontFamily: 'var(--mono)' },
            }),
          ),
          control,
        ));
      }

      editorHost.replaceChildren(rows);
    }

    async function sendTargets() {
      const group = groups.find((g) => g.key === activeGroup);
      if (!group) return;
      const payload = {};
      for (const joint of group.joints) {
        if (joint.min_deg === joint.max_deg) continue;
        payload[joint.name] = targets.get(joint.name) ?? 0;
      }
      await command('/api/joints', {
        group: activeGroup,
        mode: controlMode,
        targets: payload,
        stiffness: stiffness.getValue(),
        damping: damping.getValue(),
      }, { successTitle: `${group.label} command sent` });
    }

    function zeroTargets() {
      const group = groups.find((g) => g.key === activeGroup);
      for (const joint of group?.joints || []) {
        targets.set(joint.name, 0);
        sliders.get(joint.name)?.setValue(0);
      }
    }

    function syncFromRobot() {
      const live = store.state?.joints?.[activeGroup] || [];
      for (const joint of live) {
        targets.set(joint.name, joint.position);
        sliders.get(joint.name)?.setValue(joint.position * RAD2DEG);
      }
    }

    const groupTabs = segmented(
      groups.map((g) => ({ value: g.key, label: g.label })),
      activeGroup,
      (value) => { activeGroup = value; buildEditor(); paintState(); },
    );

    const modeTabs = segmented(
      (spec?.joint_modes || []).map((m) => ({ value: m.id, label: m.label })),
      controlMode,
      (value) => { controlMode = value; },
    );

    buildEditor();

    root.appendChild(el('div.grid.pair', {},
      card('Joint targets', { sub: 'JointCommandArray', actions: [groupTabs] },
        el('div.stack', {},
          el('div.row.between', {},
            el('span', { text: 'Control mode', style: { fontSize: '12px', color: 'var(--text-2)' } }),
            modeTabs,
          ),
          editorHost,
          el('div.grid.cols-2', { style: { gap: '10px' } },
            el('div', {},
              el('div', { text: 'Stiffness', style: { fontSize: '12px', color: 'var(--text-2)', marginBottom: '4px' } }),
              stiffness,
            ),
            el('div', {},
              el('div', { text: 'Damping', style: { fontSize: '12px', color: 'var(--text-2)', marginBottom: '4px' } }),
              damping,
            ),
          ),
          el('div.row', {},
            button('Send targets', sendTargets, { style: 'primary' }),
            button('Sync from robot', syncFromRobot),
            button('Zero all', zeroTargets, { style: 'ghost' }),
          ),
          note('Head pitch has a published range of 0° on the X2 Ultra — the axis exists in the '
             + 'message but is not articulated, so its slider is disabled.', 'info'),
        ),
      ),
      card('Measured joint state', { sub: 'JointStateArray' }, stateHost),
    ));

    /* ==================================================================
       End effectors
       ================================================================== */

    let handSide = 'right';
    const handSliders = [];
    const handHost = el('div');
    const handStateHost = el('div');

    function buildHand() {
      const joints = spec?.dexhand_joints || [];
      handSliders.length = 0;
      const rows = el('div.stack', { style: { gap: '8px' } });

      joints.forEach((name, index) => {
        const control = range({ min: 0, max: 1.6, step: 0.02, value: 0, precision: 2, unit: ' rad' });
        handSliders.push(control);
        rows.appendChild(el('div', {},
          el('div', { text: name, style: { fontSize: '11.5px', color: 'var(--text-2)', marginBottom: '2px' } }),
          control,
        ));
      });

      handHost.replaceChildren(rows);
    }

    async function sendHand() {
      await command('/api/hand', {
        side: handSide,
        positions: handSliders.map((s) => s.getValue()),
      }, { successTitle: `${handSide} hand` });
    }

    async function applyHandPreset(preset) {
      preset.positions.forEach((value, index) => handSliders[index]?.setValue(value));
      await command('/api/hand', { side: handSide, preset: preset.key },
                    { successTitle: `${preset.label} · ${handSide}` });
    }

    buildHand();

    const handSendBtn = button('Send hand pose', sendHand, { style: 'primary' });

    const presetRow = el('div.row.tight', {},
      ...(spec?.hand_presets || []).map((preset) =>
        button(preset.label, () => applyHandPreset(preset), { size: 'sm' })),
    );

    const sideTabs = segmented(
      [{ value: 'left', label: 'Left' }, { value: 'right', label: 'Right' }],
      handSide,
      (value) => { handSide = value; paintState(); },
    );

    root.appendChild(el('div.grid.pair', {},
      card('Dexterous hand', { sub: 'HandCommandArray · OmniHand', actions: [sideTabs] },
        el('div.stack', {},
          presetRow,
          handHost,
          handSendBtn,
          note('Disable native motor control first — run "aima em stop-app mc" on the robot — or the '
             + 'built-in controller and these commands will fight each other.', 'warning', '⚠'),
        ),
      ),
      card('Hand state', { sub: 'HandStateArray' }, handStateHost),
    ));

    /* ==================================================================
       Live state
       ================================================================== */

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

      const group = groups.find((g) => g.key === activeGroup);
      const live = s.joints?.[activeGroup] || [];
      const limits = Object.fromEntries((group?.joints || []).map((j) => [j.name, j]));

      if (!live.length) {
        stateHost.replaceChildren(emptyState(
          'No joint state yet',
          `Nothing has been received on ${group?.state_topic || 'the joint state topic'}.`,
        ));
      } else {
        const rows = live.map((joint) => {
          const limit = limits[joint.name] || { min_deg: -180, max_deg: 180 };
          const deg = joint.position * RAD2DEG;
          const range_ = (limit.max_deg - limit.min_deg) || 1;
          const headroom = Math.min(deg - limit.min_deg, limit.max_deg - deg) / range_;
          return {
            label: joint.label || joint.name,
            value: deg,
            min: limit.min_deg,
            max: limit.max_deg,
            colorIndex: 0,
            tone: joint.error ? 'critical' : headroom < 0.03 ? 'warning' : 'default',
          };
        });

        stateHost.replaceChildren(
          bars(rows, { precision: 1, unit: '°' }),
          el('div', { style: { marginTop: '14px' } },
            table([
              { key: 'name', label: 'Joint', get: (r) => r.label || r.name },
              { key: 'pos', label: 'Position', align: 'right', get: (r) => `${num(r.position * RAD2DEG, 1)}°` },
              { key: 'vel', label: 'Velocity', align: 'right', get: (r) => num(r.velocity, 3) },
              { key: 'eff', label: 'Effort', align: 'right', get: (r) => num(r.effort, 2) },
              {
                key: 'err', label: 'Fault',
                get: (r) => (r.error ? badge(`code ${r.error}`, 'critical') : badge('ok', 'good')),
              },
            ], live),
          ),
        );
      }

      const hand = s.hand_state?.[handSide] || [];
      const attached = (s.hand_type || 'None') !== 'None';

      handStateHost.replaceChildren(
        el('div.row', { style: { marginBottom: '10px' } },
          badge(s.hand_type || 'unknown', attached ? 'accent' : 'warning'),
          badge(`${hand.length} joints reported`),
        ),
        hand.length
          ? bars(hand.map((joint) => ({
              label: joint.name, value: joint.position, min: 0, max: 1.6, colorIndex: 2,
            })), { precision: 2, unit: ' rad', diverging: false })
          : emptyState(
              attached ? 'No hand state yet' : 'No hand hardware attached',
              attached
                ? 'Nothing received on /aima/hal/joint/hand/state.'
                : 'GetHandType reports NONE for both sides on this robot, and the state message '
                  + 'carries no joints. Attach an OmniHand or OmniPicker to use this panel.',
            ),
      );

      // Sending hand commands with no hand attached would silently do nothing.
      for (const control of handSliders) {
        control.querySelector('input').disabled = !attached;
      }
      handSendBtn.disabled = !attached;
      for (const btn of presetRow.children) btn.disabled = !attached;
    }

    paintState();

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

    return {
      node: root,
      dispose: () => {
        unsubscribe();
      },
    };
  },
};
