const path = window.require('path');
const fs = window.require('fs');
const electron = window.require('electron');
const Module =  window.require('module').Module;
Module.globalPaths.push(path.resolve(electron.remote.app.getAppPath(), 'node_modules'));
const currentWindow = electron.remote.getCurrentWindow();
if (currentWindow.__preload) require(currentWindow.__preload);

//Get inject directory
if (!process.env.injDir) process.env.injDir = __dirname;

//set up global functions
const c = {
    log: function(msg, plugin) {
        if (plugin && plugin.name)
            console.log(`%c[EnhancedDiscord] %c[${plugin.name}]`, 'color: red;', `color: ${plugin.color}`, msg);
        else console.log('%c[EnhancedDiscord]', 'color: red;', msg);
    },
    info: function(msg, plugin) {
        if (plugin && plugin.name)
            console.info(`%c[EnhancedDiscord] %c[${plugin.name}]`, 'color: red;', `color: ${plugin.color}`, msg);
        else console.info('%c[EnhancedDiscord]', 'color: red;', msg);
    },
    warn: function(msg, plugin) {
        if (plugin && plugin.name)
            console.warn(`%c[EnhancedDiscord] %c[${plugin.name}]`, 'color: red;', `color: ${plugin.color}`, msg);
        else console.warn('%c[EnhancedDiscord]', 'color: red;', msg);
    },
    error: function(msg, plugin) {
        if (plugin && plugin.name)
            console.error(`%c[EnhancedDiscord] %c[${plugin.name}]`, 'color: red;', `color: ${plugin.color}`, msg);
        else console.error('%c[EnhancedDiscord]', 'color: red;', msg);
    },
    sleep: function(ms) {
        return new Promise(resolve => {
            setTimeout(resolve, ms);
        });
    }
}
// config util
window.ED = { plugins: {}, version: '2.6.2' };
Object.defineProperty(window.ED, 'config', {
    get: function() {
        let conf;
        try{
            conf = require('./config.json');
        } catch (err) {
            if(err.code !== 'MODULE_NOT_FOUND')
                c.error(err);
            conf = {};
        }
        return conf;
    },
    set: function(newSets = {}) {
        let confPath;
        let bDelCache;
        try{
            confPath = require.resolve('./config.json');
            bDelCache = true;
        } catch (err) {
            if(err.code !== 'MODULE_NOT_FOUND')
                c.error(err);
            confPath = path.join(process.env.injDir, 'config.json');
            bDelCache = false;
        }

        try {
            fs.writeFileSync(confPath, JSON.stringify(newSets));
            if(bDelCache)
                delete require.cache[confPath];
        } catch(err) {
            c.error(err);
        }
        return this.config;
    }
});

function loadPlugin(plugin) {
    try {
        if (plugin.preload)
            console.log(`%c[EnhancedDiscord] %c[PRELOAD] %cLoading plugin %c${plugin.name}`, 'color: red;', 'color: yellow;', '', `color: ${plugin.color}`, `by ${plugin.author}...`);
        else console.log(`%c[EnhancedDiscord] %cLoading plugin %c${plugin.name}`, 'color: red;', '', `color: ${plugin.color}`, `by ${plugin.author}...`);
        plugin.load();
    } catch(err) {
        c.error(`Failed to load:\n${err.stack}`, plugin);
    }
}

window.ED.localStorage = window.localStorage;

