import React, { Fragment, useCallback, useLayoutEffect, useMemo, useState } from 'react';
import {
  BeforeCapture,
  DragDropContext,
  Draggable,
  DraggableProvided,
  DraggableRubric,
  DraggableStateSnapshot,
  DragStart,
  DragUpdate,
  Droppable,
  DroppableId,
  DroppableProvided,
  DroppableStateSnapshot,
  DropResult,
  ResponderProvided,
} from 'react-beautiful-dnd';
import { bem } from 'utils/bem';
import { Id } from 'backend-api/models';
import { SMultipleListsDragNDrop } from './s-multiple-lists-drag-n-drop';
import { SDraggableItemContainer } from './components';
import { DroppableItemLoader } from './components/droppable-item-loader';

export interface DraggableItem {
  id: Id | string;
  isDragDisabled?: boolean;
}

export interface ListProps<T> {
  id: string;
  title?: React.ReactChild;
  placeholder?: string;

  renderItem(
    item: T,
    provided: DraggableProvided,
    snapshot: DraggableStateSnapshot,
    index: number,
    isClone?: boolean
  ): React.ReactChild;
  sourceArray: T[];
  containerItemClassName?: string;
  isDropOutsideDisabled?: boolean;
  isDropDisabled?: boolean;
  isDragDisabled?: boolean;
  isShuffleEnabled?: boolean;
}

export interface DragNDropEvents {
  onBeforeCapture?(before: BeforeCapture): void;
  onBeforeDragStart?(initial: DragStart): void;
  onDragStart?(initial: DragStart, provided: ResponderProvided): void;
  onDragUpdate?(initial: DragUpdate, provided: ResponderProvided): void;
  onDragEnd(result: DropResult, provided: ResponderProvided): void;
}

interface Props<T extends DraggableItem> extends DragNDropEvents {
  listProps: ListProps<T>[];
  clonesContainer?: HTMLElement;
  shouldUseClones?: boolean;
  isLoading?: boolean;
}

interface ClientRect {
  clientHeight: number;
  clientWidth: number;
  clientY: number;
  clientX: number;
}

type PlaceholderProps = Record<DroppableId, ClientRect | undefined>;

const classes = bem('lists-drag-n-drop');

const queryAttr = 'data-rbd-drag-handle-draggable-id';
const droppableQueryAttr = 'data-rbd-droppable-id';

