import type { FunctionalComponent } from 'preact';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { useAsukaStore, type RulerUnit } from '../../store/asukaStore';
import { getRuntimeRenderEngine } from '../../core/renderEngine';

type Size = { width: number; height: number };

function clamp(n: number, min: number, max: number): number {
  return Math.max(min, Math.min(max, n));
}

function niceStep(target: number): number {
  // Choose a nice 1/2/5 * 10^n step close to target.
  if (!Number.isFinite(target) || target <= 0) return 1;
  const pow = Math.pow(10, Math.floor(Math.log10(target)));
  const n = target / pow;
  let m = 1;
  if (n >= 5) m = 5;
  else if (n >= 2) m = 2;
  return m * pow;
}

function unitWorldUnitsPerUnit(unit: RulerUnit, dpi: number, productWorldPerUnit: number): number {
  // Fabric "world" units are treated as CSS pixels in the default setup.
  // - px: 1 unit = 1 world unit
  // - mm/cm: require a DPI assumption
  // - product: configurable worldUnitsPerUnit
  if (unit === 'px') return 1;
  const safeDpi = clamp(Number(dpi) || 96, 10, 1000);
  if (unit === 'mm') return safeDpi / 25.4;
  if (unit === 'cm') return safeDpi / 2.54;
  return clamp(Number(productWorldPerUnit) || 1, 1e-6, 1e9);
}

