// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
// TODO(ry) Combine this implementation with //deno_typescript/compiler_main.js

// This module is the entry point for "compiler" isolate, ie. the one
// that is created when Deno needs to compile TS/WASM to JS.
//
// It provides a two functions that should be called by Rust:
//  - `bootstrapTsCompilerRuntime`
//  - `bootstrapWasmCompilerRuntime`
// Either of these functions must be called when creating isolate
// to properly setup runtime.

// NOTE: this import has side effects!
import "./ts_global.d.ts";

import { TranspileOnlyResult } from "./compiler_api.ts";
import { TS_SNAPSHOT_PROGRAM } from "./compiler_bootstrap.ts";
import { setRootExports } from "./compiler_bundler.ts";
import {
  CompilerHostTarget,
  defaultBundlerOptions,
  defaultRuntimeCompileOptions,
  defaultTranspileOptions,
  Host
} from "./compiler_host.ts";
import {
  processImports,
  processLocalImports,
  resolveModules
} from "./compiler_imports.ts";
import {
  createWriteFile,
  CompilerRequestType,
  convertCompilerOptions,
  ignoredDiagnostics,
  WriteFileState,
  processConfigureResponse
} from "./compiler_util.ts";
import { Diagnostic } from "./diagnostics.ts";
import { fromTypeScriptDiagnostic } from "./diagnostics_util.ts";
import { assert } from "./util.ts";
import * as util from "./util.ts";
import { bootstrapWorkerRuntime } from "./runtime_worker.ts";

interface CompilerRequestCompile {
  type: CompilerRequestType.Compile;
  target: CompilerHostTarget;
  rootNames: string[];
  // TODO(ry) add compiler config to this interface.
  // options: ts.CompilerOptions;
  configPath?: string;
  config?: string;
  bundle?: boolean;
  outFile?: string;
}

interface CompilerRequestRuntimeCompile {
  type: CompilerRequestType.RuntimeCompile;
  target: CompilerHostTarget;
  rootName: string;
  sources?: Record<string, string>;
  bundle?: boolean;
  options?: string;
}

interface CompilerRequestRuntimeTranspile {
  type: CompilerRequestType.RuntimeTranspile;
  sources: Record<string, string>;
  options?: string;
}

/** The format of the work message payload coming from the privileged side */
type CompilerRequest =
  | CompilerRequestCompile
  | CompilerRequestRuntimeCompile
  | CompilerRequestRuntimeTranspile;

/** The format of the result sent back when doing a compilation. */
interface CompileResult {
  emitSkipped: boolean;
  diagnostics?: Diagnostic;
}

