'use strict';

import React, {Children, cloneElement} from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import {isEmpty, includes} from 'lodash';
import Nav from 'react-bootstrap/lib/Nav';
import {KeyCodes} from '../../utils/constants';
import {NavDropdown, TabItem} from '..';
import {cleanEmptyChildren, isElementHasType} from '../utils/functions';

type TabPanelStyle = {
    TABS: 'tabs';
    PILLS: 'pills';
    MENU: 'menu';
};

type BSStyle = TabPanelStyle['TABS'] | TabPanelStyle['PILLS'] | TabPanelStyle['MENU'];

type TabPanelProps = {
    testAutomationId?: string;
    children: React.ReactNode;
    centered?: boolean;
    noBorder?: boolean;
    topMenu?: boolean;
    clearRightPadding?: boolean;
    clearLeftPadding?: boolean;
    clearLeftWrapperPadding?: boolean;
    clearRightWrapperPadding?: boolean;
    fullWidthBorder?: boolean;
    noPadding?: boolean;
    preserveBottomBorder?: boolean;
    className?: string;
    activeKey?: string | number;
    bsStyle?: BSStyle;
} & Partial<TabPanelDefaultProps> &
    React.HTMLProps<TabPanel>;

type TabPanelDefaultProps = {
    testAutomationId: string;
    centered: boolean;
    noBorder: boolean;
    topMenu: boolean;
    clearRightPadding: boolean;
    clearLeftPadding: boolean;
    clearLeftWrapperPadding: boolean;
    clearRightWrapperPadding: boolean;
    fullWidthBorder: boolean;
    preserveBottomBorder: boolean;
    noPadding: boolean;
    bsStyle: BSStyle;
};

// export const tabPanelStyle: TabPanelStyle;

export const TAB_PANEL_STYLES = {
    TABS: 'tabs',
    PILLS: 'pills',
    MENU: 'menu'
};

export const TAB_PANEL_ITEM_ROLES = {
    menuitem: 'menuitem',
    tab: 'tab'
};

export class TabPanel extends React.Component<TabPanelProps, any> {
    static propTypes = {
        testAutomationId: PropTypes.string,
        children: PropTypes.node.isRequired,
        centered: PropTypes.bool,
        noBorder: PropTypes.bool,
        topMenu: PropTypes.bool,
        clearRightPadding: PropTypes.bool,
        clearLeftPadding: PropTypes.bool,
        clearLeftWrapperPadding: PropTypes.bool,
        clearRightWrapperPadding: PropTypes.bool,
        fullWidthBorder: PropTypes.bool,
        noPadding: PropTypes.bool,
        preserveBottomBorder: PropTypes.bool,
        className: PropTypes.string,
        activeKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
        bsStyle: PropTypes.oneOf([
            TAB_PANEL_STYLES.TABS,
            TAB_PANEL_STYLES.MENU,
            TAB_PANEL_STYLES.PILLS
        ])
    };

    static defaultProps = {
        testAutomationId: 'tabs',
        centered: false,
        noBorder: false,
        topMenu: false,
        clearRightPadding: false,
        clearLeftPadding: false,
        clearLeftWrapperPadding: false,
        clearRightWrapperPadding: false,
        fullWidthBorder: false,
        preserveBottomBorder: false,
        noPadding: false,
        className: null,
        activeKey: null,
        bsStyle: TAB_PANEL_STYLES.TABS
    };

    static contextTypes = {
        router: PropTypes.object
    };

    static childContextTypes = {
        isStyledAsTab: PropTypes.func
    };

    static getChildIndex(containerNode, childNode) {
        let index = -1;

        let childrenArray = Array.from(containerNode.children);

        while (index === -1 && childNode !== null) {
            index = childrenArray.indexOf(childNode);
            childNode = childNode.parentNode;
        }
        return index;
    }

    static getItemPath(item) {
        if (isEmpty(item) || isEmpty(item.props)) {
            return '';
        }

        let {to} = item.props;

        return (to && (to.pathname || to)) || '';
    }

    static isItemCanBeFocusedWithKeyboard(item) {
        let isAllowedByDefault =
            TabPanel.isItemOfDropdownType(item) || TabPanel.isItemOfTabItemType(item);

        return isAllowedByDefault || item.props.role in TAB_PANEL_ITEM_ROLES;
    }

    static isItemOfDropdownType(item) {
        return item && isElementHasType(item, NavDropdown);
    }

    static isItemOfTabItemType(item) {
        return item && isElementHasType(item, TabItem);
    }

    state = {
        focusedItemIndex: 0,
        isAnyDropdownOpen: false,
        isDropdownOpen: false,
        dropdownOpenStatuses: {},
        panelTabIndex: 0 // init with 0 to be able to get focus from another elements
    };

