const isEqual = require('lodash.isequal');
const isFunction = require('lodash.isfunction');
const mergeWith = require('lodash.mergewith');
const unionWith = require('lodash.unionwith');
const differenceWith = require('lodash.differencewith');
const joinArrays = require('./join-arrays');

const isArray = Array.isArray;

function uniteRules(newRule, rule, strategy) {
  if (String(rule.test) !== String(newRule.test)
      || (newRule.enforce && rule.enforce !== newRule.enforce)
      || (newRule.include && !isSameValue(rule.include, newRule.include))
      || (newRule.exclude && !isSameValue(rule.exclude, newRule.exclude))) {
    return false;
  } else if (!rule.test && !rule.include && !rule.exclude
      && (rule.loader && rule.loader.split('?')[0]) !== (newRule.loader && newRule.loader.split('?')[0])) {
    // Don't merge the rule if there isn't any identifying fields and the loaders don't match
    return false;
  }

  // webpack 2 nested rules support
  if (rule.rules) {
    const _uniteRules = (b, a) => uniteRules(b, a, strategy);
    switch (strategy) {
      case 'prepend':
        rule.rules = [...differenceWith(newRule.rules, rule.rules, _uniteRules), ...rule.rules];
        break;
      default:
        rule.rules = unionWith(rule.rules, newRule.rules, uniteRules);
        break;
    }
  }

  // newRule.loader should always override
  if (newRule.loader) {
    const optionsKey = newRule.options ? 'options' : newRule.query && 'query';

    delete rule.use;
    delete rule.loaders;
    rule.loader = newRule.loader;

    if (optionsKey) {
      rule[optionsKey] = newRule[optionsKey];
    }
  } else if ((rule.use || rule.loaders || rule.loader) && (newRule.use || newRule.loaders)) {
    const expandEntry = loader => (
      typeof loader === 'string' ? { loader } : loader
    );
    // this is only here to avoid breaking existing tests
    const unwrapEntry = entry => (
      !entry.options && !entry.query ? entry.loader : entry
    );

    let entries;
    if (rule.loader) {
      const optionsKey = rule.options ? 'options' : rule.query && 'query';
      entries = [{ loader: rule.loader }];

      if (optionsKey) {
        entries[0][optionsKey] = rule[optionsKey];
      }

      delete rule.loader;

      if (optionsKey) {
        delete rule[optionsKey];
      }
    } else {
      entries = [].concat(rule.use || rule.loaders).map(expandEntry);
    }
    const newEntries = [].concat(newRule.use || newRule.loaders).map(expandEntry);

    const loadersKey = rule.use || newRule.use ? 'use' : 'loaders';
    switch (strategy) {
      case 'prepend':
        rule[loadersKey] = [
          ...differenceWith(newEntries, entries, uniteEntries),
          ...entries
        ].map(unwrapEntry);
        break;
      default:
        rule[loadersKey] = unionWith(entries, newEntries, uniteEntries).map(unwrapEntry);
        break;
    }
  }

  if (newRule.include) {
    rule.include = newRule.include;
  }

  if (newRule.exclude) {
    rule.exclude = newRule.exclude;
  }

  return true;
}

/**
 * Check equality of two values using lodash's isEqual
 * Arrays need to be sorted for equality checking
 * but clone them first so as not to disrupt the sort order in tests
 */
function isSameValue(a, b) {
  const [propA, propB] = [a, b].map(value => (
    isArray(value) ? [...value].sort() : value
  ));

  return isEqual(propA, propB);
}

function uniteEntries(newEntry, entry) {
  const loaderNameRe = /^([^?]+)/ig;

  const [loaderName] = entry.loader.match(loaderNameRe);
  const [newLoaderName] = newEntry.loader.match(loaderNameRe);

  if (loaderName !== newLoaderName) {
    return false;
  }

  // Replace query values with newer ones
  mergeWith(entry, newEntry);
  return true;
}

function unitePlugins(newPlugin, plugin) {
  if (!(newPlugin instanceof plugin.constructor)) return false;

  // functions shouldn't be merged here
  mergeWith(plugin, newPlugin, (a, b, k) => (isFunction(a) ? a : joinArrays()(a, b, k)));

  return true;
}

exports.uniteRules = uniteRules;
exports.uniteEntries = uniteEntries;
exports.unitePlugins = unitePlugins;
