import type Printer from "../printer.ts";
import type * as t from "@babel/types";
import { isIdentifier, type ParentMaps } from "@babel/types";
import * as charCodes from "charcodes";
import { TokenContext } from "../node/index.ts";

type ParentsOf<T extends t.Node> = ParentMaps[T["type"]];

export function _params(
  this: Printer,
  node: t.Function | t.TSDeclareMethod | t.TSDeclareFunction,
  noLineTerminator: boolean,
  idNode?: t.Expression | t.PrivateName | null,
  parentNode?: ParentsOf<typeof node>,
) {
  this.print(node.typeParameters);

  if (idNode !== undefined || parentNode !== undefined) {
    const nameInfo = _getFuncIdName.call(this, idNode!, parentNode!);
    if (nameInfo) {
      this.sourceIdentifierName(nameInfo.name, nameInfo.pos);
    }
  }

  this.token("(");
  _parameters.call(this, node.params, charCodes.rightParenthesis);

  this.print(node.returnType, noLineTerminator);

  this._noLineTerminator = noLineTerminator;
}

export function _parameters(
  this: Printer,
  parameters: t.Function["params"],
  endToken: number,
) {
  const oldNoLineTerminatorAfterNode = this.enterDelimited();

  const trailingComma = this.shouldPrintTrailingComma(endToken);

  const paramLength = parameters.length;
  for (let i = 0; i < paramLength; i++) {
    _param.call(this, parameters[i]);

    if (trailingComma || i < paramLength - 1) {
      this.tokenChar(charCodes.comma, i);
      this.space();
    }
  }

  this.tokenChar(endToken);
  this._noLineTerminatorAfterNode = oldNoLineTerminatorAfterNode;
}

export function _param(
  this: Printer,
  parameter: t.Identifier | t.RestElement | t.Pattern | t.TSParameterProperty,
) {
  this.printJoin(
    // @ts-expect-error decorators is not in VoidPattern
    parameter.decorators,
    undefined,
    undefined,
    undefined,
    undefined,
    true,
  );
  this.print(parameter, undefined, true);
  if (
    // @ts-expect-error optional is not in TSParameterProperty
    parameter.optional
  ) {
    this.token("?"); // TS / flow
  }

  this.print(
    // @ts-expect-error typeAnnotation is not in TSParameterProperty
    parameter.typeAnnotation,
    undefined,
    true,
  ); // TS / flow
}

export function _methodHead(this: Printer, node: t.Method | t.TSDeclareMethod) {
  const kind = node.kind;
  const key = node.key;

  if (kind === "get" || kind === "set") {
    this.word(kind);
    this.space();
  }

  if (node.async) {
    this.word("async", true);
    this.space();
  }

  if (
    kind === "method" ||
    // @ts-expect-error Fixme: kind: "init" is not defined
    kind === "init"
  ) {
    if (node.generator) {
      this.token("*");
    }
  }

  if (node.computed) {
    this.token("[");
    this.print(key);
    this.token("]");
  } else {
    this.print(key);
  }

  if (
    // @ts-expect-error optional is not in ObjectMethod
    node.optional
  ) {
    // TS
    this.token("?");
  }

  if (this._buf._map) {
    _params.call(
      this,
      node,
      false,
      node.computed && node.key.type !== "StringLiteral" ? undefined : node.key,
    );
  } else {
    _params.call(this, node, false);
  }
}

export function _predicate(
  this: Printer,
  node:
    | t.FunctionDeclaration
    | t.FunctionExpression
    | t.ArrowFunctionExpression,
  noLineTerminatorAfter?: boolean,
) {
  if (node.predicate) {
    if (!node.returnType) {
      this.token(":");
    }
    this.space();
    this.print(node.predicate, noLineTerminatorAfter);
  }
}

export function _functionHead(
  this: Printer,
  node: t.FunctionDeclaration | t.FunctionExpression | t.TSDeclareFunction,
  parent: ParentsOf<typeof node>,
  hasPredicate: boolean,
) {
  if (node.async) {
    this.word("async");
    if (!this.format.preserveFormat) {
      // We prevent inner comments from being printed here,
      // so that they are always consistently printed in the
      // same place regardless of the function type.
      this._innerCommentsState = 0 /* INNER_COMMENT_STATE.DISALLOWED */;
    }
    this.space();
  }
  this.word("function");
  if (node.generator) {
    if (!this.format.preserveFormat) {
      // We prevent inner comments from being printed here,
      // so that they are always consistently printed in the
      // same place regardless of the function type.
      this._innerCommentsState = 0 /* INNER_COMMENT_STATE.DISALLOWED */;
    }
    this.token("*");
  }

  this.space();
  if (node.id) {
    this.print(node.id);
  }

  if (this._buf._map) {
    _params.call(this, node, false, node.id, parent);
  } else {
    _params.call(this, node, false);
  }
  if (hasPredicate) {
    _predicate.call(this, node as t.FunctionDeclaration | t.FunctionExpression);
  }
}

