//  TODO revisit `getValue` is deprecated.
/* eslint-disable deprecation/deprecation */
import type { AstPath, Doc, Printer } from "prettier";
import { builders } from "prettier/doc";
import { isIdentifierContinue, isIdentifierStart, utf16CodeUnits } from "../../core/charcode.js";
import { compilerAssert } from "../../core/diagnostics.js";
import { Keywords } from "../../core/scanner.js";
import {
  AliasStatementNode,
  ArrayExpressionNode,
  AugmentDecoratorStatementNode,
  BlockComment,
  BooleanLiteralNode,
  Comment,
  DecoratorDeclarationStatementNode,
  DecoratorExpressionNode,
  DirectiveExpressionNode,
  DocNode,
  EnumMemberNode,
  EnumSpreadMemberNode,
  EnumStatementNode,
  FunctionDeclarationStatementNode,
  FunctionParameterNode,
  IdentifierNode,
  InterfaceStatementNode,
  IntersectionExpressionNode,
  LineComment,
  MemberExpressionNode,
  ModelExpressionNode,
  ModelPropertyNode,
  ModelSpreadPropertyNode,
  ModelStatementNode,
  Node,
  NodeFlags,
  NumericLiteralNode,
  OperationSignatureDeclarationNode,
  OperationSignatureReferenceNode,
  OperationStatementNode,
  ProjectionArithmeticExpressionNode,
  ProjectionBlockExpressionNode,
  ProjectionCallExpressionNode,
  ProjectionDecoratorReferenceExpressionNode,
  ProjectionEqualityExpressionNode,
  ProjectionExpressionStatementNode,
  ProjectionIfExpressionNode,
  ProjectionLambdaExpressionNode,
  ProjectionLambdaParameterDeclarationNode,
  ProjectionLogicalExpressionNode,
  ProjectionMemberExpressionNode,
  ProjectionModelExpressionNode,
  ProjectionModelPropertyNode,
  ProjectionModelSpreadPropertyNode,
  ProjectionNode,
  ProjectionParameterDeclarationNode,
  ProjectionRelationalExpressionNode,
  ProjectionStatementNode,
  ProjectionTupleExpressionNode,
  ProjectionUnaryExpressionNode,
  ReturnExpressionNode,
  ScalarStatementNode,
  Statement,
  StringLiteralNode,
  SyntaxKind,
  TemplateParameterDeclarationNode,
  TextRange,
  TupleExpressionNode,
  TypeReferenceNode,
  TypeSpecScriptNode,
  UnionExpressionNode,
  UnionStatementNode,
  UnionVariantNode,
  UsingStatementNode,
  ValueOfExpressionNode,
} from "../../core/types.js";
import { FlattenedNamespaceStatementNode } from "../types.js";
import { commentHandler } from "./comment-handler.js";
import { needsParens } from "./needs-parens.js";
import { DecorableNode, PrettierChildPrint, TypeSpecPrettierOptions } from "./types.js";
import { util } from "./util.js";
const { align, breakParent, group, hardline, ifBreak, indent, join, line, softline } = builders;

const { isNextLineEmpty } = util as any;

/**
 * If the decorators for that node should try to be kept inline.
 */
const DecoratorsTryInline = {
  modelProperty: true,
  enumMember: true,
  unionVariant: true,
};

export const typespecPrinter: Printer<Node> = {
  print: printTypeSpec,
  isBlockComment: (node: any) => isBlockComment(node),
  canAttachComment: canAttachComment,
  printComment: printComment,
  handleComments: commentHandler,
};

export function printTypeSpec(
  // Path to the AST node to print
  path: AstPath<Node>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
): Doc {
  const node = path.getValue();
  const docs = printDocComments(path, options, print);
  const directives = shouldPrintDirective(node) ? printDirectives(path, options, print) : "";
  const printedNode = printNode(path, options, print);
  const value = needsParens(path, options) ? ["(", printedNode, ")"] : printedNode;
  const parts: Doc[] = [docs, directives, value];
  if (node.kind === SyntaxKind.TypeSpecScript) {
    // For TypeSpecScript(root of typespec document) we had a new line at the end.
    // This must be done here so the hardline entry can be the last item of the doc array returned by the printer
    // so the markdown(and other embedded formatter) can omit that extra line.
    parts.push(hardline);
  }
  return parts;
}

function shouldPrintDirective(node: Node) {
  // Model property handle printing directive itself.
  return node.kind !== SyntaxKind.ModelProperty;
}

