/**
 * BetterDiscord Reflection Modules
 * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
 * All rights reserved.
 * https://betterdiscord.net
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
*/

import { Utils, Filters } from 'common';
import Events from '../events';

const KnownModules = {
    React: Filters.byProperties(['createElement', 'cloneElement']),
    ReactDOM: Filters.byProperties(['render', 'findDOMNode']),

    Events: Filters.byPrototypeFields(['setMaxListeners', 'emit']),

    /* Guild Info, Stores, and Utilities */
    GuildStore: Filters.byProperties(['getGuild']),
    SortedGuildStore: Filters.byProperties(['getSortedGuilds']),
    SelectedGuildStore: Filters.byProperties(['getLastSelectedGuildId']),
    GuildSync: Filters.byProperties(['getSyncedGuilds']),
    GuildInfo: Filters.byProperties(['getAcronym']),
    GuildChannelsStore: Filters.byProperties(['getChannels', 'getDefaultChannel']),
    GuildMemberStore: Filters.byProperties(['getMember']),
    MemberCountStore: Filters.byProperties(['getMemberCounts']),
    GuildEmojiStore: Filters.byProperties(['getEmojis']),
    GuildActions: Filters.byProperties(['markGuildAsRead']),
    GuildPermissions: Filters.byProperties(['getGuildPermissions']),

    /* Channel Store & Actions */
    ChannelStore: Filters.byProperties(['getChannels', 'getDMFromUserId']),
    SelectedChannelStore: Filters.byProperties(['getLastSelectedChannelId']),
    ChannelActions: Filters.byProperties(['selectChannel']),
    PrivateChannelActions: Filters.byProperties(['openPrivateChannel']),
    ChannelSelector: Filters.byProperties(['selectGuild', 'selectChannel']),
    VoiceChannelActions: Filters.byProperties(['selectVoiceChannel']),

    /* Current User Info, State and Settings */
    UserInfoStore: Filters.byProperties(['getToken']),
    UserSettingsStore: Filters.byProperties(['guildPositions']),
    AccountManager: Filters.byProperties(['register', 'login']),
    UserSettingsUpdater: Filters.byProperties(['updateRemoteSettings']),
    OnlineWatcher: Filters.byProperties(['isOnline']),
    CurrentUserIdle: Filters.byProperties(['getIdleTime']),
    RelationshipStore: Filters.byProperties(['isBlocked', 'isFriend']),
    RelationshipManager: Filters.byProperties(['addRelationship']),
    MentionStore: Filters.byProperties(['getMentions']),

    /* User Stores and Utils */
    UserStore: Filters.byProperties(['getCurrentUser']),
    UserStatusStore: Filters.byProperties(['getStatuses']),
    UserTypingStore: Filters.byProperties(['isTyping']),
    UserActivityStore: Filters.byProperties(['getActivity']),
    UserNameResolver: Filters.byProperties(['getName']),
    UserNoteStore: Filters.byProperties(['getNote']),
    UserNoteActions: Filters.byProperties(['updateNote']),
    DraftActions: Filters.byProperties(['changeDraft']),

    /* Emoji Store and Utils */
    EmojiInfo: Filters.byProperties(['isEmojiDisabled']),
    EmojiUtils: Filters.byProperties(['getGuildEmoji']),
    EmojiStore: Filters.byProperties(['getByCategory', 'EMOJI_NAME_RE']),

    /* Invite Store and Utils */
    InviteStore: Filters.byProperties(['getInvites']),
    InviteResolver: Filters.byProperties(['findInvite']),
    InviteActions: Filters.byProperties(['acceptInvite']),

    /* Discord Objects & Utils */
    DiscordConstants: Filters.byProperties(['Permissions', 'ActivityTypes', 'StatusTypes']),
    Permissions: Filters.byProperties(['getHighestRole']),
    ColorConverter: Filters.byProperties(['hex2int']),
    ColorShader: Filters.byProperties(['darken']),
    TinyColor: Filters.byPrototypeFields(['toRgb']),
    ClassResolver: Filters.byProperties(['getClass']),
    ButtonData: Filters.byProperties(['ButtonSizes']),
    IconNames: Filters.byProperties(['IconNames']),
    NavigationUtils: Filters.byProperties(['transitionTo', 'replaceWith', 'getHistory']),

    /* Discord Messages */
    MessageStore: Filters.byProperties(['getMessages']),
    MessageActions: Filters.byProperties(['jumpToMessage', '_sendMessage']),
    MessageQueue: Filters.byProperties(['enqueue']),
    MessageParser: Filters.byProperties(['createMessage', 'parse', 'unparse']),

    /* In-Game Overlay */
    OverlayUserPopoutSettings: Filters.byProperties(['openUserPopout']),
    OverlayUserPopoutInfo: Filters.byProperties(['getOpenedUserPopout']),

    /* Experiments */
    ExperimentStore: Filters.byProperties(['getExperimentOverrides']),
    ExperimentsManager: Filters.byProperties(['isDeveloper']),
    CurrentExperiment: Filters.byProperties(['getExperimentId']),

    /* Images, Avatars and Utils */
    ImageResolver: Filters.byProperties(['getUserAvatarURL']),
    ImageUtils: Filters.byProperties(['getSizedImageSrc']),
    AvatarDefaults: Filters.byProperties(['getUserAvatarURL', 'DEFAULT_AVATARS']),

    /* Drag & Drop */
    DNDActions: Filters.byProperties(['beginDrag']),
    DNDSources: Filters.byProperties(['addTarget']),
    DNDObjects: Filters.byProperties(['DragSource']),

    /* Electron & Other Internals with Utils */
    ElectronModule: Filters.byProperties(['_getMainWindow']),
    Dispatcher: Filters.byProperties(['dirtyDispatch']),
    PathUtils: Filters.byProperties(['hasBasename']),
    NotificationModule: Filters.byProperties(['showNotification']),
    RouterModule: Filters.byProperties(['Router']),
    APIModule: Filters.byProperties(['getAPIBaseURL']),
    AnalyticEvents: Filters.byProperties(['AnalyticEventConfigs']),
    KeyGenerator: Filters.byCode(/"binary"/),
    Buffers: Filters.byProperties(['Buffer', 'kMaxLength']),
    DeviceStore: Filters.byProperties(['getDevices']),
    SoftwareInfo: Filters.byProperties(['os']),
    CurrentContext: Filters.byProperties(['setTagsContext']),

    /* Media Stuff (Audio/Video) */
    MediaDeviceInfo: Filters.byProperties(['Codecs', 'SUPPORTED_BROWSERS']),
    MediaInfo: Filters.byProperties(['getOutputVolume']),
    MediaEngineInfo: Filters.byProperties(['MediaEngineFeatures']),
    VoiceInfo: Filters.byProperties(['EchoCancellation']),
    VideoStream: Filters.byProperties(['getVideoStream']),
    SoundModule: Filters.byProperties(['playSound']),

    /* Window, DOM, HTML */
    WindowInfo: Filters.byProperties(['isFocused', 'windowSize']),
    TagInfo: Filters.byProperties(['VALID_TAG_NAMES']),
    DOMInfo: Filters.byProperties(['canUseDOM']),

    /* Locale/Location and Time */
    LocaleManager: Filters.byProperties(['setLocale']),
    Moment: Filters.byProperties(['parseZone']),
    LocationManager: Filters.byProperties(['createLocation']),
    Timestamps: Filters.byProperties(['fromTimestamp']),
    TimeFormatter: Filters.byProperties(['dateFormat']),

    /* Strings and Utils */
    Strings: Filters.byProperties(['TEXT', 'TEXTAREA_PLACEHOLDER']),
    StringFormats: Filters.byProperties(['a', 'z']),
    StringUtils: Filters.byProperties(['toASCII']),

    /* URLs and Utils */
    URLParser: Filters.byProperties(['Url', 'parse']),
    ExtraURLs: Filters.byProperties(['getArticleURL']),

    /* Text Processing */
    hljs: Filters.byProperties(['highlight', 'highlightBlock']),
    SimpleMarkdown: Filters.byProperties(['parseBlock', 'parseInline', 'defaultOutput']),

    /* DOM/React Components */
    /* ==================== */
    LayerManager: Filters.byProperties(['popLayer', 'pushLayer']),
    UserSettingsWindow: Filters.byProperties(['open', 'updateAccount']),
    ChannelSettingsWindow: Filters.byProperties(['open', 'updateChannel']),
    GuildSettingsWindow: Filters.byProperties(['open', 'updateGuild']),

    /* Modals */
    ModalStack: Filters.byProperties(['push', 'update', 'pop', 'popWithKey']),
    ConfirmModal: Filters.byPrototypeFields(['handleCancel', 'handleSubmit', 'handleMinorConfirm']),
    UserProfileModal: Filters.byProperties(['fetchMutualFriends', 'setSection']),
    ChangeNicknameModal: Filters.byProperties(['open', 'changeNickname']),
    CreateChannelModal: Filters.byProperties(['open', 'createChannel']),
    PruneMembersModal: Filters.byProperties(['open', 'prune']),
    NotificationSettingsModal: Filters.byProperties(['open', 'updateNotificationSettings']),
    PrivacySettingsModal: Filters.byCode(/PRIVACY_SETTINGS_MODAL_OPEN/, m => m.open),
    CreateInviteModal: Filters.byProperties(['open', 'createInvite']),

    /* Popouts */
    PopoutStack: Filters.byProperties(['open', 'close', 'closeAll']),
    PopoutOpener: Filters.byProperties(['openPopout']),
    EmojiPicker: Filters.byPrototypeFields(['onHoverEmoji', 'selectEmoji']),

    /* Context Menus */
    ContextMenuActions: Filters.byCode(/CONTEXT_MENU_CLOSE/, c => c.close),
    ContextMenuItemsGroup: Filters.byCode(/itemGroup/),
    ContextMenuItem: Filters.byCode(/\.label\b.*\.hint\b.*\.action\b/),

    /* In-Message Links */
    ExternalLink: Filters.byCode(/\.trusted\b/)
};

