"use strict";

const addEmptyLineBefore = require("../../utils/addEmptyLineBefore");
const getPreviousNonSharedLineCommentNode = require("../../utils/getPreviousNonSharedLineCommentNode");
const hasEmptyLine = require("../../utils/hasEmptyLine");
const isAfterSingleLineComment = require("../../utils/isAfterSingleLineComment");
const isFirstNested = require("../../utils/isFirstNested");
const isFirstNodeOfRoot = require("../../utils/isFirstNodeOfRoot");
const isSingleLineString = require("../../utils/isSingleLineString");
const isStandardSyntaxRule = require("../../utils/isStandardSyntaxRule");
const optionsMatches = require("../../utils/optionsMatches");
const removeEmptyLinesBefore = require("../../utils/removeEmptyLinesBefore");
const report = require("../../utils/report");
const ruleMessages = require("../../utils/ruleMessages");
const validateOptions = require("../../utils/validateOptions");

const ruleName = "rule-empty-line-before";

const messages = ruleMessages(ruleName, {
  expected: "Expected empty line before rule",
  rejected: "Unexpected empty line before rule"
});

const rule = function(expectation, options, context) {
  return (root, result) => {
    const validOptions = validateOptions(
      result,
      ruleName,
      {
        actual: expectation,
        possible: ["always", "never", "always-multi-line", "never-multi-line"]
      },
      {
        actual: options,
        possible: {
          ignore: ["after-comment", "inside-block"],
          except: [
            "after-rule",
            "after-single-line-comment",
            "first-nested",
            "inside-block-and-after-rule",
            "inside-block"
          ]
        },
        optional: true
      }
    );

    if (!validOptions) {
      return;
    }

    root.walkRules(rule => {
      if (!isStandardSyntaxRule(rule)) {
        return;
      }

      // Ignore the first node
      if (isFirstNodeOfRoot(rule)) {
        return;
      }

      // Optionally ignore the expectation if a comment precedes this node
      if (
        optionsMatches(options, "ignore", "after-comment") &&
        rule.prev() &&
        rule.prev().type === "comment"
      ) {
        return;
      }

      const isNested = rule.parent.type !== "root";

      // Optionally ignore the expectation if inside a block
      if (optionsMatches(options, "ignore", "inside-block") && isNested) {
        return;
      }

      // Ignore if the expectation is for multiple and the rule is single-line
      if (
        expectation.indexOf("multi-line") !== -1 &&
        isSingleLineString(rule.toString())
      ) {
        return;
      }

      let expectEmptyLineBefore =
        expectation.indexOf("always") !== -1 ? true : false;

      // Optionally reverse the expectation if any exceptions apply
      if (
        (optionsMatches(options, "except", "first-nested") &&
          isFirstNested(rule)) ||
        (optionsMatches(options, "except", "after-rule") &&
          isAfterRule(rule)) ||
        (optionsMatches(options, "except", "inside-block-and-after-rule") &&
          isNested &&
          isAfterRule(rule)) ||
        (optionsMatches(options, "except", "after-single-line-comment") &&
          isAfterSingleLineComment(rule)) ||
        (optionsMatches(options, "except", "inside-block") && isNested)
      ) {
        expectEmptyLineBefore = !expectEmptyLineBefore;
      }

      const hasEmptyLineBefore = hasEmptyLine(rule.raws.before);

      // Return if the expectation is met
      if (expectEmptyLineBefore === hasEmptyLineBefore) {
        return;
      }

      // Fix
      if (context.fix) {
        if (expectEmptyLineBefore) {
          addEmptyLineBefore(rule, context.newline);
        } else {
          removeEmptyLinesBefore(rule, context.newline);
        }

        return;
      }

      const message = expectEmptyLineBefore
        ? messages.expected
        : messages.rejected;

      report({
        message,
        node: rule,
        result,
        ruleName
      });
    });
  };
};

function isAfterRule(rule) {
  const prevNode = getPreviousNonSharedLineCommentNode(rule);
  return prevNode && prevNode.type === "rule";
}

rule.ruleName = ruleName;
rule.messages = messages;
module.exports = rule;