export function printNode(
  // Path to the AST node to print
  path: AstPath<Node>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
): Doc {
  const node: Node = path.getValue();

  switch (node.kind) {
    // Root
    case SyntaxKind.TypeSpecScript:
      return printTypeSpecScript(path as AstPath<TypeSpecScriptNode>, options, print);
    // Statements
    case SyntaxKind.ImportStatement:
      return [`import "${node.path.value}";`];
    case SyntaxKind.UsingStatement:
      return [`using `, (path as AstPath<UsingStatementNode>).call(print, "name"), `;`];
    case SyntaxKind.OperationStatement:
      return printOperationStatement(path as AstPath<OperationStatementNode>, options, print);
    case SyntaxKind.OperationSignatureDeclaration:
      return printOperationSignatureDeclaration(
        path as AstPath<OperationSignatureDeclarationNode>,
        options,
        print
      );
    case SyntaxKind.OperationSignatureReference:
      return printOperationSignatureReference(
        path as AstPath<OperationSignatureReferenceNode>,
        options,
        print
      );
    case SyntaxKind.NamespaceStatement:
      return printNamespaceStatement(
        path as AstPath<FlattenedNamespaceStatementNode>,
        options,
        print
      );
    case SyntaxKind.ModelStatement:
      return printModelStatement(path as AstPath<ModelStatementNode>, options, print);
    case SyntaxKind.ScalarStatement:
      return printScalarStatement(path as AstPath<ScalarStatementNode>, options, print);
    case SyntaxKind.AliasStatement:
      return printAliasStatement(path as AstPath<AliasStatementNode>, options, print);
    case SyntaxKind.EnumStatement:
      return printEnumStatement(path as AstPath<EnumStatementNode>, options, print);
    case SyntaxKind.UnionStatement:
      return printUnionStatement(path as AstPath<UnionStatementNode>, options, print);
    case SyntaxKind.InterfaceStatement:
      return printInterfaceStatement(path as AstPath<InterfaceStatementNode>, options, print);
    // Others.
    case SyntaxKind.Identifier:
      return printIdentifier(node, options);
    case SyntaxKind.StringLiteral:
      return printStringLiteral(path as AstPath<StringLiteralNode>, options);
    case SyntaxKind.NumericLiteral:
      return printNumberLiteral(path as AstPath<NumericLiteralNode>, options);
    case SyntaxKind.BooleanLiteral:
      return printBooleanLiteral(path as AstPath<BooleanLiteralNode>, options);
    case SyntaxKind.ModelExpression:
      return printModelExpression(path as AstPath<ModelExpressionNode>, options, print);
    case SyntaxKind.ModelProperty:
      return printModelProperty(path as AstPath<ModelPropertyNode>, options, print);
    case SyntaxKind.DecoratorExpression:
      return printDecorator(path as AstPath<DecoratorExpressionNode>, options, print);
    case SyntaxKind.AugmentDecoratorStatement:
      return printAugmentDecorator(path as AstPath<AugmentDecoratorStatementNode>, options, print);
    case SyntaxKind.DirectiveExpression:
      return printDirective(path as AstPath<DirectiveExpressionNode>, options, print);
    case SyntaxKind.UnionExpression:
      return printUnion(path as AstPath<UnionExpressionNode>, options, print);
    case SyntaxKind.IntersectionExpression:
      return printIntersection(path as AstPath<IntersectionExpressionNode>, options, print);
    case SyntaxKind.ArrayExpression:
      return printArray(path as AstPath<ArrayExpressionNode>, options, print);
    case SyntaxKind.TupleExpression:
      return printTuple(path as AstPath<TupleExpressionNode>, options, print);
    case SyntaxKind.MemberExpression:
      return printMemberExpression(path as AstPath<MemberExpressionNode>, options, print);
    case SyntaxKind.EnumMember:
      return printEnumMember(path as AstPath<EnumMemberNode>, options, print);
    case SyntaxKind.EnumSpreadMember:
      return printEnumSpreadMember(path as AstPath<EnumSpreadMemberNode>, options, print);
    case SyntaxKind.UnionVariant:
      return printUnionVariant(path as AstPath<UnionVariantNode>, options, print);
    case SyntaxKind.TypeReference:
      return printTypeReference(path as AstPath<TypeReferenceNode>, options, print);
    case SyntaxKind.ValueOfExpression:
      return printValueOfExpression(path as AstPath<ValueOfExpressionNode>, options, print);
    case SyntaxKind.TemplateParameterDeclaration:
      return printTemplateParameterDeclaration(
        path as AstPath<TemplateParameterDeclarationNode>,
        options,
        print
      );
    case SyntaxKind.ModelSpreadProperty:
      return printModelSpread(path as AstPath<ModelSpreadPropertyNode>, options, print);
    case SyntaxKind.DecoratorDeclarationStatement:
      return printDecoratorDeclarationStatement(
        path as AstPath<DecoratorDeclarationStatementNode>,
        options,
        print
      );
    case SyntaxKind.FunctionDeclarationStatement:
      return printFunctionDeclarationStatement(
        path as AstPath<FunctionDeclarationStatementNode>,
        options,
        print
      );
    case SyntaxKind.FunctionParameter:
      return printFunctionParameterDeclaration(
        path as AstPath<FunctionParameterNode>,
        options,
        print
      );
    case SyntaxKind.ExternKeyword:
      return "extern";
    case SyntaxKind.VoidKeyword:
      return "void";
    case SyntaxKind.NeverKeyword:
      return "never";
    case SyntaxKind.UnknownKeyword:
      return "unknown";
    case SyntaxKind.ProjectionStatement:
      return printProjectionStatement(path as AstPath<ProjectionStatementNode>, options, print);
    case SyntaxKind.ProjectionModelSelector:
      return "model";
    case SyntaxKind.ProjectionModelPropertySelector:
      return "modelproperty";
    case SyntaxKind.ProjectionOperationSelector:
      return "op";
    case SyntaxKind.ProjectionUnionSelector:
      return "union";
    case SyntaxKind.ProjectionUnionVariantSelector:
      return "unionvariant";
    case SyntaxKind.ProjectionInterfaceSelector:
      return "interface";
    case SyntaxKind.ProjectionEnumSelector:
      return "enum";
    case SyntaxKind.ProjectionEnumMemberSelector:
      return "enummember";
    case SyntaxKind.Projection:
      return printProjection(path as AstPath<ProjectionNode>, options, print);
    case SyntaxKind.ProjectionParameterDeclaration:
      return printProjectionParameterDeclaration(
        path as AstPath<ProjectionParameterDeclarationNode>,
        options,
        print
      );
    case SyntaxKind.ProjectionExpressionStatement:
      return printProjectionExpressionStatement(
        path as AstPath<ProjectionExpressionStatementNode>,
        options,
        print
      );
    case SyntaxKind.ProjectionIfExpression:
      return printProjectionIfExpressionNode(
        path as AstPath<ProjectionIfExpressionNode>,
        options,
        print
      );
    case SyntaxKind.ProjectionBlockExpression:
      return printProjectionBlockExpressionNode(
        path as AstPath<ProjectionBlockExpressionNode>,
        options,
        print
      );
    case SyntaxKind.ProjectionMemberExpression:
      return printProjectionMemberExpression(
        path as AstPath<ProjectionMemberExpressionNode>,
        options,
        print
      );
    case SyntaxKind.ProjectionLogicalExpression:
    case SyntaxKind.ProjectionEqualityExpression:
    case SyntaxKind.ProjectionRelationalExpression:
    case SyntaxKind.ProjectionArithmeticExpression:
      return printProjectionLeftRightExpression(
        path as AstPath<ProjectionLogicalExpressionNode>,
        options,
        print
      );
    case SyntaxKind.ProjectionUnaryExpression:
      return printProjectionUnaryExpression(
        path as AstPath<ProjectionUnaryExpressionNode>,
        options,
        print
      );
    case SyntaxKind.ProjectionCallExpression:
      return printProjectionCallExpression(
        path as AstPath<ProjectionCallExpressionNode>,
        options,
        print
      );
    case SyntaxKind.ProjectionLambdaExpression:
      return printProjectionLambdaExpression(
        path as AstPath<ProjectionLambdaExpressionNode>,
        options,
        print
      );
    case SyntaxKind.ProjectionLambdaParameterDeclaration:
      return printProjectionLambdaParameterDeclaration(
        path as AstPath<ProjectionLambdaParameterDeclarationNode>,
        options,
        print
      );
    case SyntaxKind.ProjectionModelExpression:
      return printModelExpression(path as AstPath<ProjectionModelExpressionNode>, options, print);
    case SyntaxKind.ProjectionModelProperty:
      return printModelProperty(path as AstPath<ProjectionModelPropertyNode>, options, print);
    case SyntaxKind.ProjectionModelSpreadProperty:
      return printModelSpread(path as AstPath<ProjectionModelSpreadPropertyNode>, options, print);
    case SyntaxKind.ProjectionTupleExpression:
      return printTuple(path as AstPath<ProjectionTupleExpressionNode>, options, print);
    case SyntaxKind.ProjectionDecoratorReferenceExpression:
      return (path as AstPath<ProjectionDecoratorReferenceExpressionNode>).call(print, "target");
    case SyntaxKind.Return:
      return printReturnExpression(path as AstPath<ReturnExpressionNode>, options, print);
    case SyntaxKind.Doc:
      return printDoc(path as AstPath<DocNode>, options, print);
    case SyntaxKind.DocText:
    case SyntaxKind.DocParamTag:
    case SyntaxKind.DocTemplateTag:
    case SyntaxKind.DocReturnsTag:
    case SyntaxKind.DocUnknownTag:
      // https://github.com/microsoft/typespec/issues/1319 Tracks pretty-printing doc comments.
      compilerAssert(
        false,
        "Currently, doc comments are only handled as regular comments and we do not opt in to parsing them so we shouldn't reach here."
      );
      return "";
    case SyntaxKind.EmptyStatement:
      return "";
    case SyntaxKind.JsSourceFile:
    case SyntaxKind.JsNamespaceDeclaration:
    case SyntaxKind.InvalidStatement:
      return getRawText(node, options);
    default:
      // Dummy const to ensure we handle all node types.
      // If you get an error here, add a case for the new node type
      // you added..
      const _assertNever: never = node;
      return getRawText(node, options);
  }
}

