import {bdConfig, bdplugins, bdthemes, settingsCookie} from "../0globals";
import pluginModule from "./pluginModule";
import themeModule from "./themeModule";
import Utils from "./utils";
import dataStore from "./dataStore";
import { encryptSettingsCache, decryptSettingsCache, processFile } from "./pluginCertifier";
import * as electron from "electron"

const path = require("path");
const fs = require("fs");
const Module = require("module").Module;
Module.globalPaths.push(path.resolve(electron.ipcRenderer.sendSync("LIGHTCORD_GET_APP_PATH"), "node_modules"));
class MetaError extends Error {
    constructor(message) {
        super(message);
        this.name = "MetaError";
    }
}
const originalJSRequire = Module._extensions[".js"];
const originalCSSRequire = Module._extensions[".css"] ? Module._extensions[".css"] : () => {return null;};
const splitRegex = /[^\S\r\n]*?(?:\r\n|\n)[^\S\r\n]*?\*[^\S\r\n]?/;
const escapedAtRegex = /^\\@/;

export let addonCache = {}

let hasPatched = false
export default new class ContentManager {

    constructor() {
        this.timeCache = {};
        this.watchers = {};
    }

    patchExtensions(){
        if(hasPatched)return
        hasPatched = true
        Module._extensions[".js"] = this.getContentRequire("plugin");
        Module._extensions[".css"] = this.getContentRequire("theme");
    }

    get pluginsFolder() {return this._pluginsFolder || (this._pluginsFolder = fs.realpathSync(path.resolve(bdConfig.dataPath + "plugins/")));}
    get themesFolder() {return this._themesFolder || (this._themesFolder = fs.realpathSync(path.resolve(bdConfig.dataPath + "themes/")));}

    loadAddonCertifierCache(){
        if(typeof dataStore.getSettingGroup("PluginCertifierHashes") !== "string"){
            dataStore.setSettingGroup("PluginCertifierHashes", encryptSettingsCache("{}"))
        }else{
            try{
                addonCache = JSON.parse(decryptSettingsCache(dataStore.getSettingGroup("PluginCertifierHashes")))
            }catch(e){
                dataStore.setSettingGroup("PluginCertifierHashes", encryptSettingsCache("{}"))
                addonCache = {}
            }
        }
        Object.keys(addonCache)
        .forEach(key => {
            let value = addonCache[key]
            if(!value || typeof value !== "object" || Array.isArray(value))return delete addonCache[key]

            let props = [{
                key: "timestamp",
                type: "number"
            }, {
                key: "result",
                type: "object"
            }, {
                key: "hash",
                type: "string"
            }]
            for(let prop of props){
                if(!(prop.key in value) || typeof value[prop.key] !== prop.type){
                    delete addonCache[key]
                    return
                }
            }
            if(value.hash !== key){
                delete addonCache[key]
                return
            }
            if(value.result.suspect){ // refetch from remote to be sure you're up to date.
                delete addonCache[key]
                return
            }
        })
        this.saveAddonCache()
    }

    saveAddonCache(){
        dataStore.setSettingGroup("PluginCertifierHashes", encryptSettingsCache(JSON.stringify(addonCache)))
    }

    resetAddonCache(){
        Object.keys(addonCache).forEach(key => {
            delete addonCache[key]
        })
        console.log(addonCache)
        this.saveAddonCache()
    }

    watchContent(contentType) {
        if (this.watchers[contentType]) return;
        const isPlugin = contentType === "plugin";
        const baseFolder = isPlugin ? this.pluginsFolder : this.themesFolder;
        const fileEnding = isPlugin ? ".plugin.js" : ".theme.css";
        this.watchers[contentType] = fs.watch(baseFolder, {persistent: false}, async (eventType, filename) => {
            if (!eventType || !filename || !filename.endsWith(fileEnding)) return;
            await new Promise(r => setTimeout(r, 50));
            try {fs.statSync(path.resolve(baseFolder, filename));}
            catch (err) {
                if (err.code !== "ENOENT") return;
                delete this.timeCache[filename];
                if (isPlugin) return pluginModule.unloadPlugin(filename);
                return themeModule.unloadTheme(filename);
            }
            if (!fs.statSync(path.resolve(baseFolder, filename)).isFile()) return;
            const stats = fs.statSync(path.resolve(baseFolder, filename));
            if (!stats || !stats.mtime || !stats.mtime.getTime()) return;
            if (typeof(stats.mtime.getTime()) !== "number") return;
            if (this.timeCache[filename] == stats.mtime.getTime()) return;
            this.timeCache[filename] = stats.mtime.getTime();
            if (eventType == "rename") {
                if (isPlugin) await pluginModule.loadPlugin(filename);
                else await themeModule.loadTheme(filename);
            }
            if (eventType == "change") {
                if (isPlugin) await pluginModule.reloadPlugin(filename);
                else await themeModule.reloadTheme(filename);
            }
        });
    }

    unwatchContent(contentType) {
        if (!this.watchers[contentType]) return;
        this.watchers[contentType].close();
        delete this.watchers[contentType];
    }

    extractMeta(content) {
        const firstLine = content.split("\n")[0];
        const hasOldMeta = firstLine.includes("//META");
        if (hasOldMeta) return this.parseOldMeta(content);
        const hasNewMeta = firstLine.includes("/**");
        if (hasNewMeta) return this.parseNewMeta(content);
        throw new MetaError("META was not found.");
    }

    parseOldMeta(content) {
        const meta = content.split("\n")[0];
        const rawMeta = meta.substring(meta.lastIndexOf("//META") + 6, meta.lastIndexOf("*//"));
        if (meta.indexOf("META") < 0) throw new MetaError("META was not found.");
        const parsed = Utils.testJSON(rawMeta);
        if (!parsed) throw new MetaError("META could not be parsed.");
        if (!parsed.name) throw new MetaError("META missing name data.");
        parsed.format = "json";
        return parsed;
    }

    parseNewMeta(content) {
        const block = content.split("/**", 2)[1].split("*/", 1)[0];
        const out = {};
        let field = "";
        let accum = "";
        for (const line of block.split(splitRegex)) {
            if (line.length === 0) continue;
            if (line.charAt(0) === "@" && line.charAt(1) !== " ") {
                out[field] = accum;
                const l = line.indexOf(" ");
                field = line.substr(1, l - 1);
                accum = line.substr(l + 1);
            }
            else {
                accum += " " + line.replace("\\n", "\n").replace(escapedAtRegex, "@");
            }
        }
        out[field] = accum.trim();
        delete out[""];
        out.format = "jsdoc";
        return out;
    }

    getContentRequire(type) {
        const isPlugin = type === "plugin";
        const self = this;
        const originalRequire = isPlugin ? originalJSRequire : originalCSSRequire;
        return function(module, filename) {
            const baseFolder = isPlugin ? self.pluginsFolder : self.themesFolder;
            const possiblePath = path.resolve(baseFolder, path.basename(filename));
            if (!fs.existsSync(possiblePath) || filename !== fs.realpathSync(possiblePath)) return Reflect.apply(originalRequire, this, arguments);
            let content = fs.readFileSync(filename, "utf8");
            content = Utils.stripBOM(content);

            const stats = fs.statSync(filename);
            const meta = self.extractMeta(content);
            meta.filename = path.basename(filename);
            meta.added = stats.atimeMs;
            meta.modified = stats.mtimeMs;
            meta.size = stats.size;
            if (!isPlugin) {
                meta.css = content;
                if (meta.format == "json") meta.css = meta.css.split("\n").slice(1).join("\n");
                content = `module.exports = ${JSON.stringify(meta)};`;
            }
            if (isPlugin) {
                module._compile(content, module.filename);
                const didExport = !Utils.isEmpty(module.exports);
                if (didExport) {
                    meta.type = module.exports;
                    module.exports = meta;
                    content = "";
                }
                else {
                    Utils.warn("Module Not Exported", `${meta.name}, please start setting module.exports`);
                    content += `\nmodule.exports = ${JSON.stringify(meta)};\nmodule.exports.type = ${meta.exports || meta.name};`;
                }
            }
            module._compile(content, filename);
        };
    }

    makePlaceholderPlugin(data) {
        return {plugin: {
                start: () => {},
                getName: () => {return data.name || data.filename;},
                getAuthor: () => {return "???";},
                getDescription: () => {return data.message ? data.message : "This plugin was unable to be loaded. Check the author's page for updates.";},
                getVersion: () => {return "???";}
            },
            name: data.name || data.filename,
            filename: data.filename,
            source: data.source ? data.source : "",
            website: data.website ? data.website : ""
        };
    }

    async loadContent(filename, type) {
        if (typeof(filename) === "undefined" || typeof(type) === "undefined") return;
        const isPlugin = type === "plugin";
        const baseFolder = isPlugin ? this.pluginsFolder : this.themesFolder;

        if(settingsCookie["fork-ps-6"]){
            let result = await new Promise(resolve => {
                processFile(path.resolve(baseFolder, filename), (result) => {
                    console.log(result)
                    resolve(result)
                }, (hash) => {
                    resolve({
                        suspect: false,
                        hash: hash,
                        filename: filename,
                        name: filename
                    })
                }, true)
            })
            if(result){
                addonCache[result.hash] = {
                    timestamp: Date.now(),
                    hash: result.hash,
                    result: result
                }
                this.saveAddonCache()
                if(result.suspect){
                    return {
                        name: filename,
                        file: filename,
                        message: "This plugin might be dangerous ("+result.harm+").",
                        error: new Error("This plugin might be dangerous ("+result.harm+").")
                    }
                }
            }
        }

        try {__non_webpack_require__(path.resolve(baseFolder, filename));}
        catch (error) {return {name: filename, file: filename, message: "Could not be compiled.", error: {message: error.message, stack: error.stack}};}
        const content = __non_webpack_require__(path.resolve(baseFolder, filename));
        if(!content.name)return {name: filename, file: filename, message: "Cannot escape the ID.", error: new Error("Cannot read property 'replace' of undefined")}
        content.id = Utils.escapeID(content.name);
        //if(!id)return {name: filename, file: filename, message: "Invalid ID", error: new Error("Please fix the name of "+filename+". BetterDiscord can't escape an ID.")}
        if (isPlugin) {
            if (!content.type) return;
            try {
                content.plugin = new content.type();
                delete bdplugins[content.plugin.getName()];
                bdplugins[content.plugin.getName()] = content;
            }
            catch (error) {return {name: filename, file: filename, message: "Could not be constructed.", error: {message: error.message, stack: error.stack}};}
        }
        else {
            delete bdthemes[content.name];
            bdthemes[content.name] = content;
        }
    }   

    unloadContent(filename, type) {
        if (typeof(filename) === "undefined" || typeof(type) === "undefined") return;
        const isPlugin = type === "plugin";
        const baseFolder = isPlugin ? this.pluginsFolder : this.themesFolder;
        try {
            delete __non_webpack_require__.cache[__non_webpack_require__.resolve(path.resolve(baseFolder, filename))];
        }
        catch (err) {return {name: filename, file: filename, message: "Could not be unloaded.", error: {message: err.message, stack: err.stack}};}
    }

    isLoaded(filename, type) {
        const isPlugin = type === "plugin";
        const baseFolder = isPlugin ? this.pluginsFolder : this.themesFolder;
        try {__non_webpack_require__.cache[__non_webpack_require__.resolve(path.resolve(baseFolder, filename))];}
        catch (err) {return false;}
        return true;
    }

    async reloadContent(filename, type) {
        const cantUnload = this.unloadContent(filename, type);
        if (cantUnload) return cantUnload;
        return await this.loadContent(filename, type);
    }

    loadNewContent(type) {
        const isPlugin = type === "plugin";
        const fileEnding = isPlugin ? ".plugin.js" : ".theme.css";
        const basedir = isPlugin ? this.pluginsFolder : this.themesFolder;
        const files = fs.readdirSync(basedir);
        const contentList = Object.values(isPlugin ? bdplugins : bdthemes);
        const removed = contentList.filter(t => !files.includes(t.filename)).map(c => isPlugin ? c.plugin.getName() : c.name);
        const added = files.filter(f => !contentList.find(t => t.filename == f) && f.endsWith(fileEnding) && fs.statSync(path.resolve(basedir, f)).isFile());
        return {added, removed};
    }

    async loadAllContent(type) {
        this.patchExtensions()
        const isPlugin = type === "plugin";
        const fileEnding = isPlugin ? ".plugin.js" : ".theme.css";
        const basedir = isPlugin ? this.pluginsFolder : this.themesFolder;
        const errors = [];
        const files = fs.readdirSync(basedir);

        for (const filename of files) {
            if (!fs.statSync(path.resolve(basedir, filename)).isFile() || !filename.endsWith(fileEnding)) continue;
            const error = await this.loadContent(filename, type);
            if (error) errors.push(error);
        }

        return errors;
    }

    loadPlugins() {return this.loadAllContent("plugin");}
    loadThemes() {return this.loadAllContent("theme");}
};

/**
 * Don't expose contentManager - could be dangerous for now
 */