import {BrowserBuilderOutput, executeBrowserBuilder, ExecutionTransformer} from '@angular-devkit/build-angular';
import {JsonObject} from '@angular-devkit/core';
import {createBuilder, BuilderContext} from '@angular-devkit/architect';
import * as fs from 'fs';
import * as webpack from 'webpack';
import {tap} from 'rxjs/operators';
import {Observable} from 'rxjs';
import {EXTERNALS} from './externals';


interface Options extends JsonObject {
    /**
     * A string of the form `path/to/file#exportName` that acts as a path to include to bundle
     */
    modulePath: string;

    /**
     * A name of compiled bundle
     */
    pluginName: string;

    /**
     * A comma-delimited list of shared lib names used by current plugin
     */
    sharedLibs: string;

    /**
     * Defines whether to add hash to the output file or not
     */
    addHash: boolean;
}

let entryPointPath;

function buildPlugin(options: Options,
                     context: BuilderContext,
                     transforms: {
                         webpackConfiguration?: ExecutionTransformer<webpack.Configuration>,
                     } = {}): Observable<BrowserBuilderOutput> {
    validateOptions(options);

    const originalWebpackConfigurationFn = transforms.webpackConfiguration;
    transforms.webpackConfiguration = (config: webpack.Configuration) => {
        patchWebpackConfig(config, options);

        return originalWebpackConfigurationFn ? originalWebpackConfigurationFn(config) : config;
    };

    const result = executeBrowserBuilder(options as any, context, transforms);

    return result.pipe(tap(() => {
        patchEntryPoint('');
    }));
}

function patchEntryPoint(contents: string) {
    fs.writeFileSync(entryPointPath, contents);
}

function validateOptions(options: Options) {
    const { modulePath } = options;

    if (!modulePath) {
        throw Error('Please define modulePath!');
    }
}

function patchWebpackConfig(config: webpack.Configuration, options: Options) {
    const { sharedLibs } = options;

    // Make sure we are producing a single bundle
    // @ts-ignore
    delete config.entry.polyfills;
    delete config.entry['polyfills-es5'];
    delete config.optimization.runtimeChunk;
    delete config.optimization.splitChunks;
    // @ts-ignore
    delete config.entry.styles;

    config.externals = EXTERNALS;

    if (sharedLibs) {
        config.externals = [config.externals];
        const sharedLibsArr = sharedLibs.split(',');
        sharedLibsArr.forEach(sharedLibName => {
            config.externals[0][sharedLibName] = sharedLibName; // define external for code
            // const factoryRegexp = new RegExp(`${sharedLibName}.ngfactory$`);
            // config.externals.push((context, request, callback) => {
            //     if (factoryRegexp.test(request)) {
            //         return callback(null, sharedLibName); // define external for factory
            //     }
            //     callback();
            // });
        });
    }

    const ngCompilerPluginInstance = config.plugins.find(
        x => x.constructor && x.constructor.name === 'AngularCompilerPlugin'
    );
    if (ngCompilerPluginInstance) {
        // @ts-ignore
        ngCompilerPluginInstance._entryModule = options.modulePath;
    }

    // preserve path to entry point
    // so that we can clear use it within `run` method to clear that file
    // @ts-ignore
    entryPointPath = config.entry.main[0];

    const [modulePath, moduleName] = options.modulePath.split('#');

    // const factoryPath = `${
    //   modulePath.includes('.') ? modulePath : `${modulePath}/${modulePath}`
    //   }.ngfactory`;
    const entryPointContents = `
    export * from '${modulePath}';
    import { ${moduleName} } from '${modulePath}';
    export default ${moduleName};
  `;
    patchEntryPoint(entryPointContents);
    
    const pluginName = options.pluginName 
        || modulePath.split('/').pop().split('.module')[0]
        || moduleName;

    const filename = options.addHash ?
        `${pluginName}.${Date.now()}.js` : // add output file hash to prevent bugs due to browser caching
        `${pluginName}.js`;

    config.output.filename = filename;
    config.output.library = pluginName;
    config.output.libraryTarget = 'umd';
    // workaround to support bundle on nodejs
    config.output.globalObject = `(typeof self !== 'undefined' ? self : this)`;

    // TODO #22944, #22945 remove global.ng.common.locales from plugin.js since it is already presented in base app
}

export default createBuilder<Options>(buildPlugin);
