/*
 * grunt-bowercopy
 *
 * Copyright (c) 2014 Timmy Willison
 * Licensed under the MIT license.
 */

module.exports = function (grunt) {
	'use strict';

	// Logging
	var log = grunt.log,
		verbose = grunt.verbose,
		fail = grunt.fail;

	// Utilities
	var _ = require('lodash');

	// Modules
	var path = require('path'),
		bower = require('bower'),
		glob = require('glob'),
		sep = path.sep;

	// Get path to bower config file
	var bowerrc = grunt.file.exists('.bowerrc') ? grunt.file.readJSON('.bowerrc') : {},
		bowerConfigPath = 'bower.json';
	if (bowerrc.cwd) {
		bowerConfigPath = path.join(bowerrc.cwd, bowerConfigPath);
	}

	// Get all modules
	var bowerConfig = grunt.file.readJSON(bowerConfigPath);
	var allModules = Object.keys(
		_.extend({}, bowerConfig.dependencies, bowerConfig.devDependencies)
	);
	var unused = allModules.slice(0);

	// Track number of runs
	var numTargets;
	var numRuns = 0;

	// Regex
	var rperiod = /\./;
	var rmain = /^(.+):main$/;

	/**
	 * Retrieve the number of targets from the grunt config
	 * @returns {number|undefined} Returns the number of targets,
	 *  or undefined if the bowercopy config could not be found
	 */
	function getNumTargets() {
		if (numTargets) {
			return numTargets;
		}
		var targets = grunt.config('bowercopy');
		if (targets) {
			delete targets.options;
			numTargets = Object.keys(targets).length;
		}
		return numTargets;
	}

	/**
	 * Convert from grunt to a cleaner format
	 * @param {Array} files
	 */
	function convert(files) {
		var converted = [];
		files.forEach(function(file) {
			// We need originals as the destinations may not yet exist
			file = file.orig;
			var dest = file.dest;

			// Use destination for source if no source is available
			if (!file.src.length) {
				converted.push({
					src: dest,
					dest: dest
				});
				return;
			}

			file.src.forEach(function(source) {
				converted.push({
					src: source,
					dest: dest
				});
			});
		});
		return converted;
	}

	/**
	 * Filter out all of the modules represented in the filesSrc array
	 * @param {Array} modules
	 * @param {Array} files
	 * @param {Object} options
	 */
	function filterRepresented(modules, files, options) {
		return _.filter(modules, function(module) {
			if (options.ignore.indexOf(module) > -1) {
				return false;
			}
			return !_.some(files, function(file) {
				// Look for the module name somewhere in the source path
				return path.join(sep, options.srcPrefix, file.src.replace(rmain, '$1'), sep)
					.indexOf(sep + module + sep) > -1;
			});
		});
	}

	/**
	 * Ensure all bower dependencies are accounted for
	 * @param {Array} files Files property from the task
	 * @param {Object} options
	 * @returns {boolean} Returns whether all dependencies are accounted for
	 */
	function ensure(files, options) {
		// Update the global array of represented modules
		unused = filterRepresented(unused, files, options);

		verbose.writeln('Unrepresented modules list currently at ', unused);

		// Only print message when all targets have been run
		if (++numRuns === getNumTargets()) {
			if (unused.length) {
				if (options.clean) {
					log.error('Could not clean directory. Some bower components are not configured: ', unused);
				} else if (options.report) {
					log.writeln('Some bower components are not configured: ', unused);
				}
			} else {
				// Remove the bower_components directory as it's no longer needed
				if (options.clean) {
					grunt.file.delete(options.srcPrefix);
					log.ok('Bower directory cleaned');
				}
				if (options.report) {
					log.ok('All modules accounted for');
				}
			}
		}
	}

	/**
	 * Convert an array of files sources to our format
	 * @param {Array} files
	 * @param {Object} options
	 * @param {string} [dest] A folder destination for all of these sources
	 */
	function convertMatches(files, options, dest) {
		return files.map(function(source) {
			return {
				src: source,
				dest: path.join(
					// Build a destination from the new source if no dest
					// was specified
					dest != null ?
						dest :
						path.dirname(source).replace(options.srcPrefix + sep, ''),
					path.basename(source)
				)
			};
		});
	}

	/**
	 * Get the main files for a particular package
	 * @param {string} src
	 * @param {Object} options
	 * @param {string} dest
	 * @returns {Array} Returns an array of file locations from the main property
	 */
	function getMain(src, options, dest) {
		var meta = grunt.file.readJSON(path.join(src, '.bower.json'));
		if (!meta.main) {
			fail.fatal('No main property specified by ' + path.normalize(src.replace(options.srcPrefix, '')));
		}
		var files = typeof meta.main === 'string' ? [meta.main] : meta.main;
		return files.map(function(source) {
			return {
				src: path.join(src, source),
				dest: dest
			};
		});
	}

	/**
	 * Copy over specified component files from the bower directory
	 *  files format: [{ src: '', dest: '' }, ...]
	 * @param {Array} files
	 * @param {Object} options
	 * @returns {boolean} Returns whether anything was copied for the list of files
	 */
	function copy(files, options) {
		var copied = false;
		files.forEach(function(file) {
			// Normalize input
			var src = path.normalize(file.src);
			// Use source for destination if no destination is available
			// This is done here so globbing can use the original dest
			var dest = path.normalize(file.dest || src);

			// Add source prefix if not already added
			if (src.indexOf(options.srcPrefix) !== 0) {
				src = path.join(options.srcPrefix, src);
			}

			// Add dest prefix if not already added
			if (dest.indexOf(options.destPrefix) !== 0) {
				dest = path.join(options.destPrefix, dest);
			}

			// Copy main files if :main is specified
			var main = rmain.exec(src);
			if (main) {
				//Trim :main from dest strings
				//(required if the user did not also provide an explicit dest)
				var temp = rmain.exec(dest);
				if (temp) {
					dest = temp[1];
				}

				copied = copy(getMain(main[1], options, dest), options) || copied;
				return;
			}

			// Copy folders
			if (grunt.file.isDir(src)) {
				grunt.file.recurse(src, function(abspath, rootdir, subdir, filename) {
					copied = true;
					grunt.file.copy(
						abspath,
						path.join(dest, subdir || '', filename),
						options.copyOptions
					);
				});
				log.writeln(src + ' -> ' + dest);
			// Copy files
			} else if (grunt.file.exists(src)) {
				copied = true;
				if (!rperiod.test(path.basename(dest))) {
					dest = path.join(dest, path.basename(src));
				}
				grunt.file.copy(src, dest, options.copyOptions);
				log.writeln(src + ' -> ' + dest);
			// Glob
			} else {
				var matches = glob.sync(file.src, { cwd: options.srcPrefix });
				if (matches.length) {
					matches = convertMatches(matches, options, file.dest);
					copied = copy(matches, options) || copied;
				} else {
					fail.warn(src + ' was not found');
				}
			}
		});
		return copied;
	}

	/**
	 * Top-level copying run
	 *  files format is Grunt's default:
	 *  [{ orig: { src: '', dest: '' }, src: '', dest: '' }, ...]
	 *  convert to copy()'s format before calling copy()
	 * @param {Array} files
	 * @param {Object} options
	 */
	var run = function(files, options) {
		// Normalize paths
		options.srcPrefix = path.normalize(options.srcPrefix);
		options.destPrefix = path.normalize(options.destPrefix);

		verbose.writeln('Using srcPrefix: ' + options.srcPrefix);
		verbose.writeln('Using destPrefix: ' + options.destPrefix);

		// Build the file list
		files = convert(files);

		// Copy files
		if (!copy(files, options)) {
			fail.warn('Nothing was copied for the "' + this.target + '" target');
		}

		// Report if any dependencies have not been copied
		ensure(files, options);
	};

	grunt.registerMultiTask(
		'bowercopy',
		[
			'Copy only the needed files from bower components',
			'over to their specified file locations'
		].join(' '),
		function bowercopy() {
			var self = this;
			var files = this.files;
			
			var srcPrefix = '';
			if (bower.config.directory && grunt.file.isPathAbsolute(bower.config.directory)) {
				srcPrefix = bower.config.directory;
			}
			else {
				srcPrefix = path.join(bower.config.cwd, bower.config.directory);
			}

			// Options
			var options = this.options({
				srcPrefix: srcPrefix,
				destPrefix: '',
				ignore: [],
				report: true,
				runBower: true,
				clean: false,
				copyOptions: {}
			});

			// Back-compat. Non-camelcase
			if (options.runBower || options.runbower) {
				// Run `bower install`
				var done = this.async();

				bower.commands.install().on('log', function(result) {
					log.writeln(['bower', result.id.cyan, result.message].join(' '));
				}).on('error', function(code) {
					fail.fatal(code);
				}).on('end', function() {
					run.call(self, files, options);
					done();
				});
			} else {
				run.call(self, files, options);
			}
		}
	);
};
