'use client';

import React, {
  Suspense,
  lazy,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';

import type {
  GradientController,
  GradientStateName,
} from '../hooks/useGradientController';
import {
  type GradientPreset,
  type GradientUniformKey,
  gradientUniformKeys,
} from './gradientConfig';
import type {
  GradientWorkerInboundMessage,
  GradientWorkerOutboundMessage,
} from './gradientWorkerTypes';

// Lazy load the debug panel to reduce initial bundle size
const TweakpaneDebugPanel = lazy(() => import('./TweakpaneDebugPanel'));

type GradientStateAPI = {
  getUniformsObject: () => GradientPreset;
  setUniform: (
    key: GradientUniformKey | string,
    value: number,
    options?: { immediate?: boolean }
  ) => void;
  setState: (
    state: GradientStateName | string,
    options?: { immediate?: boolean }
  ) => void;
  clearOverrides: () => void;
  presets: Record<string, unknown>;
};

type SunoDevApi = {
  devToolsVisible?: boolean;
};

type GradientDebugWindow = Window & {
  __SUNO_DEV__?: SunoDevApi;
  __gradientState__?: GradientStateAPI;
  __gradientUniforms__?: GradientPreset;
};

interface GradientBackgroundProps {
  controller: GradientController;
  className?: string;
  enableDebugControls?: boolean;
  maxDevicePixelRatio?: number;
  maxRenderPixels?: number;
}

interface OffscreenCanvasElement extends HTMLCanvasElement {
  __gradientOffscreenTransferred?: boolean;
}

const OFFSCREEN_CANVAS_FLAG = '__gradientOffscreenTransferred';
const DEVTOOLS_TOGGLE_EVENT = 'suno-devtools-toggle';
// Small helper to treat near-zero flow speed as "static" so we can skip RAF work.
const FLOW_SPEED_EPSILON = 0.001;
const DEFAULT_MAX_RENDER_PIXELS = 1920 * 1080;
const UNIFORM_EPSILON = 0.0005;

export const GradientBackground: React.FC<GradientBackgroundProps> = ({
  controller,
  className = '',
  enableDebugControls = true,
  maxDevicePixelRatio = 2,
  maxRenderPixels = DEFAULT_MAX_RENDER_PIXELS,
}) => {
  const {
    motionValues,
    getSnapshot,
    setUniform,
    setState,
    clearOverrides,
    presets,
  } = controller;

  const uniformEntries = useMemo(
    () => gradientUniformKeys.map((key) => [key, motionValues[key]] as const),
    [motionValues]
  );

  const canvasRef = useRef<HTMLCanvasElement>(null);
  const workerRef = useRef<Worker | null>(null);
  const containerRef = useRef<HTMLDivElement>(null);
  const resizeObserverRef = useRef<ResizeObserver | null>(null);
  const uniformSubscriptionsRef = useRef<Array<() => void>>([]);
  const uniformsSnapshotRef = useRef<GradientPreset>(getSnapshot());
  const lastSentUniformsRef = useRef<GradientPreset>({
    ...uniformsSnapshotRef.current,
  });
  const pendingUniformUpdatesRef = useRef<Partial<GradientPreset>>({});
  const pendingFlowSpeedRef = useRef<number | null>(null);
  const uniformFlushHandleRef = useRef<number | null>(null);
  const lastRenderConfigRef = useRef<{
    width: number;
    height: number;
    dpr: number;
  } | null>(null);
  // Tracks the animation state we last told the worker about so we avoid noisy re-posts.
  const flowAnimationStateRef = useRef<boolean | null>(null);
  const [devToolsVisible, setDevToolsVisible] = useState<boolean>(() => {
    if (typeof window === 'undefined') {
      return true;
    }
    const devWindow = window as GradientDebugWindow;
    return devWindow.__SUNO_DEV__?.devToolsVisible !== false;
  });

  const getCappedDevicePixelRatio = useCallback(() => {
    if (typeof window === 'undefined') {
      return 1;
    }
    return Math.min(window.devicePixelRatio ?? 1, maxDevicePixelRatio);
  }, [maxDevicePixelRatio]);

  useEffect(() => {
    if (typeof window === 'undefined') {
      return;
    }

    const devWindow = window as GradientDebugWindow;

    const handleDevToolsToggle = (event: Event) => {
      const detail = (event as CustomEvent<{ visible?: boolean }>).detail;
      if (detail && typeof detail.visible === 'boolean') {
        setDevToolsVisible(detail.visible);
      } else {
        setDevToolsVisible(Boolean(devWindow.__SUNO_DEV__?.devToolsVisible));
      }
    };

    window.addEventListener(
      DEVTOOLS_TOGGLE_EVENT,
      handleDevToolsToggle as EventListener
    );

    setDevToolsVisible(Boolean(devWindow.__SUNO_DEV__?.devToolsVisible));

    return () => {
      window.removeEventListener(
        DEVTOOLS_TOGGLE_EVENT,
        handleDevToolsToggle as EventListener
      );
    };
  }, []);

  const replaceCanvasElement = useCallback(() => {
    const canvas = canvasRef.current;
    if (!canvas) {
      return null;
    }
    const parent = canvas.parentElement;
    if (!parent) {
      return canvas;
    }
    const replacement = canvas.cloneNode(false) as HTMLCanvasElement;
    replacement.className = canvas.className;
    replacement.width = canvas.width;
    replacement.height = canvas.height;
    const styleAttr = canvas.getAttribute('style');
    if (styleAttr) {
      replacement.setAttribute('style', styleAttr);
    } else {
      replacement.removeAttribute('style');
    }
    delete (replacement as OffscreenCanvasElement)[OFFSCREEN_CANVAS_FLAG];
    parent.replaceChild(replacement, canvas);
    canvasRef.current = replacement;
    return replacement;
  }, []);

  const ensureCanvasElement = useCallback(() => {
    let canvas = canvasRef.current;
    if (!canvas) {
      return null;
    }
    if ((canvas as OffscreenCanvasElement)[OFFSCREEN_CANVAS_FLAG]) {
      canvas = replaceCanvasElement();
    }
    return canvas ?? null;
  }, [replaceCanvasElement]);

  const postToWorker = useCallback(
    (message: GradientWorkerInboundMessage, transfer?: Transferable[]) => {
      const worker = workerRef.current;
      if (!worker) {
        return;
      }
      if (transfer && transfer.length > 0) {
        worker.postMessage(message, transfer);
      } else {
        worker.postMessage(message);
      }
    },
    []
  );

  // Notify the worker when flow speed crosses the “moving” threshold so it can
  // stop requesting frames for static states and resume once motion returns.
  const syncFlowAnimationState = useCallback(
    (speed: number, force: boolean = false) => {
      const shouldAnimate = Math.abs(speed) > FLOW_SPEED_EPSILON;
      if (!force && flowAnimationStateRef.current === shouldAnimate) {
        return;
      }
      flowAnimationStateRef.current = shouldAnimate;
      postToWorker({
        type: 'set-animation-active',
        active: shouldAnimate,
      });
    },
    [postToWorker]
  );

  const cancelPendingUniformFlush = useCallback(() => {
    if (typeof window !== 'undefined') {
      const handle = uniformFlushHandleRef.current;
      if (handle !== null) {
        window.cancelAnimationFrame(handle);
      }
    }
    uniformFlushHandleRef.current = null;
    pendingUniformUpdatesRef.current = {};
    pendingFlowSpeedRef.current = null;
  }, []);

  const queueUniformUpdate = useCallback(
    (key: GradientUniformKey, value: number) => {
      if (!Number.isFinite(value)) {
        return;
      }

      uniformsSnapshotRef.current[key] = value;

      const isFlowSpeed = key === 'flowSpeed';
      if (isFlowSpeed) {
        syncFlowAnimationState(value);
      }

      if (isFlowSpeed && workerRef.current) {
        lastSentUniformsRef.current.flowSpeed = value;
        pendingFlowSpeedRef.current = null;
        delete pendingUniformUpdatesRef.current.flowSpeed;
        postToWorker({
          type: 'set-uniform',
          key,
          value,
        });
        return;
      }

      pendingUniformUpdatesRef.current[key] = value;
      if (key === 'flowSpeed') {
        pendingFlowSpeedRef.current = value;
      } else if (workerRef.current && uniformFlushHandleRef.current === null) {
        const lastValue = lastSentUniformsRef.current[key];
        if (
          typeof lastValue === 'number' &&
          Math.abs(lastValue - value) <= UNIFORM_EPSILON
        ) {
          delete pendingUniformUpdatesRef.current[key];
          return;
        }
      }

      if (typeof window === 'undefined') {
        return;
      }

      function flushPendingUpdates() {
        uniformFlushHandleRef.current = null;

        const pending = pendingUniformUpdatesRef.current;
        pendingUniformUpdatesRef.current = {};
        const flowSpeedCandidate = pendingFlowSpeedRef.current;

        if (!workerRef.current) {
          pendingUniformUpdatesRef.current = {
            ...pending,
            ...pendingUniformUpdatesRef.current,
          };
          scheduleFlush();
          return;
        }

        const updates: Partial<GradientPreset> = {};
        const previousValues: Partial<Record<GradientUniformKey, number>> = {};

        (
          Object.entries(pending) as Array<[GradientUniformKey, number]>
        ).forEach(([pendingKey, pendingValue]) => {
          if (!Number.isFinite(pendingValue)) {
            return;
          }
          const previousValue = lastSentUniformsRef.current[pendingKey];
          if (
            previousValue === undefined ||
            Number.isNaN(previousValue) ||
            Math.abs(pendingValue - previousValue) > UNIFORM_EPSILON
          ) {
            updates[pendingKey] = pendingValue;
            previousValues[pendingKey] = previousValue;
            lastSentUniformsRef.current[pendingKey] = pendingValue;
          }
        });

        if (Object.keys(updates).length > 0) {
          postToWorker({
            type: 'set-uniforms',
            uniforms: updates,
          });
        }

        if (typeof flowSpeedCandidate === 'number') {
          pendingFlowSpeedRef.current = null;
          const nextFlowSpeed =
            (updates.flowSpeed as number | undefined) ?? flowSpeedCandidate;
          const previousFlowSpeed =
            (previousValues.flowSpeed as number | undefined) ??
            lastSentUniformsRef.current.flowSpeed ??
            0;

          const animationStateChanged =
            Math.abs(previousFlowSpeed) > FLOW_SPEED_EPSILON !==
            Math.abs(nextFlowSpeed) > FLOW_SPEED_EPSILON;
          const flowChanged =
            Math.abs(nextFlowSpeed - previousFlowSpeed) > UNIFORM_EPSILON;

          if (animationStateChanged || flowChanged) {
            syncFlowAnimationState(nextFlowSpeed);
          }
        }
      }

      function scheduleFlush() {
        if (uniformFlushHandleRef.current !== null) {
          return;
        }
        uniformFlushHandleRef.current =
          window.requestAnimationFrame(flushPendingUpdates);
      }

      scheduleFlush();
    },
    [postToWorker, syncFlowAnimationState]
  );

  const computeRenderSize = useCallback(
    (width: number, height: number) => {
      if (!(Number.isFinite(width) && Number.isFinite(height))) {
        return { width: 0, height: 0 };
      }
      const normalizedWidth = Math.max(1, Math.round(width));
      const normalizedHeight = Math.max(1, Math.round(height));
      const maxPixels = maxRenderPixels;

      if (
        !maxPixels ||
        !Number.isFinite(maxPixels) ||
        maxPixels <= 0 ||
        normalizedWidth * normalizedHeight <= maxPixels
      ) {
        return {
          width: normalizedWidth,
          height: normalizedHeight,
        };
      }

      const scale = Math.sqrt(maxPixels / (normalizedWidth * normalizedHeight));
      return {
        width: Math.max(1, Math.round(normalizedWidth * scale)),
        height: Math.max(1, Math.round(normalizedHeight * scale)),
      };
    },
    [maxRenderPixels]
  );

  // Expose gradient state API to window for debugging
  useEffect(() => {
    if (typeof window === 'undefined') {
      return;
    }

    const devWindow = window as GradientDebugWindow;
    const api: GradientStateAPI = {
      getUniformsObject: () => uniformsSnapshotRef.current,
      setUniform: (
        key: GradientUniformKey | string,
        value: number,
        options?: { immediate?: boolean }
      ) => setUniform(key as GradientUniformKey, value, options),
      setState: (
        state: GradientStateName | string,
        options?: { immediate?: boolean }
      ) => setState(state as GradientStateName, options),
      clearOverrides,
      presets,
    };

    devWindow.__gradientState__ = api;
    const initialUniformSnapshot = uniformsSnapshotRef.current;
    devWindow.__gradientUniforms__ = initialUniformSnapshot;

    return () => {
      if (devWindow.__gradientState__ === api) {
        delete devWindow.__gradientState__;
      }

      if (devWindow.__gradientUniforms__ === initialUniformSnapshot) {
        delete devWindow.__gradientUniforms__;
      }
    };
  }, [setUniform, setState, clearOverrides, presets]);

  // Main worker initialization and lifecycle
  useEffect(() => {
    const containerElement = containerRef.current;
    let canvasElement = ensureCanvasElement();

    if (!containerElement || !canvasElement) {
      return;
    }

    const supportsOffscreen =
      typeof window !== 'undefined' &&
      typeof Worker !== 'undefined' &&
      'OffscreenCanvas' in window &&
      typeof canvasElement.transferControlToOffscreen === 'function';

    if (!supportsOffscreen) {
      console.error(
        'OffscreenCanvas is required for GradientBackground rendering.'
      );
      return;
    }

    const cleanupFns: Array<() => void> = [];

    const detachMotionSubscriptions = () => {
      uniformSubscriptionsRef.current.forEach((unsubscribe) => {
        unsubscribe?.();
      });
      uniformSubscriptionsRef.current = [];
      cancelPendingUniformFlush();
    };

    detachMotionSubscriptions();

    const initialUniforms = getSnapshot();
    gradientUniformKeys.forEach((key) => {
      uniformsSnapshotRef.current[key] = initialUniforms[key];
    });
    lastSentUniformsRef.current = { ...initialUniforms };

    const attachMotionSubscriptions = () => {
      uniformSubscriptionsRef.current = uniformEntries.map(([key, motion]) =>
        motion.on('change', (value) => {
          queueUniformUpdate(key, value);
        })
      );
    };

    try {
      const worker = new Worker(
        new URL('./gradientWorker.ts', import.meta.url),
        {
          type: 'module',
        }
      );
      workerRef.current = worker;

      const handleWorkerMessage = (
        event: MessageEvent<GradientWorkerOutboundMessage>
      ) => {
        if (event.data?.type === 'error') {
          console.error('Gradient worker error:', event.data.message);
        }
      };

      worker.addEventListener('message', handleWorkerMessage);
      cleanupFns.push(() => {
        worker.removeEventListener('message', handleWorkerMessage);
      });

      attachMotionSubscriptions();

      const measuredWidth = containerElement.clientWidth;
      const measuredHeight = containerElement.clientHeight;
      const renderSize = computeRenderSize(measuredWidth, measuredHeight);
      const cappedDpr = getCappedDevicePixelRatio();
      lastRenderConfigRef.current = {
        width: renderSize.width,
        height: renderSize.height,
        dpr: cappedDpr,
      };
      canvasElement.width = renderSize.width;
      canvasElement.height = renderSize.height;

      let offscreenCanvas: OffscreenCanvas;
      try {
        offscreenCanvas = canvasElement.transferControlToOffscreen();
        (canvasElement as OffscreenCanvasElement)[OFFSCREEN_CANVAS_FLAG] = true;
      } catch (transferError) {
        console.error(
          'Failed to transfer canvas control to offscreen',
          transferError
        );
        worker.terminate();
        workerRef.current = null;
        detachMotionSubscriptions();
        canvasElement = ensureCanvasElement() ?? canvasElement;
        return;
      }

      worker.postMessage(
        {
          type: 'init',
          canvas: offscreenCanvas,
          size: renderSize,
          dpr: cappedDpr,
          uniforms: initialUniforms,
        },
        [offscreenCanvas]
      );

      syncFlowAnimationState(initialUniforms.flowSpeed, true);

      const handleWindowResize = () => {
        const nextWidth = containerElement.clientWidth;
        const nextHeight = containerElement.clientHeight;
        const nextSize = computeRenderSize(nextWidth, nextHeight);
        const nextDpr = getCappedDevicePixelRatio();
        const lastConfig = lastRenderConfigRef.current;
        if (
          lastConfig &&
          lastConfig.width === nextSize.width &&
          lastConfig.height === nextSize.height &&
          lastConfig.dpr === nextDpr
        ) {
          return;
        }
        lastRenderConfigRef.current = {
          width: nextSize.width,
          height: nextSize.height,
          dpr: nextDpr,
        };
        postToWorker({
          type: 'resize',
          size: nextSize,
          dpr: nextDpr,
        });
      };

      const resizeObserver = new ResizeObserver(() => {
        handleWindowResize();
      });
      resizeObserver.observe(containerElement);
      resizeObserverRef.current = resizeObserver;
      cleanupFns.push(() => {
        resizeObserver.disconnect();
        if (resizeObserverRef.current === resizeObserver) {
          resizeObserverRef.current = null;
        }
      });

      const orientationResize = () => handleWindowResize();
      const delayedOrientation = () => {
        setTimeout(handleWindowResize, 100);
      };

      window.addEventListener('resize', handleWindowResize);
      window.addEventListener('orientationchange', orientationResize);
      window.addEventListener('orientationchange', delayedOrientation);
      cleanupFns.push(() => {
        window.removeEventListener('resize', handleWindowResize);
        window.removeEventListener('orientationchange', orientationResize);
        window.removeEventListener('orientationchange', delayedOrientation);
      });

      cleanupFns.push(() => {
        worker.postMessage({ type: 'dispose' });
        worker.terminate();
        workerRef.current = null;
      });
    } catch (error) {
      console.error('Failed to initialize gradient worker renderer', error);
      workerRef.current = null;
      detachMotionSubscriptions();
      return;
    }

    return () => {
      cleanupFns.reverse();
      cleanupFns.forEach((fn) => {
        try {
          fn();
        } catch (error) {
          console.error('Gradient background cleanup error:', error);
        }
      });

      detachMotionSubscriptions();

      if (
        (canvasRef.current as OffscreenCanvasElement)?.[OFFSCREEN_CANVAS_FLAG]
      ) {
        replaceCanvasElement();
      }

      lastRenderConfigRef.current = null;
      workerRef.current = null;
      flowAnimationStateRef.current = null;
    };
  }, [
    ensureCanvasElement,
    getCappedDevicePixelRatio,
    getSnapshot,
    replaceCanvasElement,
    uniformEntries,
    postToWorker,
    syncFlowAnimationState,
    computeRenderSize,
    queueUniformUpdate,
    cancelPendingUniformFlush,
  ]);

  return (
    <>
      <div
        ref={containerRef}
        className={`absolute inset-0 overflow-hidden ${className}`}
      >
        <canvas ref={canvasRef} className='h-full w-full' />
      </div>
      {enableDebugControls && (
        <Suspense fallback={null}>
          <TweakpaneDebugPanel
            controller={controller}
            visible={devToolsVisible}
          />
        </Suspense>
      )}
    </>
  );
};

export default GradientBackground;
