/**
 * gimbal_panel.js — Saltybot Gimbal Control Panel (Issue #551)
 *
 * Connects to rosbridge via WebSocket (roslib.js).
 * Publishes pan/tilt commands, subscribes to gimbal state + camera.
 *
 * ROS topics:
 *   /gimbal/cmd              geometry_msgs/Vector3   publish  (x=pan°, y=tilt°)
 *   /gimbal/state            geometry_msgs/Vector3   subscribe (x=pan°, y=tilt°)
 *   /gimbal/tracking_enabled std_msgs/Bool           publish
 *   /camera/front/image_raw/compressed  sensor_msgs/CompressedImage  subscribe
 */

'use strict';

// ── Config ────────────────────────────────────────────────────────────────────

const PAN_MIN   = -180;  // degrees
const PAN_MAX   =  180;
const TILT_MIN  =  -45;  // degrees
const TILT_MAX  =   90;
const CMD_TOPIC      = '/gimbal/cmd';
const STATE_TOPIC    = '/gimbal/state';
const TRACKING_TOPIC = '/gimbal/tracking_enabled';
const CAM_TOPIC      = '/camera/front/image_raw/compressed';
const CAM_TOPICS = {
  front: '/camera/front/image_raw/compressed',
  rear:  '/camera/rear/image_raw/compressed',
  left:  '/camera/left/image_raw/compressed',
  right: '/camera/right/image_raw/compressed',
};

const PRESETS = [
  { name: 'CENTER', pan:   0, tilt:  0 },
  { name: 'LEFT',   pan: -45, tilt:  0 },
  { name: 'RIGHT',  pan:  45, tilt:  0 },
  { name: 'UP',     pan:   0, tilt: 45 },
  { name: 'DOWN',   pan:   0, tilt:-30 },
  { name: 'HOME',   pan:   0, tilt:  0 },
];

// ── State ─────────────────────────────────────────────────────────────────────

let ros           = null;
let cmdTopic      = null;
let trackingTopic = null;
let stateSub      = null;
let camSub        = null;

let currentPan    = 0;
let currentTilt   = 0;
let targetPan     = 0;
let targetTilt    = 0;
let trackingOn    = false;
let selectedCam   = 'front';

// Camera FPS tracking
let fpsCount = 0;
let fpsTime  = Date.now();
let fps      = 0;

// Pad dragging state
let padDragging = false;

// ── DOM refs ──────────────────────────────────────────────────────────────────

const connDot    = document.getElementById('conn-dot');
const connStatus = document.getElementById('conn-status');
const wsInput    = document.getElementById('ws-input');
const padCanvas  = document.getElementById('gimbal-pad');
const panVal     = document.getElementById('pan-val');
const tiltVal    = document.getElementById('tilt-val');
const camImg     = document.getElementById('camera-img');
const noSignal   = document.getElementById('no-signal');
const fpsBadge   = document.getElementById('fps-badge');
const angleOvl   = document.getElementById('angle-overlay');

// ── Connection ────────────────────────────────────────────────────────────────

function connect() {
  const url = wsInput.value.trim();
  if (!url) return;
  if (ros) { ros.close(); }

  ros = new ROSLIB.Ros({ url });

  ros.on('connection', () => {
    connDot.className = 'connected';
    connStatus.textContent = url;
    document.getElementById('btn-connect').textContent = 'RECONNECT';
    setupTopics();
  });

  ros.on('error', (err) => {
    connDot.className = 'error';
    connStatus.textContent = 'ERROR: ' + (err?.message || err);
  });

  ros.on('close', () => {
    connDot.className = '';
    connStatus.textContent = 'DISCONNECTED';
    teardownTopics();
  });
}

function setupTopics() {
  // Publish: /gimbal/cmd
  cmdTopic = new ROSLIB.Topic({
    ros, name: CMD_TOPIC,
    messageType: 'geometry_msgs/Vector3',
  });

  // Publish: /gimbal/tracking_enabled
  trackingTopic = new ROSLIB.Topic({
    ros, name: TRACKING_TOPIC,
    messageType: 'std_msgs/Bool',
  });

  // Subscribe: /gimbal/state
  stateSub = new ROSLIB.Topic({
    ros, name: STATE_TOPIC,
    messageType: 'geometry_msgs/Vector3',
    throttle_rate: 100,
  });
  stateSub.subscribe((msg) => {
    currentPan  = msg.x ?? 0;
    currentTilt = msg.y ?? 0;
    updateAngleDisplay();
    drawPad();
  });

  // Subscribe: camera
  subscribeCamera(selectedCam);
}

