import React from "react";
import PropTypes from "prop-types";
import Tether from "tether";

/**
 * Intelligently position elements on a page.
 * @param {Object} props - Properties passed to component
 * @returns {ReactElement}
 */

class OverlayWrapper extends React.Component {
    constructor(props) {
        super(props);
        this.state = { isOverlayOpen: false };
        this.onChildClick = this.onChildClick.bind(this);
        this.onChildMouseOver = this.onChildMouseOver.bind(this);
        this.onChildMouseOut = this.onChildMouseOut.bind(this);
        this.disableTether = this.disableTether.bind(this);
    }

    getChildContext() {
        return {
            hideOverlay: this.disableTether,
        };
    }

    componentDidMount() {
        let options = {
            element: this._overlayEl,
            target: this._activatorEl.firstChild,
            attachment: `${this.props.verticalAttachment} ${this.props.horizontalAttachment}`,
            constraints: [
                {
                    to: "window",
                    attachment: this.props.attachmentConstraint,
                    pin: this.props.isConstrainedToScreen,
                },
            ],
        };

        if (this.props.verticalTargetAttachment && this.props.horizontalTargetAttachment) {
            options.targetAttachment = `${this.props.verticalTargetAttachment} ${this.props.horizontalTargetAttachment}`;
        }

        this.initTether = () => new Tether(options);
    }

    enableTether() {
        // If tether hasn't yet been created, do it now.
        if (!this._tether) {
            this._tether = this.initTether();
        }

        this.setState({ isOverlayOpen: true });
        this._tether.enable();

        if (typeof this.props.onShow === "function") {
            this.props.onShow();
        }

        // Reposition once the overlay is visible because Tether can't
        // properly calculate the positioning when the overlay is not
        // displayed.
        setTimeout(() => {
            this._tether.position();
        });

        // These functions must be stored so that it can be still be removed even
        // though `bind` was used: http://stackoverflow.com/a/22870717/316602
        this._bodyClickListener = this.props.shouldHideOnClick && this.isClickWithinOverlayOrChildren.bind(this);
        this._documentEscapeListener = this.onEscapeKey.bind(this);

        if (this._bodyClickListener) {
            document.addEventListener("click", this._bodyClickListener);
        }

        document.addEventListener("keyup", this._documentEscapeListener);
    }

    disableTether() {
        // If tether hasn't been created, return early
        if (!this._tether) {
            return;
        }

        let shouldHide = true;
        if (typeof this.props.onHide === "function") {
            shouldHide = this.props.onHide();
        }

        if (shouldHide === false) {
            // Exit out of this function before hiding the `overlay` if the supplied
            // `onHide` function returns false. This is useful for fine-tuned control
            // of when the `overlay` should close.
            return;
        }

        this.setState({ isOverlayOpen: false });
        this._tether.disable();
        this.removeBodyEventListener();
    }

    removeBodyEventListener() {
        if (this._bodyClickListener) {
            document.removeEventListener("click", this._bodyClickListener);
        }
        if (this._documentEscapeListener) {
            document.removeEventListener("keyup", this._documentEscapeListener);
        }
    }

    /**
     * Disable Tether if a user clicks outside of the `overlay` and `children`.
     * @param {Object} event - Click event
     */
    isClickWithinOverlayOrChildren(event) {
        let target = event.target;
        let shouldCloseOverlay = true;

        while (target.parentNode !== null && shouldCloseOverlay) {
            if (target === this._overlayEl || target === this._activatorEl) {
                shouldCloseOverlay = false;
            }
            target = target.parentNode;
        }

        if (shouldCloseOverlay) {
            this.disableTether();
        }
    }

    componentWillUnmount() {
        this.removeBodyEventListener();
        // If tether hasn't been created, return early
        if (!this._tether) {
            return;
        }
        this._tether.destroy();
        if (this._tether.element && this._tether.element.remove) {
            this._tether.element.remove();
        }
    }

    onChildClick(event, child) {
        // Toggle the `overlay` by enabling or disabling Tether.
        if (this.state.isOverlayOpen) {
            // Disable Tether when not visible for performance reasons.
            this.disableTether();
        } else {
            // Enable Tether when visible.
            this.enableTether();
        }

        // Run the `children`'s `onClick` if it exists.
        if (child.props.onClick) {
            child.props.onClick(event);
        }
    }

