import * as path from "path";
import * as escodegen from "escodegen";
import * as acorn from "acorn";
import { CodeWithSourceMap, SourceMapConsumer } from "source-map";
import TranslateLoaderContext from "../translate-loader-context";
import createTranslateVisitor from "./translate-visitor";
import * as loaderUtils from "loader-utils";

/**
 * The optional options passed to the plugin
 */
interface LoaderOptions {
  /**
   * Optional acorn options that are passed to the parser
   */
  parserOptions?: acorn.Options;
}

/**
 * Webpack loader that extracts translations from calls to the angular-translate $translate service.
 * Additionally it provides the `i18n.registerTranslation(translationId, defaultText)` and `i18n.registerTranslations({})`
 * functions that can be used to register new translations directly in code.
 *
 * The loader uses acorn to parse the input file and creates the output javascript using escodegen.
 * @param source
 * @param inputSourceMaps
 */
async function jsLoader(source: string, inputSourceMaps: any) {
  const loader: TranslateLoaderContext = this;
  const callback = this.async();

  if (!loader.registerTranslation) {
    return callback(
      new Error(
        "The WebpackAngularTranslate plugin is missing. Add the plugin to your webpack configurations 'plugins' section."
      ),
      source,
      inputSourceMaps
    );
  }

  if (loader.cacheable) {
    loader.cacheable();
  }

  if (isExcludedResource(loader.resourcePath)) {
    return callback(null, source, inputSourceMaps);
  }

  const { code, sourceMaps } = await extractTranslations(
    loader,
    source,
    inputSourceMaps
  );

  callback(null, code, sourceMaps);
}

async function extractTranslations(
  loader: TranslateLoaderContext,
  source: string,
  sourceMaps: any
) {
  const options: LoaderOptions = loaderUtils.getOptions(loader) || {};
  const parserOptions = options.parserOptions || { ecmaVersion: "latest" };

  loader.pruneTranslations(path.relative(loader.context, loader.resourcePath));

  const visitor = createTranslateVisitor(loader, parserOptions);
  const sourceAst = acorn.parse(source, visitor.options);
  const transformedAst = visitor.visit(sourceAst);

  let code = source;

  if (visitor.changedAst) {
    const generateSourceMaps = !!(loader.sourceMap || sourceMaps);
    const result = escodegen.generate(transformedAst, {
      comment: true,
      sourceMap: generateSourceMaps ? loader.resourcePath : undefined,
      sourceMapWithCode: generateSourceMaps,
      sourceContent: generateSourceMaps ? source : undefined,
    });

    if (generateSourceMaps) {
      const codeWithSourceMap = <CodeWithSourceMap>(result as any);
      code = codeWithSourceMap.code;
      if (sourceMaps) {
        // Create a new source maps that is a mapping from original Source -> result from previous loader -> result from this loader
        const originalSourceMap = await new SourceMapConsumer(sourceMaps);
        codeWithSourceMap.map.applySourceMap(
          originalSourceMap,
          loader.resourcePath
        );
      }

      sourceMaps = (<any>codeWithSourceMap.map).toJSON();
    }
  }

  return { code, sourceMaps };
}

function isExcludedResource(resource: string): boolean {
  return /angular-translate[\/\\]dist[\/\\]angular-translate\.js$/.test(
    resource
  );
}

export = jsLoader;
