"use strict";

const t = require("../../");
const stringifyValidator = require("../utils/stringifyValidator");
const toFunctionName = require("../utils/toFunctionName");

let code = `// NOTE: This file is autogenerated. Do not modify.
// See packages/babel-types/scripts/generators/typescript.js for script used.

interface BaseComment {
  value: string;
  start: number;
  end: number;
  loc: SourceLocation;
  type: "CommentBlock" | "CommentLine";
}

export interface CommentBlock extends BaseComment {
  type: "CommentBlock";
}

export interface CommentLine extends BaseComment {
  type: "CommentLine";
}

export type Comment = CommentBlock | CommentLine;

export interface SourceLocation {
  start: {
    line: number;
    column: number;
  };

  end: {
    line: number;
    column: number;
  };
}

interface BaseNode {
  leadingComments: ReadonlyArray<Comment> | null;
  innerComments: ReadonlyArray<Comment> | null;
  trailingComments: ReadonlyArray<Comment> | null;
  start: number | null;
  end: number | null;
  loc: SourceLocation | null;
  type: Node["type"];
}

export type Node = ${t.TYPES.sort().join(" | ")};\n\n`;

//

const lines = [];

for (const type in t.NODE_FIELDS) {
  const fields = t.NODE_FIELDS[type];
  const fieldNames = sortFieldNames(Object.keys(t.NODE_FIELDS[type]), type);

  const struct = ['type: "' + type + '";'];
  const args = [];

  fieldNames.forEach(fieldName => {
    const field = fields[fieldName];
    let typeAnnotation = stringifyValidator(field.validate, "");

    if (isNullable(field) && !hasDefault(field)) {
      typeAnnotation += " | null";
    }

    if (areAllRemainingFieldsNullable(fieldName, fieldNames, fields)) {
      args.push(
        `${t.toBindingIdentifierName(fieldName)}${
          isNullable(field) ? "?:" : ":"
        } ${typeAnnotation}`
      );
    } else {
      args.push(
        `${t.toBindingIdentifierName(fieldName)}: ${typeAnnotation}${
          isNullable(field) ? " | undefined" : ""
        }`
      );
    }

    const alphaNumeric = /^\w+$/;

    if (t.isValidIdentifier(fieldName) || alphaNumeric.test(fieldName)) {
      struct.push(`${fieldName}: ${typeAnnotation};`);
    } else {
      struct.push(`"${fieldName}": ${typeAnnotation};`);
    }
  });

  code += `export interface ${type} extends BaseNode {
  ${struct.join("\n  ").trim()}
}\n\n`;

  // super and import are reserved words in JavaScript
  if (type !== "Super" && type !== "Import") {
    lines.push(
      `export function ${toFunctionName(type)}(${args.join(", ")}): ${type};`
    );
  } else {
    const functionName = toFunctionName(type);
    lines.push(
      `declare function _${functionName}(${args.join(", ")}): ${type};`,
      `export { _${functionName} as ${functionName}}`
    );
  }
}

for (let i = 0; i < t.TYPES.length; i++) {
  let decl = `export function is${t.TYPES[i]}(node: object | null | undefined, opts?: object | null): `;

  if (t.NODE_FIELDS[t.TYPES[i]]) {
    decl += `node is ${t.TYPES[i]};`;
  } else if (t.FLIPPED_ALIAS_KEYS[t.TYPES[i]]) {
    decl += `node is ${t.TYPES[i]};`;
  } else {
    decl += `boolean;`;
  }

  lines.push(decl);
}

lines.push(
  `export function validate(n: Node, key: string, value: any): void;`,
  `export function clone<T extends Node>(n: T): T;`,
  `export function cloneDeep<T extends Node>(n: T): T;`,
  `export function cloneNode<T extends Node>(n: T, deep?: boolean): T;`,
  `export function removeProperties(
  n: Node,
  opts?: { preserveComments: boolean } | null
): void;`,
  `export function removePropertiesDeep<T extends Node>(
  n: T,
  opts?: { preserveComments: boolean } | null
): T;`,
  `export type TraversalAncestors = ReadonlyArray<{
    node: Node,
    key: string,
    index?: number,
  }>;
  export type TraversalHandler<T> = (node: Node, parent: TraversalAncestors, type: T) => void;
  export type TraversalHandlers<T> = {
    enter?: TraversalHandler<T>,
    exit?: TraversalHandler<T>,
  };`.replace(/(^|\n) {2}/g, "$1"),
  // eslint-disable-next-line
  `export function traverse<T>(n: Node, h: TraversalHandler<T> | TraversalHandlers<T>, state?: T): void;`,
  `export function is(type: string, n: Node, opts: object): boolean;`,
  `export function isBinding(node: Node, parent: Node, grandparent?: Node): boolean`,
  `export function isBlockScoped(node: Node): boolean`,
  `export function isImmutable(node: Node): boolean`,
  `export function isLet(node: Node): boolean`,
  `export function isNode(node: object | null | undefined): boolean`,
  `export function isNodesEquivalent(a: any, b: any): boolean`,
  `export function isPlaceholderType(placeholderType: string, targetType: string): boolean`,
  `export function isReferenced(node: Node, parent: Node, grandparent?: Node): boolean`,
  `export function isScope(node: Node, parent: Node): boolean`,
  `export function isSpecifierDefault(specifier: ModuleSpecifier): boolean`,
  `export function isType(nodetype: string | null | undefined, targetType: string): boolean`,
  `export function isValidES3Identifier(name: string): boolean`,
  `export function isValidES3Identifier(name: string): boolean`,
  `export function isValidIdentifier(name: string): boolean`,
  `export function isVar(node: Node): boolean`
);

for (const type in t.DEPRECATED_KEYS) {
  code += `/**
 * @deprecated Use \`${t.DEPRECATED_KEYS[type]}\`
 */
export type ${type} = ${t.DEPRECATED_KEYS[type]};\n
`;
}

for (const type in t.FLIPPED_ALIAS_KEYS) {
  const types = t.FLIPPED_ALIAS_KEYS[type];
  code += `export type ${type} = ${types
    .map(type => `${type}`)
    .join(" | ")};\n`;
}
code += "\n";

code += "export interface Aliases {\n";
for (const type in t.FLIPPED_ALIAS_KEYS) {
  code += `  ${type}: ${type};\n`;
}
code += "}\n\n";

code += lines.join("\n") + "\n";

//

process.stdout.write(code);

//

function areAllRemainingFieldsNullable(fieldName, fieldNames, fields) {
  const index = fieldNames.indexOf(fieldName);
  return fieldNames.slice(index).every(_ => isNullable(fields[_]));
}

function hasDefault(field) {
  return field.default != null;
}

function isNullable(field) {
  return field.optional || hasDefault(field);
}

function sortFieldNames(fields, type) {
  return fields.sort((fieldA, fieldB) => {
    const indexA = t.BUILDER_KEYS[type].indexOf(fieldA);
    const indexB = t.BUILDER_KEYS[type].indexOf(fieldB);
    if (indexA === indexB) return fieldA < fieldB ? -1 : 1;
    if (indexA === -1) return 1;
    if (indexB === -1) return -1;
    return indexA - indexB;
  });
}