export function printTypeSpecScript(
  path: AstPath<TypeSpecScriptNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
): Doc {
  const node = path.getValue();
  const nodeHasComments = hasComments(node, CommentCheckFlags.Dangling);
  const body = [];
  if (nodeHasComments) {
    body.push(printDanglingComments(path, options, { sameIndent: true }));
  }
  body.push(printStatementSequence(path, options, print, "statements"));
  return body;
}
export function printAliasStatement(
  path: AstPath<AliasStatementNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const id = path.call(print, "id");
  const template = printTemplateParameters(path, options, print, "templateParameters");
  return ["alias ", id, template, " = ", path.call(print, "value"), ";"];
}

function printTemplateParameters<T extends Node>(
  path: AstPath<T>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint,
  propertyName: keyof T
) {
  const node = path.getValue();
  const args = node[propertyName] as any as TemplateParameterDeclarationNode[];
  if ((args as any).length === 0) {
    return "";
  }

  const shouldHug = (args as any).length === 1;
  if (shouldHug) {
    return ["<", join(", ", path.map(print, propertyName as any)), ">"];
  } else {
    const body = indent([softline, join([", ", softline], path.map(print, propertyName as any))]);
    return group(["<", body, softline, ">"]);
  }
}

export function canAttachComment(node: Node): boolean {
  const kind = node.kind as SyntaxKind;
  return Boolean(
    kind &&
      kind !== SyntaxKind.LineComment &&
      kind !== SyntaxKind.BlockComment &&
      kind !== SyntaxKind.EmptyStatement &&
      kind !== SyntaxKind.Doc &&
      kind !== SyntaxKind.DocParamTag &&
      kind !== SyntaxKind.DocReturnsTag &&
      kind !== SyntaxKind.DocTemplateTag &&
      kind !== SyntaxKind.DocText &&
      kind !== SyntaxKind.DocUnknownTag &&
      !(node.flags & NodeFlags.Synthetic)
  );
}

export function printComment(
  commentPath: AstPath<Node | Comment>,
  options: TypeSpecPrettierOptions
): Doc {
  const comment = commentPath.getValue();
  (comment as any).printed = true;

  switch (comment.kind) {
    case SyntaxKind.BlockComment:
      return printBlockComment(commentPath as AstPath<BlockComment>, options);
    case SyntaxKind.LineComment:
      return `${options.originalText.slice(comment.pos, comment.end).trimEnd()}`;
    default:
      throw new Error(`Not a comment: ${JSON.stringify(comment)}`);
  }
}

function printBlockComment(commentPath: AstPath<BlockComment>, options: TypeSpecPrettierOptions) {
  const comment = commentPath.getValue();
  const rawComment = options.originalText.slice(comment.pos + 2, comment.end - 2);

  const printed = isIndentableBlockComment(rawComment)
    ? printIndentableBlockCommentContent(rawComment)
    : rawComment;
  return ["/*", printed, "*/"];
}

function isIndentableBlockComment(rawComment: string): boolean {
  // If the comment has multiple lines and every line starts with a star
  // we can fix the indentation of each line. The stars in the `/*` and
  // `*/` delimiters are not included in the comment value, so add them
  // back first.
  const lines = `*${rawComment}*`.split("\n");
  return lines.length > 1 && lines.every((line) => line.trim()[0] === "*");
}

function printIndentableBlockCommentContent(rawComment: string): Doc {
  const lines = rawComment.split("\n");

  return [
    join(
      hardline,
      lines.map((line, index) =>
        index === 0
          ? line.trimEnd()
          : " " + (index < lines.length - 1 ? line.trim() : line.trimStart())
      )
    ),
  ];
}

/** Print a doc comment. */
function printDoc(
  path: AstPath<DocNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const node = path.getValue();
  const rawComment = options.originalText.slice(node.pos + 3, node.end - 2);

  const printed = isIndentableBlockComment(rawComment)
    ? printIndentableBlockCommentContent(rawComment)
    : rawComment.includes("\n")
    ? rawComment
    : ` ${rawComment.trim()} `;
  return ["/**", printed, "*/"];
}