export const RulerOverlay: FunctionalComponent<{ hostRef: { current: HTMLDivElement | null }; frameRef?: { current: HTMLDivElement | null } }> = ({ hostRef, frameRef }) => {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const rafRef = useRef<number | null>(null);
  const lastSignatureRef = useRef<string>('');

  const rulerEnabled = useAsukaStore((s) => (s as any).rulerEnabled as boolean);
  const unit = useAsukaStore((s) => (s as any).rulerUnit as RulerUnit);
  const dpi = useAsukaStore((s) => (s as any).rulerDpi as number);
  const product = useAsukaStore((s) => (s as any).rulerProduct as any);
  const activeViewId = useAsukaStore((s) => (s as any).activeViewId as string | null);
  const views = useAsukaStore((s) => (s as any).documentViews as Array<any>);

  const [size, setSize] = useState<Size>({ width: 0, height: 0 });

  const bar = 22; // ruler thickness (px)

  const unitLabel = useMemo(() => {
    if (unit === 'product') return String(product?.unitLabel ?? 'u');
    return unit;
  }, [unit, product?.unitLabel]);

  const worldPerUnit = useMemo(() => {
    return unitWorldUnitsPerUnit(unit, dpi, Number(product?.worldUnitsPerUnit));
  }, [unit, dpi, product?.worldUnitsPerUnit]);

  // Track host size.
  useEffect(() => {
    if (!rulerEnabled) return;
    const el = hostRef.current;
    if (!el) return;

    const update = () => {
      const rect = el.getBoundingClientRect();
      setSize({ width: Math.max(0, rect.width), height: Math.max(0, rect.height) });
    };
    update();

    const ro = new ResizeObserver(() => update());
    ro.observe(el);
    return () => ro.disconnect();
  }, [rulerEnabled, hostRef]);

  // Main draw loop.
  useEffect(() => {
    if (!rulerEnabled) return;
    const el = hostRef.current;
    if (!el) return;
    const overlay = canvasRef.current;
    if (!overlay) return;

    const draw = () => {
      try {
        const ctx = overlay.getContext('2d');
        if (!ctx) return;

        const dpr = Math.max(1, (window as any).devicePixelRatio || 1);
        const w = Math.max(0, size.width);
        const h = Math.max(0, size.height);
        const hostRect = el.getBoundingClientRect();
        const rendererFrame = getRuntimeRenderEngine('fabric')?.viewport.getFrame?.() ?? null;
        const fallbackRect = frameRef?.current?.getBoundingClientRect?.() ?? null;
        const renderedRect = rendererFrame ?? fallbackRect;

        const activeView = (Array.isArray(views) ? (views.find((x: any) => x?.id === activeViewId) ?? views[0]) : null) as any;
        const docWidth = Math.max(1, Math.floor(activeView?.width ?? rendererFrame?.documentWidth ?? 800));
        const docHeight = Math.max(1, Math.floor(activeView?.height ?? rendererFrame?.documentHeight ?? 600));

        // Source of truth: the actual rendered lower canvas element.
        // Never use the host/container size to position or size the ruler.
        const frameLeft = Math.max(bar, ((renderedRect?.left ?? hostRect.left + bar) - hostRect.left));
        const frameTop = Math.max(bar, ((renderedRect?.top ?? hostRect.top + bar) - hostRect.top));
        const frameWidth = Math.max(1, Math.min(w - frameLeft, renderedRect?.width ?? docWidth));
        const frameHeight = Math.max(1, Math.min(h - frameTop, renderedRect?.height ?? docHeight));

        // Visual px per logical document unit, based on the displayed canvas itself.
        const effectiveZoomX = Math.max(1e-6, frameWidth / Math.max(1, docWidth));
        const effectiveZoomY = Math.max(1e-6, frameHeight / Math.max(1, docHeight));

        const sigParts: any[] = [w, h, frameLeft, frameTop, frameWidth, frameHeight, docWidth, docHeight, unit, dpi, product?.unitLabel, product?.worldUnitsPerUnit, effectiveZoomX, effectiveZoomY];
        const sig = sigParts.join('|');
        if (sig === lastSignatureRef.current) {
          return;
        }
        lastSignatureRef.current = sig;

        overlay.style.width = `${w}px`;
        overlay.style.height = `${h}px`;
        overlay.width = Math.max(1, Math.floor(w * dpr));
        overlay.height = Math.max(1, Math.floor(h * dpr));
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);

        // Background
        ctx.clearRect(0, 0, w, h);
        ctx.fillStyle = 'rgba(255,255,255,0.75)';
        ctx.fillRect(0, 0, w, bar);
        ctx.fillRect(0, 0, bar, h);
        ctx.fillStyle = 'rgba(255,255,255,0.92)';
        ctx.fillRect(0, 0, bar, bar);

        // Borders
        ctx.strokeStyle = 'rgba(15,23,42,0.18)';
        ctx.beginPath();
        ctx.moveTo(0, bar + 0.5);
        ctx.lineTo(w, bar + 0.5);
        ctx.moveTo(bar + 0.5, 0);
        ctx.lineTo(bar + 0.5, h);
        ctx.stroke();

        // Keep the ruler visually tied to the actual visible canvas frame.
        ctx.fillStyle = 'rgba(255,255,255,0.96)';
        if (frameLeft > bar) ctx.fillRect(bar, 0, Math.max(0, frameLeft - bar), bar);
        if (frameTop > bar) ctx.fillRect(0, bar, bar, Math.max(0, frameTop - bar));
        const rightGap = Math.max(0, w - (frameLeft + frameWidth));
        const bottomGap = Math.max(0, h - (frameTop + frameHeight));
        if (rightGap > 0) ctx.fillRect(frameLeft + frameWidth, 0, rightGap, bar);
        if (bottomGap > 0) ctx.fillRect(0, frameTop + frameHeight, bar, bottomGap);

        // Determine nice tick steps (major & minor).
        // We aim for a major tick every ~90 screen pixels.
        const desiredMajorPx = 90;
        const worldPerPxX = 1 / effectiveZoomX;
        const desiredMajorWorldX = desiredMajorPx * worldPerPxX;
        const desiredMajorUnitsX = desiredMajorWorldX / worldPerUnit;
        const majorUnitsStepX = niceStep(desiredMajorUnitsX);
        const majorWorldStepX = majorUnitsStepX * worldPerUnit;
        const minorWorldStepX = majorWorldStepX / 5;

        const worldPerPxY = 1 / effectiveZoomY;
        const desiredMajorWorldY = desiredMajorPx * worldPerPxY;
        const desiredMajorUnitsY = desiredMajorWorldY / worldPerUnit;
        const majorUnitsStepY = niceStep(desiredMajorUnitsY);
        const majorWorldStepY = majorUnitsStepY * worldPerUnit;
        const minorWorldStepY = majorWorldStepY / 5;

        // Draw top ruler (x axis)
        ctx.font = '10px ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial';
        ctx.fillStyle = 'rgba(15,23,42,0.82)';
        ctx.strokeStyle = 'rgba(15,23,42,0.22)';

        const startMajorX = 0;
        const endWorldX = docWidth;
        const minorStartX = 0;

        // Minor ticks
        for (let wx = minorStartX; wx <= endWorldX + 1e-6; wx += minorWorldStepX) {
          const sx = frameLeft + wx * effectiveZoomX;
          if (sx < frameLeft || sx > frameLeft + frameWidth) continue;
          const isMajor = Math.abs((wx / majorWorldStepX) - Math.round(wx / majorWorldStepX)) < 1e-6;
          const len = isMajor ? 10 : 6;
          ctx.beginPath();
          ctx.moveTo(sx + 0.5, bar);
          ctx.lineTo(sx + 0.5, bar - len);
          ctx.stroke();
        }

        // Major labels
        for (let wx = startMajorX; wx <= endWorldX + 1e-6; wx += majorWorldStepX) {
          const sx = frameLeft + wx * effectiveZoomX;
          if (sx < frameLeft || sx > frameLeft + frameWidth) continue;
          const unitsValue = wx / worldPerUnit;
          const label = `${Math.round(unitsValue * 100) / 100}`;
          const labelX = Math.abs(wx) < 1e-6 ? sx + 4 : sx + 3;
          ctx.fillText(label, labelX, 12);
        }

        // Draw left ruler (y axis)
        const startMajorY = 0;
        const endWorldY = docHeight;
        const minorStartY = 0;

        for (let wy = minorStartY; wy <= endWorldY + 1e-6; wy += minorWorldStepY) {
          const sy = frameTop + wy * effectiveZoomY;
          if (sy < frameTop || sy > frameTop + frameHeight) continue;
          const isMajor = Math.abs((wy / majorWorldStepY) - Math.round(wy / majorWorldStepY)) < 1e-6;
          const len = isMajor ? 10 : 6;
          ctx.beginPath();
          ctx.moveTo(bar, sy + 0.5);
          ctx.lineTo(bar - len, sy + 0.5);
          ctx.stroke();
        }

        for (let wy = startMajorY; wy <= endWorldY + 1e-6; wy += majorWorldStepY) {
          const sy = frameTop + wy * effectiveZoomY;
          if (sy < frameTop || sy > frameTop + frameHeight) continue;
          const unitsValue = wy / worldPerUnit;
          const label = `${Math.round(unitsValue * 100) / 100}`;
          // Rotate text for left ruler to save space.
          ctx.save();
          ctx.translate(8, Math.abs(wy) < 1e-6 ? sy + 8 : sy - 2);
          ctx.rotate(-Math.PI / 2);
          ctx.fillText(label, 0, 0);
          ctx.restore();
        }


        // Canvas/frame origin markers on the rulers.
        ctx.strokeStyle = 'rgba(37,99,235,0.42)';
        ctx.beginPath();
        ctx.moveTo(frameLeft + 0.5, 0);
        ctx.lineTo(frameLeft + 0.5, bar);
        ctx.moveTo(0, frameTop + 0.5);
        ctx.lineTo(bar, frameTop + 0.5);
        ctx.stroke();

        // Unit label in corner
        ctx.fillStyle = 'rgba(15,23,42,0.65)';
        ctx.fillText(unitLabel, 4, bar - 6);
      } catch {
        // no-op: keep overlay resilient
      }
    };

    const loop = () => {
      draw();
      rafRef.current = window.requestAnimationFrame(loop);
    };

    rafRef.current = window.requestAnimationFrame(loop);
    return () => {
      if (rafRef.current) window.cancelAnimationFrame(rafRef.current);
      rafRef.current = null;
    };
  }, [rulerEnabled, size.width, size.height, unit, dpi, product?.unitLabel, product?.worldUnitsPerUnit, worldPerUnit, unitLabel, hostRef, frameRef, activeViewId, views]);

  if (!rulerEnabled) return null;

  return (
    <canvas
      ref={canvasRef}
      class="ask-absolute ask-inset-0 ask-pointer-events-none ask-z-30"
      aria-hidden="true"
    />
  );
};
