"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const glob = require("glob");
const json5 = require("json5");
const parse5 = require("parse5");
const path = require("path");
const errors_1 = require("./server/errors");
const metadata_1 = require("./server/metadata/metadata");
const npm_1 = require("./server/npm");
const util_1 = require("./server/util");
/**
 * Environment variable set when the webpack process is spawned from `miix serve`.
 */
exports.devEnvironmentVar = '__MIIX_DEV_SERVER';
/**
 * The path to the "static" folder relative to this module.
 */
const staticPath = path.resolve(__dirname, '../../static');
/**
 * Path prefix for miix dev server assets.
 */
const devAssetPrefix = '__miix_static';
/**
 * The HTMLInjector is a helper that is able to inject HTML fragments into
 * an HTML document.
 */
class HTMLInjector {
    constructor(filepath) {
        this.filepath = filepath;
    }
    render(compiler) {
        return __awaiter(this, void 0, void 0, function* () {
            const parsed = yield this.getDocument();
            const head = this.findNode(parsed, ['html', 'head']);
            if (!head) {
                throw new errors_1.MixerPluginError('Your homepage is missing a <head> section!');
            }
            this.prepend(head, ...(yield this.injectHead(compiler)));
            return parse5.serialize(parsed);
        });
    }
    /**
     * injectHead returns a list of HTML fragments to insert into the page <head>.
     */
    injectHead(_compiler) {
        return __awaiter(this, void 0, void 0, function* () {
            return [];
        });
    }
    getDocument() {
        return __awaiter(this, void 0, void 0, function* () {
            const og = yield util_1.readFile(this.filepath);
            try {
                return parse5.parse(og);
            }
            catch (e) {
                throw util_1.wrapErr(e, 'Could not parse HTML from your homeage');
            }
        });
    }
    prepend(parent, ...elements) {
        if (!parent.childNodes) {
            throw new Error('Attempted to append to non-parent node');
        }
        const casted = parent;
        elements.forEach(element => casted.childNodes.unshift(this.stringToNode(element)));
    }
    stringToNode(src) {
        const fragment = parse5.parseFragment(src);
        return fragment.childNodes[0];
    }
    findNode(parent, nodePath) {
        for (let i = 0; i < nodePath.length; i++) {
            if (!parent.childNodes) {
                return undefined;
            }
            const child = parent.childNodes.find(n => n.tagName === nodePath[i]);
            if (!child) {
                return undefined;
            }
            parent = child;
        }
        return parent;
    }
}
/**
 * HomepageRenderer is responsible for modifying and injecting Mixer stdlib
 * scripts into the developer's index.html.
 */
class HomepageRenderer extends HTMLInjector {
    constructor(filepath, packaged, locales) {
        super(filepath);
        this.packaged = packaged;
        this.locales = locales;
    }
    injectHead() {
        return __awaiter(this, void 0, void 0, function* () {
            const output = [];
            if (process.env[exports.devEnvironmentVar]) {
                output.push(`<script>window.self!=window.top||(window.location='/editor.html')</script>`);
            }
            output.push(`<script>window.mixerPackageConfig=${JSON.stringify(this.packaged)};` +
                `window.mixerLocales=${JSON.stringify(this.locales)}</script>`);
            return output;
        });
    }
}
/**
 * EdtiroRenderer is responsible for injecting the address of the dev server
 * into the editor's HTML.
 */
class EditorRenderer extends HTMLInjector {
    injectHead() {
        return __awaiter(this, void 0, void 0, function* () {
            return [`<script>window.miixDev=${process.env[exports.devEnvironmentVar]}</script>`];
        });
    }
}
function contentsToAsset(contents) {
    return {
        source: () => contents,
        size: () => contents.length,
    };
}
function fileToAsset(...segments) {
    return __awaiter(this, void 0, void 0, function* () {
        return util_1.readFile(path.resolve(...segments)).then(contentsToAsset);
    });
}
/**
 * The LocalePackager takes a glob on the filesystem and returns a map of
 * locale names to their parsed data.
 */
