/**
 * @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 { Rule } from '@angular-devkit/schematics';
import * as ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
import { findNodes } from '../../utility/ast-utils';
import { allWorkspaceTargets, getWorkspace } from '../../utility/workspace';
import { Builders } from '../../utility/workspace-models';

/**
 * Update the `main.server.ts` file by adding exports to `renderModule` and `renderModuleFactory` which are
 * now required for Universal and App-Shell for Ivy and `bundleDependencies`.
 */
export function updateServerMainFile(): Rule {
  return async tree => {
    const workspace = await getWorkspace(tree);

    for (const [targetName, target] of allWorkspaceTargets(workspace)) {
      if (targetName !== 'server' || target.builder !== Builders.Server) {
        continue;
      }

      // find the main server file
      const mainFilePath = target.options?.main;
      if (!mainFilePath || typeof mainFilePath !== 'string') {
        continue;
      }

      const content = tree.read(mainFilePath);
      if (!content) {
        continue;
      }

      const source = ts.createSourceFile(
        mainFilePath,
        content.toString().replace(/^\uFEFF/, ''),
        ts.ScriptTarget.Latest,
        true,
      );

      // find exports in main server file
      const exportDeclarations = findNodes(source, ts.SyntaxKind.ExportDeclaration) as ts.ExportDeclaration[];

      const platformServerExports = exportDeclarations.filter(({ moduleSpecifier }) => (
        moduleSpecifier && ts.isStringLiteral(moduleSpecifier) && moduleSpecifier.text === '@angular/platform-server'
      ));

      let hasRenderModule = false;
      let hasRenderModuleFactory = false;

      // find exports of renderModule or renderModuleFactory
      for (const { exportClause } of platformServerExports) {
        if (exportClause && ts.isNamedExports(exportClause)) {
          if (!hasRenderModuleFactory) {
            hasRenderModuleFactory = exportClause.elements.some(({ name }) => name.text === 'renderModuleFactory');
          }

          if (!hasRenderModule) {
            hasRenderModule = exportClause.elements.some(({ name }) => name.text === 'renderModule');
          }
        }
      }

      if (hasRenderModule && hasRenderModuleFactory) {
        // We have both required exports
        continue;
      }

      let exportSpecifiers: ts.ExportSpecifier[] = [];
      let updateExisting = false;

      // Add missing exports
      if (platformServerExports.length) {
        const { exportClause } = platformServerExports[0] as ts.ExportDeclaration;
        if (!exportClause || ts.isNamespaceExport(exportClause)) {
          continue;
        }

        exportSpecifiers = [...exportClause.elements];
        updateExisting = true;
      }

      if (!hasRenderModule) {
        exportSpecifiers.push(ts.createExportSpecifier(
          undefined,
          ts.createIdentifier('renderModule'),
        ));
      }

      if (!hasRenderModuleFactory) {
        exportSpecifiers.push(ts.createExportSpecifier(
          undefined,
          ts.createIdentifier('renderModuleFactory'),
        ));
      }

      // Create a TS printer to get the text of the export node
      const printer = ts.createPrinter();

      const moduleSpecifier = ts.createStringLiteral('@angular/platform-server');

      // TypeScript will emit the Node with double quotes.
      // In schematics we usually write code with a single quotes
      // tslint:disable-next-line: no-any
      (moduleSpecifier as any).singleQuote = true;

      const newExportDeclarationText = printer.printNode(
        ts.EmitHint.Unspecified,
        ts.createExportDeclaration(
          undefined,
          undefined,
          ts.createNamedExports(exportSpecifiers),
          moduleSpecifier,
        ),
        source,
      );

      const recorder = tree.beginUpdate(mainFilePath);
      if (updateExisting) {
        const start = platformServerExports[0].getStart();
        const width = platformServerExports[0].getWidth();

        recorder
          .remove(start, width)
          .insertLeft(start, newExportDeclarationText);
      } else {
        recorder.insertLeft(source.getWidth(), '\n' + newExportDeclarationText);
      }

      tree.commitUpdate(recorder);
    }
  };
}