export function printDecorators(
  path: AstPath<DecorableNode>,
  options: object,
  print: PrettierChildPrint,
  { tryInline }: { tryInline: boolean }
): { decorators: Doc; multiline: boolean } {
  const node = path.getValue();
  if (node.decorators.length === 0) {
    return { decorators: "", multiline: false };
  }

  const shouldBreak = shouldDecoratorBreakLine(path, options, { tryInline });
  const decorators = path.map((x) => [print(x as any), ifBreak(line, " ")], "decorators");

  return {
    decorators: group([shouldBreak ? breakParent : "", decorators]),
    multiline: shouldBreak,
  };
}

/** Check if the decorators of the given node should be broken in sparate line */
function shouldDecoratorBreakLine(
  path: AstPath<DecorableNode>,
  options: object,
  { tryInline }: { tryInline: boolean }
) {
  const node = path.getValue();

  return (
    !tryInline || node.decorators.length >= 3 || hasNewlineBetweenOrAfterDecorators(node, options)
  );
}

/**
 * Check if there is already new lines in between the decorators of the node.
 */
function hasNewlineBetweenOrAfterDecorators(node: DecorableNode, options: any) {
  return node.decorators.some((decorator) => util.hasNewline(options.originalText, decorator.end));
}

export function printDecorator(
  path: AstPath<DecoratorExpressionNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const args = printDecoratorArgs(path, options, print);
  return ["@", path.call(print, "target"), args];
}

export function printAugmentDecorator(
  path: AstPath<AugmentDecoratorStatementNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const args = printAugmentDecoratorArgs(path, options, print);
  return ["@@", path.call(print, "target"), args, ";"];
}

function printAugmentDecoratorArgs(
  path: AstPath<AugmentDecoratorStatementNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  return [
    "(",
    group([
      indent(
        join(", ", [
          path.call(print, "targetType"),
          ...path.map((arg) => [softline, print(arg)], "arguments"),
        ])
      ),
      softline,
    ]),
    ")",
  ];
}

export function printDocComments(path: AstPath<Node>, options: object, print: PrettierChildPrint) {
  const node = path.getValue();
  if (node.docs === undefined || node.docs.length === 0) {
    return "";
  }

  const docs = path.map((x) => [print(x as any), line], "docs");
  return group([...docs, breakParent]);
}

export function printDirectives(path: AstPath<Node>, options: object, print: PrettierChildPrint) {
  const node = path.getValue();
  if (node.directives === undefined || node.directives.length === 0) {
    return "";
  }

  const directives = path.map((x) => [print(x as any), line], "directives");

  return group([...directives, breakParent]);
}

export function printDirective(
  path: AstPath<DirectiveExpressionNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const args = printDirectiveArgs(path, options, print);
  return ["#", path.call(print, "target"), " ", args];
}

function printDecoratorArgs(
  path: AstPath<DecoratorExpressionNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const node = path.getValue();
  if (node.arguments.length === 0) {
    return "";
  }

  // So that decorator with single object arguments have ( and { hugging.
  // @deco({
  //   value: "foo"
  // })
  const shouldHug =
    node.arguments.length === 1 &&
    (node.arguments[0].kind === SyntaxKind.ModelExpression ||
      node.arguments[0].kind === SyntaxKind.StringLiteral);

  if (shouldHug) {
    return [
      "(",
      join(
        ", ",
        path.map((arg) => [print(arg)], "arguments")
      ),
      ")",
    ];
  }

  return [
    "(",
    group([
      indent(
        join(
          ", ",
          path.map((arg) => [softline, print(arg)], "arguments")
        )
      ),
      softline,
    ]),
    ")",
  ];
}

export function printDirectiveArgs(
  path: AstPath<DirectiveExpressionNode>,
  options: object,
  print: PrettierChildPrint
) {
  const node = path.getValue();

  if (node.arguments.length === 0) {
    return "";
  }

  return join(
    " ",
    path.map((arg) => [print(arg)], "arguments")
  );
}

export function printEnumStatement(
  path: AstPath<EnumStatementNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const { decorators } = printDecorators(path, options, print, { tryInline: false });
  const id = path.call(print, "id");
  return [decorators, "enum ", id, " ", printEnumBlock(path, options, print)];
}

function printEnumBlock(
  path: AstPath<EnumStatementNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const node = path.getValue();
  if (node.members.length === 0) {
    return "{}";
  }

  return group([
    "{",
    indent([
      hardline,
      join(
        hardline,
        path.map((x) => [print(x as any), ","], "members")
      ),
    ]),
    hardline,
    "}",
  ]);
}

export function printEnumMember(
  path: AstPath<EnumMemberNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const node = path.getValue();
  const id = path.call(print, "id");
  const value = node.value ? [": ", path.call(print, "value")] : "";
  const { decorators, multiline } = printDecorators(path, options, print, {
    tryInline: DecoratorsTryInline.enumMember,
  });
  const propertyIndex = path.stack[path.stack.length - 2];
  const isNotFirst = typeof propertyIndex === "number" && propertyIndex > 0;
  return [multiline && isNotFirst ? hardline : "", decorators, id, value];
}

function printEnumSpreadMember(
  path: AstPath<EnumSpreadMemberNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
): Doc {
  return ["...", path.call(print, "target")];
}

export function printUnionStatement(
  path: AstPath<UnionStatementNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const id = path.call(print, "id");
  const { decorators } = printDecorators(path, options, print, { tryInline: false });
  const generic = printTemplateParameters(path, options, print, "templateParameters");
  return [decorators, "union ", id, generic, " ", printUnionVariantsBlock(path, options, print)];
}

export function printUnionVariantsBlock(
  path: AstPath<UnionStatementNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const node = path.getValue();
  if (node.options.length === 0) {
    return "{}";
  }

  return group([
    "{",
    indent([
      hardline,
      join(
        hardline,
        path.map((x) => [print(x as any), ","], "options")
      ),
    ]),
    hardline,
    "}",
  ]);
}

export function printUnionVariant(
  path: AstPath<UnionVariantNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const id = path.call(print, "id");
  const value = [": ", path.call(print, "value")];
  const { decorators } = printDecorators(path, options, print, {
    tryInline: DecoratorsTryInline.unionVariant,
  });
  return [decorators, id, value];
}