    getChildContext() {
        return {
            isStyledAsTab: this.isStyledAsTab
        };
    }

    componentDidMount() {
        this.focusActiveItem();

        // we need to handle focusout instead of onblur, see more here: https://github.com/facebook/react/issues/6410
        let tabPanel = ReactDOM.findDOMNode(this) as any;
        tabPanel.addEventListener('focusout', this.handleFocusOut);
    }

    componentDidUpdate(previousProps, previousState) {
        if (this.isStyledAsTab()) {
            let {focusedItemIndex: previousFocusedItemIndex} = previousState;
            let {focusedItemIndex, isDropdownOpen} = this.state;

            if (previousFocusedItemIndex !== focusedItemIndex && !isDropdownOpen) {
                this.focusToItemNodeByIndex(focusedItemIndex);
            }
        }
    }

    focusToItemNodeByIndex(focusedItemIndex) {
        let childNodes = (ReactDOM.findDOMNode(this) as any).children;
        let activeNode = childNodes[focusedItemIndex];

        if (activeNode) {
            activeNode.firstChild.focus();
        }
    }

    isStyledAsTab = () => {
        let {bsStyle, activeKey} = this.props;
        return bsStyle === TAB_PANEL_STYLES.TABS || (activeKey || activeKey === 0);
    };

    focusActiveItem() {
        let activeItemIndex = this.getActiveItemIndex();

        this.setFocusedItemByIndex(activeItemIndex);
        this.focusToItemNodeByIndex(activeItemIndex);
    }

    setFocusedItemByIndex = index => {
        let {children: rawChildren} = this.props;
        let children = cleanEmptyChildren(rawChildren);

        if (index !== -1 && !(children[index] as any).props.disabled) {
            this.setState({
                focusedItemIndex: index
            });
        }
    };

    getActiveItemIndex() {
        let {activeKey, children: rawChildren} = this.props;
        let children = cleanEmptyChildren(rawChildren);

        let activeItemIndex;
        if (activeKey === null) {
            activeItemIndex = children.findIndex(
                (child: any) =>
                    child &&
                    (child.props.active ||
                        (this.context.router &&
                            this.context.router.isActive(TabPanel.getItemPath(child), false)))
            );
        } else {
            activeItemIndex = children.findIndex(
                (child: any) => child && child.props.eventKey === activeKey
            );
        }

        return activeItemIndex;
    }

    handleClick = event => {
        let tabPanelNode = ReactDOM.findDOMNode(this);
        let childNode = event.target.parentNode;

        let index = TabPanel.getChildIndex(tabPanelNode, childNode);

        this.setFocusedItemByIndex(index);
    };

    handleKeyDown = event => {
        let {children: rawChildren} = this.props;
        let children = cleanEmptyChildren(rawChildren);
        let {focusedItemIndex, isAnyDropdownOpen} = this.state;
        let focusedTab = children[focusedItemIndex];

        switch (event.keyCode) {
            case KeyCodes.ARROW_LEFT:
            case KeyCodes.ARROW_UP:
                if (!isAnyDropdownOpen) {
                    let previousTab = this.getPreviousTab();
                    this.setFocusedItemByIndex(previousTab);
                }
                break;
            case KeyCodes.ARROW_RIGHT:
                if (!isAnyDropdownOpen) {
                    let nextTab = this.getNextTab();
                    this.setFocusedItemByIndex(nextTab);
                }
                break;
            case KeyCodes.ARROW_DOWN:
                if (!isAnyDropdownOpen && !TabPanel.isItemOfDropdownType(focusedTab)) {
                    let nextTab = this.getNextTab();
                    this.setFocusedItemByIndex(nextTab);
                }
                break;
            case KeyCodes.TAB:
                let activeItemIndex = this.getActiveItemIndex();

                if (
                    activeItemIndex >= 0 &&
                    ((activeItemIndex < focusedItemIndex && event.shiftKey) ||
                        (activeItemIndex > focusedItemIndex && !event.shiftKey))
                ) {
                    this.setFocusedItemByIndex(activeItemIndex);
                    event.preventDefault();
                }
                break;
            case KeyCodes.ENTER:
            case KeyCodes.SPACE:
                if (!isAnyDropdownOpen && TabPanel.isItemCanBeFocusedWithKeyboard(focusedTab)) {
                    this.setActiveItem(focusedTab, event);
                    event.preventDefault();
                }
                break;
        }
    };

