"use strict";

const { getOrderedListItemInfo, mapAst, splitText } = require("./utils");

// 0x0 ~ 0x10ffff
// eslint-disable-next-line no-control-regex
const isSingleCharRegex = /^([\u0000-\uffff]|[\ud800-\udbff][\udc00-\udfff])$/;

function preprocess(ast, options) {
  ast = restoreUnescapedCharacter(ast, options);
  ast = mergeContinuousTexts(ast);
  ast = transformInlineCode(ast);
  ast = transformIndentedCodeblockAndMarkItsParentList(ast, options);
  ast = markAlignedList(ast, options);
  ast = splitTextIntoSentences(ast, options);
  ast = transformImportExport(ast);
  ast = mergeContinuousImportExport(ast);
  return ast;
}

function transformImportExport(ast) {
  return mapAst(ast, node => {
    if (node.type !== "import" && node.type !== "export") {
      return node;
    }

    return Object.assign({}, node, { type: "importExport" });
  });
}

function transformInlineCode(ast) {
  return mapAst(ast, node => {
    if (node.type !== "inlineCode") {
      return node;
    }

    return Object.assign({}, node, {
      value: node.value.replace(/\s+/g, " ")
    });
  });
}

function restoreUnescapedCharacter(ast, options) {
  return mapAst(ast, node => {
    return node.type !== "text"
      ? node
      : Object.assign({}, node, {
          value:
            node.value !== "*" &&
            node.value !== "_" &&
            node.value !== "$" && // handle these cases in printer
            isSingleCharRegex.test(node.value) &&
            node.position.end.offset - node.position.start.offset !==
              node.value.length
              ? options.originalText.slice(
                  node.position.start.offset,
                  node.position.end.offset
                )
              : node.value
        });
  });
}

function mergeContinuousImportExport(ast) {
  return mergeChildren(
    ast,
    (prevNode, node) =>
      prevNode.type === "importExport" && node.type === "importExport",
    (prevNode, node) => ({
      type: "importExport",
      value: prevNode.value + "\n\n" + node.value,
      position: {
        start: prevNode.position.start,
        end: node.position.end
      }
    })
  );
}

function mergeChildren(ast, shouldMerge, mergeNode) {
  return mapAst(ast, node => {
    if (!node.children) {
      return node;
    }
    const children = node.children.reduce((current, child) => {
      const lastChild = current[current.length - 1];
      if (lastChild && shouldMerge(lastChild, child)) {
        current.splice(-1, 1, mergeNode(lastChild, child));
      } else {
        current.push(child);
      }
      return current;
    }, []);
    return Object.assign({}, node, { children });
  });
}

function mergeContinuousTexts(ast) {
  return mergeChildren(
    ast,
    (prevNode, node) => prevNode.type === "text" && node.type === "text",
    (prevNode, node) => ({
      type: "text",
      value: prevNode.value + node.value,
      position: {
        start: prevNode.position.start,
        end: node.position.end
      }
    })
  );
}

function splitTextIntoSentences(ast, options) {
  return mapAst(ast, (node, index, [parentNode]) => {
    if (node.type !== "text") {
      return node;
    }

    let value = node.value;

    if (parentNode.type === "paragraph") {
      if (index === 0) {
        value = value.trimLeft();
      }
      if (index === parentNode.children.length - 1) {
        value = value.trimRight();
      }
    }

    return {
      type: "sentence",
      position: node.position,
      children: splitText(value, options)
    };
  });
}

function transformIndentedCodeblockAndMarkItsParentList(ast, options) {
  return mapAst(ast, (node, index, parentStack) => {
    if (node.type === "code") {
      // the first char may point to `\n`, e.g. `\n\t\tbar`, just ignore it
      const isIndented = /^\n?( {4,}|\t)/.test(
        options.originalText.slice(
          node.position.start.offset,
          node.position.end.offset
        )
      );

      node.isIndented = isIndented;

      if (isIndented) {
        for (let i = 0; i < parentStack.length; i++) {
          const parent = parentStack[i];

          // no need to check checked items
          if (parent.hasIndentedCodeblock) {
            break;
          }

          if (parent.type === "list") {
            parent.hasIndentedCodeblock = true;
          }
        }
      }
    }
    return node;
  });
}

function markAlignedList(ast, options) {
  return mapAst(ast, (node, index, parentStack) => {
    if (node.type === "list" && node.children.length !== 0) {
      // if one of its parents is not aligned, it's not possible to be aligned in sub-lists
      for (let i = 0; i < parentStack.length; i++) {
        const parent = parentStack[i];
        if (parent.type === "list" && !parent.isAligned) {
          node.isAligned = false;
          return node;
        }
      }

      node.isAligned = isAligned(node);
    }

    return node;
  });

  function getListItemStart(listItem) {
    return listItem.children.length === 0
      ? -1
      : listItem.children[0].position.start.column - 1;
  }

  function isAligned(list) {
    if (!list.ordered) {
      /**
       * - 123
       * - 123
       */
      return true;
    }

    const [firstItem, secondItem] = list.children;

    const firstInfo = getOrderedListItemInfo(firstItem, options.originalText);

    if (firstInfo.leadingSpaces.length > 1) {
      /**
       * 1.   123
       *
       * 1.   123
       * 1. 123
       */
      return true;
    }

    const firstStart = getListItemStart(firstItem);

    if (firstStart === -1) {
      /**
       * 1.
       *
       * 1.
       * 1.
       */
      return false;
    }

    if (list.children.length === 1) {
      /**
       * aligned:
       *
       * 11. 123
       *
       * not aligned:
       *
       * 1. 123
       */
      return firstStart % options.tabWidth === 0;
    }

    const secondStart = getListItemStart(secondItem);

    if (firstStart !== secondStart) {
      /**
       * 11. 123
       * 1. 123
       *
       * 1. 123
       * 11. 123
       */
      return false;
    }

    if (firstStart % options.tabWidth === 0) {
      /**
       * 11. 123
       * 12. 123
       */
      return true;
    }

    /**
     * aligned:
     *
     * 11. 123
     * 1.  123
     *
     * not aligned:
     *
     * 1. 123
     * 2. 123
     */
    const secondInfo = getOrderedListItemInfo(secondItem, options.originalText);
    return secondInfo.leadingSpaces.length > 1;
  }
}

module.exports = preprocess;
