/* @flow */
"use strict"
const configurationError = require("./utils/configurationError")
const getModulePath = require("./utils/getModulePath")
const _ = require("lodash")
const fs = require("fs")
const globjoin = require("globjoin")
const normalizeRuleSettings = require("./normalizeRuleSettings")
const path = require("path")
const rules = require("./rules")

const DEFAULT_IGNORE_FILENAME = ".stylelintignore"
const FILE_NOT_FOUND_ERROR_CODE = "ENOENT"

// - Merges config and configOverrides
// - Makes all paths absolute
// - Merges extends
function augmentConfigBasic(
  stylelint/*: stylelint$internalApi*/,
  config/*: stylelint$config*/,
  configDir/*: string*/,
  allowOverrides/*:: ?: boolean*/
)/*: Promise<stylelint$config>*/ {
  return Promise.resolve().then(() => {
    if (!allowOverrides) return config
    return _.merge(config, stylelint._options.configOverrides)
  }).then(augmentedConfig => {
    return extendConfig(stylelint, augmentedConfig, configDir)
  }).then(augmentedConfig => {
    return absolutizePaths(augmentedConfig, configDir)
  })
}

// Extended configs need to be run through augmentConfigBasic
// but do not need the full treatment. Things like pluginFunctions
// will be resolved and added by the parent config.
function augmentConfigExtended(
  stylelint/*: stylelint$internalApi*/,
  cosmiconfigResultArg/*: ?{
     config: stylelint$config,
     filepath: string,
   }*/
 )/*: Promise<?{ config: stylelint$config, filepath: string }>*/ {
  const cosmiconfigResult = cosmiconfigResultArg // Lock in for Flow
  if (!cosmiconfigResult) return Promise.resolve(null)

  const configDir = path.dirname(cosmiconfigResult.filepath || "")
  const cleanedConfig = _.omit(cosmiconfigResult.config, "ignoreFiles")
  return augmentConfigBasic(stylelint, cleanedConfig, configDir).then(augmentedConfig => {
    return {
      config: augmentedConfig,
      filepath: cosmiconfigResult.filepath,
    }
  })
}

function augmentConfigFull(
  stylelint/*: stylelint$internalApi*/,
   cosmiconfigResultArg/*: ?{
   config: stylelint$config,
   filepath: string,
  }*/
)/*: Promise<?{ config: stylelint$config, filepath: string }>*/ {
  const cosmiconfigResult = cosmiconfigResultArg // Lock in for Flow
  if (!cosmiconfigResult) return Promise.resolve(null)

  const config = cosmiconfigResult.config,
    filepath = cosmiconfigResult.filepath

  const configDir = stylelint._options.configBasedir || path.dirname(filepath || "")

  return augmentConfigBasic(stylelint, config, configDir, true).then(augmentedConfig => {
    return addIgnorePatterns(stylelint, augmentedConfig)
  }).then(augmentedConfig => {
    return addPluginFunctions(augmentedConfig)
  }).then(augmentedConfig => {
    return addProcessorFunctions(augmentedConfig)
  }).then(augmentedConfig => {
    if (!augmentedConfig.rules) {
      throw configurationError("No rules found within configuration. Have you provided a \"rules\" property?")
    }

    return normalizeAllRuleSettings(augmentedConfig)
  }).then(augmentedConfig => {
    return {
      config: augmentedConfig,
      filepath: cosmiconfigResult.filepath,
    }
  })
}

// Load a file ignore ignore patterns, if there is one;
// then add them to the config as an ignorePatterns property
function addIgnorePatterns(
  stylelint/*: stylelint$internalApi*/,
  config/*: stylelint$config*/
)/*: Promise<stylelint$config>*/ {
  const ignoreFilePath = stylelint._options.ignorePath || DEFAULT_IGNORE_FILENAME
  const absoluteIgnoreFilePath = path.isAbsolute(ignoreFilePath) ? ignoreFilePath : path.resolve(process.cwd(), ignoreFilePath)

  return new Promise((resolve, reject) => {
    fs.readFile(absoluteIgnoreFilePath, "utf8", (err, data) => {
      if (err) {
        // If the file's not found, fine, we'll just
        // consider it an empty array of globs
        if (err.code === FILE_NOT_FOUND_ERROR_CODE) {
          return resolve(config)
        }
        return reject(err)
      }
      // Add an ignorePatterns property to the config, containing the
      // .gitignore-patterned globs loaded from .stylelintignore
      const augmentedConfig/*: stylelint$config*/ = Object.assign({}, config, {
        ignorePatterns: data,
      })
      resolve(augmentedConfig)
    })
  })
}

