import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import styled, { keyframes } from 'styled-components';

import type { JSX, TouchEvent as ReactTouchEvent, WheelEvent, MouseEvent, ReactNode } from 'react';

import { useModalScrollLock } from '@redocly/theme/core/hooks';
import { Button } from '@redocly/theme/components/Button/Button';
import { Tooltip } from '@redocly/theme/components/Tooltip/Tooltip';
import { AddIcon } from '@redocly/theme/icons/AddIcon/AddIcon';
import { SubtractIcon } from '@redocly/theme/icons/SubtractIcon/SubtractIcon';
import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
import { FitToViewIcon } from '@redocly/theme/icons/FitToViewIcon/FitToViewIcon';

export type SvgViewerLabels = {
  zoomIn?: string;
  zoomOut?: string;
  fitToView?: string;
  close?: string;
  dialogLabel?: string;
};

export type SvgViewerProps = {
  isOpen: boolean;
  onClose: () => void;
  children: ReactNode;
  labels?: SvgViewerLabels;
};

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

const MIN_SCALE_FACTOR = 0.1;
const MAX_SCALE_FACTOR = 5;
const ZOOM_STEP = 0.1;
const WHEEL_SENSITIVITY = 0.002;
const VIEWPORT_PADDING = 60;
const FIT_SCALE_FACTOR = 0.9;

