/* eslint-disable react/forbid-prop-types */
/* eslint-disable react/require-default-props */
import React, { Fragment } from 'react'
import PropTypes from 'prop-types'
import { pick } from 'lodash'
import { findDOMNode } from 'react-dom'
import { compose, toClass } from 'recompose'
import { DragSource, DropTarget } from 'react-dnd'

const itemSource = {
  beginDrag(props) {
    return pick(props, props.beginDragProps)
  },
}

const itemTarget = {
  hover({ moveItem, index, listId }, monitor, component) {
    const { index: dragIndex, listId: toListId } = monitor.getItem()
    const hoverIndex = index

    if (listId !== toListId) {
      return
    }

    if (dragIndex === hoverIndex) {
      return
    }

    const hoverBoundingRect = findDOMNode(component).getBoundingClientRect() // eslint-disable-line
    const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2
    const clientOffset = monitor.getClientOffset()
    const hoverClientY = clientOffset.y - hoverBoundingRect.top

    if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
      return
    }

    if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
      return
    }
    if (typeof moveItem === 'function') {
      moveItem(dragIndex, hoverIndex, monitor.getItem())
    }
    monitor.getItem().index = hoverIndex
  },
  drop({ dropItem, ...restProps }, monitor) {
    if (dropItem && typeof dropItem === 'function')
      dropItem(monitor.getItem(), restProps)
  },
}

const Item = ({
  listItem,
  dragHandle,
  connectDragSource,
  connectDropTarget,
  connectDragPreview,
  ...rest
}) =>
  dragHandle
    ? connectDragPreview(
        connectDropTarget(
          <div style={{ flex: 1 }}>
            {React.createElement(listItem, {
              ...rest,
              dragHandle: connectDragSource(
                <div style={{ display: 'flex', alignSelf: 'stretch' }}>
                  {React.createElement(dragHandle)}
                </div>,
              ),
            })}
          </div>,
        ),
      )
    : connectDropTarget(
        connectDragSource(
          <div style={{ flex: 1 }}>{React.createElement(listItem, rest)}</div>,
        ),
      )

const DecoratedItem = compose(
  DropTarget('item', itemTarget, (connect, monitor) => ({
    connectDropTarget: connect.dropTarget(),
    isOver: monitor.isOver(),
  })),
  DragSource('item', itemSource, (connect, monitor) => ({
    connectDragSource: connect.dragSource(),
    connectDragPreview: connect.dragPreview(),
    isDragging: monitor.isDragging(),
  })),
  toClass,
)(Item)

const SortableList = ({
  listItem,
  dragHandle,
  items = [],
  itemKey = 'id',
  ...rest
}) => (
  <Fragment>
    {items.map((item, i) => (
      <DecoratedItem
        dragHandle={dragHandle}
        index={i}
        item={item}
        key={item[itemKey]}
        listItem={listItem}
        {...item}
        {...rest}
      />
    ))}
  </Fragment>
)

SortableList.propTypes = {
  /** List items. */
  items: PropTypes.array,
  /** Render prop for list's item. */
  listItem: PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired,
  /** Key used to map through items. */
  itemKey: PropTypes.string,
  /** Function invoked to change the order of the list's items. */
  moveItem: PropTypes.func,
  /** Function invoked when the currently dragged item is dropped. */
  dropItem: PropTypes.func,
  /**
   * What props to pick from the dragged item. E.g.: if a specific property is needed
   * in the move function.
   * */
  beginDragProps: PropTypes.array,
}

SortableList.moveItem = (items, dragIndex, hoverIndex) => {
  if (!dragIndex) return items
  if (dragIndex <= hoverIndex) {
    return [
      ...items.slice(0, dragIndex),
      items[hoverIndex],
      items[dragIndex],
      ...items.slice(hoverIndex + 1),
    ]
  }
  return [
    ...items.slice(0, hoverIndex),
    items[dragIndex],
    items[hoverIndex],
    ...items.slice(dragIndex + 1),
  ]
}

export default SortableList