import React, { useEffect, useLayoutEffect, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import withListener from '../withListener';
import Carousel from '../carousel/Carousel';
import FeaturedCustomers from '../product-featured-customers/FeaturedCustomers';
import ProductFeatureDetails from '../product-feature-details/FeatureDetails';
import ProductQuestions from '../product-questions/ProductQuestions';
import ProductReviews from '../product-reviews/ProductReviewsAdapter';
import ProductAdditionalInfo from '../product-additional-info/AdditionalInfo';
import ProductResources from '../product-resources/Resources';
import Slider from '../product-slider/ProductSlider';
import ProductDescription from '../product-viewer-description/ProductViewerDescription';
import ProductTopFeatures from '../product-top-features/TopFeatures';

import { createNamespace } from '../../tools/namingTools';
import './styles/PageOrderableContainer.scss';

const n = createNamespace('PageOrderableContainer');

export const ORDERABLE_COMPONENTS = {
    Carousel,
    FeaturedCustomers,
    ProductFeatureDetails,
    ProductDescription,
    ProductQuestions,
    ProductReviews,
    ProductAdditionalInfo,
    ProductResources,
    Slider,
    ProductTopFeatures
};

const isComponentNull = (Component, data) => {
    if (!Component) {
        return true;
    }

    let componentNull = false;
    const componentRequiredDataFunc = Component.schema().requiredDataFunc;
    if (componentRequiredDataFunc) {
        componentNull = !componentRequiredDataFunc(data);
    }
    return componentNull;
};

const scrollToComponent = id => {
    const element = document.getElementById(id);
    if (element) {
        const top = (element.offsetTop + window.pageYOffset) - 14;
        const scrollOptions = {
            left: 0,
            top,
            behavior: 'auto'
        };
        setTimeout(() => window.scrollTo(scrollOptions), 0);
    }
};

const PageOrderableContainer = ({ orderableComponents, selectedComponentId, replaceIndex }) => {
    const orderableContainer = useRef(); // DOM nodes for PageOrderableContainer div
    const selectedIndex = useRef(null); // current index in the list of component being dragged
    const containerArray = useRef(null); // updated re-order list of id
    const swapRef = useRef(null); // array of boolean. element has been swap or not
    const [reorder, setReorder] = useState(false); // Flag to determine if reorder flow has been done

    /**
     * Return the array index of the orderable component in PageOrderableContainer DOM nodes
     * @param {string} id id attribute of a component in the orderable container
     */
    const findComponentInitialIndex = id => {
        const containerNodes = orderableContainer.current.childNodes;
        let index = -1;
        Object.keys(containerNodes).forEach(key => {
            if (containerNodes[key].id === id) {
                index = parseInt(key, 10);
            }
        });
        return index;
    };

    /**
     * Initialize the container array that will maintain the updated orderable components list
     */
    const buildContainerArray = () => {
        containerArray.current = [];
        const nodes = orderableContainer.current.childNodes;
        Object.keys(nodes)
            .forEach(key => {
                containerArray.current = [
                    ...containerArray.current,
                    nodes[key].id
                ];
            });
    };

    /**
     * Reset every css transform done to orderable node during reorder.
     * This is called onComponentDragEnd.
     */
    const resetComponentTransform = () => {
        const containerNodes = orderableContainer.current.childNodes;
        Object.keys(containerNodes)
            .forEach(key => {
                containerNodes[key].style.transform = '';
            });
    };

    /**
     * Swap component when we reorder component so we can see where the component being reordered
     * will be placed on the page.
     * @param {array} containerNodes the orderable component in PageOrderableContainer DOM nodes
     * @param {int} from initial index of the component being reordered
     * @param {int} to final index of the component being reordered
     * @param {int} componentHeight height of the component being reordered
     */
    const swapComponent = (containerNodes, from, to, componentHeight) => {
        const swapElement = (nodes, index, invert) => {
            let currentComponent = nodes[index];
            if (swapRef.current[index]) {
                currentComponent = document.getElementById(containerArray.current[index]);
                currentComponent.style.transform = 'translateY(0px)';
                const swapIndex = findComponentInitialIndex(containerArray.current[index]);
                swapRef.current[swapIndex] = false;
            } else {
                currentComponent.style.transform = `translateY(${invert}px)`;
                swapRef.current[index] = true;
            }
        };

        if (to - from > 0) {
            for (let i = from + 1; i <= to; i += 1) {
                swapElement(containerNodes, i, -componentHeight);
            }
        } else {
            for (let i = to; i < from; i += 1) {
                swapElement(containerNodes, i, componentHeight);
            }
        }
    };

    /**
     * Update the array of ordered items after reorder
     * @param {array} array array of ordered items
     * @param {int} from initial index
     * @param {int} to final index
     */
    const arrayMove = (array, from, to) => {
        if (from === to) {
            return array.slice();
        }

        const resultArray = array.slice();
        const startIndex = to < 0 ? resultArray.length + to : to;
        const [item] = resultArray.splice(from, 1);
        resultArray.splice(startIndex, 0, item);
        return resultArray;
    };

    const onComponentDragOver = () => {
        const componentNewPosition = orderableContainer.current
            .childNodes[replaceIndex];
        const selectedComponent = document.getElementById(selectedComponentId);

        if (componentNewPosition && selectedComponent) {
            const selectedComponentIndex = selectedIndex.current
                || containerArray.current.indexOf(selectedComponentId);
            const swapIndex = findComponentInitialIndex(selectedComponentId);
            let invert = componentNewPosition.offsetTop - selectedComponent.offsetTop;

            if (invert > 0) {
                invert += componentNewPosition.offsetHeight - selectedComponent.offsetHeight;
            }

            swapRef.current[swapIndex] = true;
            selectedComponent.style.transform = `translateY(${invert}px)`;
            selectedComponent.style.opacity = 0.85;
            selectedComponent.style.zIndex = 2;
            selectedIndex.current = replaceIndex;

            swapComponent(
                orderableContainer.current.childNodes,
                selectedComponentIndex,
                replaceIndex,
                selectedComponent.offsetHeight
            );
            containerArray.current = arrayMove(
                containerArray.current, selectedComponentIndex, replaceIndex
            );
        }
    };

    const onComponentDragStart = () => {
        const size = orderableContainer.current.childNodes.length;
        swapRef.current = Array(size).fill(false);
        buildContainerArray();
    };

    const onComponentDragEnd = () => {
        selectedIndex.current = null;
        containerArray.current = null;
        swapRef.current = null;

        resetComponentTransform();
    };

    useLayoutEffect(() => {
        // When a component is being dragged (onDragOver).
        if (replaceIndex !== null && replaceIndex !== 'undefined') {
            onComponentDragOver();
        } else { // onDragEnd
            onComponentDragEnd();
        }
    }, [replaceIndex]);

    useEffect(() => {
        if (selectedComponentId) {
            onComponentDragStart();
            setReorder(false);
        }
    }, [selectedComponentId]);

    useEffect(() => {
        if (selectedComponentId) {
            if (!reorder) {
                scrollToComponent(selectedComponentId);
            }
            setReorder(true);
            // Need to call onComponentDragStart here
            // in case we start reordering the same component
            // because selectedComponentId won't change
            onComponentDragStart();
        }
    }, [orderableComponents]);

    // We wrap orderable component inside a div with the orderable component id
    // in order to have scroll to animation to the orderable component on preview.
    // Although, we define id on both div and component, only the id in div will
    // be generated.
    const withOrderableComponent = (Component, { id, data, settings }) => (
        !isComponentNull(Component, data) ? (
            <div {...n('item', selectedComponentId === id && replaceIndex !== null && 'selected').props} id={id} key={id}>
                {Component && <Component key={id} id={id} data={data} settings={settings} />}
            </div>
        ) : null
    );

    const renderOrderableComponents = components => (
        components.map(
            ({ id, name, data, settings }) => (
                withOrderableComponent(ORDERABLE_COMPONENTS[name], { id, data, settings })
            )
        )
    );

    return (
        <div {...n('container').withTestId().props} ref={orderableContainer}>
            {renderOrderableComponents(orderableComponents)}
        </div>
    );
};

const orderableComponentsShape = PropTypes.shape({
    id: PropTypes.string,
    name: PropTypes.string,
    data: PropTypes.object,
    settings: PropTypes.object
});

PageOrderableContainer.propTypes = {
    orderableComponents: PropTypes.arrayOf(orderableComponentsShape),
    selectedComponentId: PropTypes.string,
    replaceIndex: PropTypes.number
};

PageOrderableContainer.defaultProps = {
    orderableComponents: [],
    selectedComponentId: '',
    replaceIndex: null
};

export default withListener(PageOrderableContainer);