// Make all paths in the config absolute:
// - ignoreFiles
// - plugins
// - processors
// (extends handled elsewhere)
function absolutizePaths(
  config/*: stylelint$config*/,
  configDir/*: string*/
)/*: stylelint$config*/ {
  if (config.ignoreFiles) {
    config.ignoreFiles = [].concat(config.ignoreFiles).map(glob => {
      if (path.isAbsolute(glob.replace(/^!/, ""))) return glob
      return globjoin(configDir, glob)
    })
  }

  if (config.plugins) {
    config.plugins = [].concat(config.plugins).map(lookup => {
      return getModulePath(configDir, lookup)
    })
  }

  if (config.processors) {
    config.processors = absolutizeProcessors(config.processors, configDir)
  }

  return config
}

// Processors are absolutized in their own way because
// they can be and return a string or an array
function absolutizeProcessors(
  processors/*: stylelint$configProcessors*/,
  configDir/*: string*/
)/*: stylelint$configProcessors*/ {
  const normalizedProcessors = Array.isArray(processors) ? processors : [processors]

  return normalizedProcessors.map(item => {
    if (typeof item === "string") {
      return getModulePath(configDir, item)
    }

    return [ getModulePath(configDir, item[0]), item[1] ]
  })
}

function extendConfig(
  stylelint/*: stylelint$internalApi*/,
  config/*: stylelint$config*/,
  configDir/*: string*/
)/*: Promise<stylelint$config>*/ {
  if (config.extends === undefined) return Promise.resolve(config)
  const normalizedExtends = Array.isArray(config.extends) ? config.extends : [config.extends]

  const originalWithoutExtends = _.omit(config, "extends")
  const loadExtends = normalizedExtends.reduce((resultPromise, extendLookup) => {
    return resultPromise.then(resultConfig => {
      return loadExtendedConfig(stylelint, resultConfig, configDir, extendLookup).then(extendResult => {
        if (!extendResult) return resultConfig
        return mergeConfigs(resultConfig, extendResult.config)
      })
    })
  }, Promise.resolve(originalWithoutExtends))

  return loadExtends.then(resultConfig => {
    return mergeConfigs(resultConfig, originalWithoutExtends)
  })
}

function loadExtendedConfig(
  stylelint/*: stylelint$internalApi*/,
  config/*: stylelint$config*/,
  configDir/*: string*/,
  extendLookup/*: string*/
)/*: Promise<?{ config: stylelint$config, filepath: string }>*/ {
  const extendPath = getModulePath(configDir, extendLookup)
  return stylelint._extendExplorer.load(null, extendPath)
}

// When merging configs (via extends)
// - plugin and processor arrays are joined
// - rules are merged via Object.assign, so there is no attempt made to
//   merge any given rule's settings. If b contains the same rule as a,
//   b's rule settings will override a's rule settings entirely.
// - Everything else is merged via Object.assign
function mergeConfigs(a/*: stylelint$config*/, b/*: stylelint$config*/)/*: stylelint$config*/ {
  const pluginMerger = {}
  if (a.plugins || b.plugins) {
    pluginMerger.plugins = []
    if (a.plugins) {
      pluginMerger.plugins = pluginMerger.plugins.concat(a.plugins)
    }
    if (b.plugins) {
      pluginMerger.plugins = _.uniq(pluginMerger.plugins.concat(b.plugins))
    }
  }

  const processorMerger = {}
  if (a.processors || b.processors) {
    processorMerger.processors = []
    if (a.processors) {
      processorMerger.processors = processorMerger.processors.concat(a.processors)
    }
    if (b.processors) {
      processorMerger.processors = _.uniq(processorMerger.processors.concat(b.processors))
    }
  }

  const rulesMerger = {}
  if (a.rules || b.rules) {
    rulesMerger.rules = Object.assign({}, a.rules, b.rules)
  }

  const result = Object.assign({}, a, b, processorMerger, pluginMerger, rulesMerger)
  return result
}

