
// for us typescript ignorati, having an import makes this file a module
import * as fs from 'fs';
import * as process from 'process';
import * as ts from 'typescript';

// This file contains various utilities having to do with producing strings
// and managing output

// ------ create files
let dir = process.env['HOME'];
const srcDir = '/vscode-languageserver-node';
export const fnames = [
  `${dir}${srcDir}/protocol/src/common/protocol.ts`,
  `${dir}/${srcDir}/protocol/src/browser/main.ts`, `${dir}${srcDir}/types/src/main.ts`,
  `${dir}${srcDir}/jsonrpc/src/node/main.ts`
];
export const gitHash = 'f17727af04704c0e2ede73dfdbeb463156e94561';
let outFname = 'tsprotocol.go';
let fda: number, fdb: number, fde: number;  // file descriptors

export function createOutputFiles() {
  fda = fs.openSync('/tmp/ts-a', 'w');  // dump of AST
  fdb = fs.openSync('/tmp/ts-b', 'w');  // unused, for debugging
  fde = fs.openSync(outFname, 'w');     // generated Go
}
export function pra(s: string) {
  return (fs.writeSync(fda, s));
}
export function prb(s: string) {
  return (fs.writeSync(fdb, s));
}
export function prgo(s: string) {
  return (fs.writeSync(fde, s));
}

// Get the hash value of the git commit
export function git(): string {
  let a = fs.readFileSync(`${dir}${srcDir}/.git/HEAD`).toString();
  // ref: refs/heads/foo, or a hash like
  // cc12d1a1c7df935012cdef5d085cdba04a7c8ebe
  if (a.charAt(a.length - 1) == '\n') {
    a = a.substring(0, a.length - 1);
  }
  if (a.length == 40) {
    return a;  // a hash
  }
  if (a.substring(0, 5) == 'ref: ') {
    const fname = `${dir}${srcDir}/.git/` + a.substring(5);
    let b = fs.readFileSync(fname).toString();
    if (b.length == 41) {
      return b.substring(0, 40);
    }
  }
  throw new Error('failed to find the git commit hash');
}

// Produce a header for Go output files
export function computeHeader(pkgDoc: boolean): string {
  let lastMod = 0;
  let lastDate = new Date();
  for (const f of fnames) {
    const st = fs.statSync(f);
    if (st.mtimeMs > lastMod) {
      lastMod = st.mtimeMs;
      lastDate = st.mtime;
    }
  }
  const cp = `// Copyright 2019 The Go Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style
  // license that can be found in the LICENSE file.

  `;
  const a =
    '// Package protocol contains data types and code for LSP jsonrpcs\n' +
    '// generated automatically from vscode-languageserver-node\n' +
    `// commit: ${gitHash}\n` +
    `// last fetched ${lastDate}\n`;
  const b = 'package protocol\n';
  const c = '\n// Code generated (see typescript/README.md) DO NOT EDIT.\n\n';
  if (pkgDoc) {
    return cp + a + b + c;
  }
  else {
    return cp + b + a + c;
  }
}

// Turn a typescript name into an exportable Go name, and appease lint
export function goName(s: string): string {
  let ans = s;
  if (s.charAt(0) == '_') {
    // in the end, none of these are emitted.
    ans = 'Inner' + s.substring(1);
  }
  else { ans = s.substring(0, 1).toUpperCase() + s.substring(1); }
  ans = ans.replace(/Uri$/, 'URI');
  ans = ans.replace(/Id$/, 'ID');
  return ans;
}

// Generate JSON tag for a struct field
export function JSON(n: ts.PropertySignature): string {
  const json = `\`json:"${n.name.getText()}${n.questionToken !== undefined ? ',omitempty' : ''}"\``;
  return json;
}

// Generate modifying prefixes and suffixes to ensure
// consts are unique. (Go consts are package-level, but Typescript's are
// not.) Use suffixes to minimize changes to gopls.
export function constName(nm: string, type: string): string {
  let pref = new Map<string, string>([
    ['DiagnosticSeverity', 'Severity'], ['WatchKind', 'Watch'],
    ['SignatureHelpTriggerKind', 'Sig'], ['CompletionItemTag', 'Compl'],
    ['Integer', 'INT_'], ['Uinteger', 'UINT_'], ['CodeActionTriggerKind', 'CodeAction']
  ]);  // typeName->prefix
  let suff = new Map<string, string>([
    ['CompletionItemKind', 'Completion'], ['InsertTextFormat', 'TextFormat'],
    ['SymbolTag', 'Symbol'], ['FileOperationPatternKind', 'Op'],
  ]);
  let ans = nm;
  if (pref.get(type)) ans = pref.get(type) + ans;
  if (suff.has(type)) ans = ans + suff.get(type);
  return ans;
}

