import type {
  ElementRef } from '../lib/teact/teact';
import {
  useEffect, useSignal, useState,
} from '../lib/teact/teact';

import type { Point, Size } from '../types';

import { RESIZE_HANDLE_SELECTOR } from '../config';
import buildStyle from '../util/buildStyle';
import { captureEvents } from '../util/captureEvents';
import getPointerPosition from '../util/events/getPointerPosition';
import useFlag from './useFlag';
import useLastCallback from './useLastCallback';

export enum ResizeHandleType {
  Top,
  Bottom,
  Left,
  Right,
  TopLeft,
  TopRight,
  BottomLeft,
  BottomRight,
}

type ResizeHandleSelectorType = 'top' | 'bottom' | 'left'
  | 'right' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';

const resizeHandleSelectorsMap: Record<ResizeHandleSelectorType, ResizeHandleType> = {
  top: ResizeHandleType.Top,
  bottom: ResizeHandleType.Bottom,
  left: ResizeHandleType.Left,
  right: ResizeHandleType.Right,
  topLeft: ResizeHandleType.TopLeft,
  topRight: ResizeHandleType.TopRight,
  bottomLeft: ResizeHandleType.BottomLeft,
  bottomRight: ResizeHandleType.BottomRight,
};

const resizeHandleSelectors = Object.keys(resizeHandleSelectorsMap) as ResizeHandleSelectorType[];

let resizeTimeout: number | undefined;
const FULLSCREEN_POSITION = { x: 0, y: 0 };

