"use strict"

const isWhitespace = require("../../utils/isWhitespace")
const report = require("../../utils/report")
const ruleMessages = require("../../utils/ruleMessages")
const validateOptions = require("../../utils/validateOptions")
const balancedMatch = require("balanced-match")
const styleSearch = require("style-search")
const valueParser = require("postcss-value-parser")

const ruleName = "function-calc-no-unspaced-operator"

const messages = ruleMessages(ruleName, {
  expectedBefore: operator => `Expected single space before "${operator}" operator`,
  expectedAfter: operator => `Expected single space after "${operator}" operator`,
  expectedOperatorBeforeSign: operator => `Expected an operator before sign "${operator}"`,
})

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

    function complain(message, node, index) {
      report({ message, node, index, result, ruleName })
    }

    root.walkDecls(decl => {
      valueParser(decl.value).walk(node => {
        if (node.type !== "function" || node.value.toLowerCase() !== "calc") {
          return
        }

        const parensMatch = balancedMatch("(", ")", valueParser.stringify(node))
        const rawExpression = parensMatch.body
        const expressionIndex = decl.source.start.column + decl.prop.length + (decl.raws.between || "").length + node.sourceIndex
        const expression = blurVariables(rawExpression)

        checkSymbol("+")
        checkSymbol("-")
        checkSymbol("*")
        checkSymbol("/")

        function checkSymbol(symbol) {
          const styleSearchOptions = {
            source: expression,
            target: symbol,
            functionArguments: "skip",
          }

          styleSearch(styleSearchOptions, match => {
            const index = match.startIndex

            // Deal with signs.
            // (@ and $ are considered "digits" here to allow for variable syntaxes
            // that permit signs in front of variables, e.g. `-$number`)
            // As is "." to deal with fractional numbers without a leading zero
            if ((symbol === "+" || symbol === "-") && /[\d@\$.]/.test(expression[index + 1])) {
              const expressionBeforeSign = expression.substr(0, index)

              // Ignore signs that directly follow a opening bracket
              if (expressionBeforeSign[expressionBeforeSign.length - 1] === "(") {
                return
              }

              // Ignore signs at the beginning of the expression
              if (/^\s*$/.test(expressionBeforeSign)) {
                return
              }

              // Otherwise, ensure that there is a real operator preceeding them
              if (/[\*/+-]\s*$/.test(expressionBeforeSign)) {
                return
              }

              // And if not, complain
              complain(messages.expectedOperatorBeforeSign(symbol), decl, expressionIndex + index)
              return
            }

            const beforeOk = expression[index - 1] === " " && !isWhitespace(expression[index - 2]) || newlineBefore(expression, index - 1)
            if (!beforeOk) {
              complain(messages.expectedBefore(symbol), decl, expressionIndex + index)
            }

            const afterOk = expression[index + 1] === " " && !isWhitespace(expression[index + 2]) || expression[index + 1] === "\n" || expression.substr(index + 1, 2) === "\r\n"

            if (!afterOk) {
              complain(messages.expectedAfter(symbol), decl, expressionIndex + index)
            }
          })
        }
      })
    })
  }
}

function blurVariables(source) {
  return source.replace(/[\$@][^\)\s]+|#{.+?}/g, "0")
}

function newlineBefore(str, startIndex) {
  let index = startIndex
  while (index && isWhitespace(str[index])) {
    if (str[index] === "\n") return true
    index--
  }
  return false
}

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