// Layout tab: drag/resize placement canvas for arranging widgets on the
// panel, like placing widgets on an Android home screen. Pointer events
// (not native HTML5 drag-and-drop, which has known touch
// inconsistencies) drive move/resize; every mutation is re-validated
// server-side (see routers/api_widgets.py) regardless of what this file
// already checked, so after any move/resize/add/remove this just
// reloads the canvas from the server's actual state rather than trusting
// an optimistic update -- simplest way to guarantee the canvas never
// drifts from what a rejected request left in place.
//
// Widget/canvas geometry is computed and applied in *pixels* from JS,
// not CSS percentages/aspect-ratio -- aspect-ratio isn't supported on
// every mobile browser this app gets viewed from, and a percentage
// height on the widget boxes silently collapses to 0 against an
// indeterminate-height ancestor on those browsers (the canvas would
// render with no visible size at all, which is exactly what happened
// before this was pixel-based).

let gridState = null; // last-loaded GET .../widgets response

// WIDGET_LABELS comes from common.js (shared with frame_config.js's
// button-assignment UI).

function renderControlBanner(control) {
  const banner = document.getElementById('control-banner');
  if (!banner) return;
  if (!control || control.you) {
    banner.style.display = 'none';
    return;
  }
  banner.style.display = 'flex';
  document.getElementById('control-holder').textContent = control.controller
    ? `${control.controller} currently has control of this frame.`
    : 'Nobody has control of this frame yet.';
}

async function takeControl() {
  try {
    const resp = await fetch(`${window.FRAME_API}/take-control`, { method: 'POST' });
    if (!resp.ok) throw new Error(await apiError(resp));
    showStatus(true, 'You have control now.');
    loadWidgets();
  } catch (e) {
    showStatus(false, e.message);
  }
}
document.getElementById('take-control').addEventListener('click', takeControl);

// Cached each time the canvas is (re)laid out (see layoutCanvas) so drag
// math doesn't re-measure the DOM on every pointermove.
let canvasMetrics = { width: 0, height: 0, cellW: 0, cellH: 0 };

function layoutCanvas() {
  if (!gridState) return;
  const wrap = document.getElementById('widget-canvas-wrap');
  const canvas = document.getElementById('widget-canvas');
  const cols = gridState.grid.cols, rows = gridState.grid.rows;

  // wrap's own width comes from ordinary CSS (100% of the card, capped
  // at max-width) -- only its height is JS-driven, from that measured
  // width, to keep the grid's aspect ratio without relying on the CSS
  // aspect-ratio property.
  const width = wrap.getBoundingClientRect().width;
  const height = width * (rows / cols);
  wrap.style.height = height + 'px';
  canvas.style.width = width + 'px';
  canvas.style.height = height + 'px';

  const cellW = width / cols, cellH = height / rows;
  canvasMetrics = { width, height, cellW, cellH };
  wrap.style.backgroundImage =
    `linear-gradient(to right, var(--border) 1px, transparent 1px),` +
    `linear-gradient(to bottom, var(--border) 1px, transparent 1px)`;
  wrap.style.backgroundSize = `${cellW}px ${cellH}px`;

  for (const box of canvas.children) {
    positionBox(box, box._rect);
  }
}

function positionBox(box, rect) {
  box._rect = rect;
  const { cellW, cellH } = canvasMetrics;
  box.style.left = (rect.x * cellW) + 'px';
  box.style.top = (rect.y * cellH) + 'px';
  box.style.width = (rect.w * cellW) + 'px';
  box.style.height = (rect.h * cellH) + 'px';
}

function startDrag(e, widget, box, isResize) {
  e.preventDefault();
  box.setPointerCapture(e.pointerId);
  const { cellW, cellH } = canvasMetrics;
  const startX = e.clientX, startY = e.clientY;
  const orig = { x: widget.x, y: widget.y, w: widget.w, h: widget.h };
  const cols = gridState.grid.cols, rows = gridState.grid.rows;
  const minFootprint = gridState.min_footprint[widget.widget_type] || [1, 1];
  let pending = null;

  box.classList.add('dragging');

  function onMove(ev) {
    const dxCells = Math.round((ev.clientX - startX) / cellW);
    const dyCells = Math.round((ev.clientY - startY) / cellH);
    const next = { ...orig };
    if (isResize) {
      next.w = Math.max(minFootprint[0], Math.min(cols - orig.x, orig.w + dxCells));
      next.h = Math.max(minFootprint[1], Math.min(rows - orig.y, orig.h + dyCells));
    } else {
      next.x = Math.max(0, Math.min(cols - orig.w, orig.x + dxCells));
      next.y = Math.max(0, Math.min(rows - orig.h, orig.y + dyCells));
    }
    pending = next;
    positionBox(box, next);
  }

  function onUp() {
    box.removeEventListener('pointermove', onMove);
    box.removeEventListener('pointerup', onUp);
    box.classList.remove('dragging');
    if (pending && (pending.x !== orig.x || pending.y !== orig.y || pending.w !== orig.w || pending.h !== orig.h)) {
      moveWidget(widget.id, pending);
    }
  }

  box.addEventListener('pointermove', onMove);
  box.addEventListener('pointerup', onUp);
}

