"use strict";

const { insertPragma, isPragma } = require("./pragma");
const {
  getAncestorCount,
  getBlockValueLineContents,
  getFlowScalarLineContents,
  getLast,
  getLastDescendantNode,
  hasLeadingComments,
  hasMiddleComments,
  hasIndicatorComment,
  hasTrailingComment,
  hasEndComments,
  hasPrettierIgnore,
  isLastDescendantNode,
  isNextLineEmpty,
  isNode,
  isEmptyNode,
  defineShortcut,
  mapNode
} = require("./utils");
const docBuilders = require("../doc").builders;
const {
  conditionalGroup,
  breakParent,
  concat,
  dedent,
  dedentToRoot,
  fill,
  group,
  hardline,
  ifBreak,
  join,
  line,
  lineSuffix,
  literalline,
  markAsRoot,
  softline
} = docBuilders;
const { replaceEndOfLineWith } = require("../common/util");

function preprocess(ast) {
  return mapNode(ast, defineShortcuts);
}

function defineShortcuts(node) {
  switch (node.type) {
    case "document":
      defineShortcut(node, "head", () => node.children[0]);
      defineShortcut(node, "body", () => node.children[1]);
      break;
    case "documentBody":
    case "sequenceItem":
    case "flowSequenceItem":
    case "mappingKey":
    case "mappingValue":
      defineShortcut(node, "content", () => node.children[0]);
      break;
    case "mappingItem":
    case "flowMappingItem":
      defineShortcut(node, "key", () => node.children[0]);
      defineShortcut(node, "value", () => node.children[1]);
      break;
  }
  return node;
}

function genericPrint(path, options, print) {
  const node = path.getValue();
  const parentNode = path.getParentNode();

  const tag = !node.tag ? "" : path.call(print, "tag");
  const anchor = !node.anchor ? "" : path.call(print, "anchor");

  const nextEmptyLine =
    isNode(node, [
      "mapping",
      "sequence",
      "comment",
      "directive",
      "mappingItem",
      "sequenceItem"
    ]) && !isLastDescendantNode(path)
      ? printNextEmptyLine(path, options.originalText)
      : "";

  return concat([
    node.type !== "mappingValue" && hasLeadingComments(node)
      ? concat([join(hardline, path.map(print, "leadingComments")), hardline])
      : "",
    tag,
    tag && anchor ? " " : "",
    anchor,
    tag || anchor
      ? isNode(node, ["sequence", "mapping"]) && !hasMiddleComments(node)
        ? hardline
        : " "
      : "",
    hasMiddleComments(node)
      ? concat([
          node.middleComments.length === 1 ? "" : hardline,
          join(hardline, path.map(print, "middleComments")),
          hardline
        ])
      : "",
    hasPrettierIgnore(path)
      ? concat(
          replaceEndOfLineWith(
            options.originalText.slice(
              node.position.start.offset,
              node.position.end.offset
            ),
            literalline
          )
        )
      : group(_print(node, parentNode, path, options, print)),
    hasTrailingComment(node) && !isNode(node, ["document", "documentHead"])
      ? lineSuffix(
          concat([
            node.type === "mappingValue" && !node.content ? "" : " ",
            parentNode.type === "mappingKey" &&
            path.getParentNode(2).type === "mapping" &&
            isInlineNode(node)
              ? ""
              : breakParent,
            path.call(print, "trailingComment")
          ])
        )
      : "",
    nextEmptyLine,
    hasEndComments(node) && !isNode(node, ["documentHead", "documentBody"])
      ? align(
          node.type === "sequenceItem" ? 2 : 0,
          concat([hardline, join(hardline, path.map(print, "endComments"))])
        )
      : ""
  ]);
}