export function SvgViewer({
  isOpen,
  onClose,
  children,
  labels = {},
}: SvgViewerProps): JSX.Element | null {
  const [scale, setScale] = useState(1);
  const [baseScale, setBaseScale] = useState(1);
  const [position, setPosition] = useState<Position>({ x: 0, y: 0 });
  const [isDragging, setIsDragging] = useState(false);
  const [dragStart, setDragStart] = useState<Position>({ x: 0, y: 0 });
  const [pinchState, setPinchState] = useState<{ distance: number; scale: number } | null>(null);
  const [isWheelZooming, setIsWheelZooming] = useState(false);

  const wheelTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const overlayRef = useRef<HTMLDivElement>(null);
  const viewportRef = useRef<HTMLDivElement>(null);
  const contentRef = useRef<HTMLDivElement>(null);
  const renderedScaleRef = useRef(scale);

  useModalScrollLock(isOpen);

  // Keep track of the actually rendered scale for accurate measurements
  useLayoutEffect(() => {
    renderedScaleRef.current = scale;
  }, [scale]);

  const minScale = baseScale * MIN_SCALE_FACTOR;
  const maxScale = baseScale * MAX_SCALE_FACTOR;

  const clampScale = useCallback(
    (value: number) => Math.min(maxScale, Math.max(minScale, value)),
    [minScale, maxScale],
  );

  const calculateFitScale = useCallback(() => {
    if (!viewportRef.current || !contentRef.current) return 1;

    const viewport = viewportRef.current.getBoundingClientRect();
    const svg = contentRef.current.querySelector('svg');
    if (!svg) return 1;

    const svgRect = svg.getBoundingClientRect();
    if (!svgRect.width || !svgRect.height) return 1;

    // getBoundingClientRect returns transformed size, so compensate for current scale
    const currentScale = renderedScaleRef.current || 1;
    const naturalWidth = svgRect.width / currentScale;
    const naturalHeight = svgRect.height / currentScale;

    const availableWidth = viewport.width - VIEWPORT_PADDING * 2;
    const availableHeight = viewport.height - VIEWPORT_PADDING * 2;

    return (
      Math.min(availableWidth / naturalWidth, availableHeight / naturalHeight) * FIT_SCALE_FACTOR
    );
  }, []);

  const resetView = useCallback(() => {
    setScale(baseScale);
    setPosition({ x: 0, y: 0 });
  }, [baseScale]);

  const zoomIn = useCallback(() => {
    setScale((s) => clampScale(s + baseScale * ZOOM_STEP));
  }, [baseScale, clampScale]);

  const zoomOut = useCallback(() => {
    setScale((s) => clampScale(s - baseScale * ZOOM_STEP));
  }, [baseScale, clampScale]);

  const handleKeyDown = useCallback(
    (e: React.KeyboardEvent) => {
      switch (e.key) {
        case 'Escape':
          onClose();
          break;
        case '+':
        case '=':
          zoomIn();
          break;
        case '-':
          zoomOut();
          break;
        case '0':
          resetView();
          break;
      }
    },
    [onClose, zoomIn, zoomOut, resetView],
  );

  const handleWheel = useCallback(
    (e: WheelEvent) => {
      setIsWheelZooming(true);
      if (wheelTimeoutRef.current) clearTimeout(wheelTimeoutRef.current);
      wheelTimeoutRef.current = setTimeout(() => setIsWheelZooming(false), 150);

      const delta = -e.deltaY * WHEEL_SENSITIVITY;
      setScale((s) => clampScale(s + s * delta));
    },
    [clampScale],
  );

  const handleMouseDown = useCallback(
    (e: MouseEvent) => {
      if (e.button !== 0) return;
      e.preventDefault();
      setIsDragging(true);
      setDragStart({ x: e.clientX - position.x, y: e.clientY - position.y });
    },
    [position],
  );

  const handleMouseMove = useCallback(
    (e: MouseEvent) => {
      if (!isDragging) return;
      setPosition({ x: e.clientX - dragStart.x, y: e.clientY - dragStart.y });
    },
    [isDragging, dragStart],
  );

  const handleMouseUp = useCallback(() => setIsDragging(false), []);

  const getTouchDistance = (touches: React.TouchList): number => {
    if (touches.length !== 2) return 0;
    const dx = touches[0].clientX - touches[1].clientX;
    const dy = touches[0].clientY - touches[1].clientY;
    return Math.hypot(dx, dy);
  };

  const handleTouchStart = useCallback(
    (e: ReactTouchEvent) => {
      if (e.touches.length === 2) {
        setPinchState({ distance: getTouchDistance(e.touches), scale });
      } else if (e.touches.length === 1) {
        setIsDragging(true);
        setDragStart({
          x: e.touches[0].clientX - position.x,
          y: e.touches[0].clientY - position.y,
        });
      }
    },
    [position, scale],
  );

  const handleTouchMove = useCallback(
    (e: ReactTouchEvent) => {
      if (e.touches.length === 2 && pinchState) {
        const distance = getTouchDistance(e.touches);
        setScale(clampScale(pinchState.scale * (distance / pinchState.distance)));
      } else if (e.touches.length === 1 && isDragging) {
        setPosition({
          x: e.touches[0].clientX - dragStart.x,
          y: e.touches[0].clientY - dragStart.y,
        });
      }
    },
    [pinchState, isDragging, dragStart, clampScale],
  );

  const handleTouchEnd = useCallback(() => {
    setIsDragging(false);
    setPinchState(null);
  }, []);

  useEffect(() => {
    if (!isOpen) return;

    setPosition({ x: 0, y: 0 });
    overlayRef.current?.focus();

    // Wait for DOM to be ready before measuring
    requestAnimationFrame(() => {
      const fitScale = calculateFitScale();
      setBaseScale(fitScale);
      setScale(fitScale);
    });
  }, [isOpen, calculateFitScale]);

  if (!isOpen) return null;

  const zoomPercentage = baseScale > 0 ? Math.round((scale / baseScale) * 100) : 100;
  const isAnimating = !isDragging && !isWheelZooming && !pinchState;

  return (
    <Overlay
      ref={overlayRef}
      onClick={onClose}
      onKeyDown={handleKeyDown}
      tabIndex={0}
      aria-modal="true"
      role="dialog"
      aria-label={labels.dialogLabel || 'SVG viewer'}
    >
      <Viewport
        ref={viewportRef}
        onClick={(e) => e.stopPropagation()}
        onWheel={handleWheel}
        onMouseDown={handleMouseDown}
        onMouseMove={handleMouseMove}
        onMouseUp={handleMouseUp}
        onMouseLeave={handleMouseUp}
        onTouchStart={handleTouchStart}
        onTouchMove={handleTouchMove}
        onTouchEnd={handleTouchEnd}
        $isDragging={isDragging}
      >
        <Content
          ref={contentRef}
          $isAnimating={isAnimating}
          style={{
            transform: `translate(calc(-50% + ${position.x}px), calc(-50% + ${position.y}px)) scale(${scale})`,
          }}
        >
          {children}
        </Content>
        <Controls>
          <ControlGroup>
            <Tooltip tip={labels.zoomOut || 'Zoom out'} placement="top">
              <ControlButton
                variant="text"
                size="small"
                icon={<SubtractIcon />}
                onClick={zoomOut}
                disabled={scale <= minScale}
              />
            </Tooltip>
            <ZoomLabel>{zoomPercentage}%</ZoomLabel>
            <Tooltip tip={labels.zoomIn || 'Zoom in'} placement="top">
              <ControlButton
                variant="text"
                size="small"
                icon={<AddIcon />}
                onClick={zoomIn}
                disabled={scale >= maxScale}
              />
            </Tooltip>
            <Divider />
            <Tooltip tip={labels.fitToView || 'Fit to view'} placement="top">
              <ControlButton
                variant="text"
                size="small"
                icon={<FitToViewIcon />}
                onClick={resetView}
              />
            </Tooltip>
            <Tooltip tip={labels.close || 'Close'} placement="top">
              <ControlButton variant="text" size="small" icon={<CloseIcon />} onClick={onClose} />
            </Tooltip>
          </ControlGroup>
        </Controls>
      </Viewport>
    </Overlay>
  );
}

