import { ZoomInOutlined, ZoomOutOutlined } from '@ant-design/icons';
import { Button, Typography } from 'antd';
import { useEffect, useRef, useState, type ReactNode, type TouchEvent as ReactTouchEvent } from 'react';

const { Text } = Typography;

type PreviewOffset = {
  x: number;
  y: number;
};

type ZoomablePreviewSurfaceProps = {
  children: ReactNode;
  className?: string;
  stageClassName?: string;
  contentClassName?: string;
  minZoom?: number;
  maxZoom?: number;
  showControls?: boolean;
  onZoomChange?: (zoom: number) => void;
};

type TouchPointLike = {
  clientX: number;
  clientY: number;
};

function calculateTouchDistance(first: TouchPointLike, second: TouchPointLike) {
  return Math.hypot(second.clientX - first.clientX, second.clientY - first.clientY);
}

function clampZoom(value: number, minZoom: number, maxZoom: number) {
  return Math.min(maxZoom, Math.max(minZoom, Number.isFinite(value) ? value : minZoom));
}

function clampOffsetValue(value: number, zoom: number, viewportSize: number) {
  if (zoom <= 1 || viewportSize <= 0) {
    return 0;
  }

  const maxOffset = ((zoom - 1) * viewportSize) / 2;
  return Math.min(maxOffset, Math.max(-maxOffset, value));
}

export function ZoomablePreviewSurface({
  children,
  className,
  stageClassName,
  contentClassName,
  minZoom = 1,
  maxZoom = 3,
  showControls = true,
  onZoomChange,
}: ZoomablePreviewSurfaceProps) {
  const shellRef = useRef<HTMLDivElement | null>(null);
  const gestureRef = useRef<
    | { mode: 'pinch'; distance: number; zoom: number }
    | { mode: 'pan'; touchX: number; touchY: number; offsetX: number; offsetY: number }
    | null
  >(null);
  const [zoom, setZoom] = useState(minZoom);
  const [offset, setOffset] = useState<PreviewOffset>({ x: 0, y: 0 });

  useEffect(() => {
    onZoomChange?.(zoom);
  }, [onZoomChange, zoom]);

  const clampOffset = (nextOffset: PreviewOffset, nextZoom = zoom) => {
    const shell = shellRef.current;
    if (!shell) {
      return { x: 0, y: 0 };
    }

    return {
      x: clampOffsetValue(nextOffset.x, nextZoom, shell.clientWidth),
      y: clampOffsetValue(nextOffset.y, nextZoom, shell.clientHeight),
    };
  };

  useEffect(() => {
    if (zoom <= minZoom) {
      setOffset({ x: 0, y: 0 });
      return;
    }

    setOffset((current) => clampOffset(current, zoom));
  }, [minZoom, zoom]);

  const handleTouchStart = (event: ReactTouchEvent<HTMLDivElement>) => {
    if (event.touches.length >= 2) {
      event.stopPropagation();
      gestureRef.current = {
        mode: 'pinch',
        distance: calculateTouchDistance(event.touches[0], event.touches[1]),
        zoom,
      };
      return;
    }

    const touch = event.touches[0];
    if (!touch || zoom <= minZoom) {
      gestureRef.current = null;
      return;
    }

    event.stopPropagation();
    gestureRef.current = {
      mode: 'pan',
      touchX: touch.clientX,
      touchY: touch.clientY,
      offsetX: offset.x,
      offsetY: offset.y,
    };
  };

  const handleTouchMove = (event: ReactTouchEvent<HTMLDivElement>) => {
    const gesture = gestureRef.current;
    if (!gesture) {
      return;
    }

    if (gesture.mode === 'pinch') {
      if (event.touches.length < 2) {
        return;
      }

      const nextDistance = calculateTouchDistance(event.touches[0], event.touches[1]);
      if (!Number.isFinite(nextDistance) || nextDistance <= 0 || gesture.distance <= 0) {
        return;
      }

      event.preventDefault();
      event.stopPropagation();
      setZoom(clampZoom(gesture.zoom * (nextDistance / gesture.distance), minZoom, maxZoom));
      return;
    }

    const touch = event.touches[0];
    if (!touch || zoom <= minZoom) {
      return;
    }

    event.preventDefault();
    event.stopPropagation();
    setOffset(
      clampOffset({
        x: gesture.offsetX + touch.clientX - gesture.touchX,
        y: gesture.offsetY + touch.clientY - gesture.touchY,
      }),
    );
  };

  const handleTouchEnd = (event: ReactTouchEvent<HTMLDivElement>) => {
    if (gestureRef.current) {
      event.stopPropagation();
    }

    if (event.touches.length >= 2) {
      gestureRef.current = {
        mode: 'pinch',
        distance: calculateTouchDistance(event.touches[0], event.touches[1]),
        zoom,
      };
      return;
    }

    const touch = event.touches[0];
    if (touch && zoom > minZoom) {
      gestureRef.current = {
        mode: 'pan',
        touchX: touch.clientX,
        touchY: touch.clientY,
        offsetX: offset.x,
        offsetY: offset.y,
      };
      return;
    }

    gestureRef.current = null;
  };

  return (
    <div className={['zoomable-preview-surface', className ?? ''].filter(Boolean).join(' ')}>
      {showControls ? (
        <div className="zoomable-preview-surface__toolbar">
          <Button
            type="text"
            className="fullscreen-preview-modal__icon-button"
            aria-label="축소"
            icon={<ZoomOutOutlined />}
            onClick={() => setZoom((current) => clampZoom(current - 0.1, minZoom, maxZoom))}
          />
          <Text className="zoomable-preview-surface__value">{Math.round(zoom * 100)}%</Text>
          <Button
            type="text"
            className="fullscreen-preview-modal__icon-button"
            aria-label="확대"
            icon={<ZoomInOutlined />}
            onClick={() => setZoom((current) => clampZoom(current + 0.1, minZoom, maxZoom))}
          />
        </div>
      ) : null}
      <div
        ref={shellRef}
        className={['zoomable-preview-surface__stage', stageClassName ?? ''].filter(Boolean).join(' ')}
        onTouchStart={handleTouchStart}
        onTouchMove={handleTouchMove}
        onTouchEnd={handleTouchEnd}
        onTouchCancel={handleTouchEnd}
      >
        <div
          className={['zoomable-preview-surface__content', contentClassName ?? ''].filter(Boolean).join(' ')}
          style={{ transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom})` }}
        >
          {children}
        </div>
      </div>
    </div>
  );
}