export const MultipleListsDragNDrop = React.memo(<T extends DraggableItem>(props: Props<T>) => {
  const [placeholderProps, setPlaceholderProps] = useState<PlaceholderProps>({});
  const [lastUpdateEvent, setLastUpdateEvent] = useState<DragUpdate>();
  const [sourceId, setSourceId] = useState<string>();

  const setPlaceholder = useCallback((event?: DragUpdate | DragStart) => {
    const getDraggedDom = (draggableId: string) => {
      const domQuery = `[${queryAttr}='${draggableId}']`;
      return document.querySelector(domQuery);
    };

    const getDroppableDom = (droppableId: string) => {
      const domQuery = `[${droppableQueryAttr}='${droppableId}']`;
      return document.querySelector(domQuery);
    };

    const isDragUpdate = (event?: DragUpdate | DragStart): event is DragUpdate => {
      return !!event && event.hasOwnProperty('destination');
    };

    const destination = isDragUpdate(event) ? event?.destination : event?.source;

    if (!event || !destination) {
      return;
    }

    const draggedDOM = getDraggedDom(event.draggableId);
    const droppableNode = getDroppableDom(destination.droppableId);

    if (!draggedDOM || !droppableNode) {
      return;
    }

    const { clientHeight, clientWidth } = draggedDOM;
    const destinationIndex = destination.index;
    const childrenArray = [].slice.call(droppableNode.children);

    const clientY = childrenArray.slice(0, destinationIndex).reduce((total: number, curr: HTMLElement) => {
      const style = window.getComputedStyle(curr);
      const marginBottom = parseFloat(style.marginBottom);
      const marginTop = parseFloat(style.marginTop);
      return total + curr.clientHeight + marginBottom + marginTop;
    }, 0);

    setPlaceholderProps({
      [destination.droppableId]: {
        clientHeight,
        clientWidth,
        clientY,
        clientX: 0,
      },
    });
  }, []);

  useLayoutEffect(() => setPlaceholder(lastUpdateEvent), [setPlaceholder, lastUpdateEvent]);

  const onDragUpdate = useCallback(
    (initial: DragUpdate, provided: ResponderProvided) => {
      setLastUpdateEvent(initial);
      if (props.onDragUpdate) {
        props.onDragUpdate(initial, provided);
      }
    },
    [props]
  );

  const onDragEnd = useCallback(
    (initial: DropResult, provided: ResponderProvided) => {
      setPlaceholderProps({});
      setSourceId('');
      if (props.onDragEnd) {
        props.onDragEnd(initial, provided);
      }
    },
    [props]
  );

  const onDragStart = useCallback(
    (initial: DragStart, provided: ResponderProvided) => {
      setLastUpdateEvent(initial);
      setSourceId(initial.source.droppableId);
      if (props.onDragStart) {
        props.onDragStart(initial, provided);
      }
    },
    [props]
  );

  const renderDroppableContent = useCallback(
    (provided: DroppableProvided, snapshot: DroppableStateSnapshot, listProp: ListProps<T>) => {
      const placeholder = placeholderProps[provided.droppableProps['data-rbd-droppable-id']];
      const shouldShowItems = listProp.sourceArray.length > 0 || snapshot.isDraggingOver;
      return (
        <div
          {...provided.droppableProps}
          ref={provided.innerRef}
          className={classes('items-list')}
          data-selector={`${listProp.id}-droppable-area`}
        >
          {shouldShowItems && (
            <>
              {listProp.sourceArray.map(
                (item, index) =>
                  item && (
                    <Draggable
                      key={item.id}
                      draggableId={item.id.toString()}
                      index={index}
                      isDragDisabled={listProp.isDragDisabled || item.isDragDisabled}
                    >
                      {(provided, snapshot) => (
                        <SDraggableItemContainer
                          ref={provided.innerRef}
                          {...provided.draggableProps}
                          {...provided.dragHandleProps}
                          className={classes('list-item-container', undefined, listProp.containerItemClassName)}
                        >
                          {listProp.renderItem(item, provided, snapshot, index)}
                        </SDraggableItemContainer>
                      )}
                    </Draggable>
                  )
              )}
              {provided.placeholder}
              {!!placeholder && snapshot.isDraggingOver && (
                <div
                  className={classes('droppable-placeholder')}
                  style={{
                    position: 'absolute',
                    top: placeholder.clientY,
                    left: placeholder.clientX,
                    height: placeholder.clientHeight,
                    width: placeholder.clientWidth,
                  }}
                />
              )}
            </>
          )}
          <div
            className={classes('placeholder-container', { hidden: shouldShowItems || !listProp.placeholder })}
            data-selector={`${listProp.id}-placeholder-container`}
          >
            <div className={classes('placeholder-text')}>{listProp.placeholder}</div>
          </div>
        </div>
      );
    },
    [placeholderProps]
  );

  const getClonesContainer = useCallback(() => <>{props.clonesContainer}</>, [props.clonesContainer]);

  const droppablelists = useMemo(
    () =>
      props.listProps.map(listProp => {
        const isDropDisabled =
          (listProp.isDropOutsideDisabled && sourceId !== listProp.id) ||
          listProp.isDropDisabled ||
          !listProp.isShuffleEnabled;
        return (
          <Fragment key={listProp.id}>
            {listProp.title && <span>{listProp.title}</span>}
            {props.isLoading ? (
              <DroppableItemLoader />
            ) : (
              <Droppable
                droppableId={listProp.id}
                isDropDisabled={isDropDisabled}
                renderClone={
                  props.shouldUseClones
                    ? (provided: DraggableProvided, snapshot: DraggableStateSnapshot, rubric: DraggableRubric) => (
                        <SDraggableItemContainer
                          ref={provided.innerRef}
                          {...provided.draggableProps}
                          {...provided.dragHandleProps}
                          className={classes('list-item-container', undefined, listProp.containerItemClassName)}
                        >
                          {listProp.renderItem(
                            listProp.sourceArray[rubric.source.index],
                            provided,
                            snapshot,
                            rubric.source.index,
                            true
                          )}
                        </SDraggableItemContainer>
                      )
                    : undefined
                }
                getContainerForClone={props.clonesContainer && props.shouldUseClones ? getClonesContainer : undefined}
              >
                {(provided, snapshot) => renderDroppableContent(provided, snapshot, listProp)}
              </Droppable>
            )}
          </Fragment>
        );
      }),
    [props, sourceId, renderDroppableContent, getClonesContainer]
  );

  return (
    <SMultipleListsDragNDrop>
      <DragDropContext onDragEnd={onDragEnd} onDragUpdate={onDragUpdate} onDragStart={onDragStart}>
        {droppablelists}
      </DragDropContext>
    </SMultipleListsDragNDrop>
  );
});