const scaleIn = keyframes`
  from {
    transform: scale(0.9);
  }
  to {
    transform: scale(1);
  }
`;

const slideUp = keyframes`
  from {
    opacity: 0;
    transform: translateX(-50%) translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }
`;

const Overlay = styled.div`
  position: fixed;
  inset: 0;
  background-color: var(--svg-viewer-overlay-bg-color);
  backdrop-filter: blur(var(--spacing-unit));
  z-index: var(--z-index-overlay, 1000);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--spacing-xxl);

  &:focus {
    outline: none;
  }

  @media (max-width: 768px) {
    padding: var(--spacing-md);
  }
`;

const Viewport = styled.div<{ $isDragging: boolean }>`
  position: relative;
  width: 100%;
  height: 100%;
  background-color: var(--svg-viewer-bg-color);
  border-radius: var(--svg-viewer-border-radius);
  overflow: hidden;
  cursor: ${({ $isDragging }) => ($isDragging ? 'grabbing' : 'grab')};
  touch-action: none;
  box-shadow: var(--svg-viewer-box-shadow);
  animation: ${scaleIn} 0.25s ease-in-out forwards;
`;

const Content = styled.div<{ $isAnimating: boolean }>`
  position: absolute;
  top: 50%;
  left: 50%;
  transform-origin: center center;
  user-select: none;
  pointer-events: none;
  transition: ${({ $isAnimating }) => ($isAnimating ? 'transform 0.25s ease-in-out' : 'none')};

  svg {
    display: block;
    max-width: none !important;
  }
`;

const Controls = styled.div`
  position: absolute;
  bottom: var(--spacing-sm);
  left: 50%;
  transform: translateX(-50%);
  z-index: 10;
  animation: ${slideUp} 0.3s ease-out 0.1s backwards;
`;

const ControlGroup = styled.div`
  display: flex;
  align-items: center;
  gap: 2px;
  padding: var(--spacing-xxs);
  background: var(--bg-color-raised);
  border: 1px solid var(--border-color-primary);
  border-radius: var(--border-radius-lg);
  box-shadow: var(--bg-raised-shadow);
`;

const ControlButton = styled(Button)`
  --button-icon-size: 16px;
`;

const ZoomLabel = styled.span`
  min-width: 40px;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--text-color-secondary);
  text-align: center;
  font-variant-numeric: tabular-nums;
`;

const Divider = styled.div`
  width: 1px;
  height: var(--spacing-base);
  background: var(--border-color-primary);
  margin: 0 var(--spacing-xxs);
`;
