"use strict";

const {
  concat,
  join,
  softline,
  hardline,
  line,
  group,
  indent,
  ifBreak
} = require("../doc").builders;

// http://w3c.github.io/html/single-page.html#void-elements
const voidTags = [
  "area",
  "base",
  "br",
  "col",
  "embed",
  "hr",
  "img",
  "input",
  "link",
  "meta",
  "param",
  "source",
  "track",
  "wbr"
];

// Formatter based on @glimmerjs/syntax's built-in test formatter:
// https://github.com/glimmerjs/glimmer-vm/blob/master/packages/%40glimmer/syntax/lib/generation/print.ts

function printChildren(path, options, print) {
  return concat(
    path.map((childPath, childIndex) => {
      const childNode = path.getValue();
      const isFirstNode = childIndex === 0;
      const isLastNode =
        childIndex == path.getParentNode(0).children.length - 1;
      const isLastNodeInMultiNodeList = isLastNode && !isFirstNode;
      const isWhitespace = isWhitespaceNode(childNode);

      if (isWhitespace && isLastNodeInMultiNodeList) {
        return print(childPath, options, print);
      } else if (isFirstNode) {
        return concat([softline, print(childPath, options, print)]);
      }
      return print(childPath, options, print);
    }, "children")
  );
}

function print(path, options, print) {
  const n = path.getValue();

  /* istanbul ignore if*/
  if (!n) {
    return "";
  }

  switch (n.type) {
    case "Block":
    case "Program":
    case "Template": {
      return group(concat(path.map(print, "body").filter(text => text !== "")));
    }
    case "ElementNode": {
      const tagFirstChar = n.tag[0];
      const isLocal = n.tag.indexOf(".") !== -1;
      const isGlimmerComponent =
        tagFirstChar.toUpperCase() === tagFirstChar || isLocal;
      const hasChildren = n.children.length > 0;

      const hasNonWhitespaceChildren = n.children.some(
        n => !isWhitespaceNode(n)
      );

      const isVoid =
        (isGlimmerComponent && (!hasChildren || !hasNonWhitespaceChildren)) ||
        voidTags.indexOf(n.tag) !== -1;
      const closeTagForNoBreak = isVoid ? concat([" />", softline]) : ">";
      const closeTagForBreak = isVoid ? "/>" : ">";
      const getParams = (path, print) =>
        indent(
          concat([
            n.attributes.length ? line : "",
            join(line, path.map(print, "attributes")),

            n.modifiers.length ? line : "",
            join(line, path.map(print, "modifiers")),

            n.comments.length ? line : "",
            join(line, path.map(print, "comments"))
          ])
        );

      const nextNode = getNextNode(path);

      return concat([
        group(
          concat([
            "<",
            n.tag,
            getParams(path, print),
            n.blockParams.length ? ` as |${n.blockParams.join(" ")}|` : "",
            ifBreak(softline, ""),
            ifBreak(closeTagForBreak, closeTagForNoBreak)
          ])
        ),
        !isVoid
          ? group(
              concat([
                hasNonWhitespaceChildren
                  ? indent(printChildren(path, options, print))
                  : "",
                ifBreak(hasChildren ? hardline : "", ""),
                concat(["</", n.tag, ">"])
              ])
            )
          : "",
        nextNode && nextNode.type === "ElementNode" ? hardline : ""
      ]);
    }
    case "BlockStatement": {
      const pp = path.getParentNode(1);
      const isElseIf =
        pp &&
        pp.inverse &&
        pp.inverse.body.length === 1 &&
        pp.inverse.body[0] === n &&
        pp.inverse.body[0].path.parts[0] === "if";
      const hasElseIf =
        n.inverse &&
        n.inverse.body.length === 1 &&
        n.inverse.body[0].type === "BlockStatement" &&
        n.inverse.body[0].path.parts[0] === "if";
      const indentElse = hasElseIf ? a => a : indent;
      if (n.inverse) {
        return concat([
          isElseIf
            ? concat(["{{else ", printPathParams(path, print), "}}"])
            : printOpenBlock(path, print),
          indent(concat([hardline, path.call(print, "program")])),
          n.inverse && !hasElseIf ? concat([hardline, "{{else}}"]) : "",
          n.inverse
            ? indentElse(concat([hardline, path.call(print, "inverse")]))
            : "",
          isElseIf ? "" : concat([hardline, printCloseBlock(path, print)])
        ]);
      } else if (isElseIf) {
        return concat([
          concat(["{{else ", printPathParams(path, print), "}}"]),
          indent(concat([hardline, path.call(print, "program")]))
        ]);
      }

      const hasNonWhitespaceChildren = n.program.body.some(
        n => !isWhitespaceNode(n)
      );

      return concat([
        printOpenBlock(path, print),
        group(
          concat([
            indent(concat([softline, path.call(print, "program")])),
            hasNonWhitespaceChildren ? hardline : softline,
            printCloseBlock(path, print)
          ])
        )
      ]);
    }
    case "ElementModifierStatement":
    case "MustacheStatement": {
      const pp = path.getParentNode(1);
      const isConcat = pp && pp.type === "ConcatStatement";
      return group(
        concat([
          n.escaped === false ? "{{{" : "{{",
          printPathParams(path, print, { group: false }),
          isConcat ? "" : softline,
          n.escaped === false ? "}}}" : "}}"
        ])
      );
    }
    case "SubExpression": {
      const params = getParams(path, print);
      const printedParams =
        params.length > 0
          ? indent(concat([line, group(join(line, params))]))
          : "";
      return group(
        concat(["(", printPath(path, print), printedParams, softline, ")"])
      );
    }
    case "AttrNode": {
      const isText = n.value.type === "TextNode";
      if (isText && n.value.loc.start.column === n.value.loc.end.column) {
        return concat([n.name]);
      }
      const value = path.call(print, "value");
      const quotedValue = isText
        ? printStringLiteral(value.parts.join(), options)
        : value;
      return concat([n.name, "=", quotedValue]);
    }
    case "ConcatStatement": {
      return concat([
        '"',
        group(
          indent(
            join(
              softline,
              path
                .map(partPath => print(partPath), "parts")
                .filter(a => a !== "")
            )
          )
        ),
        '"'
      ]);
    }
    case "Hash": {
      return concat([join(line, path.map(print, "pairs"))]);
    }
    case "HashPair": {
      return concat([n.key, "=", path.call(print, "value")]);
    }
    case "TextNode": {
      const maxLineBreaksToPreserve = 2;
      const isFirstElement = !getPreviousNode(path);
      const isLastElement = !getNextNode(path);
      const isWhitespaceOnly = !/\S/.test(n.chars);
      const lineBreaksCount = countNewLines(n.chars);
      const hasBlockParent = path.getParentNode(0).type === "Block";
      const hasElementParent = path.getParentNode(0).type === "ElementNode";
      const hasTemplateParent = path.getParentNode(0).type === "Template";

      let leadingLineBreaksCount = countLeadingNewLines(n.chars);
      let trailingLineBreaksCount = countTrailingNewLines(n.chars);

      if (
        (isFirstElement || isLastElement) &&
        isWhitespaceOnly &&
        (hasBlockParent || hasElementParent || hasTemplateParent)
      ) {
        return "";
      }

      if (isWhitespaceOnly && lineBreaksCount) {
        leadingLineBreaksCount = Math.min(
          lineBreaksCount,
          maxLineBreaksToPreserve
        );
        trailingLineBreaksCount = 0;
      } else {
        if (
          isNextNodeOfType(path, "ElementNode") ||
          isNextNodeOfType(path, "BlockStatement")
        ) {
          trailingLineBreaksCount = Math.max(trailingLineBreaksCount, 1);
        }

        if (
          isPreviousNodeOfSomeType(path, ["ElementNode"]) ||
          isPreviousNodeOfSomeType(path, ["BlockStatement"])
        ) {
          leadingLineBreaksCount = Math.max(leadingLineBreaksCount, 1);
        }
      }

      let leadingSpace = "";
      let trailingSpace = "";

      // preserve a space inside of an attribute node where whitespace present,
      // when next to mustache statement.
      const inAttrNode = path.stack.indexOf("attributes") >= 0;
      if (inAttrNode) {
        const parentNode = path.getParentNode(0);
        const isConcat = parentNode.type === "ConcatStatement";
        if (isConcat) {
          const parts = parentNode.parts;
          const partIndex = parts.indexOf(n);
          if (partIndex > 0) {
            const partType = parts[partIndex - 1].type;
            const isMustache = partType === "MustacheStatement";
            if (isMustache) {
              leadingSpace = " ";
            }
          }
          if (partIndex < parts.length - 1) {
            const partType = parts[partIndex + 1].type;
            const isMustache = partType === "MustacheStatement";
            if (isMustache) {
              trailingSpace = " ";
            }
          }
        }
      } else {
        if (
          trailingLineBreaksCount === 0 &&
          isNextNodeOfType(path, "MustacheStatement")
        ) {
          trailingSpace = " ";
        }

        if (
          leadingLineBreaksCount === 0 &&
          isPreviousNodeOfSomeType(path, ["MustacheStatement"])
        ) {
          leadingSpace = " ";
        }

        if (isFirstElement) {
          leadingLineBreaksCount = 0;
          leadingSpace = "";
        }

        if (isLastElement) {
          trailingLineBreaksCount = 0;
          trailingSpace = "";
        }
      }

      return concat(
        [
          ...generateHardlines(leadingLineBreaksCount, maxLineBreaksToPreserve),
          n.chars
            .replace(/^[\s ]+/g, leadingSpace)
            .replace(/[\s ]+$/, trailingSpace),
          ...generateHardlines(trailingLineBreaksCount, maxLineBreaksToPreserve)
        ].filter(Boolean)
      );
    }
    case "MustacheCommentStatement": {
      const dashes = n.value.indexOf("}}") > -1 ? "--" : "";
      return concat(["{{!", dashes, n.value, dashes, "}}"]);
    }
    case "PathExpression": {
      return n.original;
    }
    case "BooleanLiteral": {
      return String(n.value);
    }
    case "CommentStatement": {
      return concat(["<!--", n.value, "-->"]);
    }
    case "StringLiteral": {
      return printStringLiteral(n.value, options);
    }
    case "NumberLiteral": {
      return String(n.value);
    }
    case "UndefinedLiteral": {
      return "undefined";
    }
    case "NullLiteral": {
      return "null";
    }

    /* istanbul ignore next */
    default:
      throw new Error("unknown glimmer type: " + JSON.stringify(n.type));
  }
}

