/**
 * @license
 * Copyright Google LLC 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 {AbsoluteFsPath, ReadonlyFileSystem} from '@angular/compiler-cli/private/localize';
import {DiagnosticHandlingStrategy, Diagnostics} from '../../diagnostics';
import {TranslationBundle} from '../translator';

import {ParseAnalysis, TranslationParser} from './translation_parsers/translation_parser';

/**
 * Use this class to load a collection of translation files from disk.
 */
export class TranslationLoader {
  constructor(
    private fs: ReadonlyFileSystem,
    private translationParsers: TranslationParser<any>[],
    private duplicateTranslation: DiagnosticHandlingStrategy,
    /** @deprecated */ private diagnostics?: Diagnostics,
  ) {}

  /**
   * Load and parse the translation files into a collection of `TranslationBundles`.
   *
   * @param translationFilePaths An array, per locale, of absolute paths to translation files.
   *
   * For each locale to be translated, there is an element in `translationFilePaths`. Each element
   * is an array of absolute paths to translation files for that locale.
   * If the array contains more than one translation file, then the translations are merged.
   * If allowed by the `duplicateTranslation` property, when more than one translation has the same
   * message id, the message from the earlier translation file in the array is used.
   * For example, if the files are `[app.xlf, lib-1.xlf, lib-2.xlif]` then a message that appears in
   * `app.xlf` will override the same message in `lib-1.xlf` or `lib-2.xlf`.
   *
   * @param translationFileLocales An array of locales for each of the translation files.
   *
   * If there is a locale provided in `translationFileLocales` then this is used rather than a
   * locale extracted from the file itself.
   * If there is neither a provided locale nor a locale parsed from the file, then an error is
   * thrown.
   * If there are both a provided locale and a locale parsed from the file, and they are not the
   * same, then a warning is reported.
   */
  loadBundles(
    translationFilePaths: AbsoluteFsPath[][],
    translationFileLocales: (string | undefined)[],
  ): TranslationBundle[] {
    return translationFilePaths.map((filePaths, index) => {
      const providedLocale = translationFileLocales[index];
      return this.mergeBundles(filePaths, providedLocale);
    });
  }

  /**
   * Load all the translations from the file at the given `filePath`.
   */
  private loadBundle(
    filePath: AbsoluteFsPath,
    providedLocale: string | undefined,
  ): TranslationBundle {
    const fileContents = this.fs.readFile(filePath);
    const unusedParsers = new Map<TranslationParser<any>, ParseAnalysis<any>>();
    for (const translationParser of this.translationParsers) {
      const result = translationParser.analyze(filePath, fileContents);
      if (!result.canParse) {
        unusedParsers.set(translationParser, result);
        continue;
      }

      const {
        locale: parsedLocale,
        translations,
        diagnostics,
      } = translationParser.parse(filePath, fileContents, result.hint);
      if (diagnostics.hasErrors) {
        throw new Error(
          diagnostics.formatDiagnostics(`The translation file "${filePath}" could not be parsed.`),
        );
      }

      const locale = providedLocale || parsedLocale;
      if (locale === undefined) {
        throw new Error(
          `The translation file "${filePath}" does not contain a target locale and no explicit locale was provided for this file.`,
        );
      }

      if (
        parsedLocale !== undefined &&
        providedLocale !== undefined &&
        parsedLocale !== providedLocale
      ) {
        diagnostics.warn(
          `The provided locale "${providedLocale}" does not match the target locale "${parsedLocale}" found in the translation file "${filePath}".`,
        );
      }

      // If we were passed a diagnostics object then copy the messages over to it.
      if (this.diagnostics) {
        this.diagnostics.merge(diagnostics);
      }

      return {locale, translations, diagnostics};
    }

    const diagnosticsMessages: string[] = [];
    for (const [parser, result] of unusedParsers.entries()) {
      diagnosticsMessages.push(
        result.diagnostics.formatDiagnostics(
          `\n${parser.constructor.name} cannot parse translation file.`,
        ),
      );
    }
    throw new Error(
      `There is no "TranslationParser" that can parse this translation file: ${filePath}.` +
        diagnosticsMessages.join('\n'),
    );
  }

  /**
   * There is more than one `filePath` for this locale, so load each as a bundle and then merge
   * them all together.
   */
  private mergeBundles(
    filePaths: AbsoluteFsPath[],
    providedLocale: string | undefined,
  ): TranslationBundle {
    const bundles = filePaths.map((filePath) => this.loadBundle(filePath, providedLocale));
    const bundle = bundles[0];
    for (let i = 1; i < bundles.length; i++) {
      const nextBundle = bundles[i];
      if (nextBundle.locale !== bundle.locale) {
        if (this.diagnostics) {
          const previousFiles = filePaths
            .slice(0, i)
            .map((f) => `"${f}"`)
            .join(', ');
          this.diagnostics.warn(
            `When merging multiple translation files, the target locale "${nextBundle.locale}" found in "${filePaths[i]}" does not match the target locale "${bundle.locale}" found in earlier files [${previousFiles}].`,
          );
        }
      }
      Object.keys(nextBundle.translations).forEach((messageId) => {
        if (bundle.translations[messageId] !== undefined) {
          this.diagnostics?.add(
            this.duplicateTranslation,
            `Duplicate translations for message "${messageId}" when merging "${filePaths[i]}".`,
          );
        } else {
          bundle.translations[messageId] = nextBundle.translations[messageId];
        }
      });
    }
    return bundle;
  }
}
