// @flow strict-local

import {Transformer} from '@parcel/plugin';
import path from 'path';
import SourceMap from '@parcel/source-map';
import type {DiagnosticCodeFrame} from '@parcel/diagnostic';
import type {CompilerOptions} from 'typescript';

import ts from 'typescript';
import {CompilerHost, loadTSConfig} from '@parcel/ts-utils';
import {escapeMarkdown} from '@parcel/diagnostic';
import {TSModuleGraph} from './TSModuleGraph';
import nullthrows from 'nullthrows';
import {collect} from './collect';
import {shake} from './shake';

export default (new Transformer({
  loadConfig({config, options}) {
    return loadTSConfig(config, options);
  },

  transform({asset, config, options, logger}) {
    let opts: CompilerOptions = {
      // React is the default. Users can override this by supplying their own tsconfig,
      // which many TypeScript users will already have for typechecking, etc.
      jsx: ts.JsxEmit.React,
      ...config,
      // Always emit output
      noEmit: false,
      noEmitOnError: false,
      declaration: true,
      declarationMap: true,
      isolatedModules: false,
      emitDeclarationOnly: true,
      outFile: 'index.d.ts',
      moduleResolution: ts.ModuleResolutionKind.NodeJs,
      // createProgram doesn't support incremental mode
      composite: false,
      incremental: false,
    };

    let host = new CompilerHost(options.inputFS, ts, logger);
    // $FlowFixMe
    let program = ts.createProgram([asset.filePath], opts, host);

    for (let file of program.getSourceFiles()) {
      if (path.normalize(file.fileName) !== asset.filePath) {
        asset.invalidateOnFileChange(
          host.redirectTypes.get(file.fileName) ?? file.fileName,
        );
      }
    }

    let mainModuleName = path
      .relative(program.getCommonSourceDirectory(), asset.filePath)
      .slice(0, -path.extname(asset.filePath).length);
    let moduleGraph = new TSModuleGraph(mainModuleName);

    let emitResult = program.emit(undefined, undefined, undefined, true, {
      afterDeclarations: [
        // 1. Build module graph
        context => sourceFile => {
          return collect(moduleGraph, context, sourceFile);
        },
        // 2. Tree shake and rename types
        context => sourceFile => {
          return shake(moduleGraph, context, sourceFile);
        },
      ],
    });

    let diagnostics = ts
      .getPreEmitDiagnostics(program)
      .concat(emitResult.diagnostics);

    if (diagnostics.length > 0) {
      for (let diagnostic of diagnostics) {
        let filename = asset.filePath;
        let {file} = diagnostic;

        let diagnosticMessage =
          typeof diagnostic.messageText === 'string'
            ? diagnostic.messageText
            : diagnostic.messageText.messageText;

        let codeframe: ?DiagnosticCodeFrame;
        if (file != null && diagnostic.start != null) {
          let source = file.text || diagnostic.source;
          if (file.fileName) {
            filename = file.fileName;
          }

          // $FlowFixMe
          if (source) {
            let lineChar = file.getLineAndCharacterOfPosition(diagnostic.start);
            let start = {
              line: lineChar.line + 1,
              column: lineChar.character + 1,
            };
            let end = {
              line: start.line,
              column: start.column + 1,
            };

            if (
              typeof diagnostic.start === 'number' &&
              typeof diagnostic.length === 'number'
            ) {
              let endCharPosition = file.getLineAndCharacterOfPosition(
                diagnostic.start + diagnostic.length,
              );

              end = {
                line: endCharPosition.line + 1,
                column: endCharPosition.character + 1,
              };
            }

            codeframe = {
              filePath: filename,
              code: source,
              codeHighlights: [
                {
                  start,
                  end,
                  message: escapeMarkdown(diagnosticMessage),
                },
              ],
            };
          }
        }

        logger.warn({
          message: escapeMarkdown(diagnosticMessage),
          codeFrames: codeframe ? [codeframe] : undefined,
        });
      }
    }

    let code = nullthrows(host.outputCode);
    code = code.substring(0, code.lastIndexOf('//# sourceMappingURL'));

    let map = JSON.parse(nullthrows(host.outputMap));
    map.sources = map.sources.map(source =>
      path.join(path.dirname(asset.filePath), source),
    );

    let sourceMap = null;
    if (map.mappings) {
      sourceMap = new SourceMap(options.projectRoot);
      sourceMap.addVLQMap(map);
    }

    asset.type = 'ts';
    asset.setCode(code);
    asset.setMap(sourceMap);
    return [asset];
  },
}): Transformer);