    onChildMouseOver(event, child) {
        // Enable Tether when visible.
        this.enableTether();

        // Run the `children`'s `onMouseOver` if it exists.
        if (child.props.onMouseOver) {
            child.props.onMouseOver(event);
        }
    }

    onChildMouseOut(event, child) {
        // Disable Tether when not visible for performance reasons.
        this.disableTether();

        // Run the `children`'s `onMouseOut` if it exists.
        if (child.props.onMouseOut) {
            child.props.onMouseOut(event);
        }
    }

    onEscapeKey(event) {
        // Escape key
        if (event.keyCode === 27) {
            this.disableTether();
        }
    }

    render() {
        const {
            attachmentConstraint,
            behavior,
            children,
            className,
            horizontalAttachment,
            horizontalTargetAttachment,
            isConstrainedToScreen,
            onHide,
            onShow,
            overlay,
            shouldHideOnClick,
            testSection,
            verticalAttachment,
            verticalTargetAttachment,
            ...props
        } = this.props;
        let eventHandlerProps;
        let child = React.Children.only(children);

        switch (behavior) {
            case "click":
                eventHandlerProps = {
                    onClick: event => this.onChildClick.call(null, event, child),
                };
                break;
            case "hover":
                eventHandlerProps = {
                    onMouseOver: event => this.onChildMouseOver.call(null, event, child),
                    onMouseOut: event => this.onChildMouseOut.call(null, event, child),
                };
                break;
            default:
        }

        const block = { display: "inline-block" };
        const overlayState = this.state.isOverlayOpen ? { display: "block" } : { display: "none" };

        return (
            /* eslint-disable react/jsx-no-bind */
            <div className={className} data-oui-component={true} data-test-section={testSection} style={block} {...props}>
                <div
                    className="click-area"
                    style={block}
                    ref={ref => {
                        this._activatorEl = ref;
                    }}
                    {...eventHandlerProps}
                >
                    {child}
                </div>
                <div
                    ref={ref => {
                        this._overlayEl = ref;
                    }}
                    style={overlayState}
                >
                    {overlay}
                </div>
            </div>
            /* eslint-enable */
        );
    }
}

OverlayWrapper.propTypes = {
    /** Determines how the attachment is constrained to the target */
    attachmentConstraint: PropTypes.oneOf(["both", "element", "none", "target", "together"]),
    /** Event to listen to and open the overlay */
    behavior: PropTypes.oneOf(["click", "hover"]),
    /** Element that the `overlay` should attach to */
    children: PropTypes.node.isRequired,
    /** CSS class names. */
    className: PropTypes.string,
    /** Side of the `overlay` that should attach to the `children` */
    horizontalAttachment: PropTypes.oneOf(["left", "center", "right"]),
    /** Side of `children` that should attach to the `overlay` */
    horizontalTargetAttachment: PropTypes.oneOf(["left", "center", "right"]),
    /** Attach `overlay` to an edge of the screen if it is going to move off */
    isConstrainedToScreen: PropTypes.bool,
    /**
     * Function that runs when the `overlay` is hidden. Return `false` to prevent
     * the `overlay` from closing.
     */
    onHide: PropTypes.func,
    /** Function that runs when the `overlay` is shown */
    onShow: PropTypes.func,
    /** The element that is attached to the children */
    overlay: PropTypes.node.isRequired,
    /** Should the `overlay` close when clicking outside of it */
    shouldHideOnClick: PropTypes.bool,
    /** Hook for automated JavaScript tests */
    testSection: PropTypes.string,
    /** Vertical edge of the `overlay` that should touch the `children` */
    verticalAttachment: PropTypes.oneOf(["top", "middle", "bottom"]),
    /** Vertical edge of the `children` that should touch the `overlay` */
    verticalTargetAttachment: PropTypes.oneOf(["top", "middle", "bottom"]),
};

OverlayWrapper.defaultProps = {
    attachmentConstraint: "together",
    behavior: "click",
    horizontalAttachment: "center",
    isConstrainedToScreen: false,
    onHide: () => null,
    shouldHideOnClick: true,
    verticalAttachment: "top",
};

OverlayWrapper.childContextTypes = {
    hideOverlay: PropTypes.func,
};

export default OverlayWrapper;
