"use strict";

const _ = require("lodash");
const beforeBlockString = require("../../utils/beforeBlockString");
const blurComments = require("../../utils/blurComments");
const hasBlock = require("../../utils/hasBlock");
const isCustomProperty = require("../../utils/isCustomProperty");
const keywordSets = require("../../reference/keywordSets");
const optionsMatches = require("../../utils/optionsMatches");
const report = require("../../utils/report");
const ruleMessages = require("../../utils/ruleMessages");
const styleSearch = require("style-search");
const validateOptions = require("../../utils/validateOptions");
const valueParser = require("postcss-value-parser");

const ruleName = "length-zero-no-unit";

const messages = ruleMessages(ruleName, {
  rejected: "Unexpected unit"
});

const rule = function(actual, secondary, context) {
  return (root, result) => {
    const validOptions = validateOptions(result, ruleName, { actual });
    if (!validOptions) {
      return;
    }

    root.walkDecls(decl => {
      check(blurComments(decl.toString()), decl);
    });

    root.walkAtRules(atRule => {
      const source = hasBlock(atRule)
        ? beforeBlockString(atRule, { noRawBefore: true })
        : atRule.toString();
      check(source, atRule);
    });

    function check(value, node) {
      const ignorableIndexes = new Set();
      const fixPositions = [];

      if (
        optionsMatches(secondary, "ignore", "custom-properties") &&
        isCustomProperty(value)
      ) {
        return;
      }

      styleSearch({ source: value, target: "0" }, match => {
        const index = match.startIndex;

        // Given a 0 somewhere in the full property value (not in a string, thanks
        // to styleSearch) we need to isolate the value that contains the zero.
        // To do so, we'll find the last index before the 0 of a character that would
        // divide one value in a list from another, and the next index of such a
        // character; then we build a substring from those indexes, which we can
        // assess.

        // If a single value includes multiple 0's (e.g. 100.01px), we don't want
        // each 0 to be treated as a separate value, possibly resulting in multiple
        // warnings for the same value (e.g. 0.00px).
        //
        // This check prevents that from happening: we build and check against a
        // Set containing all the indexes that are part of a value already validated.
        if (ignorableIndexes.has(index)) {
          return;
        }

        const prevValueBreakIndex = _.findLastIndex(
          value.substr(0, index),
          char => {
            return (
              [" ", ",", ")", "(", "#", ":", "\n", "\t"].indexOf(char) !== -1
            );
          }
        );

        // Ignore hex colors
        if (value[prevValueBreakIndex] === "#") {
          return;
        }

        // If no prev break was found, this value starts at 0
        const valueWithZeroStart =
          prevValueBreakIndex === -1 ? 0 : prevValueBreakIndex + 1;

        const nextValueBreakIndex = _.findIndex(
          value.substr(valueWithZeroStart),
          char => {
            return [" ", ",", ")"].indexOf(char) !== -1;
          }
        );

        // If no next break was found, this value ends at the end of the string
        const valueWithZeroEnd =
          nextValueBreakIndex === -1
            ? value.length
            : nextValueBreakIndex + valueWithZeroStart;

        const valueWithZero = value.slice(valueWithZeroStart, valueWithZeroEnd);
        const parsedValue = valueParser.unit(valueWithZero);

        if (!parsedValue || (parsedValue && !parsedValue.unit)) {
          return;
        }

        // Add the indexes to ignorableIndexes so the same value will not
        // be checked multiple times.
        _.range(valueWithZeroStart, valueWithZeroEnd).forEach(i =>
          ignorableIndexes.add(i)
        );

        // Only pay attention if the value parses to 0
        // and units with lengths
        if (
          parseFloat(valueWithZero, 10) !== 0 ||
          !keywordSets.lengthUnits.has(parsedValue.unit.toLowerCase())
        ) {
          return;
        }

        if (context.fix) {
          fixPositions.unshift({
            startIndex: valueWithZeroStart,
            length: valueWithZeroEnd - valueWithZeroStart
          });
          return;
        }

        report({
          message: messages.rejected,
          node,
          index: valueWithZeroEnd - parsedValue.unit.length,
          result,
          ruleName
        });
      });

      if (fixPositions.length) {
        fixPositions.forEach(function(fixPosition) {
          let startIndex = fixPosition.startIndex;
          // all nodes can have this
          startIndex -= node.raws.between.length;

          if (node.type === "atrule") {
            // atrules have a name and raws.afterName
            startIndex -= node.name.length;
            startIndex -= node.raws.afterName.length;

            node.params = replaceZero(
              node.params,
              startIndex,
              fixPosition.length
            );
          } else {
            // other nodes have prop
            startIndex -= node.prop.length;
            node.value = replaceZero(
              node.value,
              startIndex,
              fixPosition.length
            );
          }
        });
      }
    }
  };
};

function replaceZero(input, startIndex, length) {
  const stringStart = input.slice(0, startIndex);
  const stringEnd = input.slice(startIndex + length);
  return stringStart + "0" + stringEnd;
}

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