"use strict";

const _ = require("lodash");
const addEmptyLineBefore = require("../../utils/addEmptyLineBefore");
const getPreviousNonSharedLineCommentNode = require("../../utils/getPreviousNonSharedLineCommentNode");
const hasEmptyLine = require("../../utils/hasEmptyLine");
const isAfterComment = require("../../utils/isAfterComment");
const isBlocklessAtRuleAfterBlocklessAtRule = require("../../utils/isBlocklessAtRuleAfterBlocklessAtRule");
const isBlocklessAtRuleAfterSameNameBlocklessAtRule = require("../../utils/isBlocklessAtRuleAfterSameNameBlocklessAtRule");
const isFirstNested = require("../../utils/isFirstNested");
const isFirstNodeOfRoot = require("../../utils/isFirstNodeOfRoot");
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 = "at-rule-empty-line-before";

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

const rule = function(expectation, options, context) {
  return (root, result) => {
    const validOptions = validateOptions(
      result,
      ruleName,
      {
        actual: expectation,
        possible: ["always", "never"]
      },
      {
        actual: options,
        possible: {
          except: [
            "after-same-name",
            "inside-block",
            "blockless-after-same-name-blockless",
            "blockless-after-blockless",
            "first-nested"
          ],
          ignore: [
            "after-comment",
            "inside-block",
            "blockless-after-same-name-blockless",
            "blockless-after-blockless"
          ],
          ignoreAtRules: [_.isString]
        },
        optional: true
      }
    );
    if (!validOptions) {
      return;
    }

    root.walkAtRules(atRule => {
      const isNested = atRule.parent.type !== "root";
      // Ignore the first node
      if (isFirstNodeOfRoot(atRule)) {
        return;
      }

      // Return early if at-rule is to be ignored
      if (optionsMatches(options, "ignoreAtRules", atRule.name)) {
        return;
      }

      // Optionally ignore the expectation if the node is blockless
      if (
        optionsMatches(options, "ignore", "blockless-after-blockless") &&
        isBlocklessAtRuleAfterBlocklessAtRule(atRule)
      ) {
        return;
      }

      // Optionally ignore the expection if the node is blockless
      // and following another blockless at-rule with the same name
      if (
        optionsMatches(
          options,
          "ignore",
          "blockless-after-same-name-blockless"
        ) &&
        isBlocklessAtRuleAfterSameNameBlocklessAtRule(atRule)
      ) {
        return;
      }

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

      // Optionally ignore the expectation if a comment precedes this node
      if (
        optionsMatches(options, "ignore", "after-comment") &&
        isAfterComment(atRule)
      ) {
        return;
      }

      const hasEmptyLineBefore = hasEmptyLine(atRule.raws.before);
      let expectEmptyLineBefore = expectation === "always" ? true : false;

      // Optionally reverse the expectation if any exceptions apply
      if (
        (optionsMatches(options, "except", "after-same-name") &&
          isAtRuleAfterSameNameAtRule(atRule)) ||
        (optionsMatches(options, "except", "inside-block") && isNested) ||
        (optionsMatches(options, "except", "first-nested") &&
          isFirstNested(atRule)) ||
        (optionsMatches(options, "except", "blockless-after-blockless") &&
          isBlocklessAtRuleAfterBlocklessAtRule(atRule)) ||
        (optionsMatches(
          options,
          "except",
          "blockless-after-same-name-blockless"
        ) &&
          isBlocklessAtRuleAfterSameNameBlocklessAtRule(atRule))
      ) {
        expectEmptyLineBefore = !expectEmptyLineBefore;
      }

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

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

        return;
      }

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

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

function isAtRuleAfterSameNameAtRule(atRule) {
  const previousNode = getPreviousNonSharedLineCommentNode(atRule);
  return (
    previousNode &&
    previousNode.type === "atrule" &&
    previousNode.name === atRule.name
  );
}

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