import {join, resolve as resolvePath} from 'path';
import {spawn} from 'child_process';
import {red} from 'chalk';
import {PackageBundler} from './build-bundles';
import {buildConfig} from './build-config';
import {getSecondaryEntryPointsForPackage} from './secondary-entry-points';
import {compileEntryPoint, renamePrivateReExportsToBeUnique} from './compile-entry-point';

const {packagesDir, outputDir} = buildConfig;

/** Name of the tsconfig file that is responsible for building an ES2015 package. */
const buildTsconfigName = 'tsconfig-build.json';

/** Name of the tsconfig file that is responsible for building the tests. */
const testsTsconfigName = 'tsconfig-tests.json';

export class BuildPackage {
  /** Path to the package sources. */
  sourceDir: string;

  /** Path to the ES2015 package output. */
  outputDir: string;

  /** Path to the ES5 package output. */
  esm5OutputDir: string;

  /** Whether this package will re-export its secondary-entry points at the root module. */
  exportsSecondaryEntryPointsAtRoot = false;

  /** Whether the secondary entry-point styles should be copied to the release output. */
  copySecondaryEntryPointStylesToRoot = false;

  /** Path to the entry file of the package in the output directory. */
  readonly entryFilePath: string;

  /** Path to the tsconfig file, which will be used to build the package. */
  private readonly tsconfigBuild: string;

  /** Path to the tsconfig file, which will be used to build the tests. */
  private readonly tsconfigTests: string;

  /** Package bundler instance. */
  private bundler = new PackageBundler(this);

  /** Secondary entry-points partitioned by their build depth. */
  private get secondaryEntryPointsByDepth(): string[][] {
    this.cacheSecondaryEntryPoints();
    return this._secondaryEntryPointsByDepth;
  }
  private _secondaryEntryPointsByDepth: string[][];

  /** Secondary entry points for the package. */
  get secondaryEntryPoints(): string[] {
    this.cacheSecondaryEntryPoints();
    return this._secondaryEntryPoints;
  }
  private _secondaryEntryPoints: string[];

  constructor(readonly name: string, readonly dependencies: BuildPackage[] = []) {
    this.sourceDir = join(packagesDir, name);
    this.outputDir = join(outputDir, 'packages', name);
    this.esm5OutputDir = join(outputDir, 'packages', name, 'esm5');

    this.tsconfigBuild = join(this.sourceDir, buildTsconfigName);
    this.tsconfigTests = join(this.sourceDir, testsTsconfigName);

    this.entryFilePath = join(this.outputDir, 'index.js');
  }

  /** Compiles the package sources with all secondary entry points. */
  async compile() {
    // Compile all secondary entry-points with the same depth in parallel, and each separate depth
    // group in sequence. This will look something like:
    // Depth 0: coercion, platform, keycodes, bidi
    // Depth 1: a11y, scrolling
    // Depth 2: overlay
    for (const entryPointGroup of this.secondaryEntryPointsByDepth) {
      await Promise.all(entryPointGroup.map(p => this._compileBothTargets(p)));
    }

    // Compile the primary entry-point.
    await this._compileBothTargets();
  }

  /** Compiles the TypeScript test source files for the package. */
  async compileTests() {
    await this._compileTestEntryPoint(testsTsconfigName);
  }

  /** Creates all bundles for the package and all associated entry points. */
  async createBundles() {
    await this.bundler.createBundles();
  }

  /** Compiles TS into both ES2015 and ES5, then updates exports. */
  private async _compileBothTargets(p = '') {
    return compileEntryPoint(this, buildTsconfigName, p)
      .then(() => compileEntryPoint(this, buildTsconfigName, p, this.esm5OutputDir))
      .then(() => renamePrivateReExportsToBeUnique(this, p));
  }

  /** Compiles the TypeScript sources of a primary or secondary entry point. */
  private _compileTestEntryPoint(tsconfigName: string, secondaryEntryPoint = ''): Promise<any> {
    const entryPointPath = join(this.sourceDir, secondaryEntryPoint);
    const entryPointTsconfigPath = join(entryPointPath, tsconfigName);

    return new Promise((resolve, reject) => {
      const ngcPath = resolvePath('./node_modules/.bin/ngc');
      const childProcess = spawn(ngcPath, ['-p', entryPointTsconfigPath], {shell: true});

      // Pipe stdout and stderr from the child process.
      childProcess.stdout.on('data', (data: any) => console.log(`${data}`));
      childProcess.stderr.on('data', (data: any) => console.error(red(`${data}`)));

      childProcess.on('exit', (exitCode: number) => exitCode === 0 ? resolve() : reject());
    })
    .catch(() => {
      const error = red(`Failed to compile ${secondaryEntryPoint} using ${entryPointTsconfigPath}`);
      console.error(error);
    });
  }

  /** Stores the secondary entry-points for this package if they haven't been computed already. */
  private cacheSecondaryEntryPoints() {
    if (!this._secondaryEntryPoints) {
      this._secondaryEntryPointsByDepth = getSecondaryEntryPointsForPackage(this);
      this._secondaryEntryPoints =
        this._secondaryEntryPointsByDepth.reduce((list, p) => list.concat(p), []);
    }
  }
}
