import PropTypes from 'prop-types';

import {playMessage} from '../core/utils/playMessage';
import {KeyCodes} from '../utils/constants';

import React from 'react';
const {Component} = React;

export const keyboardSortableDesc = {
    onFocusDesc: 'TRANSLATE:ON_FOCUS_DESC',
    onGrabDescA: 'TRANSLATE:ON_GRAB_DESC_A',
    onGrabDescB: 'TRANSLATE:ON_GRAB_DESC_B',
    onDragDesc: 'TRANSLATE:ON_DRAG_DESC',
    onDropDesc: 'TRANSLATE:ON_DROP_DESC',
    onCancelDesc: 'TRANSLATE:ON_CANCEL_DESC'
};

export const keyboardSortableListItemDefaultProps = {
    onMove: null,
    onHover: null,
    dragItemName: 'TRANSLATE:DRAGGABLE_ITEM'
};

export function keyboardSortableListItem(DecoratedComponent) {
    return class KeyboardSortableListItem extends Component {
        static keyboardSortableDesc = keyboardSortableDesc;

        static contextTypes = {
            register: PropTypes.func.isRequired,
            unregister: PropTypes.func.isRequired,
            getPrevDropTarget: PropTypes.func.isRequired,
            getNextDropTarget: PropTypes.func.isRequired
        };

        static propTypes = {
            index: PropTypes.number.isRequired,
            dndId: PropTypes.any.isRequired,
            onMove: checkReorderCallbackProp,
            onHover: checkReorderCallbackProp,
            dragItemName: PropTypes.string.isRequired
        };

        static defaultProps = keyboardSortableListItemDefaultProps;

        constructor(props, context) {
            super(props, context);
            this.state = {
                originalIndexBeforeOrdering: -1,
                isReorderingMode: false,
                isExistingFromReorderingMode: false
            };
            this.reorderActionHandler = this.reorderActionHandler.bind(this);
            this.dragHandler = null;
            this.prevDragHandler = null;
        }

        componentDidMount() {
            if (this.dragHandler) {
                this.register();
            }
            document.addEventListener('click', this.handleOutsideClick.bind(this));
        }

        componentDidUpdate() {
            if (this.dragHandler) {
                this.register();
                if (this.state.isReorderingMode) {
                    this.dragHandler.focus();
                }
            } else if (!this.dragHandler && !this.prevDragHandler) {
                this.unregister();
            }
        }

        componentWillUnmount() {
            if (this.dragHandler) {
                this.unregister();
            }
            document.removeEventListener('click', this.handleOutsideClick.bind(this));
        }

        register() {
            const {index, dndId} = this.props;
            this.context.register(index, dndId);
            let currentKeyDownListener = this.dragHandler._keydownListener;
            if (currentKeyDownListener) {
                this.dragHandler.removeEventListener('keydown', currentKeyDownListener);
                this.dragHandler.removeEventListener('keyup', this.onHandlerKeyUp);
            }
            this.dragHandler.addEventListener('keydown', this.reorderActionHandler);
            this.dragHandler.addEventListener('keyup', this.onHandlerKeyUp);
            this.dragHandler._keydownListener = this.reorderActionHandler;
            this.dragHandler.setAttribute('role', 'option');
            this.dragHandler.setAttribute(
                'aria-label',
                `${this.props.dragItemName}, ${
                    KeyboardSortableListItem.keyboardSortableDesc.onFocusDesc
                }`
            );
        }

        unregister() {
            const {index, dndId} = this.props;
            this.context.unregister(index, dndId);
            if (this.prevDragHandler) {
                this.prevDragHandler.removeEventListener('keydown', this.reorderActionHandler);
                this.prevDragHandler._keydownListener = null;
                this.prevDragHandler.removeAttribute('aria-label');
            }
        }

        createDragHandlerRef(node) {
            this.prevDragHandler = this.dragHandler;
            this.dragHandler = node;
        }

        onHandlerKeyUp = event => {
            if (this.state.isExistingFromReorderingMode) {
                event.stopPropagation();
            }
        };

        cancelReorder() {
            const {originalIndexBeforeOrdering} = this.state;
            let {index, onMove, onHover, dndId} = this.props;
            const isMovedDown = index > originalIndexBeforeOrdering;
            if (isMovedDown) {
                const {getPrevDropTarget} = this.context;
                while (index > originalIndexBeforeOrdering) {
                    let dropDndId = getPrevDropTarget(index).dndId;
                    if (dropDndId) {
                        reorder(dropDndId, index, isMovedDown);
                    }
                    index--;
                }
            } else {
                const {getNextDropTarget} = this.context;
                while (index < originalIndexBeforeOrdering) {
                    let dropDndId = getNextDropTarget(index).dndId;
                    if (dropDndId) {
                        reorder(dropDndId, index, isMovedDown);
                    }
                    index++;
                }
            }
            this.announceCancelReorderItem();

            function reorder(dropDndId, index, isMovingDown) {
                if (onMove) {
                    onMove(dropDndId, dndId);
                } else {
                    (index => {
                        setTimeout(_ => {
                            onHover({index}, {index: index + (isMovingDown ? -1 : 1)}, true);
                        });
                    })(index);
                }
            }
        }

        reorderActionHandler(e) {
            let {index} = this.props;
            const {originalIndexBeforeOrdering, isReorderingMode} = this.state;
            switch (e.keyCode) {
                case KeyCodes.TAB:
                case KeyCodes.ESC:
                    if (isReorderingMode) {
                        e.stopPropagation();
                        this.exitReorderingMode();
                    }
                    break;
                case KeyCodes.SPACE:
                case KeyCodes.ENTER:
                    e.preventDefault();

                    if (isReorderingMode) {
                        this.announceDropItem();
                    } else {
                        this.announceGrabItem();
                    }

                    this.setState({
                        originalIndexBeforeOrdering: originalIndexBeforeOrdering < 0 ? index : -1,
                        isReorderingMode: !isReorderingMode,
                        isExistingFromReorderingMode: isReorderingMode
                    });
                    break;
                case KeyCodes.ARROW_LEFT:
                case KeyCodes.ARROW_UP:
                    if (isReorderingMode) {
                        e.preventDefault();
                        let {dndId, index: targetIndex} = this.context.getPrevDropTarget(index);
                        if (dndId || dndId === 0) {
                            this.announceMovingItem(targetIndex);
                            this.reorder(dndId, false);
                        }
                    }
                    break;
                case KeyCodes.ARROW_RIGHT:
                case KeyCodes.ARROW_DOWN:
                    if (isReorderingMode) {
                        e.preventDefault();
                        let {dndId, index: targetIndex} = this.context.getNextDropTarget(index);
                        if (dndId || dndId === 0) {
                            this.announceMovingItem(targetIndex);
                            this.reorder(dndId, true);
                        }
                    }
            }
        }

        /**
         * @param targetId moving destination. drop target's id
         * @param movingDown moving direction. direction moving up or moving down
         */
        reorder(targetId, movingDown) {
            const {index, dndId, onMove, onHover} = this.props;
            if (onMove) {
                onMove(dndId, targetId);
            } else {
                onHover({index: index}, {index: index + (movingDown ? 1 : -1)}, true);
            }
        }

        exitReorderingMode() {
            const {index} = this.props;
            if (this.state.isReorderingMode && index !== this.state.originalIndexBeforeOrdering) {
                this.cancelReorder();
            }
            this.setState({
                isReorderingMode: false,
                originalIndexBeforeOrdering: -1,
                isExistingFromReorderingMode: this.state.isReorderingMode
            });
        }

        handleOutsideClick() {
            if (document.activeElement !== this.dragHandler && this.state.isReorderingMode) {
                this.exitReorderingMode();
            }
        }

        announceGrabItem() {
            const {index} = this.props;
            const {dragItemName} = this.props;
            const {onGrabDescA, onGrabDescB} = KeyboardSortableListItem.keyboardSortableDesc;
            playMessage(`${dragItemName}, ${onGrabDescA}: ${index + 1}. ${onGrabDescB}`);
        }

        announceMovingItem(currentIndex) {
            const {dragItemName} = this.props;
            const {onDragDesc} = KeyboardSortableListItem.keyboardSortableDesc;
            playMessage(`${dragItemName}, ${onDragDesc}: ${currentIndex + 1}`);
        }

        announceDropItem() {
            const {index} = this.props;
            const {dragItemName} = this.props;
            const {onDropDesc} = KeyboardSortableListItem.keyboardSortableDesc;
            playMessage(`${dragItemName}, ${onDropDesc}: ${index + 1}`);
        }

        announceCancelReorderItem() {
            const {dragItemName} = this.props;
            const {onCancelDesc} = KeyboardSortableListItem.keyboardSortableDesc;
            playMessage(`${dragItemName}, ${onCancelDesc}`);
        }

        render() {
            return (
                <DecoratedComponent
                    isKeyboardDragging={this.state.isReorderingMode}
                    connectDragHandler={this.createDragHandlerRef.bind(this)}
                    {...this.props}
                />
            );
        }
    };
}

function checkReorderCallbackProp(props, propName, componentName, ...rest) {
    if (!props.onMove && !props.onHover) {
        return new Error(
            `One of props 'onHide' or 'onHover' need to be specified in ${componentName}`
        );
    }
    if (props[propName]) {
        return PropTypes.func(props, propName, componentName, ...rest);
    }
    return null;
}