function _print(node, parentNode, path, options, print) {
  switch (node.type) {
    case "root":
      return concat([
        join(
          hardline,
          path.map((childPath, index) => {
            const document = node.children[index];
            const nextDocument = node.children[index + 1];
            return concat([
              print(childPath),
              shouldPrintDocumentEndMarker(document, nextDocument)
                ? concat([
                    hardline,
                    "...",
                    hasTrailingComment(document)
                      ? concat([" ", path.call(print, "trailingComment")])
                      : ""
                  ])
                : !nextDocument || hasTrailingComment(nextDocument.head)
                ? ""
                : concat([hardline, "---"])
            ]);
          }, "children")
        ),
        node.children.length === 0 ||
        (lastDescendantNode =>
          isNode(lastDescendantNode, ["blockLiteral", "blockFolded"]) &&
          lastDescendantNode.chomping === "keep")(getLastDescendantNode(node))
          ? ""
          : hardline
      ]);
    case "document": {
      const nextDocument = parentNode.children[path.getName() + 1];
      return join(
        hardline,
        [
          shouldPrintDocumentHeadEndMarker(
            node,
            nextDocument,
            parentNode,
            options
          ) === "head"
            ? join(
                hardline,
                [
                  node.head.children.length === 0 &&
                  node.head.endComments.length === 0
                    ? ""
                    : path.call(print, "head"),
                  concat([
                    "---",
                    hasTrailingComment(node.head)
                      ? concat([
                          " ",
                          path.call(print, "head", "trailingComment")
                        ])
                      : ""
                  ])
                ].filter(Boolean)
              )
            : "",
          shouldPrintDocumentBody(node) ? path.call(print, "body") : ""
        ].filter(Boolean)
      );
    }
    case "documentHead":
      return join(
        hardline,
        [].concat(path.map(print, "children"), path.map(print, "endComments"))
      );
    case "documentBody": {
      const children = join(hardline, path.map(print, "children")).parts;
      const endComments = join(hardline, path.map(print, "endComments")).parts;
      const separator =
        children.length === 0 || endComments.length === 0
          ? ""
          : (lastDescendantNode =>
              isNode(lastDescendantNode, ["blockFolded", "blockLiteral"])
                ? lastDescendantNode.chomping === "keep"
                  ? // there's already a newline printed at the end of blockValue (chomping=keep, lastDescendant=true)
                    ""
                  : // an extra newline for better readability
                    concat([hardline, hardline])
                : hardline)(getLastDescendantNode(node));
      return concat([].concat(children, separator, endComments));
    }
    case "directive":
      return concat(["%", join(" ", [node.name].concat(node.parameters))]);
    case "comment":
      return concat(["#", node.value]);
    case "alias":
      return concat(["*", node.value]);
    case "tag":
      return options.originalText.slice(
        node.position.start.offset,
        node.position.end.offset
      );
    case "anchor":
      return concat(["&", node.value]);
    case "plain":
      return printFlowScalarContent(
        node.type,
        options.originalText.slice(
          node.position.start.offset,
          node.position.end.offset
        ),
        options
      );
    case "quoteDouble":
    case "quoteSingle": {
      const singleQuote = "'";
      const doubleQuote = '"';

      const raw = options.originalText.slice(
        node.position.start.offset + 1,
        node.position.end.offset - 1
      );

      if (
        (node.type === "quoteSingle" && raw.includes("\\")) ||
        (node.type === "quoteDouble" && /\\[^"]/.test(raw))
      ) {
        // only quoteDouble can use escape chars
        // and quoteSingle do not need to escape backslashes
        const originalQuote =
          node.type === "quoteDouble" ? doubleQuote : singleQuote;
        return concat([
          originalQuote,
          printFlowScalarContent(node.type, raw, options),
          originalQuote
        ]);
      } else if (raw.includes(doubleQuote)) {
        return concat([
          singleQuote,
          printFlowScalarContent(
            node.type,
            node.type === "quoteDouble"
              ? raw
                  // double quote needs to be escaped by backslash in quoteDouble
                  .replace(/\\"/g, doubleQuote)
                  .replace(/'/g, singleQuote.repeat(2))
              : raw,
            options
          ),
          singleQuote
        ]);
      }

      if (raw.includes(singleQuote)) {
        return concat([
          doubleQuote,
          printFlowScalarContent(
            node.type,
            node.type === "quoteSingle"
              ? // single quote needs to be escaped by 2 single quotes in quoteSingle
                raw.replace(/''/g, singleQuote)
              : raw,
            options
          ),
          doubleQuote
        ]);
      }

      const quote = options.singleQuote ? singleQuote : doubleQuote;
      return concat([
        quote,
        printFlowScalarContent(node.type, raw, options),
        quote
      ]);
    }
    case "blockFolded":
    case "blockLiteral": {
      const parentIndent = getAncestorCount(path, ancestorNode =>
        isNode(ancestorNode, ["sequence", "mapping"])
      );
      const isLastDescendant = isLastDescendantNode(path);
      return concat([
        node.type === "blockFolded" ? ">" : "|",
        node.indent === null ? "" : node.indent.toString(),
        node.chomping === "clip" ? "" : node.chomping === "keep" ? "+" : "-",
        hasIndicatorComment(node)
          ? concat([" ", path.call(print, "indicatorComment")])
          : "",
        (node.indent === null ? dedent : dedentToRoot)(
          align(
            node.indent === null
              ? options.tabWidth
              : node.indent - 1 + parentIndent,
            concat(
              getBlockValueLineContents(node, {
                parentIndent,
                isLastDescendant,
                options
              }).reduce(
                (reduced, lineWords, index, lineContents) =>
                  reduced.concat(
                    index === 0 ? hardline : "",
                    fill(join(line, lineWords).parts),
                    index !== lineContents.length - 1
                      ? lineWords.length === 0
                        ? hardline
                        : markAsRoot(literalline)
                      : node.chomping === "keep" && isLastDescendant
                      ? lineWords.length === 0
                        ? dedentToRoot(hardline)
                        : dedentToRoot(literalline)
                      : ""
                  ),
                []
              )
            )
          )
        )
      ]);
    }
    case "sequence":
      return join(hardline, path.map(print, "children"));
    case "sequenceItem":
      return concat([
        "- ",
        align(2, !node.content ? "" : path.call(print, "content"))
      ]);
    case "mappingKey":
      return !node.content ? "" : path.call(print, "content");
    case "mappingValue":
      return !node.content ? "" : path.call(print, "content");
    case "mapping":
      return join(hardline, path.map(print, "children"));
    case "mappingItem":
    case "flowMappingItem": {
      const isEmptyMappingKey = isEmptyNode(node.key);
      const isEmptyMappingValue = isEmptyNode(node.value);

      if (isEmptyMappingKey && isEmptyMappingValue) {
        return concat([": "]);
      }

      const key = path.call(print, "key");
      const value = path.call(print, "value");

      if (isEmptyMappingValue) {
        return node.type === "flowMappingItem" &&
          parentNode.type === "flowMapping"
          ? key
          : node.type === "mappingItem" &&
            isAbsolutelyPrintedAsSingleLineNode(node.key.content, options) &&
            !hasTrailingComment(node.key.content) &&
            (!parentNode.tag ||
              parentNode.tag.value !== "tag:yaml.org,2002:set")
          ? concat([key, needsSpaceInFrontOfMappingValue(node) ? " " : "", ":"])
          : concat(["? ", align(2, key)]);
      }

      if (isEmptyMappingKey) {
        return concat([": ", align(2, value)]);
      }

      const groupId = Symbol("mappingKey");

      const forceExplicitKey =
        hasLeadingComments(node.value) || !isInlineNode(node.key.content);

      return forceExplicitKey
        ? concat([
            "? ",
            align(2, key),
            hardline,
            join(
              "",
              path
                .map(print, "value", "leadingComments")
                .map(comment => concat([comment, hardline]))
            ),
            ": ",
            align(2, value)
          ])
        : // force singleline
        isSingleLineNode(node.key.content) &&
          !hasLeadingComments(node.key.content) &&
          !hasMiddleComments(node.key.content) &&
          !hasTrailingComment(node.key.content) &&
          !hasEndComments(node.key) &&
          !hasLeadingComments(node.value.content) &&
          !hasMiddleComments(node.value.content) &&
          !hasEndComments(node.value) &&
          isAbsolutelyPrintedAsSingleLineNode(node.value.content, options)
        ? concat([
            key,
            needsSpaceInFrontOfMappingValue(node) ? " " : "",
            ": ",
            value
          ])
        : conditionalGroup([
            concat([
              group(
                concat([ifBreak("? "), group(align(2, key), { id: groupId })])
              ),
              ifBreak(
                concat([hardline, ": ", align(2, value)]),
                indent(
                  concat([
                    needsSpaceInFrontOfMappingValue(node) ? " " : "",
                    ":",
                    hasLeadingComments(node.value.content) ||
                    (hasEndComments(node.value) &&
                      node.value.content &&
                      !isNode(node.value.content, ["mapping", "sequence"])) ||
                    (parentNode.type === "mapping" &&
                      hasTrailingComment(node.key.content) &&
                      isInlineNode(node.value.content)) ||
                    (isNode(node.value.content, ["mapping", "sequence"]) &&
                      node.value.content.tag === null &&
                      node.value.content.anchor === null)
                      ? hardline
                      : !node.value.content
                      ? ""
                      : line,
                    value
                  ])
                ),
                { groupId }
              )
            ])
          ]);
    }
    case "flowMapping":
    case "flowSequence": {
      const openMarker = node.type === "flowMapping" ? "{" : "[";
      const closeMarker = node.type === "flowMapping" ? "}" : "]";
      const bracketSpacing =
        node.type === "flowMapping" &&
        node.children.length !== 0 &&
        options.bracketSpacing
          ? line
          : softline;
      const isLastItemEmptyMappingItem =
        node.children.length !== 0 &&
        (lastItem =>
          lastItem.type === "flowMappingItem" &&
          isEmptyNode(lastItem.key) &&
          isEmptyNode(lastItem.value))(getLast(node.children));
      return concat([
        openMarker,
        indent(
          concat([
            bracketSpacing,
            concat(
              path.map(
                (childPath, index) =>
                  concat([
                    print(childPath),
                    index === node.children.length - 1
                      ? ""
                      : concat([
                          ",",
                          line,
                          node.children[index].position.start.line !==
                          node.children[index + 1].position.start.line
                            ? printNextEmptyLine(
                                childPath,
                                options.originalText
                              )
                            : ""
                        ])
                  ]),
                "children"
              )
            ),
            ifBreak(",", "")
          ])
        ),
        isLastItemEmptyMappingItem ? "" : bracketSpacing,
        closeMarker
      ]);
    }
    case "flowSequenceItem":
      return path.call(print, "content");
    // istanbul ignore next
    default:
      throw new Error(`Unexpected node type ${node.type}`);
  }

  function indent(doc) {
    return docBuilders.align(" ".repeat(options.tabWidth), doc);
  }
}

function align(n, doc) {
  return typeof n === "number" && n > 0
    ? docBuilders.align(" ".repeat(n), doc)
    : docBuilders.align(n, doc);
}

function isInlineNode(node) {
  if (!node) {
    return true;
  }

  switch (node.type) {
    case "plain":
    case "quoteDouble":
    case "quoteSingle":
    case "alias":
    case "flowMapping":
    case "flowSequence":
      return true;
    default:
      return false;
  }
}

function isSingleLineNode(node) {
  if (!node) {
    return true;
  }

  switch (node.type) {
    case "plain":
    case "quoteDouble":
    case "quoteSingle":
      return node.position.start.line === node.position.end.line;
    case "alias":
      return true;
    default:
      return false;
  }
}

function shouldPrintDocumentBody(document) {
  return document.body.children.length !== 0 || hasEndComments(document.body);
}

function shouldPrintDocumentEndMarker(document, nextDocument) {
  return (
    /**
     *... # trailingComment
     */
    hasTrailingComment(document) ||
    (nextDocument &&
      /**
       * ...
       * %DIRECTIVE
       * ---
       */
      (nextDocument.head.children.length !== 0 ||
        /**
         * ...
         * # endComment
         * ---
         */
        hasEndComments(nextDocument.head)))
  );
}

function shouldPrintDocumentHeadEndMarker(
  document,
  nextDocument,
  root,
  options
) {
  if (
    /**
     * ---
     * preserve the first document head end marker
     */
    (root.children[0] === document &&
      /---(\s|$)/.test(
        options.originalText.slice(
          options.locStart(document),
          options.locStart(document) + 4
        )
      )) ||
    /**
     * %DIRECTIVE
     * ---
     */
    document.head.children.length !== 0 ||
    /**
     * # end comment
     * ---
     */
    hasEndComments(document.head) ||
    /**
     * --- # trailing comment
     */
    hasTrailingComment(document.head)
  ) {
    return "head";
  }

  if (shouldPrintDocumentEndMarker(document, nextDocument)) {
    return false;
  }

  return nextDocument ? "root" : false;
}

function isAbsolutelyPrintedAsSingleLineNode(node, options) {
  if (!node) {
    return true;
  }

  switch (node.type) {
    case "plain":
    case "quoteSingle":
    case "quoteDouble":
      break;
    case "alias":
      return true;
    default:
      return false;
  }

  if (options.proseWrap === "preserve") {
    return node.position.start.line === node.position.end.line;
  }

  if (
    // backslash-newline
    /\\$/m.test(
      options.originalText.slice(
        node.position.start.offset,
        node.position.end.offset
      )
    )
  ) {
    return false;
  }

  switch (options.proseWrap) {
    case "never":
      return node.value.indexOf("\n") === -1;
    case "always":
      return !/[\n ]/.test(node.value);
    // istanbul ignore next
    default:
      return false;
  }
}

function needsSpaceInFrontOfMappingValue(node) {
  return node.key.content && node.key.content.type === "alias";
}

function printNextEmptyLine(path, originalText) {
  const node = path.getValue();
  const root = path.stack[0];

  root.isNextEmptyLinePrintedChecklist =
    root.isNextEmptyLinePrintedChecklist || [];

  if (!root.isNextEmptyLinePrintedChecklist[node.position.end.line]) {
    if (isNextLineEmpty(node, originalText)) {
      root.isNextEmptyLinePrintedChecklist[node.position.end.line] = true;
      return softline;
    }
  }

  return "";
}

function printFlowScalarContent(nodeType, content, options) {
  const lineContents = getFlowScalarLineContents(nodeType, content, options);
  return join(
    hardline,
    lineContents.map(lineContentWords =>
      fill(join(line, lineContentWords).parts)
    )
  );
}

function clean(node, newNode /*, parent */) {
  if (isNode(newNode)) {
    delete newNode.position;
    switch (newNode.type) {
      case "comment":
        // insert pragma
        if (isPragma(newNode.value)) {
          return null;
        }
        break;
      case "quoteDouble":
      case "quoteSingle":
        newNode.type = "quote";
        break;
    }
  }
}

module.exports = {
  preprocess,
  print: genericPrint,
  massageAstNode: clean,
  insertPragma
};
