import { h } from 'preact';
import { useEffect, useState } from 'preact/hooks';

const LS_KEY = 'uopi:ui-zoom';
const ZOOM_EVENT = 'uopi:zoomchange';
const STEPS = [0.5, 0.6, 0.75, 0.85, 1.0, 1.15, 1.3, 1.5, 1.75, 2.0, 2.5];

// serverDefaultZoom is the base scale configured server-side ([ui] default_zoom),
// pushed in once /me resolves. It is the fallback when this browser has no saved
// per-machine override, so a fresh client on a HiDPI screen starts enlarged
// without per-browser setup while a user's local A+/A− choice still wins.
let serverDefaultZoom = 1;

// hasStoredZoom reports whether this browser has an explicit saved zoom override.
export function hasStoredZoom(): boolean {
  return localStorage.getItem(LS_KEY) !== null;
}

// setServerDefaultZoom records the server default and, unless the user has a saved
// override, applies it immediately (without persisting, so a later server change
// still propagates).
export function setServerDefaultZoom(z: number): void {
  serverDefaultZoom = isFinite(z) && z > 0 ? z : 1;
  if (!hasStoredZoom()) {
    applyZoom(serverDefaultZoom, false);
  }
}

export function getStoredZoom(): number {
  const raw = localStorage.getItem(LS_KEY);
  const v = raw === null ? serverDefaultZoom : parseFloat(raw);
  return isFinite(v) ? Math.max(STEPS[0], Math.min(STEPS[STEPS.length - 1], v)) : 1;
}

// dprFactor returns the device-pixel-ratio scaling applied on top of the manual
// zoom so the UI is proportionally larger on high-DPI displays by default.
// Firefox only honours CSS rem scaling (it does not auto-enlarge the root px the
// way Chrome does on some setups), so the ratio must be folded into the root
// font-size explicitly rather than left to the browser.
export function dprFactor(): number {
  const dpr = window.devicePixelRatio;
  return isFinite(dpr) && dpr > 0 ? dpr : 1;
}

// applyZoom sets the root font-size to scale the whole UI. When persist is true
// (a deliberate user action) the choice is saved as this browser's override;
// pass false to apply the server default transiently. A ZOOM_EVENT is dispatched
// so the ZoomControl's displayed percentage stays in sync regardless of source.
export function applyZoom(z: number, persist = true): number {
  const clamped = Math.max(STEPS[0], Math.min(STEPS[STEPS.length - 1], z));
  if (persist) {
    localStorage.setItem(LS_KEY, String(clamped));
  }
  const dpr = dprFactor();
  document.documentElement.style.setProperty('--dpr', String(dpr));
  document.documentElement.style.fontSize = `${Math.round(16 * clamped * dpr)}px`;
  window.dispatchEvent(new CustomEvent(ZOOM_EVENT, { detail: clamped }));
  return clamped;
}

// watchDpr re-applies the current zoom whenever the device-pixel-ratio changes
// (e.g. the window moves to a monitor with different scaling). matchMedia fires
// once per resolution change; the listener is re-armed each time. Returns an
// unsubscribe function.
export function watchDpr(): () => void {
  let mql: MediaQueryList | null = null;
  const onChange = () => {
    // Re-apply at the new DPR; only persist if this browser actually has an
    // override (otherwise a monitor change would freeze the server default).
    applyZoom(getStoredZoom(), hasStoredZoom());
    arm();
  };
  const arm = () => {
    mql?.removeEventListener('change', onChange);
    mql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
    mql.addEventListener('change', onChange);
  };
  arm();
  return () => mql?.removeEventListener('change', onChange);
}

export default function ZoomControl() {
  const [zoom, setZoom] = useState(getStoredZoom);

  // Keep the displayed percentage in sync when zoom is applied from elsewhere
  // (e.g. the server default arriving via /me, or a DPR change).
  useEffect(() => {
    const onZoom = (e: Event) => setZoom((e as CustomEvent<number>).detail);
    window.addEventListener(ZOOM_EVENT, onZoom);
    return () => window.removeEventListener(ZOOM_EVENT, onZoom);
  }, []);

  function step(dir: number) {
    const idx = STEPS.findIndex(s => s >= zoom - 0.01);
    const nextIdx = Math.max(0, Math.min(STEPS.length - 1, idx + dir));
    setZoom(applyZoom(STEPS[nextIdx]));
  }

  return (
    <div class="zoom-control" title="UI zoom">
      <button class="icon-btn zoom-btn" onClick={() => step(-1)} title="Zoom out (smaller UI)">A−</button>
      <span class="zoom-pct">{Math.round(zoom * 100)}%</span>
      <button class="icon-btn zoom-btn" onClick={() => step(1)} title="Zoom in (larger UI)">A+</button>
    </div>
  );
}
