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

// - 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 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
      };
    });
}

// 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 = dynamicRequire(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 = dynamicRequire(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 };
