import WebpackModules from "@modules/webpackmodules";
import Patcher from "@modules/patcher";
import Logger from "@common/logger";
import React from "@modules/react";


let startupComplete = false;

const ModulesBundle = WebpackModules.getByProps("MenuItem", "Menu");
const MenuComponents = {
    Separator: ModulesBundle?.MenuSeparator,
    CheckboxItem: ModulesBundle?.MenuCheckboxItem,
    RadioItem: ModulesBundle?.MenuRadioItem,
    ControlItem: ModulesBundle?.MenuControlItem,
    Group: ModulesBundle?.MenuGroup,
    Item: ModulesBundle?.MenuItem,
    Menu: ModulesBundle?.Menu,
};

startupComplete = Object.values(MenuComponents).every(v => v);

const ContextMenuActions = (() => {
    const out = {};

    try {
        const ActionsModule = WebpackModules.getModule((mod, target, id) => WebpackModules.require.m[id]?.toString().includes(`type:"CONTEXT_MENU_OPEN"`), {searchExports: false});

        for (const key of Object.keys(ActionsModule)) {
            if (ActionsModule[key].toString().includes("CONTEXT_MENU_CLOSE")) {
                out.closeContextMenu = ActionsModule[key];
            }
            else if (ActionsModule[key].toString().includes("renderLazy")) {
                out.openContextMenu = ActionsModule[key];
            }
        }

        startupComplete &&= typeof(out.closeContextMenu) === "function" && typeof(out.openContextMenu) === "function";
    } catch (error) {
        startupComplete = false;
        Logger.stacktrace("ContextMenu~Components", "Fatal startup error:", error);
        
        Object.assign(out, {
            closeContextMenu: () => {},
            openContextMenu: () => {}
        });
    }

    return out;
})();

class MenuPatcher {
    static MAX_PATCH_ITERATIONS = 10;
    static patches = {};
    static subPatches = new WeakMap();

    static initialize() {
        if (!startupComplete) return Logger.warn("ContextMenu~Patcher", "Startup wasn't successfully, aborting initialization.");

        const {module, key} = (() => {
            const foundModule = WebpackModules.getModule(m => Object.values(m).some(v => typeof v === "function" && v.toString().includes(`type:"CONTEXT_MENU_CLOSE"`)), {searchExports: false});
            const foundKey = Object.keys(foundModule).find(k => foundModule[k].length === 3);

            return {module: foundModule, key: foundKey};
        })();

        Patcher.before("ContextMenuPatcher", module, key, (_, methodArguments) => {
            const promise = methodArguments[1];
            methodArguments[1] = async function () {
                const render = await promise.apply(this, arguments);

                return props => {
                    const res = render(props);

                    if (res?.props.navId) {
                        MenuPatcher.runPatches(res.props.navId, res, props);
                    }
                    else if (typeof res?.type === "function") {
                        MenuPatcher.patchRecursive(res, "type");
                    }

                    return res;
                };
            };
        });
    }

    static patchRecursive(target, method, iteration = 0) {
        if (iteration >= this.MAX_PATCH_ITERATIONS) return;

        const proxyFunction = this.subPatches.get(target[method]) ?? (() => {
            const originalFunction = target[method];
            const depth = ++iteration;
            function patch() {
                const res = originalFunction.apply(this, arguments);

                if (!res) return res;

                if (res.props?.navId ?? res.props?.children?.props?.navId) {
                    MenuPatcher.runPatches(res.props.navId ?? res.props?.children?.props?.navId, res, arguments[0]);
                }
                else {
                    const layer = res.props.children ? res.props.children : res;

                    if (typeof layer?.type == "function") {
                        MenuPatcher.patchRecursive(layer, "type", depth);
                    }
                }

                return res;
            }

            patch._originalFunction = originalFunction;
            Object.assign(patch, originalFunction);
            this.subPatches.set(originalFunction, patch);

            return patch;
        })();

        target[method] = proxyFunction;
    }

    static runPatches(id, res, props) {
        if (!this.patches[id]) return;

        for (const patch of this.patches[id]) {
            try {
                patch(res, props);
            }
            catch (error) {
                Logger.error("ContextMenu~runPatches", `Could not run ${id} patch for`, patch, error);
            }
        }
    }

    static patch(id, callback) {
        this.patches[id] ??= new Set();
        this.patches[id].add(callback);
    }

    static unpatch(id, callback) {
        this.patches[id]?.delete(callback);
    }
}


/**
 * `ContextMenu` is a module to help patch and create context menus. Instance is accessible through the {@link BdApi}.
 * @type ContextMenu
 * @summary {@link ContextMenu} is a utility class for interacting with React internals.
 * @name ContextMenu
 */
class ContextMenu {

    /**
     * Allows you to patch a given context menu. Acts as a wrapper around the `Patcher`.
     * 
     * @param {string} navId Discord's internal `navId` used to identify context menus
     * @param {function} callback Callback function that accepts the React render tree
     * @returns {function} A function that automatically unpatches
     */
    patch(navId, callback) {
        MenuPatcher.patch(navId, callback);

        return () => MenuPatcher.unpatch(navId, callback);
    }

    /**
     * Allows you to remove the patch added to a given context menu.
     * 
     * @param {string} navId The original `navId` from patching
     * @param {function} callback The original callback from patching
     */
    unpatch(navId, callback) {
        MenuPatcher.unpatch(navId, callback);
    }

