"use strict";

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

const NODE_PREFIX = "BabelNode";

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

declare class ${NODE_PREFIX}Comment {
  value: string;
  start: number;
  end: number;
  loc: ${NODE_PREFIX}SourceLocation;
}

declare class ${NODE_PREFIX}CommentBlock extends ${NODE_PREFIX}Comment {
  type: "CommentBlock";
}

declare class ${NODE_PREFIX}CommentLine extends ${NODE_PREFIX}Comment {
  type: "CommentLine";
}

declare class ${NODE_PREFIX}SourceLocation {
  start: {
    line: number;
    column: number;
  };

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

declare class ${NODE_PREFIX} {
  leadingComments?: Array<${NODE_PREFIX}Comment>;
  innerComments?: Array<${NODE_PREFIX}Comment>;
  trailingComments?: Array<${NODE_PREFIX}Comment>;
  start: ?number;
  end: ?number;
  loc: ?${NODE_PREFIX}SourceLocation;
}\n\n`;

//

const lines = [];

for (const type in t.NODE_FIELDS) {
  const fields = t.NODE_FIELDS[type];

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

  Object.keys(t.NODE_FIELDS[type])
    .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;
    })
    .forEach(fieldName => {
      const field = fields[fieldName];

      let suffix = "";
      if (field.optional || field.default != null) suffix += "?";

      let typeAnnotation = "any";

      const validate = field.validate;
      if (validate) {
        typeAnnotation = stringifyValidator(validate, NODE_PREFIX);
      }

      if (typeAnnotation) {
        suffix += ": " + typeAnnotation;
      }

      args.push(t.toBindingIdentifierName(fieldName) + suffix);

      if (t.isValidIdentifier(fieldName)) {
        struct.push(fieldName + suffix + ";");
      }
    });

  code += `declare class ${NODE_PREFIX}${type} extends ${NODE_PREFIX} {
  ${struct.join("\n  ").trim()}
}\n\n`;

  // Flow chokes on super() and import() :/
  if (type !== "Super" && type !== "Import") {
    lines.push(
      `declare function ${toFunctionName(type)}(${args.join(
        ", "
      )}): ${NODE_PREFIX}${type};`
    );
  } else {
    const functionName = toFunctionName(type);
    lines.push(
      `declare function _${functionName}(${args.join(
        ", "
      )}): ${NODE_PREFIX}${type};`,
      `declare export { _${functionName} as ${functionName} }`
    );
  }
}

for (let i = 0; i < t.TYPES.length; i++) {
  let decl = `declare function is${t.TYPES[i]}(node: ?Object, opts?: ?Object): boolean`;

  if (t.NODE_FIELDS[t.TYPES[i]]) {
    decl += ` %checks (node instanceof ${NODE_PREFIX}${t.TYPES[i]})`;
  }

  lines.push(decl);
}

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

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

code += `\ndeclare module "@babel/types" {
  ${lines
    .join("\n")
    .replace(/\n/g, "\n  ")
    .trim()}
}\n`;

//

process.stdout.write(code);