import { utils } from "stylelint";
import { isString, isFinite, isBoolean } from "lodash";
import { namespace } from "../../utils";

export const ruleName = namespace("no-duplicate-dollar-variables");

export const messages = utils.ruleMessages(ruleName, {
  rejected: variable => `Unexpected duplicate dollar variable ${variable}`
});

export default function(value, secondaryOptions) {
  return (root, result) => {
    const validOptions = utils.validateOptions(
      result,
      ruleName,
      {
        actual: value
      },
      {
        actual: secondaryOptions,
        possible: {
          ignoreInside: ["at-rule", "nested-at-rule"],
          ignoreInsideAtRules: [isString],
          ignoreDefaults: [isBoolean, isFinite]
        },
        optional: true
      }
    );

    if (!validOptions) {
      return;
    }

    const vars = {};

    /**
     * Traverse the [vars] tree through the path defined by [ancestors], creating nodes as needed.
     * @param {*} ancestors
     * @returns the tree of the node defined by the last of [ancestors].
     */
    function getScope(ancestors) {
      let scope = vars;

      for (const node of ancestors) {
        if (!(node in scope)) {
          scope[node] = {};
        }

        scope = scope[node];
      }

      return scope;
    }

    /**
     * Iterates through the ancestors while checking each scope until the [variable] is found.
     * If not found, an object with empty values is returned.
     * @param {*} ancestors
     * @param {string} variable the variable name.
     * @returns The previosly declared variable data or an object with empty values.
     */
    function getVariableData(ancestors, variable) {
      let scope = vars;

      for (const node of ancestors) {
        scope = scope[node];

        if (scope[variable]) {
          return scope[variable];
        }
      }

      return {
        defaultCount: 0,
        isDeclared: false
      };
    }

    /**
     * Checks whether the given [variableData] is declared.
     * @param {{ defaultCount: number; isDeclared: boolean; }} variableData the variable data
     * containing default count and if the variable is declared.
     * @param {boolean} isDefault if the variable contains the `!default` keyword.
     * @param {boolean | number} ignoreDefaults the ignore defaults options.
     * @returns true if declared.
     */
    function isDeclared(variableData, isDefault, ignoreDefaults) {
      if (isDefault && ignoreDefaults !== undefined) {
        if (isFinite(ignoreDefaults)) {
          return variableData.defaultCount >= ignoreDefaults;
        } else if (ignoreDefaults) {
          return false;
        }
      }

      return variableData.isDeclared;
    }

    /**
     * Processes the variable data based on the given arguments.
     * @param {{ defaultCount: number; isDeclared: boolean; }} variableData the variable data
     * containing default count and if the variable is declared.
     * @param {boolean} isDefault if the variable contains the `!default` keyword.
     * @param {boolean | number} ignoreDefaults the ignore defaults options.
     * @returns the updated `variableData`.
     */
    function processVariableData(variableData, isDefault, ignoreDefaults) {
      if (isDefault && ignoreDefaults !== undefined) {
        return {
          defaultCount: ++variableData.defaultCount,
          isDeclared: variableData.isDeclared
        };
      }

      return {
        defaultCount: variableData.defaultCount,
        isDeclared: true
      };
    }

    const ignoreDefaults =
      secondaryOptions && secondaryOptions.ignoreDefaults
        ? secondaryOptions.ignoreDefaults
        : undefined;

    root.walkDecls(decl => {
      const isVar = decl.prop[0] === "$";
      const isInsideIgnoredAtRule =
        decl.parent.type === "atrule" &&
        secondaryOptions &&
        secondaryOptions.ignoreInside &&
        secondaryOptions.ignoreInside === "at-rule";
      const isInsideIgnoredNestedAtRule =
        decl.parent.type === "atrule" &&
        decl.parent.parent.type !== "root" &&
        secondaryOptions &&
        secondaryOptions.ignoreInside &&
        secondaryOptions.ignoreInside === "nested-at-rule";
      const isInsideIgnoredSpecifiedAtRule =
        decl.parent.type === "atrule" &&
        secondaryOptions &&
        secondaryOptions.ignoreInsideAtRules &&
        secondaryOptions.ignoreInsideAtRules.includes(decl.parent.name);

      if (
        !isVar ||
        isInsideIgnoredAtRule ||
        isInsideIgnoredNestedAtRule ||
        isInsideIgnoredSpecifiedAtRule
      ) {
        return;
      }

      const ancestors = [];
      let parent = decl.parent;

      while (parent !== null && parent !== undefined) {
        const parentKey = parent.toString();

        ancestors.unshift(parentKey);
        parent = parent.parent;
      }

      const scope = getScope(ancestors);
      const isDefault = /!default/.test(decl.value);
      const variableData = getVariableData(ancestors, decl.prop);

      if (isDeclared(variableData, isDefault, ignoreDefaults)) {
        utils.report({
          message: messages.rejected(decl.prop),
          node: decl,
          result,
          ruleName
        });
      }

      scope[decl.prop] = processVariableData(
        variableData,
        isDefault,
        ignoreDefaults
      );
    });
  };
}

@@ -3259,97 +3259,60 @@
     
-if (isDefault && ignoreDefaults !== undefined) %7B%0A        return %7B%0A          defaultCount:
+return %7B%0A        defaultCount: isDefault%0A          ?
  ++v
@@ -3327,33 +3327,32 @@
 ata.defaultCount
-,
 %0A          isDec
@@ -3346,26 +3346,16 @@
         
-isDeclared
 : variab
@@ -3353,32 +3353,54 @@
  : variableData.
+defaultCount,%0A        
 isDeclared%0A     
@@ -3393,16 +3393,17 @@
 Declared
+:
 %0A       
@@ -3407,102 +3407,102 @@
     
-%7D;%0A      %7D%0A%0A      return %7B%0A        defaultCount: variableData.defaultCount,%0A        isDeclared
+  isDefault && ignoreDefaults !== undefined%0A            ? variableData.isDeclared%0A            
 : tr

increment count anyway even if ignoreDefaults is not provided