class Module {

    /**
     * Finds a module using a filter function.
     * @param {Function} filter A function to use to filter modules
     * @param {Boolean} first Whether to return only the first matching module
     * @param {Array} modules An array of modules to search in
     * @return {Any}
     */
    static getModule(filter, first = true, _modules) {
        const modules = _modules || this.getAllModules();
        const rm = [];
        for (const index in modules) {
            if (!modules.hasOwnProperty(index)) continue;
            const module = modules[index];
            const { exports } = module;
            let foundModule = null;

            if (!exports) continue;
            if (exports.__esModule && exports.default && filter(exports.default)) foundModule = exports.default;
            if (filter(exports)) foundModule = exports;
            if (!foundModule) continue;
            if (first) return foundModule;
            rm.push(foundModule);
        }
        return first ? undefined : rm;
    }

    /**
     * Finds a module by it's name.
     * @param {String} name The name of the module
     * @param {Function} fallback A function to use to filter modules if not finding a known module
     * @return {Any}
     */
    static byName(name, fallback) {
        if (Cache.hasOwnProperty(name)) return Cache[name];
        if (KnownModules.hasOwnProperty(name)) fallback = KnownModules[name];
        if (!fallback) return undefined;
        const module = this.getModule(fallback, true);
        return module ? Cache[name] = module : undefined;
    }