function addPluginFunctions(config/*: stylelint$config*/)/*: stylelint$config*/ {
  if (!config.plugins) return config

  const normalizedPlugins = Array.isArray(config.plugins) ? config.plugins : [config.plugins]

  const pluginFunctions = normalizedPlugins.reduce((result, pluginLookup) => {
    let pluginImport = require(pluginLookup)
    // Handle either ES6 or CommonJS modules
    pluginImport = pluginImport.default || pluginImport

    // A plugin can export either a single rule definition
    // or an array of them
    const normalizedPluginImport = Array.isArray(pluginImport) ? pluginImport : [pluginImport]

    normalizedPluginImport.forEach(pluginRuleDefinition => {
      if (!pluginRuleDefinition.ruleName) {
        throw configurationError("stylelint v3+ requires plugins to expose a ruleName. " + `The plugin "${pluginLookup}" is not doing this, so will not work ` + "with stylelint v3+. Please file an issue with the plugin.")
      }

      if (!_.includes(pluginRuleDefinition.ruleName, "/")) {
        throw configurationError("stylelint v7+ requires plugin rules to be namspaced, " + "i.e. only `plugin-namespace/plugin-rule-name` plugin rule names are supported. " + `The plugin rule "${pluginRuleDefinition.ruleName}" does not do this, so will not work. ` + "Please file an issue with the plugin.")
      }

      result[pluginRuleDefinition.ruleName] = pluginRuleDefinition.rule
    })

    return result
  }, {})

  config.pluginFunctions = pluginFunctions
  return config
}

function normalizeAllRuleSettings(config/*: stylelint$config*/)/*: stylelint$config*/ {
  const normalizedRules = {}
  if (!config.rules) return config
  Object.keys(config.rules).forEach(ruleName => {
    const rawRuleSettings = _.get(config, [ "rules", ruleName ])
    const rule = rules[ruleName] || _.get(config, [ "pluginFunctions", ruleName ])
    if (!rule) {
      throw configurationError(`Undefined rule ${ruleName}`)
    }
    normalizedRules[ruleName] = normalizeRuleSettings(rawRuleSettings, ruleName, _.get(rule, "primaryOptionArray"))
  })
  config.rules = normalizedRules
  return config
}

// Given an array of processors strings, we want to add two
// properties to the augmented config:
// - codeProcessors: functions that will run on code as it comes in
// - resultProcessors: functions that will run on results as they go out
//
// To create these properties, we need to:
// - Find the processor module
// - Intialize the processor module by calling its functions with any
//   provided options
// - Push the processor's code and result processors to their respective arrays
const processorCache = new Map()
function addProcessorFunctions(config/*: stylelint$config*/)/*: stylelint$config*/ {
  if (!config.processors) return config

  const codeProcessors = []
  const resultProcessors = []

  ;[].concat(config.processors).forEach(processorConfig => {
    const processorKey = JSON.stringify(processorConfig)

    let initializedProcessor
    if (processorCache.has(processorKey)) {
      initializedProcessor = processorCache.get(processorKey)
    } else {
      processorConfig = [].concat(processorConfig)
      const processorLookup = processorConfig[0]
      const processorOptions = processorConfig[1]
      let processor = require(processorLookup)
      processor = processor.default || processor
      initializedProcessor = processor(processorOptions)
      processorCache.set(processorKey, initializedProcessor)
    }

    if (initializedProcessor && initializedProcessor.code) {
      codeProcessors.push(initializedProcessor.code)
    }
    if (initializedProcessor && initializedProcessor.result) {
      resultProcessors.push(initializedProcessor.result)
    }
  })

  config.codeProcessors = codeProcessors
  config.resultProcessors = resultProcessors
  return config
}

module.exports = { augmentConfigExtended, augmentConfigFull }
