text: "use strict"
  
  const isCustomProperty = require("../../utils/isCustomProperty")
  const isStandardSyntaxProperty = require("../../utils/isStandardSyntaxProperty")
  const report = require("../../utils/report")
  const ruleMessages = require("../../utils/ruleMessages")
  const validateOptions = require("../../utils/validateOptions")
  const _ = require("lodash")
  const postcss = require("postcss")
  
  const ruleName = "declaration-block-properties-order"
  
  const messages = ruleMessages(ruleName, {
    expected: (first, second) => `Expected "${first}" to come before "${second}"`,
  })
  
  const rule = function (expectation, options) {
    return (root, result) => {
      const validOptions = validateOptions(result, ruleName, {
        actual: expectation,
        possible: validatePrimaryOption,
      }, {
        actual: options,
        possible: {
          unspecified: [
            "top",
            "bottom",
            "ignore",
            "bottomAlphabetical",
          ],
        },
        optional: true,
      })
      if (!validOptions) {
        return
      }
  
      const alphabetical = expectation === "alphabetical"
      const expectedOrder = alphabetical ? null : createExpectedOrder(expectation)
      // By default, ignore unspecified properties
      const unspecified = _.get(options, ["unspecified"], "ignore")
  
      // Shallow loop
      root.each(node => {
        if (node.type === "rule" || node.type === "atrule") {
          checkNode(node)
        }
      })
  
      function checkNode(node) {
        const allPropData = []
  
        node.each(child => {
          // If the child has nested nodes with child
          // (e.g. a rule nested within a rule), make
          // sure to check the children
          if (child.nodes && child.nodes.length) {
            checkNode(child)
          }
  
          if (child.type !== "decl") {
            return
          }
  
          const prop = child.prop
  
          if (!isStandardSyntaxProperty(prop)) {
            return
          }
          if (isCustomProperty(prop)) {
            return
          }
  
          let unprefixedPropName = postcss.vendor.unprefixed(prop)
  
          // Hack to allow -moz-osx-font-smoothing to be understood
          // just like -webkit-font-smoothing
          if (unprefixedPropName.indexOf("osx-") === 0) {
            unprefixedPropName = unprefixedPropName.slice(4)
          }
  
          const propData = {
            name: prop,
            unprefixedName: unprefixedPropName,
            orderData: alphabetical ? null : getOrderData(expectedOrder, unprefixedPropName),
            before: child.raws.before,
            index: allPropData.length,
            node: child,
          }
  
          const previousPropData = _.last(allPropData)
          allPropData.push(propData)
  
          // Skip first decl
          if (!previousPropData) {
            return
          }
  
          const isCorrectOrder = alphabetical ? checkAlpabeticalOrder(previousPropData, propData) : checkOrder(previousPropData, propData)
  
          if (isCorrectOrder) {
            return
          }
  
          complain({
            message: messages.expected(propData.name, previousPropData.name),
            node: child,
          })
        })
  
        function checkOrder(firstPropData, secondPropData) {
          // If the unprefixed property names are the same, resort to alphabetical ordering
          if (firstPropData.unprefixedName === secondPropData.unprefixedName) {
            return firstPropData.name <= secondPropData.name
          }
  
          const firstPropIsUnspecified = !firstPropData.orderData
          const secondPropIsUnspecified = !secondPropData.orderData
  
          // Now check actual known properties ...
          if (!firstPropIsUnspecified && !secondPropIsUnspecified) {
            return firstPropData.orderData.expectedPosition <= secondPropData.orderData.expectedPosition
          }
  
          if (firstPropIsUnspecified && !secondPropIsUnspecified) {
            // If first prop is unspecified, look for a specified prop before it to
            // compare to the current prop
            const priorSpecifiedPropData = _.findLast(allPropData.slice(0, -1), d => !!d.orderData)
            if (priorSpecifiedPropData && priorSpecifiedPropData.orderData && priorSpecifiedPropData.orderData.expectedPosition > secondPropData.orderData.expectedPosition) {
              complain({
                message: messages.expected(secondPropData.name, priorSpecifiedPropData.name),
                node: secondPropData.node,
              })
              return true // avoid logging another warning
            }
          }
  
          // Now deal with unspecified props ...
          // Starting with bottomAlphabetical as it requires more specific conditionals
          if (unspecified === "bottomAlphabetical" && !firstPropIsUnspecified && secondPropIsUnspecified) {
            return true
          }
  
          if (unspecified === "bottomAlphabetical" && secondPropIsUnspecified && firstPropIsUnspecified) {
            if (checkAlpabeticalOrder(firstPropData, secondPropData)) {
              return true
            } else {
              return false
            }
          }
          if (unspecified === "bottomAlphabetical" && firstPropIsUnspecified) {
            return false
          }
  
          if (firstPropIsUnspecified && secondPropIsUnspecified) {
            return true
          }
  
          if (unspecified === "ignore" && (firstPropIsUnspecified || secondPropIsUnspecified)) {
            return true
          }
  
          if (unspecified === "top" && firstPropIsUnspecified) {
            return true
          }
          if (unspecified === "top" && secondPropIsUnspecified) {
            return false
          }
  
          if (unspecified === "bottom" && secondPropIsUnspecified) {
            return true
          }
          if (unspecified === "bottom" && firstPropIsUnspecified) {
            return false
          }
        }
      }
  
      function complain(opts) {
        report({
          message: opts.message,
          node: opts.node,
          result,
          ruleName,
        })
      }
    }
  }
  
  rule.primaryOptionArray = true
  
  rule.ruleName = ruleName
  rule.messages = messages
  module.exports = rule
  
  function createExpectedOrder(input) {
    const order = {}
    let expectedPosition = 0
  
    appendGroup(input)
  
    function appendGroup(items) {
      items.forEach(item => appendItem(item, false))
    }
  
    function appendItem(item, inFlexibleGroup) {
      if (_.isString(item)) {
        // In flexible groups, the expectedPosition does not ascend
        // to make that flexibility work;
        // otherwise, it will always ascend
        if (!inFlexibleGroup) {
          expectedPosition += 1
        }
        order[item] = { expectedPosition }
        return
      }
  
      if (!item.order || item.order === "strict") {
        appendGroup(item.properties)
        return
      } else if (item.order === "flexible") {
        expectedPosition += 1
        item.properties.forEach(property => {
          appendItem(property, true)
        })
      }
    }
  
    return order
  }
  
  function getOrderData(expectedOrder, propName) {
    let orderData = expectedOrder[propName]
    // If prop was not specified but has a hyphen
    // (e.g. `padding-top`), try looking for the segment preceding the hyphen
    // and use that index
    if (!orderData && propName.lastIndexOf("-") !== -1) {
      const propNamePreHyphen = propName.slice(0, propName.lastIndexOf("-"))
      orderData = getOrderData(expectedOrder, propNamePreHyphen)
    }
    return orderData
  }
  
  function checkAlpabeticalOrder(firstPropData, secondPropData) {
    // If unprefixed prop names are the same, compare the prefixed versions
    if (firstPropData.unprefixedName === secondPropData.unprefixedName) {
      return firstPropData.name <= secondPropData.name
    }
  
    return firstPropData.unprefixedName < secondPropData.unprefixedName
  }
  
  function validatePrimaryOption(actualOptions) {
    // Return true early if alphabetical
    if (actualOptions === "alphabetical") {
      return true
    }
  
    // Otherwise, begin checking array options
    if (!Array.isArray(actualOptions)) {
      return false
    }
  
    // Every item in the array must be a string or an object
    // with a "properties" property
    if (!actualOptions.every(item => {
      if (_.isString(item)) {
        return true
      }
      return _.isPlainObject(item) && !_.isUndefined(item.properties)
    })) {
      return false
    }
  
    const objectItems = actualOptions.filter(_.isPlainObject)
  
    // Every object-item's "order" property must be "strict" or "flexible"
    if (!objectItems.every(item => {
      if (_.isUndefined(item.order)) {
        return true
      }
      return _.includes([
        "strict",
        "flexible",
      ], item.order)
    })) {
      return false
    }
  
    return true
  }
  