export function printInterfaceStatement(
  path: AstPath<InterfaceStatementNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const id = path.call(print, "id");
  const { decorators } = printDecorators(path, options, print, { tryInline: false });
  const generic = printTemplateParameters(path, options, print, "templateParameters");
  const extendList = printInterfaceExtends(path, options, print);

  return [
    decorators,
    "interface ",
    id,
    generic,
    extendList,
    " ",
    printInterfaceMembers(path, options, print),
  ];
}

function printInterfaceExtends(
  path: AstPath<InterfaceStatementNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
): Doc {
  const node = path.getValue();
  if (node.extends.length === 0) {
    return "";
  }

  const keyword = "extends ";
  return [group(indent([line, keyword, indent(join([",", line], path.map(print, "extends")))]))];
}

export function printInterfaceMembers(
  path: AstPath<InterfaceStatementNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const node = path.getValue();
  const hasOperations = node.operations.length > 0;
  const nodeHasComments = hasComments(node, CommentCheckFlags.Dangling);
  if (!hasOperations && !nodeHasComments) {
    return "{}";
  }

  const lastOperation = node.operations[node.operations.length - 1];

  const parts: Doc[] = [];
  path.each((operationPath) => {
    const node = operationPath.getValue() as any as OperationStatementNode;

    const printed = print(operationPath);
    parts.push(printed);

    if (node !== lastOperation) {
      parts.push(hardline);

      if (isNextLineEmpty(options.originalText, node, options.locEnd)) {
        parts.push(hardline);
      }
    }
  }, "operations");

  const body: Doc[] = [hardline, parts];

  if (nodeHasComments) {
    body.push(printDanglingComments(path, options, { sameIndent: true }));
  }
  return group(["{", indent(body), hardline, "}"]);
}

function printDanglingComments(
  path: AstPath<any>,
  options: TypeSpecPrettierOptions,
  { sameIndent }: { sameIndent: boolean }
) {
  const node = path.getValue();
  const parts: Doc[] = [];
  if (!node || !node.comments) {
    return "";
  }
  path.each((commentPath) => {
    const comment: any = commentPath.getValue();
    if (!comment.leading && !comment.trailing) {
      parts.push(printComment(path, options));
    }
  }, "comments");

  if (parts.length === 0) {
    return "";
  }

  if (sameIndent) {
    return join(hardline, parts);
  }
  return indent([hardline, join(hardline, parts)]);
}

/**
 * Handle printing an intersection node.
 * @example `Foo & Bar` or `{foo: string} & {bar: string}`
 *
 * @param path Prettier AST Path.
 * @param options Prettier options
 * @param print Prettier child print callback.
 * @returns Prettier document.
 */
export function printIntersection(
  path: AstPath<IntersectionExpressionNode>,
  options: object,
  print: PrettierChildPrint
) {
  const node = path.getValue();
  const types = path.map(print, "options");
  const result: (Doc | string)[] = [];
  let wasIndented = false;
  for (let i = 0; i < types.length; ++i) {
    if (i === 0) {
      result.push(types[i]);
    } else if (isModelNode(node.options[i - 1]) && isModelNode(node.options[i])) {
      // If both are objects, don't indent
      result.push([" & ", wasIndented ? indent(types[i]) : types[i]]);
    } else if (!isModelNode(node.options[i - 1]) && !isModelNode(node.options[i])) {
      // If no object is involved, go to the next line if it breaks
      result.push(indent([" &", line, types[i]]));
    } else {
      // If you go from object to non-object or vis-versa, then inline it
      if (i > 1) {
        wasIndented = true;
      }
      result.push(" & ", i > 1 ? indent(types[i]) : types[i]);
    }
  }
  return group(result);
}

function isModelNode(node: Node) {
  return node.kind === SyntaxKind.ModelExpression;
}

export function printArray(
  path: AstPath<ArrayExpressionNode>,
  options: object,
  print: PrettierChildPrint
): Doc {
  return [path.call(print, "elementType"), "[]"];
}

export function printTuple(
  path: AstPath<TupleExpressionNode | ProjectionTupleExpressionNode>,
  options: object,
  print: PrettierChildPrint
): Doc {
  return group([
    "[",
    indent(
      join(
        ", ",
        path.map((arg) => [softline, print(arg)], "values")
      )
    ),
    softline,
    "]",
  ]);
}

export function printMemberExpression(
  path: AstPath<MemberExpressionNode>,
  options: object,
  print: PrettierChildPrint
): Doc {
  const node = path.getValue();

  return [node.base ? [path.call(print, "base"), node.selector] : "", path.call(print, "id")];
}

export function printModelExpression(
  path: AstPath<ModelExpressionNode | ProjectionModelExpressionNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const inBlock = isModelExpressionInBlock(path);
  const node = path.getValue();
  if (inBlock) {
    return group(printModelPropertiesBlock(path, options, print));
  } else {
    const properties =
      node.properties.length === 0
        ? ""
        : indent(joinPropertiesInBlock(path as any, options, print, ifBreak(",", ", "), softline));
    return group([properties, softline]);
  }
}

export function printModelStatement(
  path: AstPath<ModelStatementNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const node = path.getValue();
  const id = path.call(print, "id");
  const heritage = node.extends
    ? [ifBreak(line, " "), "extends ", path.call(print, "extends")]
    : "";
  const isBase = node.is ? [ifBreak(line, " "), "is ", path.call(print, "is")] : "";
  const generic = printTemplateParameters(path, options, print, "templateParameters");
  const nodeHasComments = hasComments(node, CommentCheckFlags.Dangling);
  const shouldPrintBody = nodeHasComments || !(node.properties.length === 0 && node.is);
  const body = shouldPrintBody ? [" ", printModelPropertiesBlock(path, options, print)] : ";";
  return [
    printDecorators(path, options, print, { tryInline: false }).decorators,
    "model ",
    id,
    generic,
    group(indent(["", heritage, isBase])),
    body,
  ];
}