export function FunctionExpression(
  this: Printer,
  node: t.FunctionExpression,
  parent: ParentsOf<typeof node>,
) {
  _functionHead.call(this, node, parent, true);
  this.space();
  this.print(node.body);
}

export { FunctionExpression as FunctionDeclaration };

export function ArrowFunctionExpression(
  this: Printer,
  node: t.ArrowFunctionExpression,
  parent: ParentsOf<typeof node>,
) {
  if (node.async) {
    this.word("async", true);
    this.space();
  }

  if (_shouldPrintArrowParamsParens.call(this, node)) {
    _params.call(
      this,
      node,
      true,
      undefined,
      this._buf._map ? parent : undefined,
    );
  } else {
    this.print(node.params[0], true);
  }

  _predicate.call(this, node, true);
  this.space();
  // When printing (x)/*1*/=>{}, we remove the parentheses
  // and thus there aren't two contiguous inner tokens.
  // We forcefully print inner comments here.
  this.printInnerComments();
  this.token("=>");

  this.space();

  this.tokenContext |= TokenContext.arrowBody;
  this.print(node.body);
}

// Try to avoid printing parens in simple cases, but only if we're pretty
// sure that they aren't needed by type annotations or potential newlines.
export function _shouldPrintArrowParamsParens(
  this: Printer,
  node: t.ArrowFunctionExpression,
): boolean {
  if (node.params.length !== 1) return true;

  if (node.typeParameters || node.returnType || node.predicate) {
    return true;
  }

  const firstParam = node.params[0];
  if (
    !isIdentifier(firstParam) ||
    firstParam.typeAnnotation ||
    firstParam.optional ||
    // Flow does not support `foo /*: string*/ => {};`
    firstParam.leadingComments?.length ||
    firstParam.trailingComments?.length
  ) {
    return true;
  }

  if (this.tokenMap) {
    if (node.loc == null) return true;
    if (this.tokenMap.findMatching(node, "(") !== null) return true;
    const arrowToken = this.tokenMap.findMatching(node, "=>");
    if (arrowToken?.loc == null) return true;
    return arrowToken.loc.start.line !== node.loc.start.line;
  }

  if (this.format.retainLines) return true;

  return false;
}

function isRenamedIdentifier(id: t.Identifier) {
  return !!id.loc?.identifierName && id.loc.identifierName !== id.name;
}

function _getFuncIdName(
  this: Printer,
  idNode?: t.Expression | t.PrivateName | null,
  parent?: ParentsOf<
    t.Function | t.TSDeclareMethod | t.TSDeclareFunction
  > | null,
) {
  let id:
    | t.Expression
    | t.PrivateName
    | t.LVal
    | t.VoidPattern
    | undefined
    | null = idNode;

  if (!id && parent) {
    const parentType = parent.type;

    if (parentType === "VariableDeclarator") {
      id = parent.id;
    } else if (
      parentType === "AssignmentExpression" ||
      parentType === "AssignmentPattern"
    ) {
      id = parent.left;
    } else if (
      parentType === "ObjectProperty" ||
      parentType === "ClassProperty"
    ) {
      if (!parent.computed || parent.key.type === "StringLiteral") {
        id = parent.key;
      }
    } else if (
      parentType === "ClassPrivateProperty" ||
      parentType === "ClassAccessorProperty"
    ) {
      id = parent.key;
    }
  }

  if (!id?.loc) return;

  let nameInfo;

  if (id.type === "Identifier") {
    if (!isRenamedIdentifier(id)) return;
    nameInfo = { pos: id.loc.start, name: id.loc.identifierName! };
  } else if (id.type === "PrivateName") {
    if (!isRenamedIdentifier(id.id)) return;
    nameInfo = {
      pos: id.loc.start,
      name: "#" + id.id.loc!.identifierName!,
    };
  } else if (id.type === "StringLiteral") {
    // TODO: Return empty when not renamed
    nameInfo = {
      pos: id.loc.start,
      name: id.value,
    };
  }

  return nameInfo;
}
