/**
 * Copyright 2020 Google Inc. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *     http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { promisify } from 'util';
import * as path from 'path';
import { posix } from 'path';
import glob from 'glob';
import { promises as fsp } from 'fs';

const globP = promisify(glob);
const autoGenComment =
  '// This file is autogenerated by lib/feature-plugin.js\n';

export default function () {
  let previousWorkerImports;
  let previousJoinedMetas;

  /**
   * Generates the worker file & tsconfig for all features
   *
   * @param {string[]} workerImports
   */
  async function generateWorkerFile(workerImports) {
    const workerBasePath = path.join(process.cwd(), 'src', 'features-worker');

    const featuresWorkerTsNames = workerImports.map((tsImport) => [
      path.relative(workerBasePath, tsImport).split(path.sep).join(posix.sep),
      path.basename(tsImport),
    ]);

    const workerFile = [
      autoGenComment,
      `import { expose } from 'comlink';`,
      `import { timed } from './util';`,
      featuresWorkerTsNames.map(
        ([path, name]) => `import ${name} from './${path}';`,
      ),
      `const exports = {`,
      featuresWorkerTsNames.map(([_, name]) => [
        `  ${name}(`,
        `    ...args: Parameters<typeof ${name}>`,
        `  ): ReturnType<typeof ${name}> {`,
        `    return timed('${name}', () => ${name}(...args));`,
        `  },`,
      ]),
      `};`,
      `export type ProcessorWorkerApi = typeof exports;`,
      `// 'as any' to work around the way our client code has insight into worker code`,
      `expose(exports, self as any);`,
    ]
      .flat(Infinity)
      .join('\n');

    await fsp.writeFile(path.join(workerBasePath, 'index.ts'), workerFile);
  }

  /**
   * Generates the client JS to call worker methods.
   *
   * @param {string[]} workerImports
   */
  async function generateWorkerBridge(workerImports) {
    const workerBridgeBasePath = path.join(
      process.cwd(),
      'src',
      'client',
      'lazy-app',
      'worker-bridge',
    );

    const featuresWorkerBridgeTsNames = workerImports.map((tsImport) => [
      path
        .relative(workerBridgeBasePath, tsImport)
        .split(path.sep)
        .join(posix.sep),
      path.basename(tsImport),
    ]);

    const bridgeMeta = [
      autoGenComment,
      featuresWorkerBridgeTsNames.map(
        ([path, name]) => `import type ${name} from '${path}';`,
      ),
      `export const methodNames = ${JSON.stringify(
        featuresWorkerBridgeTsNames.map(([_, name]) => name),
        null,
        '  ',
      )} as const;`,
      `export interface BridgeMethods {`,
      featuresWorkerBridgeTsNames.map(([_, name]) => [
        `  ${name}(`,
        `    signal: AbortSignal,`,
        `    ...args: Parameters<typeof ${name}>`,
        `  ): Promise<ReturnType<typeof ${name}>>;`,
      ]),
      `}`,
    ]
      .flat(Infinity)
      .join('\n');

    await fsp.writeFile(path.join(workerBridgeBasePath, 'meta.ts'), bridgeMeta);
  }

  async function generateWorkerFiles() {
    const workerImports = (
      await globP('src/features/*/**/worker/*.ts', {
        absolute: true,
      })
    )
      .filter((tsFile) => !tsFile.endsWith('.d.ts'))
      .map((tsFile) => tsFile.slice(0, -'.ts'.length));

    const joinedWorkerImports = workerImports.join();

    // Avoid regenerating if nothing's changed.
    // This also prevents an infinite loop in the watcher.
    if (joinedWorkerImports === previousWorkerImports) return;

    previousWorkerImports = joinedWorkerImports;
    await Promise.all([
      generateWorkerFile(workerImports),
      generateWorkerBridge(workerImports),
    ]);
  }

  async function generateFeatureMeta() {
    const getTsFiles = (glob) =>
      globP(glob, {
        absolute: true,
      }).then((paths) =>
        paths
          .filter((tsFile) => !tsFile.endsWith('.d.ts'))
          .map((tsFile) => tsFile.slice(0, -'.ts'.length)),
      );

    const metas = await Promise.all(
      [
        'src/features/encoders/*/shared/meta.ts',
        'src/features/processors/*/shared/meta.ts',
        'src/features/preprocessors/*/shared/meta.ts',
      ].map((glob) => getTsFiles(glob)),
    );

    const [encoderMetas, processorMetas, preprocessorMetas] = metas;

    const featureMetaBasePath = path.join(
      process.cwd(),
      'src',
      'client',
      'lazy-app',
      'feature-meta',
    );

    const joinedMetas = metas.flat().join();

    // Avoid regenerating if nothing's changed.
    // This also prevents an infinite loop in the watcher.
    if (joinedMetas === previousJoinedMetas) return;
    previousJoinedMetas = joinedMetas;

    const getTsName = (tsImport) => [
      path
        .relative(featureMetaBasePath, tsImport)
        .split(path.sep)
        .join(posix.sep),
      path.basename(tsImport.slice(0, -'/shared/meta'.length)),
    ];

    const encoderMetaTsNames = encoderMetas.map((tsImport) =>
      getTsName(tsImport),
    );
    const processorMetaTsNames = processorMetas.map((tsImport) =>
      getTsName(tsImport),
    );
    const preprocessorMetaTsNames = preprocessorMetas.map((tsImport) =>
      getTsName(tsImport),
    );

    const featureMeta = [
      autoGenComment,
      // Encoder stuff
      encoderMetaTsNames.map(
        ([path, name]) => `import * as ${name}EncoderMeta from '${path}';`,
      ),
      encoderMetaTsNames.map(
        ([path, name]) =>
          `import * as ${name}EncoderEntry from '${path.replace(
            /shared\/meta$/,
            'client',
          )}';`,
      ),
      `export type EncoderState =`,
      encoderMetaTsNames.map(
        ([_, name]) =>
          `  | { type: "${name}", options: ${name}EncoderMeta.EncodeOptions }`,
      ),
      `;`,
      `export type EncoderOptions =`,
      encoderMetaTsNames.map(
        ([_, name]) => `  | ${name}EncoderMeta.EncodeOptions`,
      ),
      `;`,
      `export const encoderMap = {`,
      encoderMetaTsNames.map(
        ([_, name]) =>
          `  ${name}: { meta: ${name}EncoderMeta, ...${name}EncoderEntry },`,
      ),
      `};`,
      `export type EncoderType = keyof typeof encoderMap`,
      // Processor stuff
      processorMetaTsNames.map(
        ([path, name]) => `import * as ${name}ProcessorMeta from '${path}';`,
      ),
      `interface Enableable { enabled: boolean; }`,
      `export interface ProcessorOptions {`,
      processorMetaTsNames.map(
        ([_, name]) => `  ${name}: ${name}ProcessorMeta.Options;`,
      ),
      `}`,
      `export interface ProcessorState {`,
      processorMetaTsNames.map(
        ([_, name]) => `  ${name}: Enableable & ${name}ProcessorMeta.Options;`,
      ),
      `}`,
      `export const defaultProcessorState: ProcessorState = {`,
      processorMetaTsNames.map(
        ([_, name]) =>
          `  ${name}: { enabled: false, ...${name}ProcessorMeta.defaultOptions },`,
      ),
      `}`,
      // Preprocessor stuff
      preprocessorMetaTsNames.map(
        ([path, name]) => `import * as ${name}PreprocessorMeta from '${path}';`,
      ),
      `export interface PreprocessorState {`,
      preprocessorMetaTsNames.map(
        ([_, name]) => `  ${name}: ${name}PreprocessorMeta.Options,`,
      ),
      `}`,
      `export const defaultPreprocessorState: PreprocessorState = {`,
      preprocessorMetaTsNames.map(
        ([_, name]) => `  ${name}: ${name}PreprocessorMeta.defaultOptions,`,
      ),
      `};`,
    ]
      .flat(Infinity)
      .join('\n');

    await fsp.writeFile(
      path.join(featureMetaBasePath, 'index.ts'),
      featureMeta,
    );
  }

  return {
    name: 'feature-plugin',
    async buildStart() {
      await Promise.all([generateWorkerFiles(), generateFeatureMeta()]);
    },
  };
}