function printModelPropertiesBlock(
  path: AstPath<
    Node & {
      properties?: readonly (
        | ModelPropertyNode
        | ModelSpreadPropertyNode
        | ProjectionModelPropertyNode
        | ProjectionModelSpreadPropertyNode
      )[];
    }
  >,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const node = path.getValue();
  const hasProperties = node.properties && node.properties.length > 0;
  const nodeHasComments = hasComments(node, CommentCheckFlags.Dangling);
  if (!hasProperties && !nodeHasComments) {
    return "{}";
  }
  const tryInline = path.getParentNode()?.kind === SyntaxKind.TemplateParameterDeclaration;
  const lineDoc = tryInline ? softline : hardline;
  const seperator = isModelAValue(path) ? "," : ";";

  const body = [joinPropertiesInBlock(path as any, options, print, seperator, lineDoc)];
  if (nodeHasComments) {
    body.push(printDanglingComments(path, options, { sameIndent: true }));
  }
  return group(["{", indent(body), lineDoc, "}"]);
}

function joinPropertiesInBlock(
  path: AstPath<
    Node & {
      properties: readonly (
        | ModelPropertyNode
        | ModelSpreadPropertyNode
        | ProjectionModelPropertyNode
        | ProjectionModelSpreadPropertyNode
      )[];
    }
  >,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint,
  separator: Doc,
  regularLine: Doc
): Doc {
  const doc: Doc[] = [regularLine];
  const propertyContainerNode = path.getValue();

  let newLineBeforeNextProp = false;
  path.each((item, propertyIndex) => {
    const isFirst = propertyIndex === 0;
    const isLast = propertyIndex === propertyContainerNode.properties.length - 1;
    const shouldWrapInNewLines = shouldWrapPropertyInNewLines(item as any, options);

    if ((newLineBeforeNextProp || shouldWrapInNewLines) && !isFirst) {
      doc.push(hardline);
      newLineBeforeNextProp = false;
    }
    doc.push(print(item));
    if (isLast) {
      doc.push(ifBreak(separator));
    } else {
      doc.push(separator);
      doc.push(regularLine);
      if (shouldWrapInNewLines) {
        newLineBeforeNextProp = true;
      }
    }
  }, "properties");
  return doc;
}

/**
 * Check if property item (PropertyNode, SpreadProperty) should be wrapped in new lines.
 * It can be wrapped for the following reasons:
 * - has decorators on lines above
 * - has leading comments
 */
function shouldWrapPropertyInNewLines(
  path: AstPath<
    | ModelPropertyNode
    | ModelSpreadPropertyNode
    | ProjectionModelPropertyNode
    | ProjectionModelSpreadPropertyNode
  >,
  options: any
): boolean {
  const node = path.getValue();
  return (
    ((node.kind === SyntaxKind.ModelProperty || node.kind === SyntaxKind.ProjectionModelProperty) &&
      shouldDecoratorBreakLine(path as any, options, {
        tryInline: DecoratorsTryInline.modelProperty,
      })) ||
    hasComments(node, CommentCheckFlags.Leading) ||
    (node.docs && node.docs?.length > 0)
  );
}

/**
 * Figure out if this model is being used as a definition or value.
 * @returns true if the model is used as a value(e.g. decorator value), false if it is used as a model definition.
 */
function isModelAValue(path: AstPath<Node>): boolean {
  let count = 0;
  let node: Node | null = path.getValue();
  do {
    switch (node.kind) {
      case SyntaxKind.ModelStatement:
      case SyntaxKind.AliasStatement:
      case SyntaxKind.OperationStatement:
        return false;
      case SyntaxKind.DecoratorExpression:
        return true;
    }
  } while ((node = path.getParentNode(count++)));
  return true;
}

export function printModelProperty(
  path: AstPath<ModelPropertyNode | ProjectionModelPropertyNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const node = path.getValue();
  const { decorators } = printDecorators(path as AstPath<DecorableNode>, options, print, {
    tryInline: DecoratorsTryInline.modelProperty,
  });
  const id = printIdentifier(node.id, options);
  return [
    printDirectives(path, options, print),
    decorators,
    id,
    node.optional ? "?: " : ": ",
    path.call(print, "value"),
    node.default ? [" = ", path.call(print, "default")] : "",
  ];
}

function printIdentifier(id: IdentifierNode, options: TypeSpecPrettierOptions) {
  return printId(id.sv);
}