    setActiveItem = (focusedTab, event) => {
        let focusedItem;
        const {target} = event;

        if (TabPanel.isItemOfTabItemType(focusedTab)) {
            focusedItem = focusedTab;
        } else {
            let navDropdownNode = target.parentNode.parentNode;
            let index = TabPanel.getChildIndex(navDropdownNode, target);

            let children = cleanEmptyChildren(focusedTab.props.children);
            focusedItem = children[index];
        }

        if (isEmpty(focusedItem)) {
            return;
        }

        let activeTabPath = TabPanel.getItemPath(focusedItem);
        this.activatePath(activeTabPath);

        let {eventKey, onClick, onSelect} = focusedItem.props;

        if (onSelect) {
            onSelect(eventKey, event);
        }

        if (onClick) {
            onClick();
        }
    };

    activatePath = activeTabPath => {
        if (activeTabPath) {
            this.context.router.push(activeTabPath);
        }
    };

    getPreviousTab() {
        let {children: rawChildren} = this.props;
        let children = cleanEmptyChildren(rawChildren);
        let {focusedItemIndex} = this.state;

        let previousItemIndex = focusedItemIndex - 1;
        let isPreviousChildPresent = previousItemIndex >= 0;
        let lastChildIndex = children.length - 1;

        if (
            isPreviousChildPresent &&
            TabPanel.isItemCanBeFocusedWithKeyboard(children[previousItemIndex])
        ) {
            return previousItemIndex;
        }

        return TabPanel.isItemCanBeFocusedWithKeyboard(children[lastChildIndex])
            ? lastChildIndex
            : lastChildIndex - 1;
    }

    getNextTab() {
        let {children: rawChildren} = this.props;
        let children = cleanEmptyChildren(rawChildren);
        let {focusedItemIndex} = this.state;

        let nextItemIndex = focusedItemIndex + 1;
        let isNextChildPresent = nextItemIndex < children.length;

        if (
            isNextChildPresent &&
            TabPanel.isItemCanBeFocusedWithKeyboard(children[nextItemIndex])
        ) {
            return nextItemIndex;
        }

        return 0;
    }

    handleFocus = event => {
        // we need this to have an ability to move out a focus from the panel with 'Shift+Tab' press
        this.setState({
            panelTabIndex: -1
        });

        let {focusedItemIndex} = this.state;
        let tabPanelNode = ReactDOM.findDOMNode(this);
        if (tabPanelNode === event.target) {
            this.focusToItemNodeByIndex(focusedItemIndex);
        }
    };

    handleFocusOut = event => {
        // this allows to get a focus to the panel from another elements after it looses a focus
        this.setState({
            panelTabIndex: 0
        });
    };

    handleDropdownToggle = (dropdownId, isOpen) => {
        this.setState(state => {
            let isAnyDropdownOpen;
            let dropdownOpenStatuses = state.dropdownOpenStatuses;

            dropdownOpenStatuses[dropdownId] = isOpen;
            isAnyDropdownOpen = includes(dropdownOpenStatuses, true);

            return {
                dropdownOpenStatuses,
                isAnyDropdownOpen
            };
        });
    };

    getChildren() {
        let {children: rawChildren} = this.props;
        let children = cleanEmptyChildren(rawChildren);

        return Children.map(children, (child, index) => {
            if (TabPanel.isItemOfDropdownType(child)) {
                let childProps = {
                    onToggle: isOpen => this.handleDropdownToggle(index, isOpen)
                };

                return cloneElement(child as any, childProps);
            }

            return child;
        });
    }

    render() {
        let {
            testAutomationId,
            centered,
            noBorder,
            topMenu,
            clearRightPadding,
            clearLeftPadding,
            clearLeftWrapperPadding,
            clearRightWrapperPadding,
            fullWidthBorder,
            noPadding,
            className,
            preserveBottomBorder,
            ...props
        } = this.props;

        let {panelTabIndex} = this.state;

        className = classnames(
            {
                'rc-nav-centered': centered,
                'rc-nav-noHorizontalPadding': noPadding,
                'rc-nav-noBorder': noBorder,
                'rc-nav-fullWidthBorder': !noBorder && fullWidthBorder,
                'rc-nav-topMenu': topMenu,
                'rc-nav-clearRightPadding': clearRightPadding,
                'rc-nav-clearLeftPadding': clearLeftPadding,
                'rc-nav-clearLeftWrapperPadding': clearLeftWrapperPadding,
                'rc-nav-clearRightWrapperPadding': clearRightWrapperPadding,
                'rc-nav-preserveBottomBorder': preserveBottomBorder
            },
            className
        );

        return (
            <Nav
                bsStyle="tabs"
                className={className}
                data-test-automation-id={testAutomationId}
                role="tablist"
                tabIndex={panelTabIndex}
                onFocus={this.handleFocus}
                onClick={this.handleClick as any}
                onKeyDown={this.handleKeyDown}
                {...props as any}>
                {this.getChildren()}
            </Nav>
        );
    }
}
