import { FlatFileGenerator } from "./FlatFileGenerator";
import { each } from "realm-utils";
import { StatementModification } from "./modifications/StatementModifaction";
import { EnvironmentConditionModification } from "./modifications/EnvironmentConditionModification";
import { BundleWriter } from "./BundleWriter";
import { IPerformable } from "./modifications/IPerformable";
import { InteropModifications } from "./modifications/InteropModifications";
import { UseStrictModification } from "./modifications/UseStrictModification";
import { ProducerAbstraction } from "../core/ProducerAbstraction";
import { BundleProducer } from "../../core/BundleProducer";
import { BundleAbstraction } from "../core/BundleAbstraction";
import { PackageAbstraction } from "../core/PackageAbstraction";
import { FileAbstraction } from "../core/FileAbstraction";
import { ResponsiveAPI } from "./ResponsiveAPI";
import { Log } from "../../Log";
import { TypeOfModifications } from "./modifications/TypeOfModifications";
import { TreeShake } from "./TreeShake";
import { QuantumOptions } from "./QuantumOptions";

import { ProcessEnvModification } from "./modifications/ProcessEnvModification";
import { string2RegExp } from "../../Utils";
import { ComputedStatementRule } from "./ComputerStatementRule";
import { RequireStatement } from "../core/nodes/RequireStatement";
import { WorkFlowContext } from "../../core/WorkflowContext";

import { Bundle } from "../../core/Bundle";
import { DynamicImportStatementsModifications } from "./modifications/DynamicImportStatements";
import { Hoisting } from "./Hoisting";
import { QuantumBit } from "./QuantumBit";


export interface QuantumStatementMapping {
    statement: RequireStatement,
    core: QuantumCore;
}
export class QuantumCore {
    public producerAbstraction: ProducerAbstraction;
    public api: ResponsiveAPI;
    public index = 0;
    public log: Log;
    public opts: QuantumOptions;
    public writer = new BundleWriter(this);
    public context: WorkFlowContext;
    public requiredMappings = new Set<RegExp>();
    public quantumBits = new Map<string, QuantumBit>();
    public customStatementSolutions = new Set<RegExp>();
    public computedStatementRules = new Map<string, ComputedStatementRule>();
    public splitFiles = new Set<FileAbstraction>();

    constructor(public producer: BundleProducer, opts: QuantumOptions) {
        this.opts = opts;


        this.api = new ResponsiveAPI(this);
        this.log = producer.fuse.context.log;
        this.log.echoBreak();
        this.log.groupHeader("Launching quantum core");
        if (this.opts.apiCallback) {
            this.opts.apiCallback(this);
        }
        this.context = this.producer.fuse.context;
    }

    public solveComputed(path: string, rules?: { mapping: string, fn: { (statement: RequireStatement, core: QuantumCore): void } }) {
        this.customStatementSolutions.add(string2RegExp(path));
        if (rules && rules.mapping) {
            this.requiredMappings.add(string2RegExp(rules.mapping));
        }
        this.computedStatementRules.set(path, new ComputedStatementRule(path, rules));
    }

    public getCustomSolution(file: FileAbstraction): ComputedStatementRule {
        let fullPath = file.getFuseBoxFullPath();
        let computedRule = this.computedStatementRules.get(fullPath);
        if (computedRule) {
            return computedRule;
        }
    }



    public async consume() {
        this.log.echoInfo("Generating abstraction, this may take a while");
        const abstraction = await this.producer.generateAbstraction({
            quantumCore: this,
            customComputedStatementPaths: this.customStatementSolutions
        })
        abstraction.quantumCore = this;
        this.producerAbstraction = abstraction;
        this.log.echoInfo("Abstraction generated");

        await each(abstraction.bundleAbstractions, (bundleAbstraction: BundleAbstraction​​) => {
            return this.prepareFiles(bundleAbstraction);
        });

        await each(abstraction.bundleAbstractions, (bundleAbstraction: BundleAbstraction​​) => {
            return this.processBundle(bundleAbstraction);
        });

        await this.prepareQuantumBits();
        await this.treeShake();
        await this.render();
        this.compriseAPI();
        await this.writer.process();

        this.printStat();

    }

