// /public/videoService.js
// Client-side helper for consuming binary JPEG frames over WebSocket,
// decoding with createImageBitmap, and drawing to a canvas with RAF.
// For /ws/video0, supports sending control messages as JSON text.

(function () {
  class FrameRenderer {
    constructor(canvas) {
      this.canvas = canvas;
      this.ctx = canvas.getContext('2d', { alpha: false, desynchronized: true, willReadFrequently: false });
      this.queue = [];
      this.maxQueue = 2; // keep at most 2 pending frames to limit latency
      this._rafId = null;
      this._running = false;
      this._lastDraw = 0;
    }
    enqueue(bitmap) {
      if (this.queue.length >= this.maxQueue) {
        // drop older frame to keep latency low
        const old = this.queue.shift();
        if (old && 'close' in old) try { old.close(); } catch {}
      }
      this.queue.push(bitmap);
      if (!this._running) this.start();
    }
    start() {
      if (this._running) return;
      this._running = true;
      const loop = (ts) => {
        if (!this._running) return;
        const frame = this.queue.shift();
        if (frame) {
          const w = this.canvas.width, h = this.canvas.height;
          this.ctx.drawImage(frame, 0, 0, w, h);
          if ('close' in frame) try { frame.close(); } catch {}
          this._lastDraw = ts;
        }
        this._rafId = requestAnimationFrame(loop);
      };
      this._rafId = requestAnimationFrame(loop);
    }
    stop() {
      this._running = false;
      if (this._rafId) cancelAnimationFrame(this._rafId);
      this._rafId = null;
      // cleanup queued frames
      while (this.queue.length) {
        const f = this.queue.shift();
        if (f && 'close' in f) try { f.close(); } catch {}
      }
      // Clear canvas
      this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    }
  }

  function prettyStatus(el, state, extra = '') {
    const color = {
      connecting: '#9eb8ff',
      open: '#8cffbf',
      closed: '#ff9e9e',
      error: '#ffb167'
    }[state] || '#e7eaf6';
    el.innerHTML = `<span style="color:${color}">${state.toUpperCase()}</span>${extra ? ' — ' + extra : ''}`;
  }

  async function blobToBitmap(blob) {
    // createImageBitmap is widely supported; fallback to <img> if needed
    return createImageBitmap(blob);
  }

  function parseWH(value) {
    if (!value) return null;
    const [w, h] = String(value).split('x').map(Number);
    if (!w || !h) return null;
    return { width: w, height: h };
  }

  function attachStream({ url, canvas, statusEl, control }) {
    const renderer = new FrameRenderer(canvas);
    let ws;
    let reconnectDelay = 1000;
    let closedOnPurpose = false;

    function connect() {
      prettyStatus(statusEl, 'connecting', url);
      ws = new WebSocket(url);
      ws.binaryType = 'blob';

      ws.onopen = () => {
        reconnectDelay = 1000;
        prettyStatus(statusEl, 'open', 'streaming…');
        // On connection, fetch the latest snapshot metadata so the UI can show
        // the freshest overlay and CSV (prevents showing an old image cached in index.html).
        if (control?.snapshotOutEl) {
          fetch('/snapshots/latest', { cache: 'no-store' })
            .then(r => r.ok ? r.json() : Promise.reject(new Error('no snapshot'))) 
            .then((j) => {
              if (!j?.ok) return;
              const overlayPNG = (j.overlay ? `${j.overlay}?_t=${Date.now()}` : null);
              const overlayCSV = (j.overlayCSV ? `${j.overlayCSV}?_t=${Date.now()}` : null);
              if (overlayPNG) {
                document.getElementById('overlayImg').src = overlayPNG;
              }
              if (window.readCSV && overlayCSV) {
                window.readCSV.renderCSV('csvTable', overlayCSV);
              }
              // Show links to the latest snapshot in the snapshot output box (non-intrusive)
              control.snapshotOutEl.innerHTML = `Latest snapshot: <a href="${j.url}" target="_blank" rel="noopener">${j.url}</a>`;
            }).catch(() => {
              // ignore — no latest snapshot yet
            });
        }
      };

      ws.onmessage = async (evt) => {
        if (typeof evt.data === 'string') {
          // Control/meta message
          try {
            const msg = JSON.parse(evt.data);
            if (msg.type === 'error') {
              console.warn('Server error:', msg.error);
              prettyStatus(statusEl, 'error', msg.error);
            }
            if (msg.type === 'snapshot' && control?.snapshotOutEl) {
              if (msg.ok && msg.url && msg.urlApp) {
                // Use overlay URL and CSV provided by the server (fallback to expected names)
                const overlayPNG = msg.overlay || msg.urlApp.replace('_annotated.jpg','_two_cam_overlay.png');
                // Prefer server-provided overlayCSV. If not present, derive from the original jpg name
                const overlayCSV = msg.overlayCSV || msg.url.replace('.jpg','_two_cam.csv');
                
                control.snapshotOutEl.innerHTML =
                    `Snapshot: <a href="${msg.url}" target="_blank" rel="noopener">${msg.url}</a></br></br>  ` +
                    ` Recognized: <a href="${msg.urlApp}" target="_blank" rel="noopener">${msg.urlApp}</a></br>  ` +
                    ` Overlay: <a href="${overlayPNG}" target="_blank" rel="noopener">PNG Overlay</a>`;
                // Update overlay immediately — server now ensures files are ready before responding
                document.getElementById('overlayImg').src = overlayPNG;
                // Render CSV values into the csvTable container (if available)
                if (window.readCSV) {
                  window.readCSV.renderCSV('csvTable', overlayCSV);
                }
              } else {
                control.snapshotOutEl.textContent = 'Snapshot failed';
              }
            }
          } catch {
            // ignore non-JSON text frames
          }
          return;
        }
        // Binary JPEG frame
        const blob = evt.data; // Blob of image/jpeg
        try {
          const bmp = await blobToBitmap(blob);
          renderer.enqueue(bmp);
        } catch (err) {
          console.warn('Bitmap decode failed:', err);
        }
      };

      ws.onerror = () => {
        prettyStatus(statusEl, 'error', 'network error');
      };

      ws.onclose = () => {
        renderer.stop();
        prettyStatus(statusEl, 'closed', closedOnPurpose ? 'by client' : 'retrying…');
        if (!closedOnPurpose) {
          setTimeout(connect, reconnectDelay);
          reconnectDelay = Math.min(reconnectDelay * 2, 10000);
        }
      };
    }

    connect();

    // Attach controls if provided (video0)
    if (control) {
      const { resSelect, fpsSelect, qSelect, applyBtn, snapshotBtn, startBtn, stopBtn, snapshotOutEl } = control;

      applyBtn?.addEventListener('click', () => {
        if (!ws || ws.readyState !== WebSocket.OPEN) return;
        const wh = parseWH(resSelect?.value);
        const fps = Number(fpsSelect?.value || 15);
        const q = Number(qSelect?.value || 5);
        ws.send(JSON.stringify({
          type: 'control',
          action: 'setParams',
          params: {
            ...(wh || {}),
            fps,
            quality: q
          }
        }));
      });

      snapshotBtn?.addEventListener('click', () => {
        if (!ws || ws.readyState !== WebSocket.OPEN) return;
        snapshotOutEl.textContent = 'Taking snapshot…';
        ws.send(JSON.stringify({ type: 'control', action: 'snapshot' }));
      });

      startBtn?.addEventListener('click', () => {
        if (!ws || ws.readyState !== WebSocket.OPEN) return;
        ws.send(JSON.stringify({ type: 'control', action: 'start' }));
      });

      stopBtn?.addEventListener('click', () => {
        if (!ws || ws.readyState !== WebSocket.OPEN) return;
        ws.send(JSON.stringify({ type: 'control', action: 'stop' }));
      });
    }

    return {
      close: () => { closedOnPurpose = true; ws?.close(); renderer.stop(); }
    };
  }

  window.VideoService = {
    attachStream
  };
})();