"use strict";

const { getLast } = require("../common/util");

function getAncestorCount(path, filter) {
  let counter = 0;
  const pathStackLength = path.stack.length - 1;
  for (let i = 0; i < pathStackLength; i++) {
    const value = path.stack[i];
    if (isNode(value) && filter(value)) {
      counter++;
    }
  }
  return counter;
}

/**
 * @param {any} value
 * @param {string[]=} types
 */
function isNode(value, types) {
  return (
    value &&
    typeof value.type === "string" &&
    (!types || types.indexOf(value.type) !== -1)
  );
}

function mapNode(node, callback, parent) {
  return callback(
    "children" in node
      ? Object.assign({}, node, {
          children: node.children.map(childNode =>
            mapNode(childNode, callback, node)
          )
        })
      : node,
    parent
  );
}

function defineShortcut(x, key, getter) {
  Object.defineProperty(x, key, {
    get: getter,
    enumerable: false
  });
}

function isNextLineEmpty(node, text) {
  let newlineCount = 0;
  const textLength = text.length;
  for (let i = node.position.end.offset - 1; i < textLength; i++) {
    const char = text[i];

    if (char === "\n") {
      newlineCount++;
    }

    if (newlineCount === 1 && /\S/.test(char)) {
      return false;
    }

    if (newlineCount === 2) {
      return true;
    }
  }

  return false;
}

function isLastDescendantNode(path) {
  const node = path.getValue();

  switch (node.type) {
    case "tag":
    case "anchor":
    case "comment":
      return false;
  }

  const pathStackLength = path.stack.length;

  for (let i = 1; i < pathStackLength; i++) {
    const item = path.stack[i];
    const parentItem = path.stack[i - 1];

    if (
      Array.isArray(parentItem) &&
      typeof item === "number" &&
      item !== parentItem.length - 1
    ) {
      return false;
    }
  }

  return true;
}

function getLastDescendantNode(node) {
  return "children" in node && node.children.length !== 0
    ? getLastDescendantNode(getLast(node.children))
    : node;
}

function isPrettierIgnore(comment) {
  return comment.value.trim() === "prettier-ignore";
}

function hasPrettierIgnore(path) {
  const node = path.getValue();

  if (node.type === "documentBody") {
    const document = path.getParentNode();
    return (
      hasEndComments(document.head) &&
      isPrettierIgnore(getLast(document.head.endComments))
    );
  }

  return (
    hasLeadingComments(node) && isPrettierIgnore(getLast(node.leadingComments))
  );
}

function isEmptyNode(node) {
  return (!node.children || node.children.length === 0) && !hasComments(node);
}

function hasComments(node) {
  return (
    hasLeadingComments(node) ||
    hasMiddleComments(node) ||
    hasIndicatorComment(node) ||
    hasTrailingComment(node) ||
    hasEndComments(node)
  );
}

function hasLeadingComments(node) {
  return node && node.leadingComments && node.leadingComments.length !== 0;
}

function hasMiddleComments(node) {
  return node && node.middleComments && node.middleComments.length !== 0;
}

function hasIndicatorComment(node) {
  return node && node.indicatorComment;
}

function hasTrailingComment(node) {
  return node && node.trailingComment;
}

function hasEndComments(node) {
  return node && node.endComments && node.endComments.length !== 0;
}

/**
 * " a   b c   d e   f " -> [" a   b", "c   d", "e   f "]
 */
function splitWithSingleSpace(text) {
  const parts = [];

  let lastPart = undefined;
  for (const part of text.split(/( +)/g)) {
    if (part !== " ") {
      if (lastPart === " ") {
        parts.push(part);
      } else {
        parts.push((parts.pop() || "") + part);
      }
    } else if (lastPart === undefined) {
      parts.unshift("");
    }

    lastPart = part;
  }

  if (lastPart === " ") {
    parts.push((parts.pop() || "") + " ");
  }

  if (parts[0] === "") {
    parts.shift();
    parts.unshift(" " + (parts.shift() || ""));
  }

  return parts;
}

