/**
 * Allows an easy way to create and show popouts.
 * @module Popouts
 */

import {DiscordModules, DOMTools, WebpackModules, Patcher} from "modules";

const {React, ReactDOM} = DiscordModules;
const {useReducer, useEffect, useRef} = React;
const AccessibilityProvider = WebpackModules.getByProps("AccessibilityPreferencesContext").AccessibilityPreferencesContext.Provider;
const Layers = WebpackModules.getByProps("AppReferencePositionLayer");
const PopoutCSSAnimator = WebpackModules.getByDisplayName("PopoutCSSAnimator");
const LayerProvider = Layers.AppLayerProvider().props.layerContext.Provider; // eslint-disable-line new-cap
const LayerModule = WebpackModules.getByProps("LayerClassName");
const {ComponentDispatch} = WebpackModules.getByProps("ComponentDispatch");
const {ComponentActions} = WebpackModules.getByProps("ComponentActions");
const AnalyticsTrackContext = WebpackModules.find(m => m._currentValue && m._currentValue.toString && m._currentValue.toString().includes("AnalyticsTrackImpressionContext function unimplemented"));
const AnalyticsTracker = WebpackModules.find(m => m.toString && m.toString().includes("setDebugTrackedData"));
const Popout = WebpackModules.getByDisplayName("Popout");

const createStore = state => {
    const listeners = new Set();

    const setState = function (getter = _ => _) {
        const partial = getter(state);
        if (partial === state) return;

        state = partial;
        
        [...listeners].forEach(e => e());
    };

    setState.getState = () => state;

    function storeListener(getter = _ => _) {
        const [, forceUpdate] = useReducer(n => !n, true);

        useEffect(() => {
            const dispatch = () => {forceUpdate();};

            listeners.add(dispatch);

            return () => {listeners.delete(dispatch);};
        });

        return getter(state);
    }

    return [
        setState,
        storeListener
    ];
};

const [setPopouts, usePopouts] = createStore([]);

const AnimationTypes = {FADE: 3, SCALE: 2, TRANSLATE: 1};

export default class Popouts {

    static get AnimationTypes() {return AnimationTypes;}

    static initialize() {
        this.dispose();
        this.popouts = 0;

        this.container = Object.assign(document.createElement("div"), {
            className: "ZeresPluginLibraryPopoutsRenderer",
            style: "display: none;"
        });
    
        this.layerContainer = Object.assign(document.createElement("div"), {
            id: "ZeresPluginLibraryPopouts",
            className: LayerModule.LayerClassName
        });

        document.body.append(this.container, this.layerContainer);
        ReactDOM.render(React.createElement(PopoutsContainer), this.container);

        Patcher.before("Popouts", LayerModule, "getParentLayerContainer", (_, [element]) => {
            if (element.parentElement === this.layerContainer) return this.layerContainer;
        });
    }

    /**
     * Shows the user popout for a user relative to a target element
     * @param {HTMLElement} target - Element to show the popout in relation to
     * @param {object} user - Discord User object for the user to show
     * @param {object} [options] - Options to modify the request
     * @param {string} [options.guild="currentGuildId"] - Id of the guild  (uses current if not specified)
     * @param {string} [options.channel="currentChannelId"] - Id of the channel (uses current if not specified)
     * @param {string} [options.position="right"] - Positioning relative to element
     * @param {string} [options.align="top"] - Positioning relative to element
     */
    static showUserPopout(target, user, options = {}) {
        const {position = "right", align = "top", guild = DiscordModules.SelectedGuildStore.getGuildId(), channel = DiscordModules.SelectedChannelStore.getChannelId()} = options;
        target = DOMTools.resolveElement(target);
        // if (target.getBoundingClientRect().right + 250 >= DOMTools.screenWidth && options.autoInvert) position = "left";
        // if (target.getBoundingClientRect().bottom + 400 >= DOMTools.screenHeight && options.autoInvert) align = "bottom";
        // if (target.getBoundingClientRect().top - 400 >= DOMTools.screenHeight && options.autoInvert) align = "top";
        this.openPopout(target, {
            position: position,
            align: align,
            animation: options.animation || Popouts.AnimationTypes.TRANSLATE,
            autoInvert: options.autoInvert,
            nudgeAlignIntoViewport: options.nudgeAlignIntoViewport,
            spacing: options.spacing,
            render: (props) => {
                return DiscordModules.React.createElement(DiscordModules.UserPopout, Object.assign({}, props, {
                    userId: user.id,
                    guildId: guild,
                    channelId: channel
                }));
            }
        });
    }