// Find the comments associated with an AST node
export function getComments(node: ts.Node): string {
  const sf = node.getSourceFile();
  const start = node.getStart(sf, false);
  const starta = node.getStart(sf, true);
  const x = sf.text.substring(starta, start);
  return x;
}


// --------- printing the AST, for debugging

export function printAST(program: ts.Program) {
  // dump the ast, for debugging
  const f = function (n: ts.Node) {
    describe(n, pra);
  };
  for (const sourceFile of program.getSourceFiles()) {
    if (!sourceFile.isDeclarationFile) {
      // walk the tree to do stuff
      ts.forEachChild(sourceFile, f);
    }
  }
  pra('\n');
  for (const key of Object.keys(seenThings).sort()) {
    pra(`${key}: ${seenThings.get(key)} \n`);
  }
}

// Used in printing the AST
let seenThings = new Map<string, number>();
function seenAdd(x: string) {
  const u = seenThings.get(x);
  seenThings.set(x, u === undefined ? 1 : u + 1);
}

// eslint-disable-next-line no-unused-vars
function describe(node: ts.Node, pr: (_: string) => any) {
  if (node === undefined) {
    return;
  }
  let indent = '';

  function f(n: ts.Node) {
    seenAdd(kinds(n));
    if (ts.isIdentifier(n)) {
      pr(`${indent} ${loc(n)} ${strKind(n)} ${n.text} \n`);
    }
    else if (ts.isPropertySignature(n) || ts.isEnumMember(n)) {
      pra(`${indent} ${loc(n)} ${strKind(n)} \n`);
    }
    else if (ts.isTypeLiteralNode(n)) {
      let m = n.members;
      pr(`${indent} ${loc(n)} ${strKind(n)} ${m.length} \n`);
    }
    else if (ts.isStringLiteral(n)) {
      pr(`${indent} ${loc(n)} ${strKind(n)} ${n.text} \n`);
    }
    else { pr(`${indent} ${loc(n)} ${strKind(n)} \n`); }
    indent += ' .';
    ts.forEachChild(n, f);
    indent = indent.slice(0, indent.length - 2);
  }
  f(node);
}


// For debugging, say where an AST node is in a file
export function loc(node: ts.Node | undefined): string {
  if (!node) throw new Error('loc called with undefined (cannot happen!)');
  const sf = node.getSourceFile();
  const start = node.getStart();
  const x = sf.getLineAndCharacterOfPosition(start);
  const full = node.getFullStart();
  const y = sf.getLineAndCharacterOfPosition(full);
  let fn = sf.fileName;
  const n = fn.search(/-node./);
  fn = fn.substring(n + 6);
  return `${fn} ${x.line + 1}: ${x.character + 1} (${y.line + 1}: ${y.character + 1})`;
}

// --- various string stuff

// return a string of the kinds of the immediate descendants
// as part of printing the AST tree
function kinds(n: ts.Node): string {
  let res = 'Seen ' + strKind(n);
  function f(n: ts.Node): void { res += ' ' + strKind(n); }
  ts.forEachChild(n, f);
  return res;
}

// What kind of AST node is it? This would just be typescript's
// SyntaxKind[n.kind] except that the default names for some nodes
// are misleading
export function strKind(n: ts.Node | undefined): string {
  if (n == null || n == undefined) {
    return 'null';
  }
  return kindToStr(n.kind);
}

function kindToStr(k: ts.SyntaxKind): string {
  const x = ts.SyntaxKind[k];
  // some of these have two names
  switch (x) {
    default:
      return x;
    case 'FirstAssignment':
      return 'EqualsToken';
    case 'FirstBinaryOperator':
      return 'LessThanToken';
    case 'FirstCompoundAssignment':
      return 'PlusEqualsToken';
    case 'FirstContextualKeyword':
      return 'AbstractKeyword';
    case 'FirstLiteralToken':
      return 'NumericLiteral';
    case 'FirstNode':
      return 'QualifiedName';
    case 'FirstTemplateToken':
      return 'NoSubstitutionTemplateLiteral';
    case 'LastTemplateToken':
      return 'TemplateTail';
    case 'FirstTypeNode':
      return 'TypePredicate';
  }
}
