import React, {useState, createContext, useContext, useCallback, useEffect, useRef} from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import {Box, Text, colors} from '@airtable/blocks/ui';

import {SvgPanZoomContext} from './SvgPanZoomWrapper';
import {LINK_PROP_TYPE, NODE_PROP_TYPE} from './constants';

export const HighlightContext = createContext({
    onTableRowMouseOver() {},
    highlightedFields: [],
    highlightedLinks: [],
});

/**
 * Displays a small tooltip in the bottom-left corner that shows the type of the currently
 * hovered link or field.
 *
 * Uses `ReactDOM#createPortal` to lift the HTMLElements out of SVG world.
 *
 * @param {string} props.text
 */
function Tooltip({text}) {
    const tooltipRoot = document.getElementById('index');
    return ReactDOM.createPortal(
        <Box
            position="absolute"
            bottom={0}
            left={0}
            margin={2}
            backgroundColor={colors.GRAY_DARK_1}
            borderRadius="default"
        >
            <Text padding={2} textColor="white">
                {text}
            </Text>
        </Box>,
        tooltipRoot,
    );
}

Tooltip.propTypes = {
    text: PropTypes.string,
};

function toggleClassFromElements(classToToggle, elementsClassName) {
    const elementsToToggle = document.getElementsByClassName(elementsClassName);
    while (elementsToToggle[0]) {
        elementsToToggle[0].classList.toggle(classToToggle);
    }
}

/**
 * Wraps children in a context provider to handle the highlighting of dependent nodes when hovering
 * over links and fields.
 *
 * @param {Element} props.children
 * @param {Object} props.dependentLinksByNodeId list of links connected to each node, by node id
 * @param {Object} props.linksById all links (connection between two nodes), by link id
 * @param {Object} props.nodesById all nodes (field or table header), by node id
 */
export default function HighlightWrapper({children, dependentLinksByNodeId, linksById, nodesById}) {
    const svgPanZoom = useContext(SvgPanZoomContext);
    const [tooltip, setTooltip] = useState(false);
    const hoveredNodeOrLinkRef = useRef(null);
    const [highlightContext, setHighlightContext] = useState({
        onTableRowMouseOver: () => {},
        onTableRowMouseOut: () => {},
        onLinkMouseOver: () => {},
        highlightedFields: [],
        highlightedLinks: [],
    });

    /**
     * Sets tooltip visibility and text.
     *
     * Called when the active hover target is changed (or cleared). The tooltip shows the type of
     * field / link being actively hovered.
     */
    const configureTooltip = useCallback(
        (nodeOrLinkIdOrNull, isNode) => {
            if (!nodeOrLinkIdOrNull) {
                setTooltip({isVisible: false});
            } else {
                setTooltip({
                    isVisible: true,
                    text: isNode
                        ? nodesById[nodeOrLinkIdOrNull].tooltipLabel
                        : linksById[nodeOrLinkIdOrNull].tooltipLabel,
                });
            }
        },
        [nodesById, linksById],
    );

    /**
     * Mouseover handler to highlight relevant links & nodes.
     *
     * Updates the highlighted links & nodes in the visualization whenever the hover target changes.
     * This handler is set on the Table / Link containers (rather than the children themselves), and
     * uses event delegation to avoid adding potentially thousands event handlers.
     */
    const onNodeOrLinkMouseOver = useCallback(
        event => {
            const hoveredNode = event.currentTarget.querySelector('svg.TableRow:hover');
            const hoveredLink = event.currentTarget.querySelector('path.Link:hover');
            const hoveredNodeOrLink = hoveredNode || hoveredLink;
            // Ignore if hovered node/link cannot be found, or if both a link AND node were found,
            // or  the panning is disabled (which occurs when dragging a table)
            if (
                !hoveredNodeOrLink ||
                (hoveredNode && hoveredLink) ||
                (svgPanZoom && !svgPanZoom.isPanEnabled())
            ) {
                return;
            }

            // Return if the hover target hasn't changed
            const hoveredNodeOrLinkId = hoveredNodeOrLink.getAttribute('id');
            if (hoveredNodeOrLinkRef.current === hoveredNodeOrLinkId) {
                return;
            }

            // The hover target is valid. Update the ref and tooltip.
            hoveredNodeOrLinkRef.current = hoveredNodeOrLinkId;
            configureTooltip(hoveredNodeOrLinkId, Boolean(hoveredNode));

            // Remove highlighted from previously highlighted links & fields
            toggleClassFromElements('highlighted', 'TableRow highlighted');
            toggleClassFromElements('highlighted', 'Link highlighted');

            // If there is no hovered ID, then user moused-out. Nothing more to do.
            if (hoveredNodeOrLinkId === null) {
                return;
            }

            // Add highlighting to the appropriate nodes and links
            const idsToHighlight = [];
            if (hoveredNode) {
                // field/table is hovered
                const dependentLinks = dependentLinksByNodeId[hoveredNodeOrLinkId] || [];
                dependentLinks.forEach(link => {
                    idsToHighlight.push(link.id, link.targetId, link.sourceId);
                });
            } else {
                // link is hovered
                const link = linksById[hoveredNodeOrLinkId];
                idsToHighlight.push(link.id, link.sourceId, link.targetId);
            }
            for (const id of idsToHighlight) {
                const elementToHighlight = document.getElementById(id);
                if (elementToHighlight) {
                    elementToHighlight.classList.add('highlighted');
                }
            }
        },
        [svgPanZoom, dependentLinksByNodeId, linksById, configureTooltip],
    );

    /**
     * Mouseout handler to clear highlighted state.
     *
     * Clears the highlighted links & nodes when no longer hovering on anything.
     */
    const onNodeOrLinkMouseOut = useCallback(() => {
        if (svgPanZoom && !svgPanZoom.isPanEnabled()) {
            return;
        }
        const hoveredNode = event.currentTarget.querySelector('svg.TableRow:hover');
        const hoveredLink = event.currentTarget.querySelector('path.Link:hover');

        // Mouseout triggers when hovering over descendents within a node (ie, the text node).
        // In this case, we don't want to clear highlighting.
        if (hoveredNode || hoveredLink) {
            return;
        }

        hoveredNodeOrLinkRef.current = null;
        configureTooltip(null);

        // Remove highlighted from previously highlighted links & fields
        toggleClassFromElements('highlighted', 'TableRow highlighted');
        toggleClassFromElements('highlighted', 'Link highlighted');
    }, [svgPanZoom, configureTooltip]);

    useEffect(() => {
        setHighlightContext(currentHighlightContext => ({
            ...currentHighlightContext,
            onNodeOrLinkMouseOver,
            onNodeOrLinkMouseOut,
        }));
    }, [onNodeOrLinkMouseOver, onNodeOrLinkMouseOut]);

    return (
        <HighlightContext.Provider value={highlightContext}>
            {tooltip.isVisible && <Tooltip text={tooltip.text} />}
            {children}
        </HighlightContext.Provider>
    );
}

HighlightWrapper.propTypes = {
    children: PropTypes.node,
    dependentLinksByNodeId: PropTypes.objectOf(PropTypes.arrayOf(LINK_PROP_TYPE)).isRequired,
    linksById: PropTypes.objectOf(LINK_PROP_TYPE).isRequired,
    nodesById: PropTypes.objectOf(NODE_PROP_TYPE).isRequired,
};