    private ensureBitBundle(bit: QuantumBit) {
        let bundle: Bundle;
        if (!this.producer.bundles.get(bit.name)) {
            this.log.echoInfo(`Create split bundle ${bit.name} with entry point ${bit.entry.getFuseBoxFullPath()}`);
            const fusebox = this.context.fuse.copy();
            bundle = new Bundle(bit.getBundleName(), fusebox, this.producer);
            bundle.quantumBit = bit;

            //bundle.context = this.producer.fuse.context;
            this.producer.bundles.set(bit.name, bundle);
            // don't allow WebIndexPlugin to include it to script tags
            bundle.webIndexed = false;
            // set the reference
            //bundle.quantumItem = quantumItem;
            // bundle abtraction needs to be created to have an isolated scope for hoisting
            const bnd = new BundleAbstraction(bit.name);
            bnd.splitAbstraction = true;

            let pkg = new PackageAbstraction(bit.entry.packageAbstraction.name, bnd);
            this.producerAbstraction.registerBundleAbstraction(bnd);
            bundle.bundleAbstraction = bnd;
            bundle.packageAbstraction = pkg;
        } else {
            bundle = this.producer.bundles.get(bit.name);
        }
        return bundle;
    }

    private async prepareQuantumBits() {

        this.context.quantumBits = this.quantumBits;
        this.quantumBits.forEach(bit => { bit.resolve() })

        await each(this.quantumBits, async (bit: QuantumBit, key: string) => {
            bit.populate();
            let bundle = this.ensureBitBundle(bit);
            bit.files.forEach(file => {
                this.log.echoInfo(`QuantumBit: Adding ${file.getFuseBoxFullPath()} to ${bit.name}`);
                // removing the file from the current package
                file.packageAbstraction.fileAbstractions.delete(file.fuseBoxPath);

                bundle.packageAbstraction.registerFileAbstraction(file);
                // add it to an additional list
                // we need to modify it later on, cuz of the loop we are in
                file.packageAbstraction = bundle.packageAbstraction;
            });

            bit.modules.forEach(pkg => {
                this.log.echoInfo(`QuantumBit: Moving module ${pkg.name} from ${pkg.bundleAbstraction.name} to ${bit.name}`);
                const bundleAbstraction = bundle.bundleAbstraction;
                pkg.assignBundle(bundleAbstraction);
            });
        });
    }
    private printStat() {
        // let apiStyle = "Optimised numbers (Best performance)";
        // if (this.api.hashesUsed()) {
        //     apiStyle = "Hashes (Might cause issues)";
        // }
        // this.log.printOptions("Stats", {
        //     warnings: this.producerAbstraction.warnings.size,
        //     apiStyle: apiStyle,
        //     target: this.opts.optsTarget,
        //     uglify: this.opts.shouldUglify(),
        //     removeExportsInterop: this.opts.shouldRemoveExportsInterop(),
        //     removeUseStrict: this.opts.shouldRemoveUseStrict(),
        //     replaceProcessEnv: this.opts.shouldReplaceProcessEnv(),
        //     ensureES5: this.opts.shouldEnsureES5(),
        //     treeshake: this.opts.shouldTreeShake(),
        // });
        if (this.opts.shouldShowWarnings()) {
            this.producerAbstraction.warnings.forEach(warning => {
                this.log.echoBreak();
                this.log.echoYellow("Warnings:");
                this.log.echoYellow("Your quantum bundle might not work");
                this.log.echoYellow(`  - ${warning.msg}`);
                this.log.echoGray("");
                this.log.echoGray("  * Set { warnings : false } if you want to hide these messages");
                this.log.echoGray("  * Read up on the subject http://fuse-box.org/page/quantum#computed-statement-resolution");
            });
        }
    }

