/* Navigation - odometry, heading and travelled path.

   This firmware does not expose the documented SLAM command interface
   (/integrated_command, /relocalization_pose, GetStoredMapByName are all absent
   from the live graph), so no mapping controls are offered - a button that
   silently does nothing is worse than no button. What the robot does publish is
   leg odometry, and that is what this tab shows.
*/

import { store, num, RAD2DEG } from '../core.js';
import { el, card, pageHead, button, note, badge, stat, kv, emptyState } from '../ui.js';
import { compass, attitude } from '../charts.js';

export default {
  id: 'navigation',
  label: 'Navigation',
  icon: 'nav',

  async render() {
    const root = el('div.stack');
    const trail = [];
    let following = true;

    root.appendChild(pageHead(
      'Navigation',
      'Where the robot has been, from leg odometry. Values advance while it walks and hold '
      + 'steady when it is parked.',
      [
        button('Clear path', () => { trail.length = 0; drawTrail(); }, { size: 'sm', style: 'ghost' }),
      ],
    ));

    /* -- Tiles ------------------------------------------------------------ */

    const tiles = el('div.grid.cols-4');
    const refs = {};
    for (const [key, label] of [
      ['x', 'Position X'],
      ['y', 'Position Y'],
      ['heading', 'Heading'],
      ['speed', 'Ground speed'],
      ['distance', 'Distance travelled'],
      ['points', 'Path points'],
      ['mode', 'Motion mode'],
      ['status', 'Odometry'],
    ]) {
      const node = stat(label, '—', { sub: ' ' });
      refs[key] = node;
      tiles.appendChild(node);
    }
    root.appendChild(tiles);

    function setTile(key, value, sub, tone = 'default', unit = '') {
      const node = refs[key];
      if (!node) return;
      node.dataset.tone = tone;
      const valueNode = node.querySelector('.stat-value');
      valueNode.textContent = String(value);
      if (unit) valueNode.appendChild(el('span.unit', { text: unit }));
      node.querySelector('.stat-sub').textContent = sub || '';
    }

    /* -- Heading and attitude --------------------------------------------- */

    const compassHost = el('div.attitude');
    const attitudeHost = el('div.attitude');
    const readoutHost = el('div');
    const trailCanvas = el('canvas', {
      width: 960, height: 620,
      style: { width: '100%', height: 'auto', display: 'block', borderRadius: 'var(--radius-sm)' },
    });

    root.appendChild(el('div.grid.split', {},
      card('Heading and attitude', { sub: 'Odometry yaw · chest IMU' },
        el('div.stack', { style: { alignItems: 'center' } },
          el('div.row', { style: { justifyContent: 'center', gap: '14px', flexWrap: 'wrap' } },
            compassHost, attitudeHost,
          ),
          readoutHost,
        ),
      ),
      card('Travelled path', {
        sub: 'Top-down, 1 m grid',
        actions: [
          (() => {
            const box = el('input', { type: 'checkbox', checked: true });
            box.addEventListener('change', () => { following = box.checked; drawTrail(); });
            return el('label.switch', {}, box, el('span.switch-track'),
                      el('span.switch-label', { text: 'Follow robot' }));
          })(),
        ],
        flush: true,
      }, trailCanvas),
    ));

    root.appendChild(note(
      'SLAM mapping and relocalization are not offered here: this firmware does not advertise the '
      + '/integrated_command topic or the GetStoredMapByName service, so there is nothing for the '
      + 'dashboard to call. Use the AGIBOT mobile app for map building on this unit.',
      'info',
    ));

    /* -- Trail rendering --------------------------------------------------- */

    const context = trailCanvas.getContext('2d');

    function drawTrail() {
      const width = trailCanvas.width;
      const height = trailCanvas.height;
      const styles = getComputedStyle(document.documentElement);
      const surface = styles.getPropertyValue('--surface-2').trim() || '#1b1e22';
      const gridColor = styles.getPropertyValue('--grid').trim() || '#22262c';
      const axis = styles.getPropertyValue('--axis').trim() || '#333941';
      const line = styles.getPropertyValue('--series-1').trim() || '#3987e5';
      const marker = styles.getPropertyValue('--series-2').trim() || '#d95926';
      const muted = styles.getPropertyValue('--text-3').trim() || '#6d7681';

      context.fillStyle = surface;
      context.fillRect(0, 0, width, height);

      if (!trail.length) {
        context.fillStyle = muted;
        context.font = '15px system-ui, sans-serif';
        context.textAlign = 'center';
        context.fillText('Waiting for odometry — walk the robot to draw a path',
                         width / 2, height / 2);
        return;
      }

      const xs = trail.map((p) => p.x);
      const ys = trail.map((p) => p.y);
      const last = trail.at(-1);

      const spanX = Math.max(2, Math.max(...xs) - Math.min(...xs));
      const spanY = Math.max(2, Math.max(...ys) - Math.min(...ys));
      const pad = 44;
      const scale = Math.min((width - pad * 2) / spanX, (height - pad * 2) / spanY, 110);

      const centreX = following ? last.x : (Math.min(...xs) + Math.max(...xs)) / 2;
      const centreY = following ? last.y : (Math.min(...ys) + Math.max(...ys)) / 2;

      // Robot X is forward and Y is left; canvas y grows downward, so flip it.
      const X = (x) => width / 2 + (x - centreX) * scale;
      const Y = (y) => height / 2 - (y - centreY) * scale;

      const step = scale >= 60 ? 1 : scale >= 25 ? 2 : 5;
      context.lineWidth = 1;
      const firstX = Math.floor(centreX - width / 2 / scale) - 1;
      const lastX = Math.ceil(centreX + width / 2 / scale) + 1;
      const firstY = Math.floor(centreY - height / 2 / scale) - 1;
      const lastY = Math.ceil(centreY + height / 2 / scale) + 1;

      for (let m = Math.ceil(firstX / step) * step; m <= lastX; m += step) {
        context.strokeStyle = m === 0 ? axis : gridColor;
        context.beginPath(); context.moveTo(X(m), 0); context.lineTo(X(m), height); context.stroke();
      }
      for (let m = Math.ceil(firstY / step) * step; m <= lastY; m += step) {
        context.strokeStyle = m === 0 ? axis : gridColor;
        context.beginPath(); context.moveTo(0, Y(m)); context.lineTo(width, Y(m)); context.stroke();
      }

      // Scale key.
      context.fillStyle = muted;
      context.font = '11px system-ui, sans-serif';
      context.textAlign = 'left';
      context.fillText(`${step} m grid`, 12, height - 12);

      if (trail.length > 1) {
        context.strokeStyle = line;
        context.lineWidth = 2;
        context.lineJoin = 'round';
        context.lineCap = 'round';
        context.beginPath();
        trail.forEach((point, index) => {
          const px = X(point.x), py = Y(point.y);
          if (index === 0) context.moveTo(px, py); else context.lineTo(px, py);
        });
        context.stroke();

        // Start marker with a surface ring so it stays legible over the line.
        const start = trail[0];
        context.beginPath();
        context.arc(X(start.x), Y(start.y), 5, 0, Math.PI * 2);
        context.fillStyle = surface;
        context.fill();
        context.strokeStyle = line;
        context.lineWidth = 2;
        context.stroke();
      }

      // Robot marker, pointing along its heading.
      context.save();
      context.translate(X(last.x), Y(last.y));
      context.rotate(-last.yaw);
      context.fillStyle = marker;
      context.strokeStyle = surface;
      context.lineWidth = 2.5;
      context.beginPath();
      context.moveTo(14, 0); context.lineTo(-9, 8); context.lineTo(-4, 0); context.lineTo(-9, -8);
      context.closePath();
      context.fill();
      context.stroke();
      context.restore();
    }

    /* -- Painting ---------------------------------------------------------- */

    let distance = 0;

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

      const odom = s.odom || { x: 0, y: 0, yaw: 0 };
      const vel = s.velocity || {};
      const chest = s.imu?.chest || {};

      const previous = trail.at(-1);
      const moved = !previous || Math.hypot(odom.x - previous.x, odom.y - previous.y) > 0.015;
      if (moved) {
        if (previous) distance += Math.hypot(odom.x - previous.x, odom.y - previous.y);
        trail.push({ x: odom.x, y: odom.y, yaw: odom.yaw });
        if (trail.length > 4000) trail.shift();
      } else if (previous) {
        previous.yaw = odom.yaw;
      }

      const speed = Math.hypot(vel.forward || 0, vel.lateral || 0);
      const yawDeg = ((odom.yaw || 0) * RAD2DEG + 360) % 360;

      setTile('x', num(odom.x, 2), 'forward positive', 'default', ' m');
      setTile('y', num(odom.y, 2), 'left positive', 'default', ' m');
      setTile('heading', num(yawDeg, 0), 'counter-clockwise positive', 'default', '°');
      setTile('speed', num(speed, 2), `yaw ${num(vel.angular, 2)} rad/s`, 'default', ' m/s');
      setTile('distance', num(distance, 1), 'since this tab opened', 'default', ' m');
      setTile('points', trail.length, 'samples held');

      const modeSpec = store.spec?.modes?.find((m) => m.id === s.mode);
      setTile('mode', modeSpec?.label || s.mode || '—', modeSpec?.group || '',
              modeSpec?.danger ? 'critical' : 'default');

      const stat = s.topic_stats?.['/aima/mc/leg_odometry'];
      const age = stat ? (Date.now() / 1000) - stat.last : null;
      const live = age !== null && age < 2;
      setTile('status', live ? 'Live' : 'Idle',
              live ? `${num(stat.hz, 0)} Hz` : 'no recent messages',
              live ? 'good' : 'default');

      compassHost.replaceChildren(compass(odom.yaw || 0, { size: 150 }));
      attitudeHost.replaceChildren(attitude(chest.roll || 0, chest.pitch || 0, { size: 150 }));

      readoutHost.replaceChildren(kv([
        ['Roll', `${num((chest.roll || 0) * RAD2DEG, 2)}°`],
        ['Pitch', `${num((chest.pitch || 0) * RAD2DEG, 2)}°`],
        ['Yaw (odometry)', `${num(yawDeg, 1)}°`],
        ['Forward', `${num(vel.forward, 3)} m/s`],
        ['Lateral', `${num(vel.lateral, 3)} m/s`],
        ['Yaw rate', `${num(vel.angular, 3)} rad/s`],
      ]));

      drawTrail();
    }

    paint();

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

    return { node: root, dispose: unsubscribe };
  },
};