    /**
     * Finds a module by it's display name.
     * @param {String} name The display name of the module
     * @return {Any}
     */
    static byDisplayName(name) {
        return this.getModule(Filters.byDisplayName(name), true);
    }

    /**
     * Finds a module using it's code.
     * @param {RegEx} regex A regular expression to use to filter modules
     * @param {Boolean} first Whether to return the only the first matching module
     * @return {Any}
     */
    static byRegex(regex, first = true) {
        return this.getModule(Filters.byCode(regex), first);
    }

    /**
     * Finds the first module using properties on it's prototype.
     * @param {any} props Properties to use to filter modules
     * @return {Any}
     */
    static byPrototypes(...prototypes) {
        return this.getModule(Filters.byPrototypeFields(prototypes), true);
    }

    /**
     * Finds all modules using properties on it's prototype.
     * @param {any} props Properties to use to filter modules
     * @return {Any}
     */
    static allByPrototypes(...prototypes) {
        return this.getModule(Filters.byPrototypeFields(prototypes), false);
    }

    /**
     * Finds the first module using it's own properties.
     * @param {any} props Properties to use to filter modules
     * @return {Any}
     */
    static byProps(...props) {
        return this.getModule(Filters.byProperties(props), true);
    }

    /**
     * Finds all modules using it's own properties.
     * @param {any} props Properties to use to filter modules
     * @return {Any}
     */
    static allByProps(...props) {
        return this.getModule(Filters.byProperties(props), false);
    }

