/**
 * Special concat/build task to handle various jQuery build requirements
 * Concats AMD modules, removes their definitions,
 * and includes/excludes specified modules
 */

"use strict";

module.exports = function( grunt ) {
	var fs = require( "fs" ),
		requirejs = require( "requirejs" ),
		Insight = require( "insight" ),
		pkg = require( "../../package.json" ),
		srcFolder = __dirname + "/../../src/",
		rdefineEnd = /\}\s*?\);[^}\w]*$/,
		read = function( fileName ) {
			return grunt.file.read( srcFolder + fileName );
		},

		// Catch `// @CODE` and subsequent comment lines event if they don't start
		// in the first column.
		wrapper = read( "wrapper.js" ).split( /[\x20\t]*\/\/ @CODE\n(?:[\x20\t]*\/\/[^\n]+\n)*/ ),

		config = {
			baseUrl: "src",
			name: "jquery",

			// Allow strict mode
			useStrict: true,

			// We have multiple minify steps
			optimize: "none",

			// Include dependencies loaded with require
			findNestedDependencies: true,

			// Avoid inserting define() placeholder
			skipModuleInsertion: true,

			// Avoid breaking semicolons inserted by r.js
			skipSemiColonInsertion: true,
			wrap: {
				start: wrapper[ 0 ].replace( /\/\*\s*eslint(?: |-).*\s*\*\/\n/, "" ),
				end: wrapper[ 1 ]
			},
			rawText: {},
			onBuildWrite: convert
		};

	/**
	 * Strip all definitions generated by requirejs
	 * Convert "var" modules to var declarations
	 * "var module" means the module only contains a return
	 * statement that should be converted to a var declaration
	 * This is indicated by including the file in any "var" folder
	 * @param {String} name
	 * @param {String} path
	 * @param {String} contents The contents to be written (including their AMD wrappers)
	 */
	function convert( name, path, contents ) {
		var amdName;

		// Convert var modules
		if ( /.\/var\//.test( path.replace( process.cwd(), "" ) ) ) {
			contents = contents
				.replace(
					/define\(\s*(["'])[\w\W]*?\1[\w\W]*?return/,
					"var " +
					( /var\/([\w-]+)/.exec( name )[ 1 ] ) +
					" ="
				)
				.replace( rdefineEnd, "" );

		} else {

			contents = contents
				.replace( /\s*return\s+[^\}]+(\}\s*?\);[^\w\}]*)$/, "$1" )

				// Multiple exports
				.replace( /\s*exports\.\w+\s*=\s*\w+;/g, "" );

			// Remove define wrappers, closure ends, and empty declarations
			contents = contents
				.replace( /define\([^{]*?{\s*(?:("|')use strict\1(?:;|))?/, "" )
				.replace( rdefineEnd, "" );

			// Remove anything wrapped with
			// /* ExcludeStart */ /* ExcludeEnd */
			// or a single line directly after a // BuildExclude comment
			contents = contents
				.replace( /\/\*\s*ExcludeStart\s*\*\/[\w\W]*?\/\*\s*ExcludeEnd\s*\*\//ig, "" )
				.replace( /\/\/\s*BuildExclude\n\r?[\w\W]*?\n\r?/ig, "" );

			// Remove empty definitions
			contents = contents
				.replace( /define\(\[[^\]]*\]\)[\W\n]+$/, "" );
		}

		// AMD Name
		if ( ( amdName = grunt.option( "amd" ) ) != null && /^exports\/amd$/.test( name ) ) {
			if ( amdName ) {
				grunt.log.writeln( "Naming jQuery with AMD name: " + amdName );
			} else {
				grunt.log.writeln( "AMD name now anonymous" );
			}

			// Remove the comma for anonymous defines
			contents = contents
				.replace( /(\s*)"jquery"(\,\s*)/, amdName ? "$1\"" + amdName + "\"$2" : "" );

		}
		return contents;
	}

	grunt.registerMultiTask(
		"build",
		"Concatenate source, remove sub AMD definitions, " +
			"(include/exclude modules with +/- flags), embed date/version",
	function() {
		var flag, index,
			done = this.async(),
			flags = this.flags,
			optIn = flags[ "*" ],
			name = grunt.option( "filename" ),
			minimum = this.data.minimum,
			removeWith = this.data.removeWith,
			excluded = [],
			included = [],
			version = grunt.config( "pkg.version" ),

			/**
			 * Recursively calls the excluder to remove on all modules in the list
			 * @param {Array} list
			 * @param {String} [prepend] Prepend this to the module name.
			 *  Indicates we're walking a directory
			 */
			excludeList = function( list, prepend ) {
				if ( list ) {
					prepend = prepend ? prepend + "/" : "";
					list.forEach( function( module ) {

						// Exclude var modules as well
						if ( module === "var" ) {
							excludeList(
								fs.readdirSync( srcFolder + prepend + module ), prepend + module
							);
							return;
						}
						if ( prepend ) {

							// Skip if this is not a js file and we're walking files in a dir
							if ( !( module = /([\w-\/]+)\.js$/.exec( module ) ) ) {
								return;
							}

							// Prepend folder name if passed
							// Remove .js extension
							module = prepend + module[ 1 ];
						}

						// Avoid infinite recursion
						if ( excluded.indexOf( module ) === -1 ) {
							excluder( "-" + module );
						}
					} );
				}
			},

			/**
			 * Adds the specified module to the excluded or included list, depending on the flag
			 * @param {String} flag A module path relative to
			 *  the src directory starting with + or - to indicate
			 *  whether it should included or excluded
			 */
			excluder = function( flag ) {
				var additional,
					m = /^(\+|\-|)([\w\/-]+)$/.exec( flag ),
					exclude = m[ 1 ] === "-",
					module = m[ 2 ];

				if ( exclude ) {

					// Can't exclude certain modules
					if ( minimum.indexOf( module ) === -1 ) {

						// Add to excluded
						if ( excluded.indexOf( module ) === -1 ) {
							grunt.log.writeln( flag );
							excluded.push( module );

							// Exclude all files in the folder of the same name
							// These are the removable dependencies
							// It's fine if the directory is not there
							try {
								excludeList( fs.readdirSync( srcFolder + module ), module );
							} catch ( e ) {
								grunt.verbose.writeln( e );
							}
						}

						additional = removeWith[ module ];

						// Check removeWith list
						if ( additional ) {
							excludeList( additional.remove || additional );
							if ( additional.include ) {
								included = included.concat( additional.include );
								grunt.log.writeln( "+" + additional.include );
							}
						}
					} else {
						grunt.log.error( "Module \"" + module + "\" is a minimum requirement." );
					}
				} else {
					grunt.log.writeln( flag );
					included.push( module );
				}
			};

		// Filename can be passed to the command line using
		// command line options
		// e.g. grunt build --filename=jquery-custom.js
		name = name ? ( "dist/" + name ) : this.data.dest;

		// append commit id to version
		if ( process.env.COMMIT ) {
			version += " " + process.env.COMMIT;
		}

		// figure out which files to exclude based on these rules in this order:
		//  dependency explicit exclude
		//  > explicit exclude
		//  > explicit include
		//  > dependency implicit exclude
		//  > implicit exclude
		// examples:
		//  *                  none (implicit exclude)
		//  *:*                all (implicit include)
		//  *:*:-css           all except css and dependents (explicit > implicit)
		//  *:*:-css:+effects  same (excludes effects because explicit include is
		//                     trumped by explicit exclude of dependency)
		//  *:+effects         none except effects and its dependencies
		//                     (explicit include trumps implicit exclude of dependency)
		delete flags[ "*" ];
		for ( flag in flags ) {
			excluder( flag );
		}

		// Replace exports/global with a noop noConflict
		if ( ( index = excluded.indexOf( "exports/global" ) ) > -1 ) {
			config.rawText[ "exports/global" ] = "define(['../core']," +
				"function( jQuery ) {\njQuery.noConflict = function() {};\n});";
			excluded.splice( index, 1 );
		}

		grunt.verbose.writeflags( excluded, "Excluded" );
		grunt.verbose.writeflags( included, "Included" );

		// append excluded modules to version
		if ( excluded.length ) {
			version += " -" + excluded.join( ",-" );

			// set pkg.version to version with excludes, so minified file picks it up
			grunt.config.set( "pkg.version", version );
			grunt.verbose.writeln( "Version changed to " + version );

			// Have to use shallow or core will get excluded since it is a dependency
			config.excludeShallow = excluded;
		}
		config.include = included;

		/**
		 * Handle Final output from the optimizer
		 * @param {String} compiled
		 */
		config.out = function( compiled ) {
			compiled = compiled

				// Embed Version
				.replace( /@VERSION/g, version )

				// Embed Date
				// yyyy-mm-ddThh:mmZ
				.replace( /@DATE/g, ( new Date() ).toISOString().replace( /:\d+\.\d+Z$/, "Z" ) );

			// Write concatenated source to file
			grunt.file.write( name, compiled );
		};

		// Turn off opt-in if necessary
		if ( !optIn ) {

			// Overwrite the default inclusions with the explicit ones provided
			config.rawText.jquery = "define([" +
				( included.length ? included.join( "," ) : "" ) +
			"]);";
		}

		// Trace dependencies and concatenate files
		requirejs.optimize( config, function( response ) {
			grunt.verbose.writeln( response );
			grunt.log.ok( "File '" + name + "' created." );
			done();
		}, function( err ) {
			done( err );
		} );
	} );

	// Special "alias" task to make custom build creation less grawlix-y
	// Translation example
	//
	//   grunt custom:+ajax,-dimensions,-effects,-offset
	//
	// Becomes:
	//
	//   grunt build:*:*:+ajax:-dimensions:-effects:-offset
	grunt.registerTask( "custom", function() {
		var args = this.args,
			modules = args.length ? args[ 0 ].replace( /,/g, ":" ) : "",
			done = this.async(),
			insight = new Insight( {
				trackingCode: "UA-1076265-4",
				pkg: pkg
			} );

		function exec( trackingAllowed ) {
			var tracks = args.length ? args[ 0 ].split( "," ) : [];
			var defaultPath = [ "build", "custom" ];

			tracks = tracks.map( function( track ) {
				return track.replace( /\//g, "+" );
			} );

			if ( trackingAllowed ) {

				// Track individuals
				tracks.forEach( function( module ) {
					var path = defaultPath.concat( [ "individual" ], module );

					insight.track.apply( insight, path );
				} );

				// Track full command
				insight.track.apply( insight, defaultPath.concat( [ "full" ], tracks ) );
			}

			grunt.task.run( [ "build:*:*" + ( modules ? ":" + modules : "" ), "uglify", "dist" ] );
			done();
		}

		grunt.log.writeln( "Creating custom build...\n" );

		// Ask for permission the first time
		if ( insight.optOut === undefined ) {
			insight.askPermission( null, function( _error, result ) {
				exec( result );
			} );
		} else {
			exec( !insight.optOut );
		}
	} );
};