process.once("loaded", async () => {
    c.log(`v${window.ED.version} is running. Validating plugins...`);

    const pluginFiles = fs.readdirSync(path.join(process.env.injDir, 'plugins'));
    const plugins = {};
    for (const i in pluginFiles) {
        if (!pluginFiles[i].endsWith('.js') || pluginFiles[i].endsWith(".plugin.js")) continue;
        let p;
        const pName = pluginFiles[i].replace(/\.js$/, '');
        try {
            p = require(path.join(process.env.injDir, 'plugins', pName));
            if (typeof p.name !== 'string' || typeof p.load !== 'function') {
                throw new Error('Plugin must have a name and load() function.');
            }
            plugins[pName] = Object.assign(p, {id: pName});
        }
        catch (err) {
            c.warn(`Failed to load ${pluginFiles[i]}: ${err}\n${err.stack}`, p);
        }
    }
    for (const id in plugins) {
        if (!plugins[id] || !plugins[id].name || typeof plugins[id].load !== 'function') {
            c.info(`Skipping invalid plugin: ${id}`); delete plugins[id]; continue;
        }
        plugins[id].settings; // this will set default settings in config if necessary
    }
    window.ED.plugins = plugins;
    c.log(`Plugins validated.`);

	while (!window.webpackJsonp)
		await c.sleep(100); // wait until this is loaded in order to use it for modules

    window.ED.webSocket = window._ws;

	/* Add helper functions that make plugins easy to create */
	window.req = window.webpackJsonp.push([[], {
        '__extra_id__': (module, exports, req) => module.exports = req
	}, [['__extra_id__']]]);
	delete window.req.m['__extra_id__'];
	delete window.req.c['__extra_id__'];

    window.findModule = window.EDApi.findModule;
    window.findModules = window.EDApi.findAllModules;
    window.findRawModule = window.EDApi.findRawModule;
    window.monkeyPatch = window.EDApi.monkeyPatch;

    while (!window.EDApi.findModule('dispatch'))
        await c.sleep(100);

    c.log(`Loading preload plugins...`);
    for (const id in plugins) {
        if (window.ED.config[id] && window.ED.config[id].enabled == false) continue;
        if (!plugins[id].preload) continue;
        loadPlugin(plugins[id]);
    }

    const d = {resolve: () => {}};
    window.monkeyPatch(window.findModule('dispatch'), 'dispatch', {before: b => {
        // modules seem to all be loaded when RPC server loads
        if (b.methodArguments[0].type === 'RPC_SERVER_READY') {
            window.findModule('dispatch').dispatch.unpatch();
            d.resolve();
        }
    }});

    await new Promise(resolve => {
        d.resolve = resolve;
    })
    c.log(`Modules done loading (${Object.keys(window.req.c).length})`);

    if (window.ED.config.bdPlugins) {
        await require('./bd_shit').setup(currentWindow);
        c.log(`Preparing BD plugins...`);
        for (const i in pluginFiles) {
            if (!pluginFiles[i].endsWith('.js') || !pluginFiles[i].endsWith(".plugin.js")) continue;
            let p;
            const pName = pluginFiles[i].replace(/\.js$/, '');
            try {
                p = require(path.join(process.env.injDir, 'plugins', pName));
                if (typeof p.name !== 'string' || typeof p.load !== 'function') {
                    throw new Error('Plugin must have a name and load() function.');
                }
                plugins[pName] = Object.assign(p, {id: pName});
            }
            catch (err) {
                c.warn(`Failed to load ${pluginFiles[i]}: ${err}\n${err.stack}`, p);
            }
        }
        for (const id in plugins) {
            if (!plugins[id] || !plugins[id].name || typeof plugins[id].load !== 'function') {
                c.info(`Skipping invalid plugin: ${id}`); delete plugins[id]; continue;
            }
        }
    }

    c.log(`Loading plugins...`);
    for (const id in plugins) {
        if (window.ED.config[id] && window.ED.config[id].enabled == false) continue;
        if (plugins[id].preload) continue;
        if (window.ED.config[id].enabled !== true && plugins[id].disabledByDefault) {
            plugins[id].settings.enabled = false; continue;
        }
        loadPlugin(plugins[id]);
    }


    const ht = window.EDApi.findModule('hideToken')
    // prevent client from removing token from localstorage when dev tools is opened, or reverting your token if you change it
    window.EDApi.monkeyPatch(ht, 'hideToken', () => {});
    window.fixedShowToken = () => {
        // Only allow this to add a token, not replace it. This allows for changing of the token in dev tools.
        if (!window.ED.localStorage || window.ED.localStorage.getItem("token")) return;
        return window.ED.localStorage.setItem("token", '"'+ht.getToken()+'"');
    };
    window.EDApi.monkeyPatch(ht, 'showToken', window.fixedShowToken);
    if (!window.ED.localStorage.getItem("token") && ht.getToken())
        window.fixedShowToken(); // prevent you from being logged out for no reason

    // change the console warning to be more fun
    const wc = require('electron').remote.getCurrentWebContents();
    wc.removeAllListeners("devtools-opened");
    wc.on("devtools-opened", () => {
        console.log("%cHold Up!", "color: #FF5200; -webkit-text-stroke: 2px black; font-size: 72px; font-weight: bold;");
        console.log("%cIf you're reading this, you're probably smarter than most Discord developers.", "font-size: 16px;");
        console.log("%cPasting anything in here could actually improve the Discord client.", "font-size: 18px; font-weight: bold; color: red;");
        console.log("%cUnless you understand exactly what you're doing, keep this window open to browse our bad code.", "font-size: 16px;");
        console.log("%cIf you don't understand exactly what you're doing, you should come work with us: https://discordapp.com/jobs", "font-size: 16px;");
    });
})