// TODO(bartlomieju): refactor this function into multiple functions
// per CompilerRequestType
async function tsCompilerOnMessage({
  data: request
}: {
  data: CompilerRequest;
}): Promise<void> {
  switch (request.type) {
    // `Compile` are requests from the internals to Deno, generated by both
    // the `run` and `bundle` sub command.
    case CompilerRequestType.Compile: {
      const {
        bundle,
        config,
        configPath,
        outFile,
        rootNames,
        target
      } = request;
      util.log(">>> compile start", {
        rootNames,
        type: CompilerRequestType[request.type]
      });

      // When a programme is emitted, TypeScript will call `writeFile` with
      // each file that needs to be emitted.  The Deno compiler host delegates
      // this, to make it easier to perform the right actions, which vary
      // based a lot on the request.  For a `Compile` request, we need to
      // cache all the files in the privileged side if we aren't bundling,
      // and if we are bundling we need to enrich the bundle and either write
      // out the bundle or log it to the console.
      const state: WriteFileState = {
        type: request.type,
        bundle,
        host: undefined,
        outFile,
        rootNames
      };
      const writeFile = createWriteFile(state);

      const host = (state.host = new Host({
        bundle,
        target,
        writeFile
      }));
      let diagnostics: readonly ts.Diagnostic[] | undefined;

      // if there is a configuration supplied, we need to parse that
      if (config && config.length && configPath) {
        const configResult = host.configure(configPath, config);
        diagnostics = processConfigureResponse(configResult, configPath);
      }

      // This will recursively analyse all the code for other imports,
      // requesting those from the privileged side, populating the in memory
      // cache which will be used by the host, before resolving.
      const resolvedRootModules = await processImports(
        rootNames.map(rootName => [rootName, rootName]),
        undefined,
        host.getCompilationSettings().checkJs
      );

      let emitSkipped = true;
      // if there was a configuration and no diagnostics with it, we will continue
      // to generate the program and possibly emit it.
      if (!diagnostics || (diagnostics && diagnostics.length === 0)) {
        const options = host.getCompilationSettings();
        const program = ts.createProgram({
          rootNames,
          options,
          host,
          oldProgram: TS_SNAPSHOT_PROGRAM
        });

        diagnostics = ts
          .getPreEmitDiagnostics(program)
          .filter(({ code }) => !ignoredDiagnostics.includes(code));

        // We will only proceed with the emit if there are no diagnostics.
        if (diagnostics && diagnostics.length === 0) {
          if (bundle) {
            // we only support a single root module when bundling
            assert(resolvedRootModules.length === 1);
            // warning so it goes to stderr instead of stdout
            console.warn(`Bundling "${resolvedRootModules[0]}"`);
            setRootExports(program, resolvedRootModules[0]);
          }
          const emitResult = program.emit();
          emitSkipped = emitResult.emitSkipped;
          // emitResult.diagnostics is `readonly` in TS3.5+ and can't be assigned
          // without casting.
          diagnostics = emitResult.diagnostics;
        }
      }

      const result: CompileResult = {
        emitSkipped,
        diagnostics: diagnostics.length
          ? fromTypeScriptDiagnostic(diagnostics)
          : undefined
      };
      globalThis.postMessage(result);

      util.log("<<< compile end", {
        rootNames,
        type: CompilerRequestType[request.type]
      });
      break;
    }
    case CompilerRequestType.RuntimeCompile: {
      // `RuntimeCompile` are requests from a runtime user, both compiles and
      // bundles.  The process is similar to a request from the privileged
      // side, but also returns the output to the on message.
      const { rootName, sources, options, bundle, target } = request;

      util.log(">>> runtime compile start", {
        rootName,
        bundle,
        sources: sources ? Object.keys(sources) : undefined
      });

      const resolvedRootName = sources
        ? rootName
        : resolveModules([rootName])[0];

      const rootNames = sources
        ? processLocalImports(sources, [[resolvedRootName, resolvedRootName]])
        : await processImports([[resolvedRootName, resolvedRootName]]);

      const state: WriteFileState = {
        type: request.type,
        bundle,
        host: undefined,
        rootNames,
        sources,
        emitMap: {},
        emitBundle: undefined
      };
      const writeFile = createWriteFile(state);

      const host = (state.host = new Host({
        bundle,
        target,
        writeFile
      }));
      const compilerOptions = [defaultRuntimeCompileOptions];
      if (options) {
        compilerOptions.push(convertCompilerOptions(options));
      }
      if (bundle) {
        compilerOptions.push(defaultBundlerOptions);
      }
      host.mergeOptions(...compilerOptions);

      const program = ts.createProgram({
        rootNames,
        options: host.getCompilationSettings(),
        host,
        oldProgram: TS_SNAPSHOT_PROGRAM
      });

      if (bundle) {
        setRootExports(program, rootNames[0]);
      }

      const diagnostics = ts
        .getPreEmitDiagnostics(program)
        .filter(({ code }) => !ignoredDiagnostics.includes(code));

      const emitResult = program.emit();

      assert(emitResult.emitSkipped === false, "Unexpected skip of the emit.");
      const result = [
        diagnostics.length
          ? fromTypeScriptDiagnostic(diagnostics).items
          : undefined,
        bundle ? state.emitBundle : state.emitMap
      ];
      globalThis.postMessage(result);

      assert(state.emitMap);
      util.log("<<< runtime compile finish", {
        rootName,
        sources: sources ? Object.keys(sources) : undefined,
        bundle,
        emitMap: Object.keys(state.emitMap)
      });

      break;
    }
    case CompilerRequestType.RuntimeTranspile: {
      const result: Record<string, TranspileOnlyResult> = {};
      const { sources, options } = request;
      const compilerOptions = options
        ? Object.assign(
            {},
            defaultTranspileOptions,
            convertCompilerOptions(options)
          )
        : defaultTranspileOptions;

      for (const [fileName, inputText] of Object.entries(sources)) {
        const { outputText: source, sourceMapText: map } = ts.transpileModule(
          inputText,
          {
            fileName,
            compilerOptions
          }
        );
        result[fileName] = { source, map };
      }
      globalThis.postMessage(result);

      break;
    }
    default:
      util.log(
        `!!! unhandled CompilerRequestType: ${
          (request as CompilerRequest).type
        } (${CompilerRequestType[(request as CompilerRequest).type]})`
      );
  }

  // The compiler isolate exits after a single message.
  globalThis.close();
}

async function wasmCompilerOnMessage({
  data: binary
}: {
  data: string;
}): Promise<void> {
  const buffer = util.base64ToUint8Array(binary);
  // @ts-ignore
  const compiled = await WebAssembly.compile(buffer);

  util.log(">>> WASM compile start");

  const importList = Array.from(
    // @ts-ignore
    new Set(WebAssembly.Module.imports(compiled).map(({ module }) => module))
  );
  const exportList = Array.from(
    // @ts-ignore
    new Set(WebAssembly.Module.exports(compiled).map(({ name }) => name))
  );

  globalThis.postMessage({ importList, exportList });

  util.log("<<< WASM compile end");

  // The compiler isolate exits after a single message.
  globalThis.close();
}

function bootstrapTsCompilerRuntime(): void {
  bootstrapWorkerRuntime("TS");
  globalThis.onmessage = tsCompilerOnMessage;
}

function bootstrapWasmCompilerRuntime(): void {
  bootstrapWorkerRuntime("WASM");
  globalThis.onmessage = wasmCompilerOnMessage;
}

Object.defineProperties(globalThis, {
  bootstrapWasmCompilerRuntime: {
    value: bootstrapWasmCompilerRuntime,
    enumerable: false,
    writable: false,
    configurable: false
  },
  bootstrapTsCompilerRuntime: {
    value: bootstrapTsCompilerRuntime,
    enumerable: false,
    writable: false,
    configurable: false
  }
});