/**
 * @license
 * Copyright 2019 Google LLC. 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 * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';

process.on('unhandledRejection', ex => {
  throw ex;
});

// Used for logging the number of snippets that have been found.
let snippetCount = 0;
// Used for counting the number of errors that have been found.
let errorCount = 0;

/**
 * Parse and evaluate snippets for the src/index.ts from where this script is
 * run.
 * @param tf The TensorFlow.js module to use when evaluating snippets. If used
 *     outside core, this should be a union of core and the separate package.
 *     This is unused here but is used in eval() of the snippets.
 */
// tslint:disable-next-line:no-any
export async function parseAndEvaluateSnippets(tf: any) {
  const index = path.join(process.cwd(), 'src/index.ts');
  const tsconfigPath = path.join(process.cwd(), 'tsconfig.json');

  // Use the same compiler options that we use to compile the library
  // here.
  const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, 'utf8'));

  delete tsconfig.compilerOptions.moduleResolution;
  const program = ts.createProgram([index], tsconfig.compilerOptions);

  const checker = program.getTypeChecker();

  for (const sourceFile of program.getSourceFiles()) {
    if (!sourceFile.isDeclarationFile) {
      const children = sourceFile.getChildren();
      for (let i = 0; i < children.length; i++) {
        await visit(tf, checker, children[i], sourceFile);
      }
    }
  }

  if (errorCount === 0) {
    console.log(`Parsed and evaluated ${snippetCount} snippets successfully.`);
  } else {
    console.log(
        `Evaluated ${snippetCount} snippets with ${errorCount} errors.`);
    process.exit(1);
  }
}

async function visit(
    // tslint:disable-next-line:no-any
    tf: any, checker: ts.TypeChecker, node: ts.Node,
    sourceFile: ts.SourceFile) {
  const children = node.getChildren();
  for (let i = 0; i < children.length; i++) {
    await visit(tf, checker, children[i], sourceFile);
  }

  if (ts.isClassDeclaration(node) || ts.isFunctionDeclaration(node) ||
      ts.isMethodDeclaration(node) || ts.isInterfaceDeclaration(node)) {
    const symbol = checker.getSymbolAtLocation(node.name);
    const jsdoc = getJSDocTag(symbol);
    if (jsdoc == null) {
      return;
    }
    // Ignore snippets of methods that have been marked with ignoreCI.
    if (jsdoc['ignoreCI']) {
      return;
    }

    const documentation = symbol.getDocumentationComment(checker);
    if (documentation == null) {
      return;
    }
    for (let i = 0; i < documentation.length; i++) {
      const doc = documentation[i];
      const re = /```js.*?```/gs;
      const matches = re.exec(doc.text);
      if (matches == null) {
        return;
      }

      for (let k = 0; k < matches.length; k++) {
        snippetCount++;

        const match = matches[k];
        const lines = match.split('\n');
        const evalLines: string[] = [];
        for (let j = 0; j < lines.length; j++) {
          let line = lines[j];
          if (line.startsWith('```js')) {
            line = line.substring('```js'.length);
          }
          if (line.endsWith('```')) {
            line = line.substring(0, line.length - '```'.length);
          }
          line = line.trim();
          if (line.startsWith('*')) {
            line = line.substring(1).trim();
          }
          evalLines.push(line);
        }

        const srcCode = evalLines.join('\n');
        const evalString = `(async () => {${srcCode}})()`;

        const oldLog = console.log;
        const oldWarn = console.warn;

        const reportError = (e: string|Error) => {
          oldLog();
          oldLog(`Error executing snippet for ${symbol.name} at ${
              sourceFile.fileName}`);
          oldLog();
          oldLog(`\`\`\`js${srcCode}\`\`\``);
          oldLog();

          console.error(e);
          errorCount++;
        };

        // Overrwrite console.log so we don't spam the console.
        console.log = (msg: string) => {};
        console.warn = (msg: string) => {};
        try {
          await eval(evalString);
        } catch (e) {
          reportError(e);
        }
        console.log = oldLog;
        console.warn = oldWarn;
      }
    }
  }
}

interface JSDoc {
  namespace?: string;
  ignoreCI?: boolean;
}

function getJSDocTag(symbol: ts.Symbol): JSDoc {
  const tags = symbol.getJsDocTags();
  for (let i = 0; i < tags.length; i++) {
    const jsdocTag = tags[i];
    if (jsdocTag.name === 'doc' && jsdocTag.text != null) {
      if (jsdocTag.text.length !== 1) {
        throw new Error('Expected exactly one jsdoc SymbolDisplayPart but got'
          + ` ${jsdocTag.text.length} instead: ${jsdocTag.text}`);
      }
      const text = jsdocTag.text[0].text.trim();
      const json = convertDocStringToDocInfoObject(text);
      return json;
    }
  }
  return null;
}

function convertDocStringToDocInfoObject(docString: string): JSDoc {
  const jsonString =
      docString.replace(/([a-zA-Z0-9]+):/g, '"$1":').replace(/\'/g, '"');
  return JSON.parse(jsonString);
}
