const { rollup } = require('rollup');
const { spawn } = require('child_process');
const { Observable } = require('rxjs');
const { copy, readFileSync, writeFile } = require('fs-extra');
const { prettySize } = require('pretty-size');
const gzipSize = require('gzip-size');
const resolve = require('rollup-plugin-node-resolve');
const pkg = require(`${process.cwd()}/package.json`);

// Rollup globals
const GLOBALS = {
  '@angular/core': 'ng.core',
  '@angular/common': 'ng.common',
  'ngd3': 'ngd3',
};

// Map of dependency versions across all packages
const VERSIONS = {
  ANGULAR_VERSION: pkg.dependencies['@angular/core'],
  NGD3_VERSION: pkg.version
};

// Constants for running typescript commands
const NGC = 'node_modules/.bin/ngc';
const TSC_ARGS = (name, config = 'build') => [`-p`, `${process.cwd()}/src/${name}/tsconfig-${config}.json`];

/**
 * Create an Observable of a spawned child process.
 * @param {string} command
 * @param {string[]} args
 */
function spawnObservable(command, args) {
  return Observable.create(observer => {
    const cmd = spawn(command, args);
    observer.next(''); // hack to kick things off, not every command will have a stdout
    cmd.stdout.on('data', (data) => { observer.next(data.toString('utf8')); });
    cmd.stderr.on('data', (data) => { observer.error(data.toString('utf8')); });
    cmd.on('close', (data) => { observer.complete(); });
  });
}

function generateBundle(entry, { dest, globals, moduleName }) {
  return rollup({ entry }).then(bundle => {
    return bundle.write({
      format: 'umd',
      external: Object.keys(globals),
      plugins: [resolve()],
      dest,
      globals,
      moduleName,
    });
  });
}

/**
 * Create a UMD bundle given a module name.
 * @param {string} name
 * @param {Object} globals
 */
function createUmd(name, globals) {
  // core module is ngd3 the rest are ngd3.feature
  const MODULE_NAMES = {
    ngd3: 'ngd3',
  };
  const ENTRIES = {
    ngd3: `${process.cwd()}/dist/index.js`,
  };
  const moduleName = MODULE_NAMES[name];
  const entry = ENTRIES[name];

  return generateBundle(entry, {
    dest: `${process.cwd()}/dist/bundles/${name}.umd.js`,
    globals,
    moduleName
  });
}

/**
 * Get the file path of the src package.json for a module
 * @param {string} moduleName
 */
function getSrcPackageFile(moduleName) {
  const PATHS = {
    ngd3: `${process.cwd()}/src/ngd3/package.json`,
  };
  return PATHS[moduleName];
}

/**
 * Get the file path of the dist package.json for a module
 * @param {string} moduleName
 */
function getDestPackageFile(moduleName) {
  const PATHS = {
    ngd3: `${process.cwd()}/dist/package.json`,
  };
  return PATHS[moduleName];
}

/**
 * Create an observable of package.json dependency version replacements.
 * This keeps the dependency versions across each package in sync.
 * @param {string} name
 * @param {Object} versions
 */
function replaceVersionsObservable(name, versions) {
  return Observable.create((observer) => {
    const package = getSrcPackageFile(name);
    let pkg = readFileSync(package, 'utf8');
    const regexs = Object.keys(versions).map(key =>
      ({ expr: new RegExp(key, 'g'), key, val: versions[key] }));
    regexs.forEach(reg => {
      pkg = pkg.replace(reg.expr, reg.val);
    });
    const outPath = getDestPackageFile(name);
    writeFile(outPath, pkg, err => {
      if (err) {
        observer.error(err);
      } else {
        observer.next(pkg);
        observer.complete();
      }
    });
  });
}

function copyPackage(moduleName) {
  return copy(getSrcPackageFile(moduleName), getDestPackageFile(moduleName));
}

function copyNpmIgnore() {
  return copy(`${process.cwd()}/.npmignore`, `${process.cwd()}/dist/.npmignore`);
}

function copyReadme() {
  return copy(`${process.cwd()}/README.md`, `${process.cwd()}/dist/README.md`);
}

function measure(module, gzip = true) {
  const path = `${process.cwd()}/dist/bundles/${module}.umd.js`;
  const file = readFileSync(path);
  const bytes = gzipSize.sync(file);

  return prettySize(bytes, gzip);
}

/**
 * Returns each version of each NgD3 module.
 * This is used to help ensure each package has the same version.
 */
function getVersions() {
  const paths = [
    getDestPackageFile('ngd3'),
  ];

  return paths
    .map(path => require(path))
    .map(pkgs => pkgs.version);
}

function verifyVersions() {
  const versions = getVersions();
  console.log(versions);

  versions.map(version => {
    if(version !== pkg.version) {
      throw new Error('Versions mismatch');
      process.exit(1);
    }
  });
}

function buildModule(name, globals) {
  const es2015$ = spawnObservable(NGC, TSC_ARGS(name));
  const esm$ = spawnObservable(NGC, TSC_ARGS(name, 'esm'));

  return Observable
    .forkJoin(es2015$, esm$)
    .switchMap(() => Observable.from(createUmd(name, globals)))
    .switchMap(() => replaceVersionsObservable(name, VERSIONS));
}

/**
 * Create an observable of module build status. This method builds
 * @param {Object} globals
 */
function buildModules(globals) {
  const ngd3$ = buildModule('ngd3', globals);

  return Observable.forkJoin(ngd3$);
}

function buildLibrary(globals) {
  const modules$ = buildModules(globals);

  return Observable
    .forkJoin(modules$)
    .switchMap(() => Observable.from(copyNpmIgnore()))
    .switchMap(() => Observable.from(copyReadme()))
    .do(() => {
      console.log(`
      ngd3.umd.js - ${measure('ngd3')}
      `);
      verifyVersions();
    });
}

buildLibrary(GLOBALS).subscribe(
  data => { console.log('data', data) },
  err => { console.log('err', err) },
  () => { console.log('complete') }
);