export default function useDraggable(
  ref: ElementRef<HTMLElement>,
  dragHandleElementRef: ElementRef<HTMLElement>,
  isDragEnabled: boolean = true,
  originalSize: Size,
  isFullscreen: boolean = false,
  minimumSize: Size = { width: 0, height: 0 },
  cachedPosition?: Point,
) {
  const [elementCurrentPosition, setElementCurrentPosition] = useState<Point | undefined>(cachedPosition);
  const [elementCurrentSize, setElementCurrentSize] = useState<Size | undefined>(undefined);

  const [getElementPositionOnStartTransform, setElementPositionOnStartTransform] = useSignal({ x: 0, y: 0 });
  const [getElementSizeOnStartTransform, setElementSizeOnStartTransform] = useSignal({ width: 0, height: 0 });
  const [getTransformStartPoint, setTransformStartPoint] = useSignal({ x: 0, y: 0 });

  const elementPositionOnStartTransform = getElementPositionOnStartTransform();
  const transformStartPoint = getTransformStartPoint();

  const element = ref.current;
  const dragHandleElement = dragHandleElementRef.current;

  const [isInitiated, setIsInitiated] = useFlag(false);
  const [wasElementShown, setWasElementShown] = useFlag(false);
  const [isDragging, startDragging, stopDragging] = useFlag(false);
  const [isResizing, startResizing, stopResizing] = useFlag(false);
  const [isWindowsResizing, startWindowResizing, stopWindowResizing] = useFlag(false);

  const [hitResizeHandle, setHitResizeHandle] = useState<ResizeHandleType | undefined>(undefined);

  function getVisibleArea() {
    return {
      width: window.innerWidth,
      height: window.innerHeight,
    };
  }

  const updateCurrentPosition = useLastCallback((position: Point) => {
    if (!isFullscreen) setElementCurrentPosition({ x: position.x, y: position.y });
  });

  const getActualPosition = useLastCallback(() => {
    return isFullscreen ? FULLSCREEN_POSITION : elementCurrentPosition;
  });

  const getCenteredPosition = useLastCallback(() => {
    if (!elementCurrentSize) return undefined;
    const { width, height } = elementCurrentSize;

    const visibleArea = getVisibleArea();
    const viewportWidth = visibleArea.width;
    const viewportHeight = visibleArea.height;

    const centeredX = (viewportWidth - width) / 2;
    const centeredY = (viewportHeight - height) / 2;

    return { x: centeredX, y: centeredY };
  });

  useEffect(() => {
    if (element) setWasElementShown();
  }, [element]);

  useEffect(() => {
    if (!isInitiated && elementCurrentSize) {
      const centeredPosition = getCenteredPosition();
      if (!centeredPosition) return;

      updateCurrentPosition(centeredPosition);
      setIsInitiated();
    }
  }, [elementCurrentSize, isInitiated, element]);

  const handleStartDrag = useLastCallback((event: MouseEvent | TouchEvent) => {
    if (event instanceof MouseEvent && event.button !== 0) {
      return;
    }

    const targetElement = event.target as HTMLElement;
    if (targetElement.closest('.no-drag') || !element) {
      return;
    }
    const { x, y } = getPointerPosition(event);

    const { left, top } = element.getBoundingClientRect();
    setElementPositionOnStartTransform({ x: left, y: top });
    setTransformStartPoint({ x, y });

    startDragging();
  });

  function getResizeHandleFromTarget(targetElement: HTMLElement) {
    const closest = (selector: string) => targetElement.closest(selector);

    if (!closest(RESIZE_HANDLE_SELECTOR)) return undefined;
    for (const selector of resizeHandleSelectors) {
      if (closest(`.${selector}`)) {
        return resizeHandleSelectorsMap[selector];
      }
    }
    return undefined;
  }

  const handleStartResize = useLastCallback((event: MouseEvent | TouchEvent) => {
    if (event instanceof MouseEvent && event.button !== 0) {
      return;
    }

    const targetElement = event.target as HTMLElement;
    if (!element || !targetElement) {
      return;
    }
    const resizeHandle = getResizeHandleFromTarget(targetElement);

    if (resizeHandle === undefined) return;
    setHitResizeHandle(resizeHandle);

    const { x, y } = getPointerPosition(event);

    const {
      left, right, top, bottom,
    } = element.getBoundingClientRect();
    setElementPositionOnStartTransform({ x: left, y: top });
    setElementSizeOnStartTransform({ width: right - left, height: bottom - top });
    setTransformStartPoint({ x, y });

    startResizing();
  });

  const handleDragRelease = useLastCallback(() => {
    stopDragging();
  });

  const handleResizeRelease = useLastCallback(() => {
    stopResizing();
  });

  useEffect(() => {
    if (!isDragEnabled) {
      stopDragging();
    }
  }, [isDragEnabled]);

  const ensurePositionInVisibleArea = (x: number, y: number) => {
    const visibleArea = getVisibleArea();

    const visibleAreaWidth = visibleArea.width;
    const visibleAreaHeight = visibleArea.height;

    const componentWidth = elementCurrentSize!.width;
    const componentHeight = elementCurrentSize!.height;

    let newX = x;
    let newY = y;

    if (newX < 0) newX = 0;
    if (newY < 0) newY = 0;
    if (newX + componentWidth > visibleAreaWidth) newX = visibleAreaWidth - componentWidth;
    if (newY + componentHeight > visibleAreaHeight) newY = visibleAreaHeight - componentHeight;

    return { x: newX, y: newY };
  };

  const adjustPositionWithinBounds = useLastCallback(() => {
    if (isFullscreen) return;
    const position = !wasElementShown && !cachedPosition ? getCenteredPosition() : elementCurrentPosition;
    if (!elementCurrentSize || !position) return;
    const newPosition = ensurePositionInVisibleArea(position.x, position.y);
    updateCurrentPosition(newPosition);
  });

  const ensureSizeInVisibleArea = useLastCallback((sizeForCheck: Size) => {
    const newSize = sizeForCheck;

    const visibleArea = getVisibleArea();

    const originalWidth = originalSize.width;
    const originalHeight = originalSize.height;
    newSize.width = Math.min(visibleArea.width, Math.max(originalWidth, newSize.width));
    newSize.height = Math.min(visibleArea.height, Math.max(originalHeight, newSize.height));

    return newSize;
  });

  useEffect(() => {
    if (isResizing) return;
    const newSize = ensureSizeInVisibleArea({ width: originalSize.width, height: originalSize.height });
    if (newSize) setElementCurrentSize(newSize);
  }, [originalSize, isResizing]);

  const adjustSizeWithinBounds = useLastCallback(() => {
    if (!elementCurrentSize || isResizing) return;
    const newSize = ensureSizeInVisibleArea(elementCurrentSize);
    if (newSize) setElementCurrentSize(newSize);
  });

  useEffect(() => {
    if (isResizing) return;
    adjustPositionWithinBounds();
  }, [elementCurrentSize, isResizing]);

  useEffect(() => {
    const handleWindowResize = () => {
      startWindowResizing();
      adjustSizeWithinBounds();
      adjustPositionWithinBounds();
      if (resizeTimeout) {
        clearTimeout(resizeTimeout);
        resizeTimeout = undefined;
      }
      resizeTimeout = window.setTimeout(() => {
        resizeTimeout = undefined;
        stopWindowResizing();
      }, 250);
    };

    window.addEventListener('resize', handleWindowResize);

    return () => {
      clearTimeout(resizeTimeout);
      resizeTimeout = undefined;
      window.removeEventListener('resize', handleWindowResize);
    };
  }, [adjustPositionWithinBounds]);

  const handleDrag = useLastCallback((event: MouseEvent | TouchEvent) => {
    if (!isDragging || !element) return;
    const { x, y } = getPointerPosition(event);

    const offsetX = x - transformStartPoint.x;
    const offsetY = y - transformStartPoint.y;

    const newX = elementPositionOnStartTransform.x + offsetX;
    const newY = elementPositionOnStartTransform.y + offsetY;

    if (elementCurrentSize) setElementCurrentPosition(ensurePositionInVisibleArea(newX, newY));
  });

  const handleResize = useLastCallback((event: MouseEvent | TouchEvent) => {
    if (!isResizing || !element || hitResizeHandle === undefined) return;
    const { x, y } = getPointerPosition(event);
    const sizeOnStartTransform = getElementSizeOnStartTransform();

    const pageVisibleX = Math.min(Math.max(0, x), getVisibleArea().width);
    const pageVisibleY = Math.min(Math.max(0, y), getVisibleArea().height);

    const offsetX = pageVisibleX - transformStartPoint.x;
    const offsetY = pageVisibleY - transformStartPoint.y;

    const maxX = elementPositionOnStartTransform.x + sizeOnStartTransform.width - minimumSize.width;
    const maxY = elementPositionOnStartTransform.y + sizeOnStartTransform.height - minimumSize.height;

    const originalBounds = {
      x: elementPositionOnStartTransform.x,
      y: elementPositionOnStartTransform.y,
      width: sizeOnStartTransform.width,
      height: sizeOnStartTransform.height,
    };

    const newBounds = { ...originalBounds };

    if (hitResizeHandle === ResizeHandleType.Left
      || hitResizeHandle === ResizeHandleType.TopLeft
      || hitResizeHandle === ResizeHandleType.BottomLeft
    ) {
      newBounds.width = Math.max(sizeOnStartTransform.width - offsetX, minimumSize.width);
      newBounds.x = Math.min(newBounds.x + offsetX, maxX);
    }

    if (hitResizeHandle === ResizeHandleType.Right
      || hitResizeHandle === ResizeHandleType.TopRight
      || hitResizeHandle === ResizeHandleType.BottomRight
    ) {
      newBounds.width = Math.max(sizeOnStartTransform.width + offsetX, minimumSize.width);
    }

    if (hitResizeHandle === ResizeHandleType.Top
      || hitResizeHandle === ResizeHandleType.TopLeft
      || hitResizeHandle === ResizeHandleType.TopRight
    ) {
      newBounds.height = Math.max(sizeOnStartTransform.height - offsetY, minimumSize.height);
      newBounds.y = Math.min(newBounds.y + offsetY, maxY);
    }

    if (hitResizeHandle === ResizeHandleType.Bottom
      || hitResizeHandle === ResizeHandleType.BottomLeft
      || hitResizeHandle === ResizeHandleType.BottomRight
    ) {
      newBounds.height = Math.max(sizeOnStartTransform.height + offsetY, minimumSize.height);
    }

    setElementCurrentSize({ width: newBounds.width, height: newBounds.height });
    setElementCurrentPosition({ x: newBounds.x, y: newBounds.y });
  });

  useEffect(() => {
    let cleanup: NoneToVoidFunction | undefined;
    if (dragHandleElement && isDragEnabled) {
      cleanup = captureEvents(dragHandleElement, {
        onCapture: handleStartDrag,
        onDrag: handleDrag,
        onRelease: handleDragRelease,
        onClick: handleDragRelease,
        onDoubleClick: handleDragRelease,
      });
    }
    return cleanup;
  }, [isDragEnabled, dragHandleElement]);

  useEffect(() => {
    const cleanups: NoneToVoidFunction[] = [];
    if (element && isDragEnabled) {
      for (const selector of resizeHandleSelectors) {
        const resizeHandler = element.querySelector(`.resizeHandle.${selector}`) as HTMLElement;

        if (resizeHandler) {
          const cleanup = captureEvents(resizeHandler, {
            onCapture: handleStartResize,
            onDrag: handleResize,
            onRelease: handleResizeRelease,
            onClick: handleResizeRelease,
            onDoubleClick: handleResizeRelease,
          });

          if (cleanup) {
            cleanups.push(cleanup);
          }
        }
      }
    }

    return () => {
      cleanups.forEach((cleanup) => cleanup());
    };
  }, [isDragEnabled, element]);

  const cursorStyle = isDragging ? 'cursor: grabbing !important; ' : '';

  const actualPosition = getActualPosition();

  if (!isInitiated || !elementCurrentSize || !actualPosition) {
    return {
      isDragging: false,
      style: cursorStyle,
    };
  }

  const style = buildStyle(
    `left: ${actualPosition.x}px;`,
    `top: ${actualPosition.y}px;`,
    !isFullscreen && `max-width: ${elementCurrentSize.width}px;`,
    !isFullscreen && `max-height: ${elementCurrentSize.height}px;`,
    'position: fixed;',
    (isDragging || isResizing || isWindowsResizing) && 'transition: none !important;',
    cursorStyle,
  );

  return {
    position: elementCurrentPosition,
    size: elementCurrentSize,
    isDragging,
    isResizing,
    style,
  };
}
