import { Insets, Size } from "@noya-app/noya-geometry";
import { KEYBOARD_SHORTCUT_PASSTHROUGH_CLASS } from "@noya-app/noya-keymap";
import { assignRef, createBeforeInputNormalizer } from "@noya-app/react-utils";
import React, {
  CSSProperties,
  ComponentPropsWithoutRef,
  ForwardedRef,
  ReactNode,
  forwardRef,
  memo,
  useCallback,
  useEffect,
  useMemo,
  useRef,
} from "react";
import { useAutomaticCanvasSize } from "../hooks/useAutomaticCanvasSize";

type MeasuredChildren = ReactNode | (({ size }: { size: Size }) => ReactNode);

export interface InfiniteCanvasProps
  extends Omit<ComponentPropsWithoutRef<"div">, "onBeforeInput" | "children"> {
  onBeforeInput?: (e: InputEvent) => void;
  onChangeSize?: (size: Size, insets: Insets) => void;
  size?: Size;
  insets?: Insets;
  autoFocus?: boolean;
  children: MeasuredChildren;
  readOnly?: boolean;
  shouldHandleKeyboardEvents?: boolean;
  /**
   * When true, indicates the user is actively editing text.
   * On touch devices, contentEditable is only enabled when this is true,
   * to prevent the keyboard from appearing during touch/drag interactions.
   */
  isEditingText?: boolean;
}

export const InfiniteCanvas = memo(
  forwardRef(function InfiniteCanvas(
    {
      onChangeSize,
      size,
      autoFocus = false,
      style,
      className: classNameProp,
      readOnly,
      onBeforeInput,
      shouldHandleKeyboardEvents = true,
      isEditingText,
      children,
      ...props
    }: InfiniteCanvasProps,
    forwardedRef: ForwardedRef<HTMLDivElement>
  ) {
    const containerRef = useRef<HTMLDivElement | null>(null);

    const { canvasSize: measuredSize } = useAutomaticCanvasSize({
      containerRef,
      onChangeSize,
    });

    const normalizer = useMemo(() => {
      if (!onBeforeInput) return undefined;

      return createBeforeInputNormalizer({
        onBeforeInput: (e) =>
          onBeforeInput(
            new InputEvent("beforeinput", {
              inputType: e.inputType,
              data: e.data ?? undefined,
              dataTransfer: (e.dataTransfer as DataTransfer) ?? undefined,
            })
          ),
      });
    }, [onBeforeInput]);

    const canvasSize = useMemo(
      () => size ?? measuredSize,
      [measuredSize, size]
    );

    useEffect(() => {
      const handleBeforeInput = (event: InputEvent) => {
        // ensure descendant of container
        const target = event.target as HTMLElement;

        if (!containerRef.current?.contains(target)) return;

        if (normalizer) {
          normalizer.handleBeforeInput(event);
        } else {
          onBeforeInput?.(event);
        }

        event.preventDefault();
      };

      if (onBeforeInput && normalizer) {
        window.addEventListener("beforeinput", handleBeforeInput);

        const handleKeyDown = (event: KeyboardEvent) => {
          if (!containerRef.current?.contains(event.target as HTMLElement))
            return;

          const handled = normalizer.handleKeyDownFallback(event);
          if (handled) event.preventDefault();
        };

        const handlePaste = (event: ClipboardEvent) => {
          if (!containerRef.current?.contains(event.target as HTMLElement))
            return;

          const handled = normalizer.handlePasteFallback(event);
          if (handled) event.preventDefault();
        };

        window.addEventListener("keydown", handleKeyDown);
        window.addEventListener("paste", handlePaste);

        return () => {
          window.removeEventListener("beforeinput", handleBeforeInput);
          window.removeEventListener("paste", handlePaste);
          window.removeEventListener("keydown", handleKeyDown);
        };
      }
    }, [normalizer, onBeforeInput]);

    const className = useMemo(() => {
      return [
        classNameProp,
        shouldHandleKeyboardEvents
          ? KEYBOARD_SHORTCUT_PASSTHROUGH_CLASS
          : undefined,
      ]
        .filter(Boolean)
        .join(" ");
    }, [classNameProp, shouldHandleKeyboardEvents]);

    // Only enable contentEditable when actively editing text.
    // This prevents the keyboard from appearing during touch/drag interactions
    // on mobile, and avoids cursor flickering during pan on desktop.
    const contentEditable =
      onBeforeInput && readOnly !== true && isEditingText;

    const childrenElement = useMemo(() => {
      if (!canvasSize) return null;

      return typeof children === "function"
        ? children({ size: canvasSize })
        : children;
    }, [canvasSize, children]);

    return (
      <div
        {...props}
        ref={useCallback(
          (value: HTMLDivElement | null) => {
            containerRef.current = value;
            assignRef(forwardedRef, value);
          },
          [forwardedRef]
        )}
        contentEditable={contentEditable}
        suppressContentEditableWarning
        style={useMemo(
          (): CSSProperties => ({
            position: "relative",
            // Prevent browser handling of touch gestures (scroll, zoom)
            // so the canvas can handle them via JavaScript
            touchAction: "none",
            ...style,
          }),
          [style]
        )}
        className={className}
        tabIndex={0}
        onFocus={useCallback(() => containerRef.current?.focus(), [])}
        data-infinite-canvas-root
      >
        {onBeforeInput && <Placeholder />}
        {childrenElement}
      </div>
    );
  })
);

/**
 * We need a placeholder to be able to handle beforeinput events on the container.
 */
const Placeholder = memo(function Placeholder() {
  return (
    <span
      style={{
        opacity: 0,
        pointerEvents: "none",
        position: "absolute",
        top: 0,
        left: -10000,
        right: 0,
        bottom: 0,
      }}
    >
      a
    </span>
  );
});
