'use strict';
const hasGulplog = require('has-gulplog');
const micromatch = require('micromatch');
const unique = require('array-unique');
const findup = require('findup-sync');
const resolve = require('resolve');
const path = require('path');

function arrayify(el) {
  return Array.isArray(el) ? el : [el];
}

function camelize(str) {
  return str.replace(/-(\w)/g, (m, p1) => p1.toUpperCase());
}

// code from https://github.com/gulpjs/gulp-util/blob/master/lib/log.js
// to use the same functionality as gulp-util for backwards compatibility
// with gulp 3x cli
function logger() {
  if (hasGulplog()) {
    // specifically deferring loading here to keep from registering it globally
    const gulplog = require('gulplog');
    gulplog.info.apply(gulplog, arguments);
  } else {
    // specifically deferring loading because it might not be used
    const fancylog = require('fancy-log');
    fancylog.apply(null, arguments);
  }
}

function getPattern(options) {
  const defaultPatterns = ['gulp-*', 'gulp.*', '@*/gulp{-,.}*'];
  const overridePattern = 'overridePattern' in options ? !!options.overridePattern : true;
  if (overridePattern) {
    return arrayify(options.pattern || defaultPatterns);
  }
  return defaultPatterns.concat(arrayify(options.pattern));
}

module.exports = function(options) {
  const finalObject = {};
  let requireFn;
  options = options || {};

  const DEBUG = options.DEBUG || false;
  const pattern = getPattern(options);
  const config = options.config || findup('package.json', { cwd: parentDir });
  const scope = arrayify(options.scope || ['dependencies', 'devDependencies', 'peerDependencies']);
  const replaceString = options.replaceString || /^gulp(-|\.)/;
  const camelizePluginName = options.camelize !== false;
  const lazy = 'lazy' in options ? !!options.lazy : true;
  const renameObj = options.rename || {};
  const maintainScope = 'maintainScope' in options ? !!options.maintainScope : true;

  logDebug(`Debug enabled with options: ${JSON.stringify(options)}`);

  const renameFn = options.renameFn || function(name) {
    name = name.replace(replaceString, '');
    return camelizePluginName ? camelize(name) : name;
  };

  const postRequireTransforms = options.postRequireTransforms || {};

  if (typeof options.requireFn === 'function') {
    requireFn = options.requireFn;
  } else if (typeof config === 'string') {
    requireFn = function(name) {
      // This searches up from the specified package.json file, making sure
      // the config option behaves as expected. See issue #56.
      const src = resolve.sync(name, { basedir: path.dirname(config) });
      return require(src);
    };
  } else {
    requireFn = require;
  }

  const configObject = (typeof config === 'string') ? require(config) : config;

  if (!configObject) {
    throw new Error('Could not find dependencies. Do you have a package.json file in your project?');
  }

  const names = scope.reduce((result, prop) => result.concat(Object.keys(configObject[prop] || {})), []);

  logDebug(`${names.length} plugin(s) found: ${names.join(' ')}`);

  pattern.push('!gulp-load-plugins');

  function logDebug(message) {
    if (DEBUG) {
      logger(`gulp-load-plugins: ${message}`);
    }
  }

  function defineProperty(object, transform, requireName, name, maintainScope) {
    let err;
    if (object[requireName]) {
      logDebug(`error: defineProperty ${name}`);
      err = maintainScope
        ? `Could not define the property "${requireName}", you may have repeated dependencies in your package.json like` + ` "gulp-${requireName}" and ` + `"${requireName}"`
        : `Could not define the property "${requireName}", you may have repeated a dependency in another scope like` + ` "gulp-${requireName}" and ` + `"@foo/gulp-${requireName}"`;
      throw new Error(err);
    }

    if (lazy) {
      logDebug(`lazyload: adding property ${requireName}`);
      Object.defineProperty(object, requireName, {
        enumerable: true,
        get: function() {
          logDebug(`lazyload: requiring ${name}...`);
          return transform(requireName, requireFn(name));
        }
      });
    } else {
      logDebug(`requiring ${name}...`);
      object[requireName] = transform(requireName, requireFn(name));
    }
  }

  function getRequireName(name) {
    let requireName;

    if (renameObj[name]) {
      requireName = options.rename[name];
    } else {
      requireName = renameFn(name);
    }

    logDebug(`renaming ${name} to ${requireName}`);

    return requireName;
  }

  function applyTransform(requireName, plugin) {
    const transform = postRequireTransforms[requireName];

    if (transform && typeof transform === 'function') { // if postRequireTransform function is passed, pass it the plugin and return it
      logDebug(`transforming ${requireName}`);
      return transform(plugin);
    } else {
      return plugin; // if no postRequireTransform function passed, return the plugin as is
    }
  }

  const scopeTest = new RegExp('^@');
  const scopeDecomposition = new RegExp('^@(.+)/(.+)');

  unique(micromatch(names, pattern)).forEach((name) => {
    let decomposition;
    let fObject = finalObject;
    if (scopeTest.test(name)) {
      decomposition = scopeDecomposition.exec(name);
      if (maintainScope) {
        if (!Object.prototype.hasOwnProperty.call(fObject, decomposition[1])) {
          finalObject[decomposition[1]] = {};
        }
        fObject = finalObject[decomposition[1]];
      }

      defineProperty(fObject, applyTransform, getRequireName(decomposition[2]), name, maintainScope);
    } else {
      defineProperty(fObject, applyTransform, getRequireName(name), name, maintainScope);
    }
  });

  return finalObject;
};

const parentDir = path.dirname(module.parent.filename);

// Necessary to get the current `module.parent` and resolve paths correctly.
delete require.cache[__filename];