/**
 * Prints a string literal with the correct surrounding quotes based on
 * `options.singleQuote` and the number of escaped quotes contained in
 * the string literal. This function is the glimmer equivalent of `printString`
 * in `common/util`, but has differences because of the way escaped characters
 * are treated in hbs string literals.
 * @param {string} stringLiteral - the string literal value
 * @param {object} options - the prettier options object
 */
function printStringLiteral(stringLiteral, options) {
  const double = { quote: '"', regex: /"/g };
  const single = { quote: "'", regex: /'/g };

  const preferred = options.singleQuote ? single : double;
  const alternate = preferred === single ? double : single;

  let shouldUseAlternateQuote = false;

  // If `stringLiteral` contains at least one of the quote preferred for
  // enclosing the string, we might want to enclose with the alternate quote
  // instead, to minimize the number of escaped quotes.
  if (
    stringLiteral.includes(preferred.quote) ||
    stringLiteral.includes(alternate.quote)
  ) {
    const numPreferredQuotes = (stringLiteral.match(preferred.regex) || [])
      .length;
    const numAlternateQuotes = (stringLiteral.match(alternate.regex) || [])
      .length;

    shouldUseAlternateQuote = numPreferredQuotes > numAlternateQuotes;
  }

  const enclosingQuote = shouldUseAlternateQuote ? alternate : preferred;
  const escapedStringLiteral = stringLiteral.replace(
    enclosingQuote.regex,
    `\\${enclosingQuote.quote}`
  );

  return `${enclosingQuote.quote}${escapedStringLiteral}${enclosingQuote.quote}`;
}

function printPath(path, print) {
  return path.call(print, "path");
}

function getParams(path, print) {
  const node = path.getValue();
  let parts = [];

  if (node.params.length > 0) {
    parts = parts.concat(path.map(print, "params"));
  }

  if (node.hash && node.hash.pairs.length > 0) {
    parts.push(path.call(print, "hash"));
  }
  return parts;
}

function printPathParams(path, print, options) {
  let parts = [];
  options = Object.assign({ group: true }, options || {});

  parts.push(printPath(path, print));
  parts = parts.concat(getParams(path, print));

  if (!options.group) {
    return indent(join(line, parts));
  }

  return indent(group(join(line, parts)));
}

function printBlockParams(path) {
  const block = path.getValue();
  if (!block.program || !block.program.blockParams.length) {
    return "";
  }
  return concat([" as |", block.program.blockParams.join(" "), "|"]);
}

function printOpenBlock(path, print) {
  return group(
    concat([
      "{{#",
      printPathParams(path, print),
      printBlockParams(path),
      softline,
      "}}"
    ])
  );
}

function printCloseBlock(path, print) {
  return concat(["{{/", path.call(print, "path"), "}}"]);
}

function isWhitespaceNode(node) {
  return node.type === "TextNode" && !/\S/.test(node.chars);
}

function getPreviousNode(path) {
  const node = path.getValue();
  const parentNode = path.getParentNode(0);

  const children = parentNode.children || parentNode.body;
  if (children) {
    const nodeIndex = children.indexOf(node);
    if (nodeIndex > 0) {
      const previousNode = children[nodeIndex - 1];
      return previousNode;
    }
  }
}

function getNextNode(path) {
  const node = path.getValue();
  const parentNode = path.getParentNode(0);

  const children = parentNode.children || parentNode.body;
  if (children) {
    const nodeIndex = children.indexOf(node);
    if (nodeIndex < children.length) {
      const nextNode = children[nodeIndex + 1];
      return nextNode;
    }
  }
}

function isPreviousNodeOfSomeType(path, types) {
  const previousNode = getPreviousNode(path);

  if (previousNode) {
    return types.some(type => previousNode.type === type);
  }
  return false;
}

function isNextNodeOfType(path, type) {
  const nextNode = getNextNode(path);
  return nextNode && nextNode.type === type;
}

function clean(ast, newObj) {
  delete newObj.loc;
  delete newObj.selfClosing;

  // (Glimmer/HTML) ignore TextNode whitespace
  if (ast.type === "TextNode") {
    if (ast.chars.replace(/\s+/, "") === "") {
      return null;
    }
    newObj.chars = ast.chars.replace(/^\s+/, "").replace(/\s+$/, "");
  }
}

function countNewLines(string) {
  /* istanbul ignore next */
  string = typeof string === "string" ? string : "";
  return string.split("\n").length - 1;
}

function countLeadingNewLines(string) {
  /* istanbul ignore next */
  string = typeof string === "string" ? string : "";
  const newLines = (string.match(/^([^\S\r\n]*[\r\n])+/g) || [])[0] || "";
  return countNewLines(newLines);
}

function countTrailingNewLines(string) {
  /* istanbul ignore next */
  string = typeof string === "string" ? string : "";
  const newLines = (string.match(/([\r\n][^\S\r\n]*)+$/g) || [])[0] || "";
  return countNewLines(newLines);
}

function generateHardlines(number = 0, max = 0) {
  return new Array(Math.min(number, max)).fill(hardline);
}

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