function teardownTopics() {
  if (stateSub)  { stateSub.unsubscribe();  stateSub  = null; }
  if (camSub)    { camSub.unsubscribe();    camSub    = null; }
  cmdTopic = trackingTopic = null;
}

function subscribeCamera(camId) {
  if (camSub) { camSub.unsubscribe(); camSub = null; }
  const topic = CAM_TOPICS[camId] ?? CAM_TOPICS.front;
  if (!ros) return;
  camSub = new ROSLIB.Topic({
    ros, name: topic,
    messageType: 'sensor_msgs/CompressedImage',
    throttle_rate: 50,
  });
  camSub.subscribe((msg) => {
    const fmt = msg.format?.includes('png') ? 'png' : 'jpeg';
    camImg.src = `data:image/${fmt};base64,${msg.data}`;
    camImg.classList.add('visible');
    noSignal.style.display = 'none';
    fpsBadge.classList.add('visible');
    angleOvl.classList.add('visible');

    // FPS
    fpsCount++;
    const now = Date.now();
    if (now - fpsTime >= 1000) {
      fps = Math.round(fpsCount * 1000 / (now - fpsTime));
      fpsCount = 0; fpsTime = now;
      fpsBadge.textContent = fps + ' fps';
    }
  });
}

// ── Publish helpers ───────────────────────────────────────────────────────────

function publishCmd(pan, tilt) {
  if (!cmdTopic) return;
  cmdTopic.publish(new ROSLIB.Message({ x: pan, y: tilt, z: 0.0 }));
}

function publishTracking(enabled) {
  if (!trackingTopic) return;
  trackingTopic.publish(new ROSLIB.Message({ data: enabled }));
}

// ── Gimbal pad drawing ────────────────────────────────────────────────────────

function drawPad() {
  const canvas = padCanvas;
  const ctx = canvas.getContext('2d');
  const W = canvas.width;
  const H = canvas.height;

  ctx.clearRect(0, 0, W, H);

  // Background
  ctx.fillStyle = '#0a0a1a';
  ctx.fillRect(0, 0, W, H);

  // Grid lines
  ctx.strokeStyle = '#1e3a5f';
  ctx.lineWidth = 1;
  // Vertical lines
  for (let i = 1; i < 4; i++) {
    ctx.beginPath();
    ctx.moveTo(W * i / 4, 0); ctx.lineTo(W * i / 4, H);
    ctx.stroke();
  }
  // Horizontal lines
  for (let i = 1; i < 4; i++) {
    ctx.beginPath();
    ctx.moveTo(0, H * i / 4); ctx.lineTo(W, H * i / 4);
    ctx.stroke();
  }

  // Center cross
  ctx.strokeStyle = '#374151';
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.moveTo(W / 2, 0); ctx.lineTo(W / 2, H);
  ctx.moveTo(0, H / 2); ctx.lineTo(W, H / 2);
  ctx.stroke();

  // Helpers to map degrees ↔ canvas coords
  const panToX  = (p) => ((p - PAN_MIN)  / (PAN_MAX  - PAN_MIN))  * W;
  const tiltToY = (t) => (1 - (t - TILT_MIN) / (TILT_MAX - TILT_MIN)) * H;

  // Target position (what we're commanding)
  const tx = panToX(targetPan);
  const ty = tiltToY(targetTilt);

  // Line from center to target
  ctx.strokeStyle = 'rgba(6,182,212,0.3)';
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.moveTo(W / 2, H / 2);
  ctx.lineTo(tx, ty);
  ctx.stroke();

  // Target ring
  ctx.strokeStyle = '#0891b2';
  ctx.lineWidth = 1.5;
  ctx.beginPath();
  ctx.arc(tx, ty, 10, 0, Math.PI * 2);
  ctx.stroke();

  // Current position (feedback)
  const cx_ = panToX(currentPan);
  const cy_ = tiltToY(currentTilt);
  ctx.fillStyle = '#22d3ee';
  ctx.beginPath();
  ctx.arc(cx_, cy_, 6, 0, Math.PI * 2);
  ctx.fill();

  // Labels: axis limits
  ctx.fillStyle = '#374151';
  ctx.font = '9px Courier New';
  ctx.fillText(`${PAN_MIN}°`, 2, H / 2 - 3);
  ctx.fillText(`${PAN_MAX}°`, W - 24, H / 2 - 3);
  ctx.fillText(`${TILT_MAX}°`, W / 2 + 3, 11);
  ctx.fillText(`${TILT_MIN}°`, W / 2 + 3, H - 3);

  // Current angle text
  ctx.fillStyle = '#0891b2';
  ctx.font = 'bold 9px Courier New';
  ctx.fillText(`PAN ${targetPan.toFixed(0)}°  TILT ${targetTilt.toFixed(0)}°`, 4, H - 3);
}