class LocalePackager {
    constructor(compilation) {
        this.compilation = compilation;
    }
    /**
     * compile loads locales matching the glob pattern to a map of locales
     * (from the file basenames) to their contents.
     */
    compile(pattern) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!pattern) {
                return {};
            }
            const files = glob.sync(pattern);
            const output = {};
            yield Promise.all(files.map((file) => __awaiter(this, void 0, void 0, function* () {
                const contents = yield util_1.readFile(file);
                let parsed;
                try {
                    parsed = json5.parse(contents);
                }
                catch (err) {
                    throw new errors_1.MixerPluginError(`Could not parse ${file}: ${err.message || err}`);
                }
                this.compilation.fileDependencies.push(file);
                output[path.basename(file, path.extname(file))] = parsed;
            })));
            return output;
        });
    }
    /**
     * Adds the compile()'d locale data to the webpack compilation,
     * in the locales folder.
     */
    addToCompilation(data) {
        Object.keys(data).forEach(key => {
            this.compilation.assets[`locales/${key}.json`] = contentsToAsset(JSON.stringify(data[key]));
        });
    }
}
/**
 * MixerPlugin is the webpack plugin to handle packaging Interactive output
 * to ship and develop with. Primarily, it modifies the user-provided
 * HTML page to insert the Mixer standard library and additional controls
 * in development mode.
 */
class MixerPlugin {
    constructor(options) {
        this.options = options;
    }
    apply(compiler) {
        const projectPath = npm_1.getProjectPath(compiler.options.context);
        if (!projectPath) {
            throw new Error('Could not find your project path, are you missing a package.json?');
        }
        compiler.plugin('emit', (compilation, callback) => __awaiter(this, void 0, void 0, function* () {
            try {
                const packageJson = npm_1.mustLoadPackageJson(projectPath);
                this.package = yield metadata_1.createPackage(packageJson, projectPath);
                const todo = [this.addProductionFiles(compiler, compilation)];
                if (process.env[exports.devEnvironmentVar]) {
                    todo.push(this.addEditorFiles(compiler, compilation));
                }
                yield Promise.all(todo);
            }
            catch (e) {
                callback(e);
                return;
            }
            callback();
        }));
    }
    addEditorFiles(compiler, compilation) {
        return __awaiter(this, void 0, void 0, function* () {
            yield Promise.all([
                new EditorRenderer(path.resolve(staticPath, 'editor/index.html'))
                    .render(compiler)
                    .then(result => {
                    compilation.assets['editor.html'] = contentsToAsset(result);
                }),
                this.addFiles(compilation, {
                    'editor.main.js': path.resolve(__dirname, 'ui/editor/main.bundle.js'),
                    'editor.polyfills.js': path.resolve(__dirname, 'ui/editor/polyfills.bundle.js'),
                }),
                this.addStaticAssets(compilation),
            ]);
        });
    }
    addProductionFiles(compiler, compilation) {
        return __awaiter(this, void 0, void 0, function* () {
            const packager = new LocalePackager(compilation);
            const locales = yield packager.compile(this.options.locales);
            packager.addToCompilation(locales);
            yield Promise.all([
                new HomepageRenderer(this.options.homepage, this.package, Object.keys(locales))
                    .render(compiler)
                    .then(result => {
                    compilation.assets['index.html'] = contentsToAsset(result);
                }),
            ]);
        });
    }
    addStaticAssets(compilation) {
        return __awaiter(this, void 0, void 0, function* () {
            const mapping = {};
            const editorAssets = path.resolve(staticPath, 'editor/assets');
            glob.sync(path.resolve(staticPath, 'editor/assets/**/*')).forEach(file => {
                compilation.fileDependencies.push(file);
                const relative = path.relative(editorAssets, file);
                mapping[`${devAssetPrefix}/${relative}`] = file;
            });
            return this.addFiles(compilation, mapping);
        });
    }
    addFiles(compilation, files) {
        return __awaiter(this, void 0, void 0, function* () {
            return Promise.all(Object.keys(files).map((assetName) => __awaiter(this, void 0, void 0, function* () {
                return fileToAsset(files[assetName]).then(asset => {
                    compilation.assets[assetName] = asset;
                    compilation.fileDependencies.push(files[assetName]);
                });
            }))).then(() => undefined);
        });
    }
}
exports.MixerPlugin = MixerPlugin;