    /**
     * Builds a single menu item. The only prop shown here is the type, the rest should
     * match the actual component being built. View those to see what options exist
     * for each, they often have less in common than you might think.
     * 
     * @param {object} props Props used to build the item
     * @param {string} [props.type="text"] Type of the item, options: text, submenu, toggle, radio, custom, separator
     * @returns {object} The created component
     * 
     * @example
     * // Creates a single menu item that prints "MENU ITEM" on click
     * ContextMenu.buildItem({
     *      label: "Menu Item",
     *      action: () => {console.log("MENU ITEM");}
     * });
     * 
     * @example
     * // Creates a single toggle item that starts unchecked
     * // and print the new value on every toggle
     * ContextMenu.buildItem({
     *      type: "toggle",
     *      label: "Item Toggle",
     *      checked: false,
     *      action: (newValue) => {console.log(newValue);}
     * });
     */
     buildItem(props) {
        const {type} = props;
        if (type === "separator") return React.createElement(MenuComponents.Separator);

        let Component = MenuComponents.Item;
        if (type === "submenu") {
            if (!props.children) props.children = this.buildMenuChildren(props.render || props.items);
        }
        else if (type === "toggle" || type === "radio") {
            Component = type === "toggle" ? MenuComponents.CheckboxItem : MenuComponents.RadioItem;
            if (props.active) props.checked = props.active;
        }
        else if (type === "control") {
            Component = MenuComponents.ControlItem;
        }
        if (!props.id) props.id = `${props.label.replace(/^[^a-z]+|[^\w-]+/gi, "-")}`;
        if (props.danger) props.color = "danger";
        if (props.onClick && !props.action) props.action = props.onClick;
        props.extended = true;

        // This is done to make sure the UI actually displays the on/off correctly
        if (type === "toggle") {
            const [active, doToggle] = React.useState(props.checked || false);
            const originalAction = props.action;
            props.checked = active;
            props.action = function(ev) {
                originalAction(ev);
                doToggle(!active);
            };
        }
        
        return React.createElement(Component, props);
    }

    /**
     * Creates the all the items **and groups** of a context menu recursively.
     * There is no hard limit to the number of groups within groups or number
     * of items in a menu.
     * 
     * @param {Array<object>} setup Array of item props used to build items. See {@link ContextMenu.buildItem}.
     * @returns {Array<object>} Array of the created component
     * 
     * @example
     * // Creates a single item group item with a toggle item
     * ContextMenu.buildMenuChildren([{
     *      type: "group",
     *      items: [{
     *          type: "toggle",
     *          label: "Item Toggle",
     *          active: false,
     *          action: (newValue) => {console.log(newValue);}
     *      }]
     * }]);
     * 
     * @example
     * // Creates two item groups with a single toggle item each
     * ContextMenu.buildMenuChildren([{
     *     type: "group",
     *     items: [{
     *         type: "toggle",
     *         label: "Item Toggle",
     *         active: false,
     *         action: (newValue) => {
     *             console.log(newValue);
     *         }
     *     }]
     * }, {
     *     type: "group",
     *     items: [{
     *         type: "toggle",
     *         label: "Item Toggle",
     *         active: false,
     *         action: (newValue) => {
     *             console.log(newValue);
     *         }
     *     }]
     * }]);
     */
    buildMenuChildren(setup) {
        const mapper = s => {
            if (s.type === "group") return buildGroup(s);
            return this.buildItem(s);
        };
        const buildGroup = function(group) {
            const items = group.items.map(mapper).filter(i => i);
            return React.createElement(MenuComponents.Group, null, items);
        };
        return setup.map(mapper).filter(i => i);
    }

    /**
     * Creates the menu *component* including the wrapping `ContextMenu`.
     * Calls {@link ContextMenu.buildMenuChildren} under the covers.
     * Used to call in combination with {@link ContextMenu.open}.
     * 
     * @param {Array<object>} setup Array of item props used to build items. See {@link ContextMenu.buildMenuChildren}.
     * @returns {function} The unique context menu component
     */
    buildMenu(setup) {
        return (props) => {return React.createElement(MenuComponents.Menu, props, this.buildMenuChildren(setup));};
    }

    /**
     * Function that allows you to open an entire context menu. Recommended to build the menu with this module.
     * 
     * @param {MouseEvent} event The context menu event. This can be emulated, requires target, and all X, Y locations.
     * @param {function} menuComponent Component to render. This can be any React component or output of {@link ContextMenu.buildMenu}.
     * @param {object} config Configuration/props for the context menu
     * @param {string} [config.position="right"] Default position for the menu, options: "left", "right"
     * @param {string} [config.align="top"] Default alignment for the menu, options: "bottom", "top"
     * @param {function} [config.onClose] Function to run when the menu is closed
     */
    open(event, menuComponent, config) {
        return ContextMenuActions.openContextMenu(event, function(e) {
            return React.createElement(menuComponent, Object.assign({}, e, {onClose: ContextMenuActions.closeContextMenu}));
        }, config);
    }

    /**
     * Closes the current opened context menu immediately.
     */
    close() {ContextMenuActions.closeContextMenu();}
}

Object.assign(ContextMenu.prototype, MenuComponents);
Object.freeze(ContextMenu);
Object.freeze(ContextMenu.prototype);

try {
    MenuPatcher.initialize();
} catch (error) {
    Logger.error("ContextMenu~Patcher", "Fatal error:", error);
}

export default ContextMenu;