function getFlowScalarLineContents(nodeType, content, options) {
  const rawLineContents = content
    .split("\n")
    .map((lineContent, index, lineContents) =>
      index === 0 && index === lineContents.length - 1
        ? lineContent
        : index !== 0 && index !== lineContents.length - 1
        ? lineContent.trim()
        : index === 0
        ? lineContent.trimRight()
        : lineContent.trimLeft()
    );

  if (options.proseWrap === "preserve") {
    return rawLineContents.map(lineContent =>
      lineContent.length === 0 ? [] : [lineContent]
    );
  }

  return rawLineContents
    .map(lineContent =>
      lineContent.length === 0 ? [] : splitWithSingleSpace(lineContent)
    )
    .reduce(
      (reduced, lineContentWords, index) =>
        index !== 0 &&
        rawLineContents[index - 1].length !== 0 &&
        lineContentWords.length !== 0 &&
        !// trailing backslash in quoteDouble should be preserved
        (nodeType === "quoteDouble" && getLast(getLast(reduced)).endsWith("\\"))
          ? reduced.concat([reduced.pop().concat(lineContentWords)])
          : reduced.concat([lineContentWords]),
      []
    )
    .map(lineContentWords =>
      options.proseWrap === "never"
        ? [lineContentWords.join(" ")]
        : lineContentWords
    );
}

function getBlockValueLineContents(
  node,
  { parentIndent, isLastDescendant, options }
) {
  const content =
    node.position.start.line === node.position.end.line
      ? ""
      : options.originalText
          .slice(node.position.start.offset, node.position.end.offset)
          // exclude open line `>` or `|`
          .match(/^[^\n]*?\n([\s\S]*)$/)[1];

  const leadingSpaceCount =
    node.indent === null
      ? (match => (match ? match[1].length : Infinity))(
          content.match(/^( *)\S/m)
        )
      : node.indent - 1 + parentIndent;

  const rawLineContents = content
    .split("\n")
    .map(lineContent => lineContent.slice(leadingSpaceCount));

  if (options.proseWrap === "preserve" || node.type === "blockLiteral") {
    return removeUnnecessaryTrailingNewlines(
      rawLineContents.map(lineContent =>
        lineContent.length === 0 ? [] : [lineContent]
      )
    );
  }

  return removeUnnecessaryTrailingNewlines(
    rawLineContents
      .map(lineContent =>
        lineContent.length === 0 ? [] : splitWithSingleSpace(lineContent)
      )
      .reduce(
        (reduced, lineContentWords, index) =>
          index !== 0 &&
          rawLineContents[index - 1].length !== 0 &&
          lineContentWords.length !== 0 &&
          !/^\s/.test(lineContentWords[0]) &&
          !/^\s|\s$/.test(getLast(reduced))
            ? reduced.concat([reduced.pop().concat(lineContentWords)])
            : reduced.concat([lineContentWords]),
        []
      )
      .map(lineContentWords =>
        lineContentWords.reduce(
          (reduced, word) =>
            // disallow trailing spaces
            reduced.length !== 0 && /\s$/.test(getLast(reduced))
              ? reduced.concat(reduced.pop() + " " + word)
              : reduced.concat(word),
          []
        )
      )
      .map(lineContentWords =>
        options.proseWrap === "never"
          ? [lineContentWords.join(" ")]
          : lineContentWords
      )
  );

  function removeUnnecessaryTrailingNewlines(lineContents) {
    if (node.chomping === "keep") {
      return getLast(lineContents).length === 0
        ? lineContents.slice(0, -1)
        : lineContents;
    }

    let trailingNewlineCount = 0;
    for (let i = lineContents.length - 1; i >= 0; i--) {
      if (lineContents[i].length === 0) {
        trailingNewlineCount++;
      } else {
        break;
      }
    }

    return trailingNewlineCount === 0
      ? lineContents
      : trailingNewlineCount >= 2 && !isLastDescendant
      ? // next empty line
        lineContents.slice(0, -(trailingNewlineCount - 1))
      : lineContents.slice(0, -trailingNewlineCount);
  }
}

module.exports = {
  getLast,
  getAncestorCount,
  isNode,
  isEmptyNode,
  mapNode,
  defineShortcut,
  isNextLineEmpty,
  isLastDescendantNode,
  getBlockValueLineContents,
  getFlowScalarLineContents,
  getLastDescendantNode,
  hasPrettierIgnore,
  hasLeadingComments,
  hasMiddleComments,
  hasIndicatorComment,
  hasTrailingComment,
  hasEndComments
};