export function printId(sv: string) {
  if (needBacktick(sv)) {
    const escapedString = sv
      .replace(/\\/g, "\\\\")
      .replace(/\n/g, "\\n")
      .replace(/\r/g, "\\r")
      .replace(/\t/g, "\\t")
      .replace(/`/g, "\\`");
    return `\`${escapedString}\``;
  } else {
    return sv;
  }
}

function needBacktick(sv: string) {
  if (sv.length === 0) {
    return false;
  }
  if (Keywords.has(sv)) {
    return true;
  }
  let cp = sv.codePointAt(0)!;
  if (!isIdentifierStart(cp)) {
    return true;
  }
  let pos = 0;
  do {
    pos += utf16CodeUnits(cp);
  } while (pos < sv.length && isIdentifierContinue((cp = sv.codePointAt(pos)!)));
  return pos < sv.length;
}

function isModelExpressionInBlock(
  path: AstPath<ModelExpressionNode | ProjectionModelExpressionNode>
) {
  const parent: Node | null = path.getParentNode() as any;

  switch (parent?.kind) {
    case SyntaxKind.OperationSignatureDeclaration:
      return parent.parameters !== path.getNode();
    default:
      return true;
  }
}

export function printScalarStatement(
  path: AstPath<ScalarStatementNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const node = path.getValue();
  const id = path.call(print, "id");
  const template = printTemplateParameters(path, options, print, "templateParameters");

  const heritage = node.extends
    ? [ifBreak(line, " "), "extends ", path.call(print, "extends")]
    : "";
  return [
    printDecorators(path, options, print, { tryInline: false }).decorators,
    "scalar ",
    id,
    template,
    group(indent(["", heritage])),
    ";",
  ];
}

export function printNamespaceStatement(
  path: AstPath<FlattenedNamespaceStatementNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const names = path.map(print, "ids");
  const currentNode = path.getNode();

  const suffix =
    currentNode?.statements === undefined
      ? ";"
      : [
          " {",
          indent([hardline, printStatementSequence(path, options, print, "statements")]),
          hardline,
          "}",
        ];

  const { decorators } = printDecorators(path, options, print, { tryInline: false });
  return [decorators, `namespace `, join(".", names), suffix];
}

export function printOperationSignatureDeclaration(
  path: AstPath<OperationSignatureDeclarationNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  return ["(", path.call(print, "parameters"), "): ", path.call(print, "returnType")];
}

export function printOperationSignatureReference(
  path: AstPath<OperationSignatureReferenceNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  return [" is ", path.call(print, "baseOperation")];
}

export function printOperationStatement(
  path: AstPath<OperationStatementNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const inInterface = (path.getParentNode()?.kind as any) === SyntaxKind.InterfaceStatement;
  const templateParams = printTemplateParameters(path, options, print, "templateParameters");
  const { decorators } = printDecorators(path as AstPath<DecorableNode>, options, print, {
    tryInline: true,
  });

  return [
    decorators,
    inInterface ? "" : "op ",
    path.call(print, "id"),
    templateParams,
    path.call(print, "signature"),
    `;`,
  ];
}

export function printStatementSequence<T extends Node>(
  path: AstPath<T>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint,
  property: keyof T
) {
  const node = path.getValue();
  const parts: Doc[] = [];
  const lastStatement = getLastStatement(node[property] as any as Statement[]);

  path.each((statementPath) => {
    const node = path.getValue();

    if (node.kind === SyntaxKind.EmptyStatement) {
      return;
    }

    const printed = print(statementPath);
    parts.push(printed);

    if (node !== lastStatement) {
      parts.push(hardline);

      if (isNextLineEmpty(options.originalText, node, options.locEnd)) {
        parts.push(hardline);
      }
    }
  }, property as any);

  return parts;
}

function getLastStatement(statements: Statement[]): Statement | undefined {
  for (let i = statements.length - 1; i >= 0; i--) {
    const statement = statements[i];
    if (statement.kind !== SyntaxKind.EmptyStatement) {
      return statement;
    }
  }
  return undefined;
}

export function printUnion(
  path: AstPath<UnionExpressionNode>,
  options: object,
  print: PrettierChildPrint
) {
  const node = path.getValue();
  const shouldHug = shouldHugType(node);

  const types = path.map((typePath) => {
    let printedType: string | Doc = print(typePath);
    if (!shouldHug) {
      printedType = align(2, printedType);
    }
    return printedType;
  }, "options");

  if (shouldHug) {
    return join(" | ", types);
  }

  const shouldAddStartLine = true;
  const code = [ifBreak([shouldAddStartLine ? line : "", "| "], ""), join([line, "| "], types)];
  return group(indent(code));
}

function shouldHugType(node: Node) {
  if (node.kind === SyntaxKind.UnionExpression || node.kind === SyntaxKind.IntersectionExpression) {
    return node.options.length < 4;
  }
  return false;
}

export function printTypeReference(
  path: AstPath<TypeReferenceNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
): Doc {
  const type = path.call(print, "target");
  const template = printTemplateParameters(path, options, print, "arguments");
  return [type, template];
}

export function printValueOfExpression(
  path: AstPath<ValueOfExpressionNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
): Doc {
  const type = path.call(print, "target");
  return ["valueof ", type];
}

function printTemplateParameterDeclaration(
  path: AstPath<TemplateParameterDeclarationNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
): Doc {
  const node = path.getValue();
  return [
    path.call(print, "id"),
    node.constraint ? [" extends ", path.call(print, "constraint")] : "",
    node.default ? [" = ", path.call(print, "default")] : "",
  ];
}

function printModelSpread(
  path: AstPath<ModelSpreadPropertyNode | ProjectionModelSpreadPropertyNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
): Doc {
  return ["...", path.call(print, "target")];
}

function printDecoratorDeclarationStatement(
  path: AstPath<DecoratorDeclarationStatementNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
): Doc {
  const id = path.call(print, "id");
  const parameters = [
    group([
      indent(
        join(", ", [
          [softline, path.call(print, "target")],
          ...path.map((arg) => [softline, print(arg)], "parameters"),
        ])
      ),
      softline,
    ]),
  ];
  return [printModifiers(path, options, print), "dec ", id, "(", parameters, ")", ";"];
}

function printFunctionDeclarationStatement(
  path: AstPath<FunctionDeclarationStatementNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
): Doc {
  const node = path.getValue();
  const id = path.call(print, "id");
  const parameters = [
    group([
      indent(
        join(
          ", ",
          path.map((arg) => [softline, print(arg)], "parameters")
        )
      ),
      softline,
    ]),
  ];
  const returnType = node.returnType ? [": ", path.call(print, "returnType")] : "";
  return [printModifiers(path, options, print), "fn ", id, "(", parameters, ")", returnType, ";"];
}

function printFunctionParameterDeclaration(
  path: AstPath<FunctionParameterNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
): Doc {
  const node = path.getValue();
  const id = path.call(print, "id");

  const type = node.type ? [": ", path.call(print, "type")] : "";

  return [
    node.rest ? "..." : "",
    printDirectives(path, options, print),
    id,
    node.optional ? "?" : "",
    type,
  ];
}

export function printModifiers(
  path: AstPath<DecoratorDeclarationStatementNode | FunctionDeclarationStatementNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
): Doc {
  const node = path.getValue();
  if (node.modifiers.length === 0) {
    return "";
  }

  return path.map((x) => [print(x as any), " "], "modifiers");
}

function printStringLiteral(
  path: AstPath<StringLiteralNode>,
  options: TypeSpecPrettierOptions
): Doc {
  const node = path.getValue();
  return getRawText(node, options);
}

function printNumberLiteral(
  path: AstPath<NumericLiteralNode>,
  options: TypeSpecPrettierOptions
): Doc {
  const node = path.getValue();
  return getRawText(node, options);
}

function printBooleanLiteral(
  path: AstPath<BooleanLiteralNode>,
  options: TypeSpecPrettierOptions
): Doc {
  const node = path.getValue();
  return node.value ? "true" : "false";
}

function printProjectionStatement(
  path: AstPath<ProjectionStatementNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const selector = path.call(print, "selector");
  const id = path.call(print, "id");
  const projections = path.map(print, "projections").flatMap((x) => [hardline, x]);
  return [
    "projection ",
    selector,
    "#",
    id,
    " {",
    indent(projections),
    projections.length > 0 ? hardline : "",
    "}",
  ];
}

function printProjection(
  path: AstPath<ProjectionNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const node = path.getValue();
  const params = printProjectionParameters(path, options, print);
  const body = printProjectionExpressionStatements(path, options, print, "body");
  return [
    ...node.modifierIds.flatMap((i) => [i.sv, " "]),
    node.directionId.sv,
    params,
    " {",
    indent(body),
    hardline,
    "}",
  ];
}

function printProjectionParameters(
  path: AstPath<ProjectionNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const node = path.getValue();
  const params = node.parameters;
  if ((params as any).length === 0) {
    return "";
  }

  const shouldHug = (params as any).length === 1;
  if (shouldHug) {
    return ["(", printItemList(path, options, print, "parameters"), ")"];
  } else {
    const body = indent([softline, join([", ", softline], path.map(print, "parameters"))]);
    return group(["(", body, softline, ")"]);
  }
}

function printProjectionExpressionStatements<T extends Node>(
  path: AstPath<T>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint,
  key: keyof T
) {
  const parts: Doc[] = [hardline];
  const lastIndex = (path.getValue()[key] as any).length - 1;
  path.each((statementPath, index) => {
    const node = path.getValue();

    if (node.kind === SyntaxKind.EmptyStatement) {
      return;
    }

    const printed = print(statementPath);
    parts.push(printed);
    parts.push(";");
    if (index < lastIndex) {
      parts.push(hardline);

      if (isNextLineEmpty(options.originalText, node, options.locEnd)) {
        parts.push(hardline);
      }
    }
  }, key as any);
  return parts;
}

function printProjectionParameterDeclaration(
  path: AstPath<ProjectionParameterDeclarationNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  return path.call(print, "id");
}

function printProjectionExpressionStatement(
  path: AstPath<ProjectionExpressionStatementNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  return path.call(print, "expr");
}
function printProjectionIfExpressionNode(
  path: AstPath<ProjectionIfExpressionNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const node = path.getValue();
  const test = path.call(print, "test");
  const consequent = path.call(print, "consequent");
  const alternate = node.alternate ? [" else ", path.call(print, "alternate")] : "";
  return ["if ", test, " ", consequent, alternate];
}

export function printProjectionBlockExpressionNode(
  path: AstPath<ProjectionBlockExpressionNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const node = path.getValue();
  if (node.statements.length === 0) {
    return "{}";
  }
  return [
    "{",
    indent(printProjectionExpressionStatements(path, options, print, "statements")),
    hardline,
    "}",
  ];
}

export function printProjectionMemberExpression(
  path: AstPath<ProjectionMemberExpressionNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const node = path.getValue();
  return [path.call(print, "base"), node.selector, path.call(print, "id")];
}

export function printProjectionLeftRightExpression(
  path: AstPath<
    | ProjectionLogicalExpressionNode
    | ProjectionRelationalExpressionNode
    | ProjectionEqualityExpressionNode
    | ProjectionArithmeticExpressionNode
  >,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const node = path.getValue();
  return [path.call(print, "left"), " ", node.op, " ", path.call(print, "right")];
}

export function printProjectionUnaryExpression(
  path: AstPath<ProjectionUnaryExpressionNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  return ["!", path.call(print, "target")];
}

export function printProjectionCallExpression(
  path: AstPath<ProjectionCallExpressionNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  const node = path.getValue();
  const target = path.call(print, "target");
  const params = printItemList(path, options, print, "arguments");

  if (node.callKind === "method") {
    return [target, "(", params, ")"];
  } else {
    return [target, "<", params, ">"];
  }
}

export function printProjectionLambdaExpression(
  path: AstPath<ProjectionLambdaExpressionNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  return [
    "(",
    printItemList(path, options, print, "parameters"),
    ")",
    " => ",
    path.call(print, "body"),
  ];
}

export function printProjectionLambdaParameterDeclaration(
  path: AstPath<ProjectionLambdaParameterDeclarationNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  return path.call(print, "id");
}

export function printReturnExpression(
  path: AstPath<ReturnExpressionNode>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint
) {
  return ["return ", path.call(print, "value")];
}

function printItemList<T extends Node>(
  path: AstPath<T>,
  options: TypeSpecPrettierOptions,
  print: PrettierChildPrint,
  key: keyof T
) {
  return join(", ", path.map(print, key as any));
}

/**
 * @param node Node that has postition information.
 * @param options Prettier options
 * @returns Raw text in the file for the given node.
 */
function getRawText(node: TextRange, options: TypeSpecPrettierOptions) {
  return options.originalText.slice(node.pos, node.end);
}

function hasComments(node: any, flags?: CommentCheckFlags) {
  if (!node.comments || node.comments.length === 0) {
    return false;
  }
  const test = getCommentTestFunction(flags);
  return test ? node.comments.some(test) : true;
}

enum CommentCheckFlags {
  /** Check comment is a leading comment */
  Leading = 1 << 1,
  /** Check comment is a trailing comment */
  Trailing = 1 << 2,
  /** Check comment is a dangling comment */
  Dangling = 1 << 3,
  /** Check comment is a block comment */
  Block = 1 << 4,
  /** Check comment is a line comment */
  Line = 1 << 5,
  /** Check comment is a `prettier-ignore` comment */
  PrettierIgnore = 1 << 6,
  /** Check comment is the first attached comment */
  First = 1 << 7,
  /** Check comment is the last attached comment */
  Last = 1 << 8,
}

type CommentTestFn = (comment: any, index: number, comments: any[]) => boolean;

function getCommentTestFunction(flags: CommentCheckFlags | undefined): CommentTestFn | undefined {
  if (flags) {
    return (comment: any, index: number, comments: any[]) =>
      !(
        (flags & CommentCheckFlags.Leading && !comment.leading) ||
        (flags & CommentCheckFlags.Trailing && !comment.trailing) ||
        (flags & CommentCheckFlags.Dangling && (comment.leading || comment.trailing)) ||
        (flags & CommentCheckFlags.Block && !isBlockComment(comment)) ||
        (flags & CommentCheckFlags.Line && !isLineComment(comment)) ||
        (flags & CommentCheckFlags.First && index !== 0) ||
        (flags & CommentCheckFlags.Last && index !== comments.length - 1)
      );
  }
  return undefined;
}

function isBlockComment(comment: Comment): comment is BlockComment {
  return comment.kind === SyntaxKind.BlockComment;
}

function isLineComment(comment: Comment): comment is LineComment {
  return comment.kind === SyntaxKind.BlockComment;
}
