/**
 * @license
 * Copyright Google Inc. All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */
import { Budget } from '../../browser/schema';

export interface Compilation {
  assets: { [name: string]: { size: () => number } };
  chunks: { name: string, files: string[], isOnlyInitial: () => boolean }[];
  warnings: string[];
  errors: string[];
}

export interface Size {
  size: number;
  label?: string;
}

export function calculateSizes(budget: Budget, compilation: Compilation): Size[] {
  const calculatorMap = {
    all: AllCalculator,
    allScript: AllScriptCalculator,
    any: AnyCalculator,
    anyScript: AnyScriptCalculator,
    bundle: BundleCalculator,
    initial: InitialCalculator,
  };
  const ctor = calculatorMap[budget.type];
  const calculator = new ctor(budget, compilation);

  return calculator.calculate();
}

export abstract class Calculator {
  constructor (protected budget: Budget, protected compilation: Compilation) {}

  abstract calculate(): Size[];
}

/**
 * A named bundle.
 */
class BundleCalculator extends Calculator {
  calculate() {
    const size: number = this.compilation.chunks
      .filter(chunk => chunk.name === this.budget.name)
      .reduce((files, chunk) => [...files, ...chunk.files], [])
      .map((file: string) => this.compilation.assets[file].size())
      .reduce((total: number, size: number) => total + size, 0);

    return [{size, label: this.budget.name}];
  }
}

/**
 * The sum of all initial chunks (marked as initial by webpack).
 */
class InitialCalculator extends Calculator {
  calculate() {
    const initialChunks = this.compilation.chunks.filter(chunk => chunk.isOnlyInitial());
    const size: number = initialChunks
      .reduce((files, chunk) => [...files, ...chunk.files], [])
      .filter((file: string) => !file.endsWith('.map'))
      .map((file: string) => this.compilation.assets[file].size())
      .reduce((total: number, size: number) => total + size, 0);

    return [{size, label: 'initial'}];
  }
}

/**
 * The sum of all the scripts portions.
 */
class AllScriptCalculator extends Calculator {
  calculate() {
    const size: number = Object.keys(this.compilation.assets)
      .filter(key => key.endsWith('.js'))
      .map(key => this.compilation.assets[key])
      .map(asset => asset.size())
      .reduce((total: number, size: number) => total + size, 0);

    return [{size, label: 'total scripts'}];
  }
}

/**
 * All scripts and assets added together.
 */
class AllCalculator extends Calculator {
  calculate() {
    const size: number = Object.keys(this.compilation.assets)
      .filter(key => !key.endsWith('.map'))
      .map(key => this.compilation.assets[key].size())
      .reduce((total: number, size: number) => total + size, 0);

    return [{size, label: 'total'}];
  }
}

/**
 * Any script, individually.
 */
class AnyScriptCalculator extends Calculator {
  calculate() {
    return Object.keys(this.compilation.assets)
      .filter(key => key.endsWith('.js'))
      .map(key => {
        const asset = this.compilation.assets[key];

        return {
          size: asset.size(),
          label: key,
        };
      });
  }
}

/**
 * Any script or asset (images, css, etc).
 */
class AnyCalculator extends Calculator {
  calculate() {
    return Object.keys(this.compilation.assets)
      .filter(key => !key.endsWith('.map'))
      .map(key => {
        const asset = this.compilation.assets[key];

        return {
          size: asset.size(),
          label: key,
        };
      });
  }
}

/**
 * Calculate the bytes given a string value.
 */
export function calculateBytes(
  input: string,
  baseline?: string,
  factor: 1 | -1 = 1,
): number {
  const matches = input.match(/^\s*(\d+(?:\.\d+)?)\s*(%|(?:[mM]|[kK]|[gG])?[bB])?\s*$/);
  if (!matches) {
    return NaN;
  }

  const baselineBytes = baseline && calculateBytes(baseline) || 0;

  let value = Number(matches[1]);
  switch (matches[2] && matches[2].toLowerCase()) {
    case '%':
      value = baselineBytes * value / 100 * factor;
      break;
    case 'kb':
      value *= 1024;
      break;
    case 'mb':
      value *= 1024 * 1024;
      break;
    case 'gb':
      value *= 1024 * 1024 * 1024;
      break;
  }

  return value + baselineBytes;
}
