/*
 * globule
 * https://github.com/cowboy/node-globule
 *
 * Copyright (c) 2018 "Cowboy" Ben Alman
 * Licensed under the MIT license.
 */

'use strict';

var fs = require('fs');
var path = require('path');

var _ = require('lodash');
var glob = require('glob');
var minimatch = require('minimatch');

// The module.
var globule = exports;

// Process specified wildcard glob patterns or filenames against a
// callback, excluding and uniquing files in the result set.
function processPatterns(patterns, options, fn) {
  var result = [];
  _.each(patterns, function(pattern) {
    // The first character is not ! (inclusion). Add all matching filepaths
    // to the result set.
    if (pattern.indexOf('!') !== 0) {
      result = _.union(result, fn(pattern));
      return;
    }
    // The first character is ! (exclusion). Remove any filepaths from the
    // result set that match this pattern, sans leading !.
    var filterFn = minimatch.filter(pattern.slice(1), options);
    result = _.filter(result, function(filepath) {
      return !filterFn(filepath);
    });
  });
  return result;
}

// Normalize paths to be unix-style.
var pathSeparatorRe = /[\/\\]/g;
function normalizePath(path) {
  return path.replace(pathSeparatorRe, '/');
}

// Match a filepath or filepaths against one or more wildcard patterns. Returns
// all matching filepaths. This behaves just like minimatch.match, but supports
// any number of patterns.
globule.match = function(patterns, filepaths, options) {
  // Return empty set if either patterns or filepaths was omitted.
  if (patterns == null || filepaths == null) { return []; }
  // Normalize patterns and filepaths to flattened arrays.
  patterns = _.isArray(patterns) ? _.flattenDeep(patterns) : [patterns];
  filepaths = _.isArray(filepaths) ? _.flattenDeep(filepaths) : [filepaths];
  // Return empty set if there are no patterns or filepaths.
  if (patterns.length === 0 || filepaths.length === 0) { return []; }
  // Return all matching filepaths.
  return processPatterns(patterns, options, function(pattern) {
    return minimatch.match(filepaths, pattern, options || {});
  });
};

// Match a filepath or filepaths against one or more wildcard patterns. Returns
// true if any of the patterns match.
globule.isMatch = function() {
  return globule.match.apply(null, arguments).length > 0;
};

// Return an array of all file paths that match the given wildcard patterns.
globule.find = function() {
  var args = _.toArray(arguments);
  // If the last argument is an options object, remove it from args.
  var options = _.isPlainObject(args[args.length - 1]) ? args.pop() : {};
  // If options.src was specified, use it. Otherwise, use all non-options
  // arguments. Flatten nested arrays.
  var patterns;
  if (options.src) {
    patterns = _.isArray(options.src) ? _.flattenDeep(options.src) : [options.src];
  } else {
    patterns = _.flattenDeep(args);
  }
  // Return empty set if there are no patterns.
  if (patterns.length === 0) { return []; }
  var srcBase = options.srcBase || options.cwd;
  // Create glob-specific options object.
  var globOptions = _.extend({}, options);
  if (srcBase) {
    globOptions.cwd = srcBase;
  }
  // Get all matching filepaths.
  var matches = processPatterns(patterns, options, function(pattern) {
    return glob.sync(pattern, globOptions);
  });
  // If srcBase and prefixBase were specified, prefix srcBase to matched paths.
  if (srcBase && options.prefixBase) {
    matches = matches.map(function(filepath) {
      return normalizePath(path.join(srcBase, filepath));
    });
  }
  // Filter result set?
  if (options.filter) {
    matches = matches.filter(function(filepath) {
      // If srcBase was specified but prefixBase was NOT, prefix srcBase
      // temporarily, for filtering.
      if (srcBase && !options.prefixBase) {
        filepath = normalizePath(path.join(srcBase, filepath));
      }
      try {
        if (_.isFunction(options.filter)) {
          return options.filter(filepath, options);
        } else {
          // If the file is of the right type and exists, this should work.
          return fs.statSync(filepath)[options.filter]();
        }
      } catch(err) {
        // Otherwise, it's probably not the right type.
        return false;
      }
    });
  }
  return matches;
};

var extDotRe = {
  first: /(\.[^\/]*)?$/,
  last: /(\.[^\/\.]*)?$/,
};
function rename(dest, options) {
  // Flatten path?
  if (options.flatten) {
    dest = path.basename(dest);
  }
  // Change the extension?
  if (options.ext) {
    dest = dest.replace(extDotRe[options.extDot], options.ext);
  }
  // Join dest and destBase?
  if (options.destBase) {
    dest = path.join(options.destBase, dest);
  }
  return dest;
}

// Build a mapping of src-dest filepaths from the given set of filepaths.
globule.mapping = function(filepaths, options) {
  // Return empty set if filepaths was omitted.
  if (filepaths == null) { return []; }
  options = _.defaults({}, options, {
    extDot: 'first',
    rename: rename,
  });
  var files = [];
  var fileByDest = {};
  // Find all files matching pattern, using passed-in options.
  filepaths.forEach(function(src) {
    // Generate destination filename.
    var dest = options.rename(src, options);
    // Prepend srcBase to all src paths.
    if (options.srcBase) {
      src = path.join(options.srcBase, src);
    }
    // Normalize filepaths to be unix-style.
    dest = normalizePath(dest);
    src = normalizePath(src);
    // Map correct src path to dest path.
    if (fileByDest[dest]) {
      // If dest already exists, push this src onto that dest's src array.
      fileByDest[dest].src.push(src);
    } else {
      // Otherwise create a new src-dest file mapping object.
      files.push({
        src: [src],
        dest: dest,
      });
      // And store a reference for later use.
      fileByDest[dest] = files[files.length - 1];
    }
  });
  return files;
};

// Return a mapping of src-dest filepaths from files matching the given
// wildcard patterns.
globule.findMapping = function() {
  var args = _.toArray(arguments);
  // If the last argument is an options object, remove it from args.
  var options = _.isPlainObject(args[args.length - 1]) ? args.pop() : {};
  // Generate mapping from found filepaths.
  return globule.mapping(globule.find(args, options), options);
};
