import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import styles from './styles.module.scss';
import classNames from 'classnames';
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
import { INodeComponentProps } from '~/redux/node/constants';
import { getURL } from '~/utils/dom';
import { PRESETS } from '~/constants/urls';
import { throttle } from 'throttle-debounce';
import { Icon } from '~/components/input/Icon';
import { useArrows } from '~/utils/hooks/keys';

interface IProps extends INodeComponentProps {}

const getX = event =>
  (event.touches && event.touches.length) || (event.changedTouches && event.changedTouches.length)
    ? (event.touches.length && event.touches[0].clientX) || event.changedTouches[0].clientX
    : event.clientX;

const NodeImageSlideBlock: FC<IProps> = ({
  node,
  is_loading,
  is_modal_shown,
  updateLayout,
  modalShowPhotoswipe,
}) => {
  const [current, setCurrent] = useState(0);
  const [height, setHeight] = useState(window.innerHeight - 143);
  const [max_height, setMaxHeight] = useState(960);
  const [loaded, setLoaded] = useState<Record<number, boolean>>({});
  const refs = useRef<Record<number, HTMLDivElement>>({});
  const [heights, setHeights] = useState({});

  const [initial_offset, setInitialOffset] = useState(0);
  const [initial_x, setInitialX] = useState(0);
  const [offset, setOffset] = useState(0);
  const [is_dragging, setIsDragging] = useState(false);
  const [drag_start, setDragStart] = useState(0);

  const slide = useRef<HTMLDivElement>(null);
  const wrap = useRef<HTMLDivElement>(null);

  const setHeightThrottled = useCallback(throttle(100, setHeight), [setHeight]);

  const images = useMemo(
    () =>
      (node && node.files && node.files.filter(({ type }) => type === UPLOAD_TYPES.IMAGE)) || [],
    [node.files]
  );

  useEffect(() => {
    setLoaded({});
    refs.current = {};
  }, [images]);

  const updateSizes = useCallback(() => {
    const values = Object.keys(refs.current).reduce((obj, key) => {
      const ref = refs.current[key];

      if (!ref || !ref.getBoundingClientRect) return 0;

      return { ...obj, [key]: ref.getBoundingClientRect().height };
    }, {});

    setHeights(values);
  }, [refs]);

  const setRef = useCallback(
    index => el => {
      refs.current[index] = el;
    },
    [refs, heights, setHeights, images]
  );

  const onImageLoad = useCallback(index => () => setLoaded({ ...loaded, [index]: true }), [
    setLoaded,
    loaded,
  ]);

  // update outside hooks
  useEffect(updateLayout, [loaded, height, images]);
  useEffect(updateSizes, [refs, current, loaded, images]);

  useEffect(() => {
    const timeout = setTimeout(updateLayout, 300);

    if (!wrap || !wrap.current) return () => clearTimeout(timeout);

    const { width } = wrap.current.getBoundingClientRect();
    const fallback = window.innerHeight - 143;

    if (is_loading) {
      setHeight(fallback);
      return () => clearTimeout(timeout);
    }

    const selected = Math.abs(-offset / width);

    if (!heights[Math.round(selected)]) {
      setHeight(fallback);
      return () => clearTimeout(timeout);
    }

    const minimal = Math.min(fallback, 120);

    const prev = Math.max(heights[Math.floor(selected)] || fallback, minimal);
    const next = Math.max(heights[Math.ceil(selected)] || fallback, minimal);
    const now = prev - (prev - next) * (selected % 1);

    if (current !== Math.round(selected)) setCurrent(Math.round(selected));

    if (!is_dragging) {
      setHeight(now);
    } else {
      setHeightThrottled(now);
    }

    // update layout after all manipulations
    return () => {
      if (timeout) clearTimeout(timeout);
    };
  }, [is_dragging, wrap, offset, heights, max_height, images, is_loading, updateLayout]);

  const onDrag = useCallback(
    event => {
      if (
        !is_dragging ||
        !slide.current ||
        !wrap.current ||
        (event.touches && event.clientY > event.clientX)
      )
        return;

      const { width: slide_width } = slide.current.getBoundingClientRect();
      const { width: wrap_width } = wrap.current.getBoundingClientRect();

      setOffset(
        Math.min(Math.max(initial_offset + getX(event) - initial_x, wrap_width - slide_width), 0)
      );
    },
    [is_dragging, initial_x, setOffset, initial_offset]
  );

  const normalizeOffset = useCallback(() => {
    if (!slide.current || !wrap.current) return;

    const { width: wrap_width } = wrap.current.getBoundingClientRect();
    const { width: slide_width } = slide.current.getBoundingClientRect();

    const shift = (initial_offset - offset) / wrap_width; // percent / 100
    const diff = initial_offset - (shift > 0 ? Math.ceil(shift) : Math.floor(shift)) * wrap_width;
    const new_offset =
      Math.abs(shift) > 0.25
        ? Math.min(Math.max(diff, wrap_width - slide_width), 0) // next or prev slide
        : Math.round(offset / wrap_width) * wrap_width; // back to this one

    setOffset(new_offset);
  }, [wrap, offset, initial_offset]);

  const updateMaxHeight = useCallback(() => {
    if (!wrap.current) return;
    setMaxHeight(window.innerHeight - 143);
    normalizeOffset();
  }, [wrap, setMaxHeight, normalizeOffset]);

  const onOpenPhotoSwipe = useCallback(() => modalShowPhotoswipe(images, current), [
    modalShowPhotoswipe,
    images,
    current,
  ]);

  const stopDragging = useCallback(
    event => {
      if (!is_dragging) return;

      setIsDragging(false);
      normalizeOffset();

      if (
        Math.abs(new Date().getTime() - drag_start) < 200 &&
        Math.abs(initial_x - getX(event)) < 5
      ) {
        onOpenPhotoSwipe();
      }
    },
    [setIsDragging, is_dragging, normalizeOffset, onOpenPhotoSwipe, drag_start]
  );

  const startDragging = useCallback(
    event => {
      setIsDragging(true);
      setInitialX(getX(event));
      setInitialOffset(offset);
      setDragStart(new Date().getTime());
    },
    [setIsDragging, setInitialX, offset, setInitialOffset, setDragStart]
  );

  useEffect(() => updateMaxHeight(), [images]);

  useEffect(() => {
    window.addEventListener('resize', updateSizes);
    window.addEventListener('resize', updateMaxHeight);

    window.addEventListener('mousemove', onDrag);
    window.addEventListener('touchmove', onDrag);

    window.addEventListener('mouseup', stopDragging);
    window.addEventListener('touchend', stopDragging);

    return () => {
      window.removeEventListener('resize', updateSizes);
      window.removeEventListener('resize', updateMaxHeight);

      window.removeEventListener('mousemove', onDrag);
      window.removeEventListener('touchmove', onDrag);

      window.removeEventListener('mouseup', stopDragging);
      window.removeEventListener('touchend', stopDragging);
    };
  }, [onDrag, stopDragging, updateMaxHeight, updateSizes]);

  const changeCurrent = useCallback(
    (item: number) => {
      if (!wrap.current) return;

      const { width } = wrap.current.getBoundingClientRect();
      setOffset(-1 * item * width);
    },
    [wrap]
  );

  const onPrev = useCallback(() => changeCurrent(current > 0 ? current - 1 : images.length - 1), [
    changeCurrent,
    current,
    images,
  ]);

  const onNext = useCallback(() => changeCurrent(current < images.length - 1 ? current + 1 : 0), [
    changeCurrent,
    current,
    images,
  ]);

  useArrows(onNext, onPrev, is_modal_shown);

  useEffect(() => {
    setOffset(0);
  }, [node.id]);

  return (
    <div className={styles.wrap}>
      <div className={classNames(styles.cutter, { [styles.is_loading]: is_loading })} ref={wrap}>
        <div
          className={classNames(styles.image_container, { [styles.is_dragging]: is_dragging })}
          style={{
            height,
            transform: `translate(${offset}px, 0)`,
            width: `${images.length * 100}%`,
          }}
          onMouseDown={startDragging}
          onTouchStart={startDragging}
          ref={slide}
        >
          {!is_loading &&
            images.map((file, index) => (
              <div
                className={classNames(styles.image_wrap, {
                  [styles.is_active]: index === current,
                })}
                ref={setRef(index)}
                key={`${node?.updated_at || ''} + ${file?.id || ''} + ${index}`}
              >
                <svg
                  viewBox={`0 0 ${file?.metadata?.width || 0} ${file?.metadata?.height || 0}`}
                  className={classNames(styles.preview, { [styles.is_loaded]: loaded[index] })}
                  style={{
                    maxHeight: max_height,
                    width: '100%',
                  }}
                >
                  <defs>
                    <filter id="f1" x="0" y="0">
                      <feGaussianBlur
                        stdDeviation="5 5"
                        x="0%"
                        y="0%"
                        width="100%"
                        height="100%"
                        in="blend"
                        edgeMode="none"
                        result="blur2"
                      />
                    </filter>
                  </defs>

                  <rect fill="#242222" width="100%" height="100%" stroke="none" rx="8" ry="8" />

                  <image
                    xlinkHref={getURL(file, PRESETS['300'])}
                    width="100%"
                    height="100%"
                    filter="url(#f1)"
                  />
                </svg>

                <img
                  className={classNames(styles.image, { [styles.is_loaded]: loaded[index] })}
                  src={getURL(file, PRESETS['1600'])}
                  alt=""
                  key={file.id}
                  onLoad={onImageLoad(index)}
                  style={{ maxHeight: max_height }}
                />
              </div>
            ))}
        </div>

        {images.length > 1 && (
          <div className={styles.image_count}>
            {current + 1}
            <small> / </small>
            {images.length}
          </div>
        )}
      </div>

      {images.length > 1 && (
        <div className={classNames(styles.image_arrow)} onClick={onPrev}>
          <Icon icon="left" size={40} />
        </div>
      )}

      {images.length > 1 && (
        <div className={classNames(styles.image_arrow, styles.image_arrow_right)} onClick={onNext}>
          <Icon icon="right" size={40} />
        </div>
      )}
    </div>
  );
};

export { NodeImageSlideBlock };