/* BD/ED joint api */
window.EDApi = window.BdApi = class EDApi {
    static get React() { return this.findModuleByProps('createElement'); }
    static get ReactDOM() { return this.findModuleByProps('findDOMNode'); }

    static escapeID(id) {
        return id.replace(/^[^a-z]+|[^\w-]+/gi, "");
    }

    static injectCSS(id, css) {
        const style = document.createElement("style");
		style.id = this.escapeID(id);
		style.innerHTML = css;
		document.head.append(style);
    }

    static clearCSS(id) {
		const element = document.getElementById(this.escapeID(id));
		if (element) element.remove();
    }

    static linkJS(id, url) {
        return new Promise(resolve => {
			const script = document.createElement("script");
			script.id = this.escapeID(id);
			script.src = url;
			script.type = "text/javascript";
			script.onload = resolve;
			document.head.append(script);
		});
    }

    static unlinkJS(id) {
        const element = document.getElementById(this.escapeID(id));
		if (element) element.remove();
    }

    static getPlugin(name) {
        const plugin = Object.values(window.ED.plugins).find(p => p.name == name);
        if (!plugin) return null;
        return plugin.bdplugin ? plugin.bdplugin : plugin;
    }

    static alert(title, body) {
        const ModalStack = this.findModuleByProps("push", "update", "pop", "popWithKey");
        const AlertModal = this.findModule(m => m.prototype && m.prototype.handleCancel && m.prototype.handleSubmit && m.prototype.handleMinorConfirm);
        if (!ModalStack || !AlertModal) return window.alert(body);
        ModalStack.push(function(props) {
            return EDApi.React.createElement(AlertModal, Object.assign({title, body}, props));
        });
    }

    static loadData(pluginName, key) {
        if (!window.ED.config[pluginName]) window.ED.config[pluginName] = {};
        return window.ED.config[pluginName][key];
    }

    static saveData(pluginName, key, data) {
        if (!window.ED.config[pluginName]) window.ED.config[pluginName] = {};
        window.ED.config[pluginName][key] = data;
        window.ED.config = window.ED.config;
    }

    static getData(pluginName, key) {
        return this.loadData(pluginName, key);
    }

    static setData(pluginName, key, data) {
        this.saveData(pluginName, key, data);
    }

    static getInternalInstance(node) {
        if (!(node instanceof window.jQuery) && !(node instanceof Element)) return undefined;
        if (node instanceof window.jQuery) node = node[0];
        return node[Object.keys(node).find(k => k.startsWith("__reactInternalInstance"))];
    }

    static showToast(content, options = {}) {
        if (!document.querySelector(".toasts")) {
            const toastWrapper = document.createElement("div");
            toastWrapper.classList.add("toasts");
            const boundingElement = document.querySelector(".chat-3bRxxu form, #friends, .noChannel-Z1DQK7, .activityFeed-28jde9");
            toastWrapper.style.setProperty("left", boundingElement ? boundingElement.getBoundingClientRect().left + "px" : "0px");
            toastWrapper.style.setProperty("width", boundingElement ? boundingElement.offsetWidth + "px" : "100%");
            toastWrapper.style.setProperty("bottom", (document.querySelector(".chat-3bRxxu form") ? document.querySelector(".chat-3bRxxu form").offsetHeight : 80) + "px");
            document.querySelector("." + this.findModule('app').app).appendChild(toastWrapper);
        }
        const {type = "", icon = true, timeout = 3000} = options;
        const toastElem = document.createElement("div");
        toastElem.classList.add("toast");
        if (type) toastElem.classList.add("toast-" + type);
        if (type && icon) toastElem.classList.add("icon");
        toastElem.innerText = content;
        document.querySelector(".toasts").appendChild(toastElem);
        setTimeout(() => {
            toastElem.classList.add("closing");
            setTimeout(() => {
                toastElem.remove();
                if (!document.querySelectorAll(".toasts .toast").length) document.querySelector(".toasts").remove();
            }, 300);
        }, timeout);
    }

    static findModule(filter, silent = true) {
        const moduleName = typeof filter === 'string' ? filter : null;
        for (const i in window.req.c) {
            if (window.req.c.hasOwnProperty(i)) {
                const m = window.req.c[i].exports;
                if (m && m.__esModule && m.default && (moduleName ? m.default[moduleName] : filter(m.default))) return m.default;
                if (m && (moduleName ? m[moduleName] : filter(m)))	return m;
            }
        }
        if (!silent) c.warn(`Could not find module ${module}.`, {name: 'Modules', color: 'black'})
        return null;
    }

    static findRawModule(filter, silent = true) {
        const moduleName = typeof filter === 'string' ? filter : null;
        for (const i in window.req.c) {
            if (window.req.c.hasOwnProperty(i)) {
                const m = window.req.c[i].exports;
                if (m && m.__esModule && m.default && (moduleName ? m.default[moduleName] : filter(m.default)))
                    return window.req.c[i];
                if (m && (moduleName ? m[moduleName] : filter(m)))
                    return window.req.c[i];
            }
        }
        if (!silent) c.warn(`Could not find module ${module}.`, {name: 'Modules', color: 'black'})
        return null;
    }

    static findAllModules(filter) {
        const moduleName = typeof filter === 'string' ? filter : null;
        const modules = [];
        for (const i in window.req.c) {
            if (window.req.c.hasOwnProperty(i)) {
                const m = window.req.c[i].exports;
                if (m && m.__esModule && m.default && (moduleName ? m.default[moduleName] : filter(m.default))) modules.push(m.default);
                else if (m && (moduleName ? m[moduleName] : filter(m))) modules.push(m);
            }
        }
        return modules;
    }

    static findModuleByProps(...props) {
        return this.findModule(module => props.every(prop => module[prop] !== undefined));
    }

    static findModuleByDisplayName(name) {
        return this.findModule(module => module.displayName === name);
    }

    static monkeyPatch(what, methodName, options) {
        if (typeof options === 'function') {
            const newOptions = {instead: options, silent: true};
            options = newOptions;
        }
        const {before, after, instead, once = false, silent = false, force = false} = options;
        const displayName = options.displayName || what.displayName || what.name || what.constructor ? (what.constructor.displayName || what.constructor.name) : null;
        if (!silent) console.log(`%c[EnhancedDiscord] %c[Modules]`, 'color: red;', `color: black;`, `Patched ${methodName} in module ${displayName || '<unknown>'}:`, what); // eslint-disable-line no-console
        if (!what[methodName]) {
            if (force) what[methodName] = function() {};
            else return console.warn(`%c[EnhancedDiscord] %c[Modules]`, 'color: red;', `color: black;`, `Method ${methodName} doesn't exist in module ${displayName || '<unknown>'}`, what); // eslint-disable-line no-console
        }
        const origMethod = what[methodName];
        const cancel = () => {
            if (!silent) console.log(`%c[EnhancedDiscord] %c[Modules]`, 'color: red;', `color: black;`, `Unpatched ${methodName} in module ${displayName || '<unknown>'}:`, what); // eslint-disable-line no-console
            what[methodName] = origMethod;
        };
        what[methodName] = function() {
            const data = {
                thisObject: this,
                methodArguments: arguments,
                cancelPatch: cancel,
                originalMethod: origMethod,
                callOriginalMethod: () => data.returnValue = data.originalMethod.apply(data.thisObject, data.methodArguments)
            };
            if (instead) {
                const tempRet = EDApi.suppressErrors(instead, "`instead` callback of " + what[methodName].displayName)(data);
                if (tempRet !== undefined) data.returnValue = tempRet;
            }
            else {
                if (before) EDApi.suppressErrors(before, "`before` callback of " + what[methodName].displayName)(data);
                data.callOriginalMethod();
                if (after) EDApi.suppressErrors(after, "`after` callback of " + what[methodName].displayName)(data);
            }
            if (once) cancel();
            return data.returnValue;
        };
        what[methodName].__monkeyPatched = true;
        what[methodName].displayName = "patched " + (what[methodName].displayName || methodName);
        what[methodName].unpatch = cancel;
        return cancel;
    }

    static testJSON(data) {
        try {
            JSON.parse(data);
            return true;
        }
        catch (err) {
            return false;
        }
    }

    static suppressErrors(method, description) {
        return (...params) => {
            try { return method(...params);	}
            catch (e) { console.error("Error occurred in " + description, e); }
        };
    }

    static formatString(string, values) {
        for (const val in values) {
            string = string.replace(new RegExp(`\\{\\{${val}\\}\\}`, 'g'), values[val]);
        }
        return string;
    }

	static isPluginEnabled(name) {
		const plugins = Object.values(window.ED.plugins);
		const plugin = plugins.find(p => p.id == name || p.name == name);
		if (!plugin) return false;
		return !(plugin.settings.enabled === false);
	}

	static isThemeEnabled() {
		return false;
	}

	static isSettingEnabled(id) {
		return window.ED.config[id];
	}
};