    public compriseAPI() {
        if (this.producerAbstraction.useComputedRequireStatements) {
            this.api.addComputedRequireStatetements();
        }
    }

    public handleMappings(fuseBoxFullPath: string, id: any) {
        this.requiredMappings.forEach(regexp => {
            if (regexp.test(fuseBoxFullPath)) {
                this.api.addMapping(fuseBoxFullPath, id);
            }
        });
    }


    public prepareFiles(bundleAbstraction: BundleAbstraction) {
        // set ids first
        let entryId;
        if (this.producer.entryPackageFile && this.producer.entryPackageName) {
            entryId = `${this.producer.entryPackageName}/${this.producer.entryPackageFile}`;
        }

        // define globals
        const globals = this.producer.fuse.context.globals;
        let globalsName;
        if (globals) {
            for (let i in globals) { globalsName = globals[i]; }
        }
        bundleAbstraction.packageAbstractions.forEach(packageAbstraction => {
            packageAbstraction.fileAbstractions.forEach((fileAbstraction, key: string) => {
                let fileId = fileAbstraction.getFuseBoxFullPath();
                const id = this.index;
                this.handleMappings(fileId, id);
                this.index++;
                if (fileId === entryId) {
                    fileAbstraction.setEnryPoint(globalsName);
                }
                fileAbstraction.setID(id);
            });
        });
    }

    public processBundle(bundleAbstraction: BundleAbstraction) {
        this.log.echoInfo(`Process bundle ${bundleAbstraction.name}`);
        return each(bundleAbstraction.packageAbstractions, (packageAbstraction: PackageAbstraction) => {
            const fileSize = packageAbstraction.fileAbstractions.size;
            this.log.echoInfo(`Process package ${packageAbstraction.name} `);
            this.log.echoInfo(`  Files: ${fileSize} `);
            return each(packageAbstraction.fileAbstractions, (fileAbstraction: FileAbstraction) => {
                return this.modify(fileAbstraction);
            });
        }).then(() => this.hoist());
    }

    public treeShake() {
        if (this.opts.shouldTreeShake()) {
            const shaker = new TreeShake(this);
            return shaker.shake();
        }
    }
    public render() {
        return each(this.producerAbstraction.bundleAbstractions, (bundleAbstraction: BundleAbstraction​​) => {

            const generator = new FlatFileGenerator(this, bundleAbstraction);
            generator.init();
            return each(bundleAbstraction.packageAbstractions, (packageAbstraction: PackageAbstraction) => {
                return each(packageAbstraction.fileAbstractions, (fileAbstraction: FileAbstraction) => {
                    return generator.addFile(fileAbstraction, this.opts.shouldEnsureES5());
                });

            }).then(() => {
                this.log.echoInfo(`Render bundle ${bundleAbstraction.name}`);
                const bundleCode = generator.render();
                this.producer.bundles.get(bundleAbstraction.name).generatedCode = new Buffer(bundleCode);
            });
        });
    }

    public hoist() {
        if (!this.api.hashesUsed() && this.opts.shouldDoHoisting()) {
            let hoisting = new Hoisting(this);
            return hoisting.start();
        }
    }

    public modify(file: FileAbstraction) {
        const modifications = [
            // modify require statements: require -> $fsx.r
            StatementModification,

            // modify dynamic statements
            DynamicImportStatementsModifications,

            // modify FuseBox.isServer and FuseBox.isBrowser
            EnvironmentConditionModification,
            // remove exports.__esModule = true
            InteropModifications,
            // removes "use strict" if required
            UseStrictModification,
            // replace typeof module, typeof exports, typeof window
            TypeOfModifications,
            // process.env removal
            ProcessEnvModification,
        ];
        return each(modifications, (modification: IPerformable) => modification.perform(this, file));
    }
}