    /**
     * Discord's __webpack_require__ function.
     */
    static get require() {
        if (this._require) return this._require;

        const __webpack_require__ = this.getWebpackRequire();
        if (!__webpack_require__) return;

        this.hookWebpackRequireCache(__webpack_require__);
        return this._require = __webpack_require__;
    }

    static getWebpackRequire() {
        const id = 'bd-webpackmodules';

        if (typeof window.webpackJsonp === 'function') {
            const __webpack_require__ = window['webpackJsonp']([], {
                [id]: (module, exports, __webpack_require__) => exports.default = __webpack_require__
            }, [id]).default;
            delete __webpack_require__.m[id];
            delete __webpack_require__.c[id];
            return __webpack_require__;
        } else if (window.webpackJsonp && window.webpackJsonp.push) {
            const __webpack_require__ = window['webpackJsonp'].push([[], {
                [id]: (module, exports, req) => exports.default = req
            }, [[id]]]).default;
            window['webpackJsonp'].pop();
            delete __webpack_require__.m[id];
            delete __webpack_require__.c[id];
            return __webpack_require__;
        }
    }

    static hookWebpackRequireCache(__webpack_require__) {
        __webpack_require__.c = new Proxy(__webpack_require__.c, {
            set(module_cache, module_id, module) {
                // Add it to our emitter cache and emit a module-loading event
                this.moduleLoading(module_id, module);
                Events.emit('module-loading', module);

                // Add the module to the cache as normal
                module_cache[module_id] = module;
            }
        });
    }

    static moduleLoading(module_id, module) {
        if (this.require.c[module_id]) return;

        if (!this.moduleLoadedEventTimeout) {
            this.moduleLoadedEventTimeout = setTimeout(() => {
                this.moduleLoadedEventTimeout = undefined;

                // Emit a module-loaded event for every module
                for (const module of this.modulesLoadingCache) {
                    Events.emit('module-loaded', module);
                }

                // Emit a modules-loaded event
                Events.emit('modules-loaded', this.modulesLoadingCache);

                this.modulesLoadedCache = [];
            }, 0);
        }

        // Add this to our own cache
        if (!this.modulesLoadingCache) this.modulesLoadingCache = [];
        this.modulesLoadingCache.push(module);
    }

    static waitForWebpackRequire() {
        return Utils.until(() => this.require, 10);
    }

    /**
     * Waits for a module to load.
     * This only returns a single module, as it can't guarentee there are no more modules that could
     * match the filter, which is pretty much what that would be asking for.
     * @param {Function} filter The name of a known module or a filter function
     * @return {Any}
     */
    static async waitForModule(filter) {
        const module = this.getModule(filter);
        if (module) return module;

        while (this.require.m.length > this.require.c.length) {
            const additionalModules = await Events.once('modules-loaded');

            const module = this.getModule(filter, true, additionalModules);
            if (module) return module;
        }

        throw new Error('All modules have now been loaded. None match the passed filter.');
    }

    /**
     * Finds a module by it's name.
     * @param {String} name The name of the module
     * @param {Function} fallback A function to use to filter modules if not finding a known module
     * @return {Any}
     */
    static async waitForModuleByName(name, fallback) {
        if (Cache.hasOwnProperty(name)) return Cache[name];
        if (KnownModules.hasOwnProperty(name)) fallback = KnownModules[name];
        if (!fallback) return undefined;
        const module = await this.waitForModule(fallback, true);
        return module ? Cache[name] = module : undefined;
    }

    static waitForModuleByDisplayName(props) {
        return this.waitForModule(Filters.byDisplayName(props));
    }
    static waitForModuleByRegex(props) {
        return this.waitForModule(Filters.byCode(props));
    }
    static waitForModuleByProps(props) {
        return this.waitForModule(Filters.byProperties(props));
    }
    static waitForModuleByPrototypes(props) {
        return this.waitForModule(Filters.byPrototypeFields(props));
    }

    /**
     * Returns all loaded modules.
     * @return {Array}
     */
    static getAllModules() {
        return this.require.c;
    }

    /**
     * Returns an array of known modules.
     * @return {Array}
     */
    static listKnownModules() {
        return Object.keys(KnownModules);
    }

    static get KnownModules() { return KnownModules }

}

const Modules = new Proxy(Module, {
    get(Module, name) {
        return Module.byName(name);
    }
});

export { Module, Modules }
