import { h } from 'preact';
import { useRef, useEffect, useState } from 'preact/hooks';
import type { Interface, Widget, SignalRef } from './lib/types';
import TextView from './widgets/TextView';
import TextLabel from './widgets/TextLabel';
import Gauge from './widgets/Gauge';
import BarH from './widgets/BarH';
import BarV from './widgets/BarV';
import Led from './widgets/Led';
import MultiLed from './widgets/MultiLed';
import SetValue from './widgets/SetValue';
import Toggle from './widgets/Toggle';
import Button from './widgets/Button';
import PlotWidget from './widgets/PlotWidget';
import ImageWidget from './widgets/ImageWidget';
import LinkWidget from './widgets/LinkWidget';
import Container from './widgets/Container';
import TableWidget from './widgets/TableWidget';
import WidgetTypePicker from './WidgetTypePicker';
import { centreInside, widgetTab, tabPanes, withContainedWidgets } from './lib/containers';

const COMPONENTS: Record<string, any> = {
  textview: TextView, textlabel: TextLabel, gauge: Gauge,
  barh: BarH, barv: BarV, led: Led, multiled: MultiLed,
  setvalue: SetValue, toggle: Toggle, button: Button, plot: PlotWidget,
  image: ImageWidget, link: LinkWidget, container: Container,
  table: TableWidget,
};

// Container panes are decorative frames that other widgets sit over, so they
// must paint behind everything else. Render them first (their relative order
// preserved) regardless of their position in the widget array.
function renderOrder(widgets: Widget[]): Widget[] {
  const containers = widgets.filter(w => w.type === 'container');
  const rest = widgets.filter(w => w.type !== 'container');
  return [...containers, ...rest];
}

export const DEFAULT_SIZES: Record<string, [number, number]> = {
  textview:  [200, 50],
  textlabel: [150, 36],
  gauge:     [120, 120],
  barh:      [200, 60],
  barv:      [60,  160],
  led:       [60,  60],
  multiled:  [200, 80],
  setvalue:  [200, 60],
  toggle:    [140, 50],
  button:    [120, 50],
  plot:      [400, 200],
  image:     [200, 150],
  link:      [140, 50],
  configselect: [220, 50],
  container: [320, 220],
  table:     [280, 160],
};

// Widget types that support multiple signals (signal can be appended)
const MULTI_SIGNAL_TYPES = new Set(['plot', 'multiled', 'table']);
// Widget types that have exactly one signal (signal can be replaced)
const SINGLE_SIGNAL_TYPES = new Set(['textview', 'gauge', 'barh', 'barv', 'led', 'setvalue', 'toggle', 'button']);

interface PickerState {
  visible: boolean;
  x: number;
  y: number;
  canvasX: number;
  canvasY: number;
  signal: SignalRef | null;
}

interface SignalDropMenuState {
  visible: boolean;
  x: number;
  y: number;
  signal: SignalRef;
  targetWidget: Widget;
}

interface DragState {
  startMouseX: number;
  startMouseY: number;
  startPositions: Array<{ id: string; x: number; y: number }>;
}

interface ResizeState {
  widgetId: string;
  dir: string;
  startMouseX: number;
  startMouseY: number;
  startX: number;
  startY: number;
  startW: number;
  startH: number;
}

interface RubberBand {
  active: boolean;
  startX: number; startY: number;
  curX: number; curY: number;
  clientStartX: number; clientStartY: number;
}

interface Props {
  iface: Interface;
  selectedIds: string[];
  onSelect: (ids: string[]) => void;
  onChange: (widgets: Widget[]) => void;
  snapGrid: number;
}

let _widgetSeq = Date.now();
export function genWidgetId() { return `w${_widgetSeq++}`; }

function snapVal(v: number, grid: number) {
  return grid > 0 ? Math.round(v / grid) * grid : Math.round(v);
}