async function moveWidget(id, rect) {
  try {
    const resp = await fetch(`${window.FRAME_API}/widgets/${id}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(rect),
    });
    if (!resp.ok) throw new Error(await apiError(resp));
    showStatus(true, 'Saved.');
  } catch (e) {
    showStatus(false, e.message);
  } finally {
    // Reload either way: reverts the box to its real position if the
    // move was rejected (e.g. it would've overlapped another widget),
    // confirms it otherwise. Simpler and more robust than trying to
    // separately handle "revert on failure" vs. "confirm on success".
    loadWidgets();
  }
}

async function removeWidget(id) {
  if (!confirm('Remove this widget? Its own settings will be lost.')) return;
  try {
    const resp = await fetch(`${window.FRAME_API}/widgets/${id}`, { method: 'DELETE' });
    if (!resp.ok) throw new Error(await apiError(resp));
    showStatus(true, 'Removed.');
  } catch (e) {
    showStatus(false, e.message);
  } finally {
    loadWidgets();
  }
}

async function clearAllWidgets() {
  const count = gridState ? gridState.widgets.length : 0;
  if (!count) return;
  const noun = count === 1 ? 'widget' : 'widgets';
  if (!confirm(`Remove all ${count} ${noun} from this frame and start over? Their settings will be lost.`)) return;
  try {
    const resp = await fetch(`${window.FRAME_API}/widgets`, { method: 'DELETE' });
    if (!resp.ok) throw new Error(await apiError(resp));
    showStatus(true, 'Cleared.');
  } catch (e) {
    showStatus(false, e.message);
  } finally {
    loadWidgets();
  }
}
document.getElementById('clear-all-widgets').addEventListener('click', clearAllWidgets);

async function addWidget(widgetType) {
  try {
    const resp = await fetch(`${window.FRAME_API}/widgets`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ widget_type: widgetType }),
    });
    if (!resp.ok) throw new Error(await apiError(resp));
    showStatus(true, `${WIDGET_LABELS[widgetType] || widgetType} widget added.`);
  } catch (e) {
    showStatus(false, e.message);
  } finally {
    loadWidgets();
  }
}

function renderCanvas() {
  const canvas = document.getElementById('widget-canvas');
  canvas.innerHTML = '';
  document.getElementById('widget-canvas-empty-hint').style.display = gridState.widgets.length ? 'none' : '';
  document.getElementById('clear-all-widgets').disabled = !gridState.widgets.length;

  for (const widget of gridState.widgets) {
    const box = document.createElement('div');
    box.className = 'widget-box';
    box.dataset.widgetType = widget.widget_type;
    box._rect = widget;

    const label = document.createElement('span');
    label.className = 'widget-box-label';
    label.textContent = WIDGET_LABELS[widget.widget_type] || widget.widget_type;
    box.appendChild(label);

    const settingsBtn = document.createElement('button');
    settingsBtn.type = 'button';
    settingsBtn.className = 'widget-box-settings';
    settingsBtn.textContent = '⚙';
    settingsBtn.title = `${WIDGET_LABELS[widget.widget_type] || widget.widget_type} settings`;
    settingsBtn.addEventListener('pointerdown', (e) => e.stopPropagation());
    settingsBtn.addEventListener('click', (e) => { e.stopPropagation(); openWidgetDialog(widget); });
    box.appendChild(settingsBtn);

    const removeBtn = document.createElement('button');
    removeBtn.type = 'button';
    removeBtn.className = 'widget-box-remove';
    removeBtn.textContent = '×';
    removeBtn.title = 'Remove this widget';
    removeBtn.addEventListener('pointerdown', (e) => e.stopPropagation());
    removeBtn.addEventListener('click', (e) => { e.stopPropagation(); removeWidget(widget.id); });
    box.appendChild(removeBtn);

    const handle = document.createElement('div');
    handle.className = 'widget-box-resize-handle';
    handle.addEventListener('pointerdown', (e) => { e.stopPropagation(); startDrag(e, widget, box, true); });
    box.appendChild(handle);

    box.addEventListener('pointerdown', (e) => startDrag(e, widget, box, false));

    canvas.appendChild(box);
  }
  layoutCanvas();
}

function renderAddButtons() {
  const container = document.getElementById('add-widget-buttons');
  container.innerHTML = '';
  for (const type of gridState.widget_types) {
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.className = 'secondary';
    btn.textContent = `+ ${WIDGET_LABELS[type] || type}`;
    btn.addEventListener('click', () => addWidget(type));
    container.appendChild(btn);
  }
  const hint = document.getElementById('add-widget-hint');
  hint.textContent = 'A new widget is placed in the first open space that fits it -- drag it afterward to reposition.';
}

async function loadWidgets() {
  try {
    const resp = await fetch(`${window.FRAME_API}/widgets`);
    if (!resp.ok) throw new Error(await apiError(resp));
    gridState = await resp.json();
    renderCanvas();
    renderAddButtons();
    renderControlBanner(gridState.control);
  } catch (e) {
    showStatus(false, e.message);
  }
}

let resizeTimer = null;
window.addEventListener('resize', () => {
  clearTimeout(resizeTimer);
  resizeTimer = setTimeout(layoutCanvas, 100);
});

// --- gear-icon dialog: each widget's own settings, fetched as an HTML
// fragment (routers/frame_pages.py's widget_dialog) and injected into a
// single shared <dialog>, rather than a separate page per widget type --
// a frame can now have several widgets of the same type, so "the
// Calendar tab" stopped meaning anything unambiguous.

// widget_dialog_{photos,calendar,whiteboard,tasks}.js each define an
// init<Type>Dialog()/close<Type>Dialog() pair (loaded unconditionally by
// frame_layout.html, since which one runs depends on which widget's gear
// icon was clicked).
const DIALOG_INIT = {
  photos: initPhotosDialog, calendar: initCalendarDialog, whiteboard: initWhiteboardDialog,
  tasks: initTasksDialog, static: initStaticDialog, text: initTextDialog, weather: initWeatherDialog,
};
const DIALOG_CLOSE = {
  photos: closePhotosDialog, calendar: closeCalendarDialog, whiteboard: closeWhiteboardDialog,
  tasks: closeTasksDialog, static: closeStaticDialog, text: closeTextDialog, weather: closeWeatherDialog,
};

let openDialogWidgetType = null;

async function openWidgetDialog(widget) {
  const dialogEl = document.getElementById('widget-dialog');
  const bodyEl = document.getElementById('widget-dialog-body');
  bodyEl.innerHTML = '<p class="sub">Loading...</p>';
  dialogEl.querySelector('.dialog-result').innerHTML = ''; // clear any message left over from a previous dialog
  openDialogWidgetType = widget.widget_type;
  dialogEl.showModal();
  try {
    const resp = await fetch(`/frames/${window.FRAME_ID}/widgets/${widget.id}/dialog`);
    if (!resp.ok) throw new Error(await apiError(resp));
    bodyEl.innerHTML = await resp.text();
    // Every dialog script's fetch calls use window.FRAME_API as their
    // base -- repointing it at this specific widget (instead of the
    // frame-level window.FRAME_BASE_API) is what makes the SAME
    // widget_dialog_photos.js/queue.js/etc. code work correctly no
    // matter which widget's dialog is currently open. Restored on close.
    window.FRAME_API = `${window.FRAME_BASE_API}/widgets/${widget.id}`;
    const init = DIALOG_INIT[widget.widget_type];
    if (init) init();
  } catch (e) {
    bodyEl.innerHTML = `<p class="sub">Could not load: ${e.message}</p>`;
  }
}

document.getElementById('widget-dialog-close').addEventListener('click', () => {
  document.getElementById('widget-dialog').close();
});

// Native <dialog> doesn't close on backdrop click by default -- a click
// that lands outside the dialog's own box (but is still technically
// "on" the dialog element, since the backdrop is part of it) counts as
// a backdrop click.
document.getElementById('widget-dialog').addEventListener('click', (e) => {
  const dialogEl = e.currentTarget;
  if (e.target !== dialogEl) return; // click landed on dialog content, not the backdrop
  const rect = dialogEl.getBoundingClientRect();
  const inside = e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
  if (!inside) dialogEl.close();
});

document.getElementById('widget-dialog').addEventListener('close', () => {
  const close = DIALOG_CLOSE[openDialogWidgetType];
  if (close) close();
  openDialogWidgetType = null;
  window.FRAME_API = window.FRAME_BASE_API;
  document.getElementById('widget-dialog-body').innerHTML = '';
});

loadWidgets();