    /**
     * Shows a react popout relative to a target element
     * @param {HTMLElement} target - Element to show the popout in relation to
     * @param {object} [options] - Options to modify the request
     * @param {string} [options.position="right"] - General position relative to element
     * @param {string} [options.align="top"] - Alignment relative to element
     * @param {Popouts.AnimationTypes} [options.animation=Popouts.AnimationTypes.TRANSLATE] - Animation type to use
     * @param {boolean} [options.autoInvert=true] - Try to automatically adjust the position if it overflows the screen
     * @param {boolean} [options.nudgeAlignIntoViewport=true] - Try to automatically adjust the alignment if it overflows the screen
     * @param {number} [options.spacing=8] - Spacing between target and popout
     */
    static openPopout(target, options) {
        const id = this.popouts++;

        setPopouts(popouts => popouts.concat({
            id: id,
            element: React.createElement(PopoutWrapper, Object.assign({}, Popout.defaultProps, {
                reference: {current: target},
                popoutId: id,
                key: "popout_" + id,
                spacing: 50
            }, options))
        }));

        return id;
    }

    static closePopout(id) {
        const popout = setPopouts.getState().find(e => e.id === id);

        if (!popout) return null;

        setPopouts(popouts => {
            const clone = [...popouts];
            clone.splice(clone.indexOf(popout), 1);
            return clone;
        });
    }

    static dispose() {
        Patcher.unpatchAll("Popouts");
        const container = document.querySelector(".ZeresPluginLibraryPopoutsRenderer");
        const layerContainer = document.querySelector("#ZeresPluginLibraryPopouts");
        if (container) ReactDOM.unmountComponentAtNode(container);
        if (container) container.remove();
        if (layerContainer) layerContainer.remove();
    }
}

function DiscordProviders({children, container}) {
    return React.createElement(AccessibilityProvider, {
        value: {
            reducedMotion: {enabled: false, rawValue: "auto"}
        }
    }, React.createElement(LayerProvider, {
        value: [container]
    }, React.createElement(AnalyticsTrackContext.Provider, {
        value: AnalyticsTracker
    }, children)));
}

function PopoutsContainer() {
    const popouts = usePopouts();

    return React.createElement(DiscordProviders,
        {container: Popouts.layerContainer},
        popouts.map((popout) => popout.element)
    );
}

function PopoutWrapper({render, animation, popoutId, ...props}) {
    const popoutRef = useRef();

    useEffect(() => {
        if (!popoutRef.current) return;

        const node = ReactDOM.findDOMNode(popoutRef.current);

        const handleClick = ({target}) => {
            if (target === node || node.contains(target)) return;

            Popouts.closePopout(popoutId);
        };

        document.addEventListener("click", handleClick);

        return () => {
            document.removeEventListener("click", handleClick);
        };
    }, [popoutRef]);

    switch (animation) {
        case PopoutCSSAnimator.Types.FADE:
        case PopoutCSSAnimator.Types.SCALE:
        case PopoutCSSAnimator.Types.TRANSLATE: {
            const renderPopout = render;
            render = (renderProps) => {
                return React.createElement(PopoutCSSAnimator, {
                    position: renderProps.position,
                    type: animation
                }, renderPopout(renderProps));
            };
        }
    }

    return React.createElement(Layers.AppReferencePositionLayer, Object.assign(props, {
        ref: popoutRef,
        positionKey: "0",
        autoInvert: true,
        id: "popout_" + popoutId,
        onMount() {
            ComponentDispatch.dispatch(ComponentActions.POPOUT_SHOW);
        },
        onUnmount() {
            ComponentDispatch.dispatch(ComponentActions.POPOUT_HIDE);
        },
        children: render
    }));
}