function rectsIntersect(
  wx: number, wy: number, ww: number, wh: number,
  rx: number, ry: number, rw: number, rh: number,
): boolean {
  return wx < rx + rw && wx + ww > rx && wy < ry + rh && wy + wh > ry;
}

export default function EditCanvas({ iface, selectedIds, onSelect, onChange, snapGrid }: Props) {
  const containerRef = useRef<HTMLDivElement>(null);

  const ifaceRef = useRef(iface);
  ifaceRef.current = iface;
  const onChangeRef = useRef(onChange);
  onChangeRef.current = onChange;
  const onSelectRef = useRef(onSelect);
  onSelectRef.current = onSelect;
  const selectedIdsRef = useRef(selectedIds);
  selectedIdsRef.current = selectedIds;
  const snapGridRef = useRef(snapGrid);
  snapGridRef.current = snapGrid;

  const dragRef = useRef<DragState | null>(null);
  const resizeRef = useRef<ResizeState | null>(null);
  const rubberRef = useRef<RubberBand>({ active: false, startX: 0, startY: 0, curX: 0, curY: 0, clientStartX: 0, clientStartY: 0 });

  const [rubberVis, setRubberVis] = useState<{ x: number; y: number; w: number; h: number } | null>(null);

  const [picker, setPicker] = useState<PickerState>({
    visible: false, x: 0, y: 0, canvasX: 0, canvasY: 0, signal: null,
  });

  const [signalDropMenu, setSignalDropMenu] = useState<SignalDropMenuState | null>(null);

  // Which tab is being edited per tabbed container (ephemeral). Widgets not on
  // the active tab are hidden so each tab's content can be laid out separately.
  const [activeTabs, setActiveTabs] = useState<Map<string, number>>(() => new Map());
  function selectTab(id: string, i: number) {
    setActiveTabs(prev => {
      const next = new Map(prev);
      next.set(id, i);
      return next;
    });
  }

  useEffect(() => {
    function onMouseMove(e: MouseEvent) {
      const grid = snapGridRef.current;

      if (dragRef.current) {
        const d = dragRef.current;
        const raw_dx = e.clientX - d.startMouseX;
        const raw_dy = e.clientY - d.startMouseY;

        const newWidgets = ifaceRef.current.widgets.map(w => {
          const start = d.startPositions.find(p => p.id === w.id);
          if (!start) return w;
          return {
            ...w,
            x: snapVal(start.x + raw_dx, grid),
            y: snapVal(start.y + raw_dy, grid),
          };
        });
        onChangeRef.current(newWidgets);
      }

      if (resizeRef.current) {
        const r = resizeRef.current;
        const dx = e.clientX - r.startMouseX;
        const dy = e.clientY - r.startMouseY;
        let x = r.startX, y = r.startY, w = r.startW, wh = r.startH;
        if (r.dir.includes('e')) w = Math.max(20, w + dx);
        if (r.dir.includes('s')) wh = Math.max(20, wh + dy);
        if (r.dir.includes('w')) { x = x + dx; w = Math.max(20, w - dx); }
        if (r.dir.includes('n')) { y = y + dy; wh = Math.max(20, wh - dy); }
        const newWidgets = ifaceRef.current.widgets.map(widget =>
          widget.id === r.widgetId
            ? { ...widget, x: snapVal(x, grid), y: snapVal(y, grid), w: snapVal(w, grid), h: snapVal(wh, grid) }
            : widget
        );
        onChangeRef.current(newWidgets);
      }

      if (rubberRef.current.active) {
        const el = containerRef.current;
        if (!el) return;
        const rect = el.getBoundingClientRect();
        const cx = e.clientX - rect.left + el.scrollLeft;
        const cy = e.clientY - rect.top + el.scrollTop;
        rubberRef.current.curX = cx;
        rubberRef.current.curY = cy;
        const rb = rubberRef.current;
        const x = Math.min(rb.startX, cx);
        const y = Math.min(rb.startY, cy);
        setRubberVis({ x, y, w: Math.abs(cx - rb.startX), h: Math.abs(cy - rb.startY) });
      }
    }

    function onMouseUp() {
      if (rubberRef.current.active) {
        const rb = rubberRef.current;
        const x = Math.min(rb.startX, rb.curX);
        const y = Math.min(rb.startY, rb.curY);
        const rw = Math.abs(rb.curX - rb.startX);
        const rh = Math.abs(rb.curY - rb.startY);
        if (rw > 4 && rh > 4) {
          const hit = ifaceRef.current.widgets
            .filter(w => rectsIntersect(w.x, w.y, w.w, w.h, x, y, rw, rh))
            .map(w => w.id);
          onSelectRef.current(hit);
        }
        rubberRef.current.active = false;
        setRubberVis(null);
      }
      dragRef.current = null;
      resizeRef.current = null;
    }

    document.addEventListener('mousemove', onMouseMove);
    document.addEventListener('mouseup', onMouseUp);
    return () => {
      document.removeEventListener('mousemove', onMouseMove);
      document.removeEventListener('mouseup', onMouseUp);
    };
  }, []);

  function getCanvasPos(e: MouseEvent): { cx: number; cy: number } {
    const el = containerRef.current;
    if (!el) return { cx: 0, cy: 0 };
    const rect = el.getBoundingClientRect();
    return {
      cx: e.clientX - rect.left + el.scrollLeft,
      cy: e.clientY - rect.top + el.scrollTop,
    };
  }

  function handleCanvasMouseDown(e: MouseEvent) {
    if ((e.target as Element).closest('.widget-overlay')) return;
    if ((e.target as Element).closest('.widget-picker-backdrop')) return;
    if ((e.target as Element).closest('.signal-drop-backdrop')) return;
    const { cx, cy } = getCanvasPos(e);
    rubberRef.current = { active: true, startX: cx, startY: cy, curX: cx, curY: cy, clientStartX: e.clientX, clientStartY: e.clientY };
    if (!e.ctrlKey && !e.metaKey) {
      onSelect([]);
    }
  }

  function handleDragOver(e: DragEvent) {
    e.preventDefault();
    if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
  }

  function handleDrop(e: DragEvent) {
    e.preventDefault();
    const json = e.dataTransfer?.getData('application/json');
    if (!json) return;
    let sig: SignalRef;
    try { sig = JSON.parse(json); } catch { return; }

    const { cx, cy } = getCanvasPos(e);

    // Check if drop lands on an existing widget. Ignore container panes so a
    // signal dropped onto a widget that overlaps a pane still targets the widget
    // (and a drop on bare pane area creates a new widget there).
    const hit = iface.widgets.filter(w => w.type !== 'container').find(w =>
      cx >= w.x && cx <= w.x + w.w && cy >= w.y && cy <= w.y + w.h
    );

    if (hit && (MULTI_SIGNAL_TYPES.has(hit.type) || SINGLE_SIGNAL_TYPES.has(hit.type))) {
      // Offer add/replace vs create-new
      setSignalDropMenu({ visible: true, x: e.clientX, y: e.clientY, signal: sig, targetWidget: hit });
      return;
    }

    // No existing widget hit — open type picker
    setPicker({ visible: true, x: e.clientX, y: e.clientY, canvasX: cx, canvasY: cy, signal: sig });
  }

  function handleAddToWidget(widget: Widget, sig: SignalRef) {
    setSignalDropMenu(null);
    let updated: Widget;
    if (MULTI_SIGNAL_TYPES.has(widget.type)) {
      // Append signal (avoid duplicates)
      const already = widget.signals.some(s => s.ds === sig.ds && s.name === sig.name);
      if (already) return;
      updated = { ...widget, signals: [...widget.signals, sig] };
    } else {
      // Replace first signal
      updated = { ...widget, signals: [sig, ...widget.signals.slice(1)] };
    }
    onChange(iface.widgets.map(w => w.id === updated.id ? updated : w));
    onSelect([updated.id]);
  }

  function handleSignalDropCreateNew(sig: SignalRef, cx: number, cy: number) {
    setSignalDropMenu(null);
    // We don't have canvas coords in the menu state, approximate from screen coords
    const el = containerRef.current;
    const rect = el ? el.getBoundingClientRect() : { left: 0, top: 0 };
    const canvasX = cx - rect.left + (el?.scrollLeft ?? 0);
    const canvasY = cy - rect.top + (el?.scrollTop ?? 0);
    setPicker({ visible: true, x: cx, y: cy, canvasX, canvasY, signal: sig });
  }

  function handlePick(type: string) {
    if (!picker.signal) return;
    const [defW, defH] = DEFAULT_SIZES[type] ?? [150, 60];
    const grid = snapGrid;
    const newWidget: Widget = {
      id: genWidgetId(),
      type,
      x: snapVal(picker.canvasX - defW / 2, grid),
      y: snapVal(picker.canvasY - defH / 2, grid),
      w: defW,
      h: defH,
      signals: [picker.signal],
      options: {},
    };
    // If dropped inside a tabbed container, assign it to the active tab so it
    // shows up alongside that tab's other content.
    if (type !== 'container') {
      const host = tPanes.find(c => centreInside(newWidget, c));
      if (host) newWidget.options.tab = String(activeTabs.get(host.id) ?? 0);
    }
    onChange([...iface.widgets, newWidget]);
    onSelect([newWidget.id]);
    setPicker(p => ({ ...p, visible: false }));
  }

  function handleOverlayMouseDown(e: MouseEvent, widget: Widget) {
    e.stopPropagation();
    const ctrl = e.ctrlKey || e.metaKey;
    const isSelected = selectedIds.includes(widget.id);

    let nextIds: string[];
    if (ctrl) {
      nextIds = isSelected
        ? selectedIds.filter(id => id !== widget.id)
        : [...selectedIds, widget.id];
    } else {
      nextIds = isSelected ? selectedIds : [widget.id];
    }
    onSelect(nextIds);

    if (ctrl && isSelected) return;

    // Dragging a container also drags the widgets sitting on it (snapshotted now).
    const movers = withContainedWidgets(
      nextIds.includes(widget.id) ? nextIds : [widget.id],
      iface.widgets,
    );
    dragRef.current = {
      startMouseX: e.clientX,
      startMouseY: e.clientY,
      startPositions: iface.widgets
        .filter(w => movers.includes(w.id))
        .map(w => ({ id: w.id, x: w.x, y: w.y })),
    };
  }

  function startResize(e: MouseEvent, widget: Widget, dir: string) {
    e.stopPropagation();
    e.preventDefault();
    resizeRef.current = {
      widgetId: widget.id,
      dir,
      startMouseX: e.clientX,
      startMouseY: e.clientY,
      startX: widget.x,
      startY: widget.y,
      startW: widget.w,
      startH: widget.h,
    };
  }

  function handleDelete(id: string) {
    onChange(iface.widgets.filter(w => w.id !== id));
    onSelect(selectedIds.filter(s => s !== id));
  }

  // A widget is hidden in the editor when it sits inside a tabbed container but
  // belongs to a tab other than the one currently being edited.
  const tPanes = tabPanes(iface.widgets);
  function tabHidden(widget: Widget): boolean {
    return tPanes.some(c =>
      c.id !== widget.id && centreInside(widget, c) && widgetTab(widget) !== (activeTabs.get(c.id) ?? 0),
    );
  }

  return (
    <div
      class="edit-canvas-container"
      ref={containerRef}
      onMouseDown={handleCanvasMouseDown}
      onDragOver={handleDragOver}
      onDrop={handleDrop}
    >
      <div class="canvas-area" style={`width:${iface.w}px;height:${iface.h}px;`}>

        {renderOrder(iface.widgets || []).map(widget => {
          if (tabHidden(widget)) return null;
          const Comp = COMPONENTS[widget.type];
          return Comp
            ? <Comp
                key={widget.id}
                widget={widget}
                activeTab={activeTabs.get(widget.id) ?? 0}
                onSelectTab={(i: number) => selectTab(widget.id, i)}
              />
            : (
              <div key={widget.id} class="unknown-widget"
                style={`left:${widget.x}px;top:${widget.y}px;width:${widget.w}px;height:${widget.h}px;`}>
                <span class="unknown-label">{widget.type}</span>
              </div>
            );
        })}

        {renderOrder(iface.widgets || []).map(widget => {
          if (tabHidden(widget)) return null;
          const isSelected = selectedIds.includes(widget.id);

          return (
            <div
              key={`ov-${widget.id}`}
              class={`widget-overlay${isSelected ? ' selected' : ''}`}
              style={`left:${widget.x}px;top:${widget.y}px;width:${widget.w}px;height:${widget.h}px;`}
              onMouseDown={(e: MouseEvent) => handleOverlayMouseDown(e, widget)}
            >
              {isSelected && (
                <div>
                  <button
                    class="overlay-delete"
                    onMouseDown={(e: MouseEvent) => e.stopPropagation()}
                    onClick={(e: MouseEvent) => { e.stopPropagation(); handleDelete(widget.id); }}
                    title="Delete widget"
                  >✕</button>
                  {(['n','s','e','w','ne','nw','se','sw'] as const).map(dir => (
                    <div
                      key={dir}
                      class={`resize-handle resize-${dir}`}
                      onMouseDown={(e: MouseEvent) => startResize(e, widget, dir)}
                    />
                  ))}
                </div>
              )}
            </div>
          );
        })}

        {rubberVis && (
          <div
            class="rubber-band"
            style={`left:${rubberVis.x}px;top:${rubberVis.y}px;width:${rubberVis.w}px;height:${rubberVis.h}px;`}
          />
        )}
      </div>

      {picker.visible && (
        <WidgetTypePicker
          x={picker.x}
          y={picker.y}
          onPick={handlePick}
          onCancel={() => setPicker(p => ({ ...p, visible: false }))}
        />
      )}

      {/* Signal drop menu: appears when dropping onto an existing widget */}
      {signalDropMenu && (
        <div class="signal-drop-backdrop" onClick={() => setSignalDropMenu(null)}>
          <div
            class="signal-drop-menu"
            style={`left:${signalDropMenu.x}px;top:${signalDropMenu.y}px;`}
            onClick={(e: MouseEvent) => e.stopPropagation()}
          >
            <div class="signal-drop-header">
              Drop signal <strong>{signalDropMenu.signal.name}</strong>
            </div>
            {MULTI_SIGNAL_TYPES.has(signalDropMenu.targetWidget.type) && (
              <button
                class="ctx-item"
                onClick={() => handleAddToWidget(signalDropMenu.targetWidget, signalDropMenu.signal)}
              >
                + Add signal to this {signalDropMenu.targetWidget.type}
              </button>
            )}
            {SINGLE_SIGNAL_TYPES.has(signalDropMenu.targetWidget.type) && (
              <button
                class="ctx-item"
                onClick={() => handleAddToWidget(signalDropMenu.targetWidget, signalDropMenu.signal)}
              >
                ↺ Replace signal in this {signalDropMenu.targetWidget.type}
              </button>
            )}
            <button
              class="ctx-item"
              onClick={() => handleSignalDropCreateNew(
                signalDropMenu.signal,
                signalDropMenu.x,
                signalDropMenu.y,
              )}
            >
              + Create new widget
            </button>
            <button class="ctx-item ctx-cancel" onClick={() => setSignalDropMenu(null)}>Cancel</button>
          </div>
        </div>
      )}
    </div>
  );
}
