import ts from 'typescript';
import fs from 'fs';
import { defaultNodePrinterOptions, dtsPatchFilePath, execTscCmd, modulePatchFilePath } from '../config';
import { getTsModule, TsModule } from '../module';
import {
  addOriginalCreateProgramTransformer, createMergeStatementsTransformer, createProgramExportFiles,
  fixTsEarlyReturnTransformer, hookTscExecTransformer, patchCreateProgramTransformer, patchEmitterTransformer
} from './transformers';
import { SourceSection } from '../module/source-section';
import { PatchError } from '../system';
import { readFileWithLock } from '../utils';
import { PatchDetail } from './patch-detail';


/* ****************************************************************************************************************** */
// region: Config
/* ****************************************************************************************************************** */

const dtsPatchSrc = '\n' + fs.readFileSync(dtsPatchFilePath, 'utf-8');
const jsPatchSrc = fs.readFileSync(modulePatchFilePath, 'utf-8')

// endregion


/* ****************************************************************************************************************** */
// region: Utils
/* ****************************************************************************************************************** */

export function patchModule(tsModule: TsModule, skipDts: boolean = false): { js: string, dts?: string } {
  let shouldWrap: boolean = false;
  switch (tsModule.moduleName) {
    case 'tsc.js':
    case 'tsserver.js':
    case 'tsserverlibrary.js':
    case 'typescript.js':
      shouldWrap = true;
  }

  const source = tsModule.getUnpatchedSource();
  const { bodyWrapper } = source;

  const printableBodyFooters: (SourceSection | string)[] = [];
  const printableFooters: (SourceSection | string)[] = [];

  /* Splice in full compiler functionality (if not already present) */
  if (tsModule.moduleName !== 'typescript.js') {
    const typescriptModule = getTsModule(tsModule.package, 'typescript.js');
    const tsSource = typescriptModule.getUnpatchedSource();

    /* Merge Headers & Footer */
    mergeStatements(source.fileHeader, tsSource.fileHeader);
    source.bodyHeader = tsSource.bodyHeader;
    mergeStatements(source.fileFooter, tsSource.fileFooter);

    /* Replace body */
    for (let i = source.body.length - 1; i >= 0; i--) {
      const bodySection = source.body[i];
      if (tsSource.body.some(s => s.srcFileName === bodySection.srcFileName)) {
        source.body.splice(i, 1);
      }
    }

    source.body.unshift(...tsSource.body);

    /* Fix early return */
    // NOTE - This exists up until TS 5.4, but isn't there for 5.5+
    if (tsModule.majorVer <= 5 && tsModule.minorVer <= 4) {
      const typescriptSection = source.body.find(s => s.srcFileName === 'src/typescript/typescript.ts');
      if (!typescriptSection) throw new PatchError(`Could not find Typescript source section`);
      typescriptSection.transform([ fixTsEarlyReturnTransformer ]);
      printableBodyFooters.push(`return returnResult;`);
    }
  }

  /* Patch Program */
  const programSection = source.body.find(s => s.srcFileName === 'src/compiler/program.ts');
  if (!programSection) throw new PatchError(`Could not find Program source section`);
  programSection.transform([ patchCreateProgramTransformer ]);

  /* Add originalCreateProgram to exports */
  let createProgramAdded = false;
  for (const fileName of createProgramExportFiles) {
    // As of TS 5.5, we have to handle cases of multiple instances of the same file name. In this case, we need to
    // handle both src/typescript/typescript.ts
    const sections = source.body.filter(s => s.srcFileName === fileName);
    for (const section of sections) {
      try {
        section.transform([ addOriginalCreateProgramTransformer ]);
        createProgramAdded = true;
      } catch (e) {
        if (!(e instanceof PatchError)) throw e;
      }
    }
  }

  if (!createProgramAdded) throw new PatchError(`Could not find any of the createProgram export files`);

  /* Patch emitter (for diagnostics tools) */
  const emitterSection = source.body.find(s => s.srcFileName === 'src/compiler/watch.ts');
  if (!emitterSection) throw new PatchError(`Could not find Emitter source section`);
  emitterSection.transform([ patchEmitterTransformer ]);

  /* Move executeCommandLine outside of closure */
  if (tsModule.moduleName === 'tsc.js') {
    const tscSection = source.body.find(s => s.srcFileName === 'src/tsc/tsc.ts');
    if (!tscSection) throw new PatchError(`Could not find Tsc source section`);

    tscSection.transform([ hookTscExecTransformer ]);

    printableFooters.push(`tsp.${execTscCmd}();`);
  }

  /* Print the module */
  const printedJs = printModule();

  /* Get Dts */
  let dts: string | undefined;
  if (!skipDts && tsModule.dtsPath) {
    const dtsText = readFileWithLock(tsModule.dtsPath);
    dts =
      dtsPatchSrc + '\n' +
      dtsText;
  }

  /* Get JS */
  const libraryName = tsModule.moduleName.replace(/\.js$/, '');
  const patchDetail = PatchDetail.fromModule(tsModule, printedJs);
  const js =
    patchDetail.toHeader() + '\n' +
    jsPatchSrc + '\n' +
    `tsp.currentLibrary = '${libraryName}';\n`+
    printedJs;

  return { dts, js };

  function getPrintList() {
    const list: [item: (string | SourceSection | undefined), indent?: number][] = [];
    let indentLevel = 0;

    /* File Header */
    list.push([ source.fileHeader, indentLevel ]);

    /* Body Wrapper Open */
    if (shouldWrap) {
      if (bodyWrapper) list.push([ `\n${bodyWrapper.start}\n`, indentLevel ]);
      indentLevel = 2;
    }

    /* Body Header*/
    list.push([ source.bodyHeader, indentLevel ]);

    /* Body */
    source.body.forEach(section => list.push([ section, indentLevel ]));

    /* Body Footers */
    printableBodyFooters.forEach(f => list.push([ f, indentLevel ]));

    /* Body Wrapper Close */
    if (shouldWrap) {
      indentLevel = 0;
      if (bodyWrapper) list.push([ `\n${bodyWrapper.end}\n`, indentLevel ]);
    }

    /* File Footer */
    list.push([ source.fileFooter, indentLevel ]);
    printableFooters.forEach(f => list.push([ f, indentLevel ]));

    return list;
  }

  function printModule() {
    const printer = ts.createPrinter(defaultNodePrinterOptions);
    let outputStr = ``;

    for (const [ item, indentLevel ] of getPrintList()) {
      let printed: string;
      let addedIndent: number | undefined;
      if (item === undefined) continue;
      if (typeof item === 'string') {
        printed = item;
      } else {
        printed = item.print(printer);
        if (indentLevel && item.indentLevel < indentLevel) {
          addedIndent = indentLevel - item.indentLevel;
        }
      }

      if (addedIndent) printed = printed.replace(/^/gm, ' '.repeat(addedIndent));

      outputStr += printed;
    }

    return outputStr;
  }

  function mergeStatements(
    baseSection: SourceSection | undefined,
    addedSection: SourceSection | undefined,
  ) {
    if (!baseSection || !addedSection) {
      if (addedSection) baseSection = addedSection;
      return;
    }

    const baseSourceFile = baseSection.getSourceFile();
    const addedSourceFile = addedSection.getSourceFile();

    const transformer = createMergeStatementsTransformer(baseSourceFile, addedSourceFile);
    baseSection.transform([ transformer ]);
  }
}

// endregion