// ── Pad interaction ───────────────────────────────────────────────────────────

function padCoordsToAngles(clientX, clientY) {
  const rect = padCanvas.getBoundingClientRect();
  const rx = (clientX - rect.left) / rect.width;
  const ry = (clientY - rect.top)  / rect.height;
  const pan  = PAN_MIN  + rx * (PAN_MAX  - PAN_MIN);
  const tilt = TILT_MAX - ry * (TILT_MAX - TILT_MIN);
  return {
    pan:  Math.max(PAN_MIN,  Math.min(PAN_MAX,  pan)),
    tilt: Math.max(TILT_MIN, Math.min(TILT_MAX, tilt)),
  };
}

function padPointerDown(e) {
  padDragging = true;
  padCanvas.setPointerCapture(e.pointerId);
  const { pan, tilt } = padCoordsToAngles(e.clientX, e.clientY);
  setTarget(pan, tilt);
}

function padPointerMove(e) {
  if (!padDragging) return;
  const { pan, tilt } = padCoordsToAngles(e.clientX, e.clientY);
  setTarget(pan, tilt);
}

function padPointerUp() {
  padDragging = false;
}

padCanvas.addEventListener('pointerdown',   padPointerDown);
padCanvas.addEventListener('pointermove',   padPointerMove);
padCanvas.addEventListener('pointerup',     padPointerUp);
padCanvas.addEventListener('pointercancel', padPointerUp);

// Touch passthrough
padCanvas.addEventListener('touchstart',  (e) => e.preventDefault(), { passive: false });
padCanvas.addEventListener('touchmove',   (e) => e.preventDefault(), { passive: false });

// ── Target update ─────────────────────────────────────────────────────────────

function setTarget(pan, tilt) {
  targetPan  = Math.round(pan  * 10) / 10;
  targetTilt = Math.round(tilt * 10) / 10;
  updateAngleDisplay();
  drawPad();
  publishCmd(targetPan, targetTilt);
}

function updateAngleDisplay() {
  panVal.textContent  = currentPan.toFixed(1);
  tiltVal.textContent = currentTilt.toFixed(1);
  angleOvl.textContent = `PAN ${currentPan.toFixed(1)}°  TILT ${currentTilt.toFixed(1)}°`;
}

// ── Presets ───────────────────────────────────────────────────────────────────

function applyPreset(pan, tilt) {
  setTarget(pan, tilt);
}

function goHome() {
  setTarget(0, 0);
}

// ── Tracking toggle ───────────────────────────────────────────────────────────

document.getElementById('tracking-toggle').addEventListener('change', (e) => {
  trackingOn = e.target.checked;
  publishTracking(trackingOn);
});

// ── Camera selector ───────────────────────────────────────────────────────────

document.querySelectorAll('.cam-btn').forEach((btn) => {
  btn.addEventListener('click', () => {
    selectedCam = btn.dataset.cam;
    document.querySelectorAll('.cam-btn').forEach((b) => b.classList.remove('active'));
    btn.classList.add('active');
    subscribeCamera(selectedCam);
    // Reset fps display
    camImg.classList.remove('visible');
    noSignal.style.display = '';
    fpsBadge.classList.remove('visible');
    angleOvl.classList.remove('visible');
    fpsCount = 0; fpsTime = Date.now();
  });
});

// ── Connect button ────────────────────────────────────────────────────────────

document.getElementById('btn-connect').addEventListener('click', connect);
wsInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') connect(); });

// ── Preset buttons ────────────────────────────────────────────────────────────

document.querySelectorAll('[data-preset]').forEach((btn) => {
  const idx = parseInt(btn.dataset.preset, 10);
  const p   = PRESETS[idx];
  btn.addEventListener('click', () => applyPreset(p.pan, p.tilt));
});

document.getElementById('btn-home').addEventListener('click', goHome);

// ── Init ──────────────────────────────────────────────────────────────────────

// Size canvas to fill its container initially
function resizePad() {
  const w = padCanvas.offsetWidth || 200;
  padCanvas.width  = w;
  padCanvas.height = Math.round(w * 0.75);   // 4:3 aspect
  drawPad();
}

window.addEventListener('resize', resizePad);
resizePad();

// Auto-connect on load using stored URL or default
const storedUrl = localStorage.getItem('gimbal_ws_url');
if (storedUrl) wsInput.value = storedUrl;
wsInput.addEventListener('change', () => {
  localStorage.setItem('gimbal_ws_url', wsInput.value);
});

// Initial draw
drawPad();
updateAngleDisplay();
