/**
 * RTLCSS https://github.com/MohammadYounes/rtlcss
 * Framework for transforming Cascading Style Sheets (CSS) from Left-To-Right (LTR) to Right-To-Left (RTL).
 * Copyright 2017 Mohammad Younes.
 * Licensed under MIT <https://opensource.org/licenses/mit-license.php>
 */

'use strict'

const postcss = require('postcss')
const state = require('./state.js')
const config = require('./config.js')
const util = require('./util.js')

module.exports = (options, plugins, hooks) => {
  const processed = Symbol('processed')
  const configuration = config.configure(options, plugins, hooks)
  const context = {
    // provides access to postcss
    postcss,
    // provides access to the current configuration
    config: configuration,
    // provides access to utilities object
    util: util.configure(configuration),
    // processed symbol
    symbol: processed
  }
  let flipped = 0
  const toBeRenamed = {}

  function shouldProcess (node, result) {
    if (!node[processed]) {
      let prevent = false
      state.walk((current) => {
        // check if current directive is expecting this node
        if (!current.metadata.blacklist && current.directive.expect[node.type]) {
          // perform action and prevent further processing if result equals true
          if (current.directive.begin(node, current.metadata, context)) {
            prevent = true
          }

          // if should end? end it.
          if (current.metadata.end && current.directive.end(node, current.metadata, context)) {
            state.pop(current)
          }
        }
      })
      node[processed] = true
      return !prevent
    }

    return false
  }

  return {
    postcssPlugin: 'rtlcss',
    Once (root, { result }) {
      context.config.hooks.pre(root, postcss)
      shouldProcess(root, result)
    },
    Rule (node, { result }) {
      if (shouldProcess(node, result)) {
        // new rule, reset flipped decl count to zero
        flipped = 0
      }
    },
    AtRule (node, { result }) {
      if (shouldProcess(node, result) &&
        // @rules requires url flipping only
        (context.config.processUrls === true || context.config.processUrls.atrule === true)
      ) {
        const params = context.util.applyStringMap(node.params, true)
        node.params = params
      }
    },
    Comment (node, { result }) {
      if (shouldProcess(node, result)) {
        state.parse(node, result, (current) => {
          let push = true
          if (current.directive === null) {
            current.preserve = !context.config.clean
            context.util.each(context.config.plugins, (plugin) => {
              const blacklist = context.config.blacklist[plugin.name]
              if (blacklist && blacklist[current.metadata.name] === true) {
                current.metadata.blacklist = true
                if (current.metadata.end) {
                  push = false
                }

                if (current.metadata.begin) {
                  result.warn(`directive "${plugin.name}.${current.metadata.name}" is blacklisted.`, { node: current.source })
                }

                // break each
                return false
              }

              current.directive = plugin.directives.control[current.metadata.name]
              if (current.directive) {
                // break each
                return false
              }
            })
          }

          if (current.directive) {
            if (!current.metadata.begin && current.metadata.end) {
              if (current.directive.end(node, current.metadata, context)) {
                state.pop(current)
              }

              push = false
            } else if (
              current.directive.expect.self && current.directive.begin(node, current.metadata, context) &&
              current.metadata.end && current.directive.end(node, current.metadata, context)
            ) {
              push = false
            }
          } else if (!current.metadata.blacklist) {
            push = false
            result.warn(`unsupported directive "${current.metadata.name}".`, { node: current.source })
          }

          return push
        })
      }
    },
    Declaration (node, { result }) {
      if (shouldProcess(node, result)) {
        // if broken by a matching value directive .. break
        if (!context.util.each(context.config.plugins, (plugin) => {
          return context.util.each(plugin.directives.value, (directive) => {
            const hasRawValue = node.raws.value && node.raws.value.raw
            const expr = context.util.regexDirective(directive.name)
            if (expr.test(`${node.raws.between}${hasRawValue ? node.raws.value.raw : ''}${node.important && node.raws.important ? node.raws.important : ''}`)) {
              expr.lastIndex = 0
              if (directive.action(node, expr, context)) {
                if (context.config.clean) {
                  node.raws.between = context.util.trimDirective(node.raws.between)
                  if (node.important && node.raws.important) {
                    node.raws.important = context.util.trimDirective(node.raws.important)
                  }

                  if (hasRawValue) {
                    node.value = node.raws.value.raw = context.util.trimDirective(node.raws.value.raw)
                  }
                }

                flipped++
                // break
                return false
              }
            }
          })
        })) return
        // loop over all plugins/property processors
        context.util.each(context.config.plugins, (plugin) => {
          return context.util.each(plugin.processors, (processor) => {
            const alias = context.config.aliases[node.prop]
            if ((alias || node.prop).match(processor.expr)) {
              const raw = node.raws.value && node.raws.value.raw ? node.raws.value.raw : node.value
              const state = context.util.saveComments(raw)
              if (context.config.processEnv) {
                state.value = context.util.swap(state.value, 'safe-area-inset-left', 'safe-area-inset-right', { ignoreCase: false })
              }

              const pair = processor.action(node.prop, state.value, context)
              state.value = pair.value
              pair.value = context.util.restoreComments(state)
              if ((!alias && pair.prop !== node.prop) || pair.value !== raw) {
                flipped++
                node.prop = pair.prop
                node.value = pair.value
              }

              // match found, break
              return false
            }
          })
        })
        // if last decl, apply auto rename
        // decl. may be found inside @rules
        if (context.config.autoRename && !flipped && node.parent.type === 'rule' && context.util.isLastOfType(node)) {
          const renamed = context.util.applyStringMap(node.parent.selector)
          if (context.config.autoRenameStrict === true) {
            const pair = toBeRenamed[renamed]
            if (pair) {
              pair.selector = node.parent.selector
              node.parent.selector = renamed
            } else {
              toBeRenamed[node.parent.selector] = node.parent
            }
          } else {
            node.parent.selector = renamed
          }
        }
      }
    },
    OnceExit (root, { result }) {
      state.walk((item) => {
        result.warn(`unclosed directive "${item.metadata.name}".`, { node: item.source })
      })

      for (const key of Object.keys(toBeRenamed)) {
        result.warn('renaming skipped due to lack of a matching pair.', { node: toBeRenamed[key] })
      }

      context.config.hooks.post(root, postcss)
    }
  }
}

module.exports.postcss = true

/**
 * Creates a new RTLCSS instance, process the input and return its result.
 * @param {String}  css  A string containing input CSS.
 * @param {Object}  options  An object containing RTLCSS settings.
 * @param {Object|Array}  plugins An array containing a list of RTLCSS plugins or a single RTLCSS plugin.
 * @param {Object}  hooks An object containing pre/post hooks.
 * @returns {String} A string contining the RTLed css.
 */
module.exports.process = function (css, options, plugins, hooks) {
  return postcss([this(options, plugins, hooks)]).process(css).css
}

/**
 * Creates a new instance of RTLCSS using the passed configuration object
 * @param {Object}  config  An object containing RTLCSS options, plugins and hooks.
 * @returns {Object}  A new RTLCSS instance.
 */
module.exports.configure = function (config) {
  config = config || {}
  return postcss([this(config.options, config.plugins, config.hooks)])
}
