define([
	"constants",
	"mediator",
	"aboutPageDemoPies",
	"hbs!aboutPageTemplate"
], function(C, mediator, aboutPageDemoPies, aboutPageTemplate) {
	"use strict";

	var _MODULE_ID = "aboutPage";
	var _demoPie1, _demoPie2, _demoPie3;
	var _firstPageLoaded = false;
	var _firstPage;
	var _isRendered = false;

	var _init = function() {
		$("#about").html(aboutPageTemplate());

		var subscriptions = {};
		subscriptions[C.EVENT.PAGE.LOAD] = _onPageSelected;
		subscriptions[C.EVENT.PAGE.RESIZE] = _onPageResize;
		mediator.subscribe(_MODULE_ID, subscriptions);
	};

	// a bit fussy, but unavoidable. Whenever a page changes, we need to re-draw the pies IFF (if and only if)
	// the initial page load wasn't the about page
	var _onPageSelected = function(msg) {
		if (!_firstPageLoaded) {
			_firstPage = msg.data.page;
			_firstPageLoaded = true;
			if (_firstPage === "about") {
				setTimeout(function() {
					_renderContent();
					var width = $(window).width();

					var breakPointIndex = null;
					for (var i=0; i< C.OTHER.BREAKPOINTS.length; i++) {
						if (width >= C.OTHER.BREAKPOINTS[i]) {
							breakPointIndex = i;
						}
					}
					if (breakPointIndex === null) {
						_handleViewport("small");
					}
				}, 10);
			}
			return;
		}

		if (msg.data.page === "about") {
			if (!_isRendered) {
				_renderContent();
			}
			$("#aboutPageSlides").slidesjs("refresh");
		}
	};

	var _renderContent = function() {
		$("#aboutPageSlides").slidesjs({
			width: 940,
			height: 320,
			navigation: true,
			callback: {
				// bah! God this plugin sucks. Doesn't even pass in the slide number we're going TO.
				start: function(number) {
					switch (number) {
						case 1:
							_demoPie2.redraw();
							_demoPie3.redraw();
							break;
						case 2:
							_demoPie1.redraw();
							_demoPie3.redraw();
							break;
						case 3:
							_demoPie1.redraw();
							_demoPie2.redraw();
							break;
					}
				}
			}
		});

		_demoPie1 = new d3pie("aboutTabDemoPie1", aboutPageDemoPies[0]);
		_demoPie2 = new d3pie("aboutTabDemoPie2", aboutPageDemoPies[1]);
		_demoPie3 = new d3pie("aboutTabDemoPie3", aboutPageDemoPies[2]);

		// fade in the "create similar" buttons
		setTimeout(function() {
			$("#demoPie1-createSimilarBtn,#demoPie2-createSimilarBtn,#demoPie3-createSimilarBtn").fadeIn(400);
		}, 2000);

		_isRendered = true;
	};

	var _onPageResize = function(msg) {
		if (msg.data.breakPoint === null) {
			_handleViewport("small");
		} else {
			_handleViewport("large");
		}
	};

	var _handleViewport = function(size) {
		if (size === "small") {
			//$(".aboutPageRow1").css("display", "block");
			$(".aboutPageRow2, .aboutPageRow3").css("display", "none");
		} else {
			$(".aboutPageRow2, .aboutPageRow3").css("display", "block");
		}
	};

	mediator.register(_MODULE_ID);

	return {
		init: _init
	};
});


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

	var fs = require("fs");
	var vm = require("vm");
	var _includeInThisScope = function (path) {
		var code = fs.readFileSync(path);
		vm.runInThisContext(code, path);
	}.bind(this);
	var packageFile = grunt.file.readJSON("package.json");

	grunt.loadNpmTasks("grunt-contrib-clean");
	grunt.loadNpmTasks("grunt-template");
	grunt.loadNpmTasks("grunt-contrib-watch");
	grunt.loadNpmTasks("grunt-contrib-uglify");
	grunt.loadNpmTasks("grunt-contrib-requirejs");
	grunt.loadNpmTasks("grunt-md5");
	grunt.loadNpmTasks("grunt-contrib-copy");
	grunt.loadNpmTasks("grunt-contrib-cssmin");
	grunt.loadNpmTasks("grunt-contrib-handlebars");
	grunt.loadNpmTasks('grunt-contrib-jshint');

	_includeInThisScope("website/grunt-templates/file-paths.js");
	_includeInThisScope("website/grunt-templates/env-specific-constants.js");

	var _createD3PieFiles = function() {
		var fs = require("fs");
		config.template.d3pieBundle.options.data = {
			_defaultSettings: fs.readFileSync("d3pie-source/_default-settings.js", 'utf8'),
			_helpers:         fs.readFileSync("d3pie-source/_helpers.js", 'utf8'),
			_labels:          fs.readFileSync("d3pie-source/_labels.js", 'utf8'),
			_math:            fs.readFileSync("d3pie-source/_math.js", 'utf8'),
			_segments:        fs.readFileSync("d3pie-source/_segments.js", 'utf8'),
			_text:            fs.readFileSync("d3pie-source/_text.js", 'utf8'),
			_tooltips:        fs.readFileSync("d3pie-source/_tooltips.js", 'utf8'),
			_validate:        fs.readFileSync("d3pie-source/_validate.js", 'utf8')
		};
		grunt.task.run("template:d3pieBundle");
		grunt.task.run("uglify:d3pie");
	};

	/**
	 * The first step of the build process. This sets various settings in the main grunt config for the current build environment.
	 * These govern how the subsequent tasks behave.
	 */
	var setEnv_Dev = function() {
		config.template.indexFile.options.data.C = _CONSTANTS.DEV;
		config.template.indexFile.options.data.D3PIE_VERSION = packageFile.version;
		config.template.devRequireConfig.options.data.handlebarsLib = _CONSTANTS.DEV.HANDLEBARS_LIB;
		config.template.devRequireConfig.options.data.baseUrl = _CONSTANTS.DEV.BASE_URL;
		var lines = [];
		for (var i in _requireJSModulePaths) {
			var file = _requireJSModulePaths[i].replace(/\.js$/, "");
			lines.push('\t\t"' + i + '": "' + file + '"');
		}
		config.template.devRequireConfig.options.data.moduleStr = lines.join(",\n");
		config.template.constants.options.data.VERSION = packageFile.version;
		config.template.constants.options.data.MINIMIZED = _CONSTANTS.DEV.MINIMIZED;
		config.template.constants.options.data.DEBUG = _CONSTANTS.DEV.DEBUG;
	};

	var setEnv_Prod = function() {
		var info = _getHandlebarsFileMap();

		config.template.indexFile.options.data.C = _CONSTANTS.PROD;
		config.template.indexFile.options.data.D3PIE_VERSION = packageFile.version;
		config.template.prodRequireConfig.options.data.handlebarsLib = _CONSTANTS.PROD.HANDLEBARS_LIB;
		config.template.prodRequireConfig.options.data.baseUrl = _CONSTANTS.PROD.BASE_URL;
		config.template.prodRequireConfig.options.data.moduleStr = info.allModules;
		config.template.constants.options.data.VERSION = packageFile.version;
		config.template.constants.options.data.MINIMIZED = _CONSTANTS.PROD.MINIMIZED;
		config.template.constants.options.data.DEBUG = _CONSTANTS.PROD.DEBUG;
		config.handlebars.compile.files = info.handlebarsMap;
	};

	var _getHandlebarsFileMap = function() {
		var obj = {};
		var allModules = [];
		for (var i in _requireJSModulePaths) {
			var path = _requireJSModulePaths[i];

			if (/\.hbs$/.test(_requireJSModulePaths[i])) {
				var parts = _requireJSModulePaths[i].split("/");
				parts[parts.length - 1] = "hbs-" + parts[parts.length - 1].replace(/\.hbs$/, ".js");
				var hbsFileNameAndPath = parts.join("/");

				obj["build/" + hbsFileNameAndPath] = "website/" + path;

				var requireJSModuleName1 = "build/" + hbsFileNameAndPath.replace(/\.js$/, "");
				allModules.push('"' + i + '": "' + requireJSModuleName1 + '"');
			} else {
				var requireJSModuleName2 = "build/" + _requireJSModulePaths[i].replace(/\.js$/, "");
				allModules.push('"' + i + '": "' + requireJSModuleName2 + '"');
			}
		}

		return {
			handlebarsMap: obj,
			allModules: allModules.join(",\n")
		}
	};

	var config = {
		clean: ["build"],

		template: {
			d3pieBundle: {
				options: { data: {} },
				files: {
					"d3pie/d3pie.js": ["d3pie-source/d3pie-source.js"]
				}
			},
			indexFile: {
				options: {
					data: {
						C: null,
						VERSION: "",
						SITE_CSS: "",
						CORE_JS: "",
						APP_START_FILE: ""
					}
				},
				files: {
					"index.html": ["website/grunt-templates/template-index.html"]
				}
			},
			devRequireConfig: {
				options: {
					data: { }
				},
				files: {
					"website/core/require.config.js": ["website/grunt-templates/template-require.config.js"]
				}
			},
			prodRequireConfig: {
				options: {
					data: { }
				},
				files: {
					"build/core/require.config.js": ["website/grunt-templates/template-require.config.js"]
				}
			},
			constants: {
				options: { data: {} },
				files: {
					"website/core/constants.js": ["website/grunt-templates/template-constants.js"]
				}
			}
		},

		watch: {
			scripts: {
				files: "d3pie-source/*.js",
				tasks: ["createD3PieFiles"]
			}
		},

		uglify: {
			d3pie: {
				files: {
					"d3pie/d3pie.min.js": "d3pie/d3pie.js"
				},
				options: {
					banner: "/*!\n" +
						"* d3pie\n" +
						"* @author Ben Keen\n" +
						"* @version " + packageFile.version + "\n" +
						"* @date March 11, 2017\n" +
						"* @repo http://github.com/benkeen/d3pie\n" +
						"*/\n"
				}
			},

			coreJS: {
				files: {
					"build/core-libs.min.js": [
						"website/libs/jquery.js",
						"website/libs/jquery-ui.js",
						"website/libs/jquery.slides.js",
						"d3pie-source/_default-settings.js",
						"website/libs/modernizr.js",
						"d3pie/d3pie.js",
						"website/libs/bootstrap.min.js",
						"website/libs/bootstrap-colorpicker.js",
						"website/libs/three.js",
						"website/libs/prettify.js",
						"website/libs/require.js"
					]
				},
				options: {
					compress: {},
					mangle: {
						except: ["jQuery", "Backbone"]
					}
				}
			},

			appStart: {
				files: {
					"build/core/appStart.js": ["build/core/appStart.js"]
				},
				options: {
					compress: {}
				}
			}
		},

		handlebars: {
			compile: {
				options: {
					amd: true,
					namespace: false
				},
				files: {}
			}
		},

		cssmin: {
			target: {
				files: {
					'build/css/core.css': [
						'website/css/site.css',
						'website/css/bootstrap.css',
						'website/css/bootstrap-colorpicker.css',
						'website/css/prettify.css',
						'website/css/slides.css'
					]
				}
			}
		},

		requirejs: {
			compile: {
				options: {
					name: "build/core/appStart.js",
					out: "build/core/appStartBuild.js",
					baseUrl: "./",
					mainConfigFile: "build/core/require.config.js"
				}
			}
		},

		md5: {
			coreCSS: {
				files: {
					"build/css/core.css": "build/css/core.css"
				},
				options: {
					encoding: null,
					keepBasename: true,
					keepExtension: true,
					after: function(fileChanges, options) {
						// store the new CSS filename in the index file template script
						config.template.indexFile.options.data.SITE_CSS = fileChanges[0].newPath;
					}
				}
			},
			coreJS: {
				files: {
					"build/core-libs.min.js": "build/core-libs.min.js"
				},
				options: {
					encoding: null,
					keepBasename: true,
					keepExtension: true,
					after: function(fileChanges, options) {
						config.template.indexFile.options.data.CORE_JS = fileChanges[0].newPath;
					}
				}
			},
			appStart: {
				files: {
					"build/core/appStartBuild.js": "build/core/appStartBuild.js"
				},
				options: {
					encoding: null,
					keepBasename: true,
					keepExtension: true,
					after: function(fileChanges, options) {
						config.template.indexFile.options.data.APP_START_FILE = fileChanges[0].newPath;
					}
				}
			}
		},

		copy: {
			core: {
				expand: true,
				cwd: "website",
				src: ['**'],
				dest: 'build/'
			}
		},
		jshint: {
			all: [
				'd3pie-source/*.js'
			],
			options: {
                reporterOutput: ""
			}
		}
	};

	grunt.initConfig(config);
	grunt.registerTask("createD3PieFiles", _createD3PieFiles);
	grunt.registerTask("setEnv_Dev", setEnv_Dev);
	grunt.registerTask("setEnv_Prod", setEnv_Prod);

	// the default task is to regenerate the d3pie.js and d3pie.min.js files based on the latest files in d3pie-source/
	grunt.registerTask("default", ["jshint","createD3PieFiles", "uglify:d3pie"]);

	// tasks for building the website. There are only 2 options: dev and prod
	grunt.registerTask("dev", [
		"jshint",
		"setEnv_Dev",
		"template:indexFile",
		"template:devRequireConfig",
		"template:constants"
	]);

	grunt.registerTask("prod", [
		"jshint",
		"setEnv_Prod", // set the build environment constants
		"clean",       // wipe out the build folder
		"template:constants", // create the constants file
		"cssmin",      // bundle the CSS into a single file
		"md5:coreCSS", // rename the file to include its file hash
		"uglify:coreJS", // bundle the core JS
		"md5:coreJS",   // rename the JS lib file

		// now copy EVERYTHING in the /website folder over to /build. We're about to do terrible things to it
		"copy:core",
		"handlebars",
		"template:prodRequireConfig",
		"uglify:appStart",
		"requirejs", // run the requireJS task to bundle up everything into a single file
		"md5:appStart",

		"template:indexFile" // alright! Now re-generate the index file
	]);
};


define([
	"constants",
	"mediator",
	"pageHelper",
	"hbs!generateTabTemplate",
	"hbs!generatedContentTemplate"
], function(C, mediator, pageHelper, generateTabTemplate, generatedContentTemplate) {
	"use strict";

	var _MODULE_ID = "generateTab";
	var _finalConfigObject;

	/**
	 * Our initialization function. Called on page load.
	 * @private
	 */
	var _init = function() {
		mediator.register(_MODULE_ID);

		var subscriptions = {};
		subscriptions[C.EVENT.DEMO_PIE.SEND_DATA] = _onSelectTab;
		mediator.subscribe(_MODULE_ID, subscriptions);
	};

	var _render = function(tabEl, config) {
		$(tabEl).html(generateTabTemplate());

		// add the event handlers
		$("#generator-result").on("click", ".triggerGenerationUpdate", _generatePieCode);
	};

	var _onSelectTab = function(msg) {
		_finalConfigObject = _generateFinalConfigObject(msg.data);
		_generatePieCode();
	};

	var _generatePieCode = function() {
		var minimizeJS = $("#minimizeJS")[0].checked;
		var configObject = (minimizeJS) ? JSON.stringify(_finalConfigObject) : JSON.stringify(_finalConfigObject, null, "\t");
		var generationFormat = $("input[name=outputFormat]:checked").val();

		$("#finalResult").removeClass("prettyprinted").addClass("prettyprint").html(generatedContentTemplate({
			generationFormat: generationFormat,
			configObject: configObject
		}));

		prettyPrint();
	};

	// yikes. The reason for all this hideousness is that we want to construct the *smallest* config object that we
	// can. So each field needs to be examined separately. I looked into object diffing scripts, but honestly there
	// was too much custom stuff needed to be done with many properties - ensuring they're the right types, etc.
	// This is prime real estate for later refactoring, but right now it's near the end of the project and I want this
	// thing out the door... don't judge me. ;-)
	var _generateFinalConfigObject = function(allSettings) {

		var finalObj = {};

		// header title
		var headerTitleTextDiff = allSettings.header.title.text != defaultSettings.header.title.text;
		var headerTitleColorDiff = allSettings.header.title.color != defaultSettings.header.title.color;
		var headerTitleFontSizeDiff = allSettings.header.title.fontSize != defaultSettings.header.title.fontSize;
		var headerTitleFontDiff = allSettings.header.title.font != defaultSettings.header.title.font;
		if (headerTitleTextDiff || headerTitleColorDiff || headerTitleFontSizeDiff || headerTitleFontDiff) {
			finalObj.header = {
				title: {}
			};
			if (headerTitleTextDiff) {
				finalObj.header.title.text = allSettings.header.title.text;
			}
			if (headerTitleColorDiff) {
				finalObj.header.title.color = allSettings.header.title.color;
			}
			if (headerTitleFontSizeDiff) {
				finalObj.header.title.fontSize = parseInt(allSettings.header.title.fontSize, 10);
			}
			if (headerTitleFontDiff) {
				finalObj.header.title.font = allSettings.header.title.font;
			}
		}

		// header subtitle
		var headerSubtitleTextDiff = allSettings.header.subtitle.text != defaultSettings.header.subtitle.text;
		var headerSubtitleColorDiff = allSettings.header.subtitle.color != defaultSettings.header.subtitle.color;
		var headerSubtitleFontSizeDiff = allSettings.header.subtitle.fontSize != defaultSettings.header.subtitle.fontSize;
		var headerSubtitleFontDiff = allSettings.header.subtitle.font != defaultSettings.header.subtitle.font;
		if (headerSubtitleTextDiff || headerSubtitleColorDiff || headerSubtitleFontSizeDiff || headerSubtitleFontDiff) {
			if (!finalObj.hasOwnProperty("header")) { finalObj.header = {}; }
			finalObj.header.subtitle = {};

			if (headerSubtitleTextDiff) {
				finalObj.header.subtitle.text = allSettings.header.subtitle.text;
			}
			if (headerSubtitleColorDiff) {
				finalObj.header.subtitle.color = allSettings.header.subtitle.color;
			}
			if (headerSubtitleFontSizeDiff) {
				finalObj.header.subtitle.fontSize = parseInt(allSettings.header.subtitle.fontSize, 10);
			}
			if (headerSubtitleFontDiff) {
				finalObj.header.subtitle.font = allSettings.header.subtitle.font;
			}
		}

		if (allSettings.header.location != defaultSettings.header.location) {
			if (!finalObj.hasOwnProperty("header")) { finalObj.header = {}; }
			finalObj.header.location = allSettings.header.location;
		}

		if (allSettings.header.titleSubtitlePadding != defaultSettings.header.titleSubtitlePadding) {
			if (!finalObj.hasOwnProperty("header")) { finalObj.header = {}; }
			finalObj.header.titleSubtitlePadding = parseInt(allSettings.header.titleSubtitlePadding, 10);
		}

		// footer
		var footerTextDiff = allSettings.footer.text != defaultSettings.footer.text;
		var footerColorDiff = allSettings.footer.color != defaultSettings.footer.color;
		var footerFontSizeDiff = allSettings.footer.fontSize != defaultSettings.footer.fontSize;
		var footerFontDiff = allSettings.footer.font != defaultSettings.footer.font;
		var footerLocationDiff = allSettings.footer.font != defaultSettings.footer.location;
		if (footerTextDiff || footerColorDiff || footerFontSizeDiff || footerFontDiff) {
			finalObj.footer = {};
			if (footerTextDiff) {
				finalObj.footer.text = allSettings.footer.text;
			}
			if (footerColorDiff) {
				finalObj.footer.color = allSettings.footer.color;
			}
			if (footerFontSizeDiff) {
				finalObj.footer.fontSize = parseInt(allSettings.footer.fontSize, 10);
			}
			if (footerFontDiff) {
				finalObj.footer.font = allSettings.footer.font;
			}
			if (footerLocationDiff) {
				finalObj.footer.location = allSettings.footer.location;
			}
		}

		// size
		var canvasHeightDiff = allSettings.size.canvasHeight != defaultSettings.size.canvasHeight;
		var canvasWidthDiff = allSettings.size.canvasWidth != defaultSettings.size.canvasWidth;
		var pieInnerRadiusDiff = allSettings.size.pieInnerRadius != defaultSettings.size.pieInnerRadius;
		var pieOuterRadiusDiff = allSettings.size.pieOuterRadius != defaultSettings.size.pieOuterRadius;

		if (canvasHeightDiff || canvasWidthDiff || pieInnerRadiusDiff || pieOuterRadiusDiff) {
			finalObj.size = {};
			if (canvasHeightDiff) {
				finalObj.size.canvasHeight = parseFloat(allSettings.size.canvasHeight, 10);
			}
			if (canvasWidthDiff) {
				finalObj.size.canvasWidth = parseFloat(allSettings.size.canvasWidth, 10);
			}
			if (pieInnerRadiusDiff) {
				finalObj.size.pieInnerRadius = allSettings.size.pieInnerRadius;
			}
			if (pieOuterRadiusDiff) {
				finalObj.size.pieOuterRadius = allSettings.size.pieOuterRadius;
			}
		}

		// data
		finalObj.data = {};
		if (allSettings.data.sortOrder != defaultSettings.data.sortOrder) {
			finalObj.data.sortOrder = allSettings.data.sortOrder;
		}

		var smallSegmentGroupingEnabledDiff = allSettings.data.smallSegmentGrouping.enabled != defaultSettings.data.smallSegmentGrouping.enabled;
		if (smallSegmentGroupingEnabledDiff) {
			finalObj.data.smallSegmentGrouping = {};
			finalObj.data.smallSegmentGrouping.enabled = allSettings.data.smallSegmentGrouping.enabled;

			var smallSegmentGroupingValDiff     = allSettings.data.smallSegmentGrouping.value != defaultSettings.data.smallSegmentGrouping.value;
			var smallSegmentGroupingValTypeDiff = allSettings.data.smallSegmentGrouping.valueType != defaultSettings.data.smallSegmentGrouping.valueType;
			var smallSegmentGroupingLabelDiff   = allSettings.data.smallSegmentGrouping.label != defaultSettings.data.smallSegmentGrouping.label;
			var smallSegmentGroupingColorDiff   = allSettings.data.smallSegmentGrouping.color != defaultSettings.data.smallSegmentGrouping.color;

			if (smallSegmentGroupingValDiff) {
				finalObj.data.smallSegmentGrouping.value = allSettings.data.smallSegmentGrouping.value;
			}
			if (smallSegmentGroupingValTypeDiff) {
				finalObj.data.smallSegmentGrouping.valueType = allSettings.data.smallSegmentGrouping.valueType;
			}
			if (smallSegmentGroupingLabelDiff) {
				finalObj.data.smallSegmentGrouping.label = allSettings.data.smallSegmentGrouping.label;
			}
			if (smallSegmentGroupingColorDiff) {
				finalObj.data.smallSegmentGrouping.color = allSettings.data.smallSegmentGrouping.color;
			}
		}

		finalObj.data.content = allSettings.data.content;

		// outer labels
		var outerLabelFormatDiff = allSettings.labels.outer.format != defaultSettings.labels.outer.format;
		var outerLabelHideDiff = allSettings.labels.outer.hideWhenLessThanPercentage != defaultSettings.labels.outer.hideWhenLessThanPercentage;
		var outerLabelPieDistDiff = allSettings.labels.outer.pieDistance != defaultSettings.labels.outer.pieDistance;
		if (outerLabelFormatDiff || outerLabelHideDiff || outerLabelPieDistDiff) {
			finalObj.labels = {
				outer: {}
			};
			if (outerLabelFormatDiff) {
				finalObj.labels.outer.format = allSettings.labels.outer.format;
			}
			if (outerLabelHideDiff) {
				finalObj.labels.outer.hideWhenLessThanPercentage = parseFloat(allSettings.labels.outer.hideWhenLessThanPercentage);
			}
			if (outerLabelPieDistDiff) {
				finalObj.labels.outer.pieDistance = allSettings.labels.outer.pieDistance;
			}
		}

		var innerLabelFormatDiff = allSettings.labels.inner.format != defaultSettings.labels.inner.format;
		var innerLabelHideDiff = allSettings.labels.inner.hideWhenLessThanPercentage != defaultSettings.labels.inner.hideWhenLessThanPercentage;
		if (innerLabelFormatDiff || innerLabelHideDiff) {
			if (!finalObj.hasOwnProperty("labels")) { finalObj.labels = {}; }
			finalObj.labels.inner = {};
			if (innerLabelFormatDiff) {
				finalObj.labels.inner.format = allSettings.labels.inner.format;
			}
			if (innerLabelHideDiff) {
				finalObj.labels.inner.hideWhenLessThanPercentage = parseFloat(allSettings.labels.inner.hideWhenLessThanPercentage);
			}
		}

		var mainLabelColorDiff    = allSettings.labels.mainLabel.color != defaultSettings.labels.mainLabel.color;
		var mainLabelFontDiff     = allSettings.labels.mainLabel.font != defaultSettings.labels.mainLabel.font;
		var mainLabelFontSizeDiff = allSettings.labels.mainLabel.fontSize != defaultSettings.labels.mainLabel.fontSize;
		if (mainLabelColorDiff || mainLabelFontDiff || mainLabelFontSizeDiff) {
			if (!finalObj.hasOwnProperty("labels")) { finalObj.labels = {}; }
			finalObj.labels.mainLabel = {};
			if (mainLabelColorDiff) {
				finalObj.labels.mainLabel.color = allSettings.labels.mainLabel.color;
			}
			if (mainLabelFontDiff) {
				finalObj.labels.mainLabel.font = allSettings.labels.mainLabel.font;
			}
			if (mainLabelFontSizeDiff) {
				finalObj.labels.mainLabel.fontSize = parseInt(allSettings.labels.mainLabel.fontSize, 10);
			}
		}

		var percentageColorDiff    = allSettings.labels.percentage.color != defaultSettings.labels.percentage.color;
		var percentageFontDiff     = allSettings.labels.percentage.font != defaultSettings.labels.percentage.font;
		var percentageFontSizeDiff = allSettings.labels.percentage.fontSize != defaultSettings.labels.percentage.fontSize;
		var percentageDecimalDiff  = allSettings.labels.percentage.decimalPlaces != defaultSettings.labels.percentage.decimalPlaces;
		if (percentageColorDiff || percentageFontDiff || percentageFontSizeDiff || percentageDecimalDiff) {
			if (!finalObj.hasOwnProperty("labels")) { finalObj.labels = {}; }
			finalObj.labels.percentage = {};
			if (percentageColorDiff) {
				finalObj.labels.percentage.color = allSettings.labels.percentage.color;
			}
			if (percentageFontDiff) {
				finalObj.labels.percentage.font = allSettings.labels.percentage.font;
			}
			if (percentageFontSizeDiff) {
				finalObj.labels.percentage.fontSize = parseInt(allSettings.labels.percentage.fontSize, 10);
			}
			if (percentageColorDiff) {
				finalObj.labels.percentage.decimalPlaces = parseInt(allSettings.labels.percentage.decimalPlaces, 10);
			}
		}

		var valueColorDiff    = allSettings.labels.value.color != defaultSettings.labels.value.color;
		var valueFontDiff     = allSettings.labels.value.font != defaultSettings.labels.value.font;
		var valueFontSizeDiff = allSettings.labels.value.fontSize != defaultSettings.labels.value.fontSize;
		if (valueColorDiff || valueFontDiff || valueFontSizeDiff) {
			if (!finalObj.hasOwnProperty("labels")) { finalObj.labels = {}; }
			finalObj.labels.value = {};
			if (valueColorDiff) {
				finalObj.labels.value.color = allSettings.labels.value.color;
			}
			if (valueFontDiff) {
				finalObj.labels.value.font = allSettings.labels.value.font;
			}
			if (valueFontSizeDiff) {
				finalObj.labels.value.fontSize = parseInt(allSettings.labels.value.fontSize, 10);
			}
		}

		// label lines
		var labelLinesDiff = allSettings.labels.lines.enabled != defaultSettings.labels.lines.enabled;
		if (!labelLinesDiff) {
			if (!finalObj.hasOwnProperty("labels")) { finalObj.labels = {}; }
			finalObj.labels.lines = {
				enabled: allSettings.labels.lines.enabled
			};
			if (allSettings.labels.lines.style != defaultSettings.labels.lines.style) {
				finalObj.labels.lines.style = allSettings.labels.lines.style;
			}
			if (allSettings.labels.lines.color != defaultSettings.labels.lines.color) {
				finalObj.labels.lines.color = allSettings.labels.lines.color;
			}
		}

		// label truncation
		var labelTruncationDiff = allSettings.labels.truncation.enabled != defaultSettings.labels.truncation.enabled;
		if (labelTruncationDiff) {
			if (!finalObj.hasOwnProperty("labels")) { finalObj.labels = {}; }
			finalObj.labels.truncation = {
				enabled: allSettings.labels.truncation.enabled
			};
			if (allSettings.labels.truncation.truncateLength != defaultSettings.labels.truncation.truncateLength) {
				finalObj.labels.truncation.truncateLength = allSettings.labels.truncation.truncateLength;
			}
		}

		// tooltips
		var tooltipsDiff = allSettings.tooltips.enabled != defaultSettings.tooltips.enabled;

		if (tooltipsDiff) {
			finalObj.tooltips = {
				enabled: allSettings.tooltips.enabled,
				type: "placeholder",
				string: allSettings.tooltips.string
			};

			if (allSettings.tooltips.styles.fadeInSpeed !== defaultSettings.tooltips.styles.fadeInSpeed) {
				if (!finalObj.tooltips.hasOwnProperty("styles")) { finalObj.tooltips.styles = {}; }
				finalObj.tooltips.styles.fadeInSpeed = allSettings.tooltips.styles.fadeInSpeed;
			}
			if (allSettings.tooltips.styles.backgroundColor !== defaultSettings.tooltips.styles.backgroundColor) {
				if (!finalObj.tooltips.hasOwnProperty("styles")) { finalObj.tooltips.styles = {}; }
				finalObj.tooltips.styles.backgroundColor = allSettings.tooltips.styles.backgroundColor;
			}
			if (allSettings.tooltips.styles.backgroundOpacity !== defaultSettings.tooltips.styles.backgroundOpacity) {
				if (!finalObj.tooltips.hasOwnProperty("styles")) { finalObj.tooltips.styles = {}; }
				finalObj.tooltips.styles.backgroundOpacity = allSettings.tooltips.styles.backgroundOpacity;
			}
			if (allSettings.tooltips.styles.color !== defaultSettings.tooltips.styles.color) {
				if (!finalObj.tooltips.hasOwnProperty("styles")) { finalObj.tooltips.styles = {}; }
				finalObj.tooltips.styles.color = allSettings.tooltips.styles.color;
			}
			if (allSettings.tooltips.styles.borderRadius !== defaultSettings.tooltips.styles.borderRadius) {
				if (!finalObj.tooltips.hasOwnProperty("styles")) { finalObj.tooltips.styles = {}; }
				finalObj.tooltips.styles.borderRadius = allSettings.tooltips.styles.borderRadius;
			}
			if (allSettings.tooltips.styles.font !== defaultSettings.tooltips.styles.font) {
				if (!finalObj.tooltips.hasOwnProperty("styles")) { finalObj.tooltips.styles = {}; }
				finalObj.tooltips.styles.font = allSettings.tooltips.styles.font;
			}
			if (allSettings.tooltips.styles.fontSize !== defaultSettings.tooltips.styles.fontSize) {
				if (!finalObj.tooltips.hasOwnProperty("styles")) { finalObj.tooltips.styles = {}; }
				finalObj.tooltips.styles.fontSize = allSettings.tooltips.styles.fontSize;
			}
			if (allSettings.tooltips.styles.padding !== defaultSettings.tooltips.styles.padding) {
				if (!finalObj.tooltips.hasOwnProperty("styles")) { finalObj.tooltips.styles = {}; }
				finalObj.tooltips.styles.padding = allSettings.tooltips.styles.padding;
			}
		}

		// effects
		var effectsLoadDiff  = allSettings.effects.load.effect != defaultSettings.effects.load.effect;
		var effectsSpeedDiff = allSettings.effects.load.speed != defaultSettings.effects.load.speed;
		if (effectsLoadDiff || effectsSpeedDiff) {
			if (!finalObj.hasOwnProperty("effects")) { finalObj.effects = {}; }
			finalObj.effects.load = {};
			if (effectsLoadDiff) {
				finalObj.effects.load.effect = allSettings.effects.load.effect;
			}
			if (effectsSpeedDiff) {
				finalObj.effects.load.speed = parseInt(allSettings.effects.load.speed, 10);
			}
		}

		var effectsPullOutDiff  = allSettings.effects.pullOutSegmentOnClick.effect != defaultSettings.effects.pullOutSegmentOnClick.effect;
		var effectsPullOutSpeedDiff  = allSettings.effects.pullOutSegmentOnClick.speed != defaultSettings.effects.pullOutSegmentOnClick.speed;
		var effectsPullOutSizeDiff  = allSettings.effects.pullOutSegmentOnClick.size != defaultSettings.effects.pullOutSegmentOnClick.size;
		if (effectsPullOutDiff || effectsPullOutSpeedDiff || effectsPullOutSizeDiff) {
			if (!finalObj.hasOwnProperty("effects")) { finalObj.effects = {}; }
			finalObj.effects.pullOutSegmentOnClick = {};
			if (effectsPullOutDiff) {
				finalObj.effects.pullOutSegmentOnClick.effect = allSettings.effects.pullOutSegmentOnClick.effect;
			}
			if (effectsPullOutSpeedDiff) {
				finalObj.effects.pullOutSegmentOnClick.speed = parseInt(allSettings.effects.pullOutSegmentOnClick.speed, 10);
			}
			if (effectsPullOutSizeDiff) {
				finalObj.effects.pullOutSegmentOnClick.size = parseInt(allSettings.effects.pullOutSegmentOnClick.size, 10);
			}
		}

		if (allSettings.effects.highlightSegmentOnMouseover != defaultSettings.effects.highlightSegmentOnMouseover) {
			if (!finalObj.hasOwnProperty("effects")) { finalObj.effects = {}; }
			finalObj.effects.highlightSegmentOnMouseover = allSettings.effects.highlightSegmentOnMouseover;

			if (allSettings.effects.highlightLuminosity != defaultSettings.effects.highlightLuminosity) {
				finalObj.effects.highlightLuminosity = parseFloat(allSettings.effects.highlightLuminosity, 10);
			}
		}

		// misc
		var miscColorBgDiff = allSettings.misc.colors.background != defaultSettings.misc.colors.background;
		// N.B. It's not possible in the generator to generate the misc.colors.segments property. This is missing on purpose.
		var miscSegmentStrokeDiff = allSettings.misc.colors.segmentStroke != defaultSettings.misc.colors.segmentStroke;

		if (miscColorBgDiff || miscSegmentStrokeDiff) {
			if (!finalObj.hasOwnProperty("misc")) { finalObj.misc = {}; }
			finalObj.misc.colors = {};
			if (miscColorBgDiff) {
				finalObj.misc.colors.background = allSettings.misc.colors.background;
			}
			if (miscSegmentStrokeDiff) {
				finalObj.misc.colors.segmentStroke = allSettings.misc.colors.segmentStroke;
			}
		}

		var gradientEnabledDiff = allSettings.misc.gradient.enabled != defaultSettings.misc.gradient.enabled;
		if (gradientEnabledDiff) {
			if (!finalObj.hasOwnProperty("misc")) { finalObj.misc = {}; }
			finalObj.misc.gradient = {
				enabled: true
			};

			var gradientPercentageDiff = allSettings.misc.gradient.percentage != defaultSettings.misc.gradient.percentage;
			var gradientColorDiff = allSettings.misc.gradient.color != defaultSettings.misc.gradient.color;

			if (gradientPercentageDiff) {
				finalObj.misc.gradient.percentage = parseInt(allSettings.misc.gradient.percentage, 10);
			}
			if (gradientColorDiff) {
				finalObj.misc.gradient.color = allSettings.misc.gradient.color;
			}
		}

		var canvasPaddingTopDiff = allSettings.misc.canvasPadding.top != defaultSettings.misc.canvasPadding.top;
		var canvasPaddingRightDiff = allSettings.misc.canvasPadding.right != defaultSettings.misc.canvasPadding.right;
		var canvasPaddingBottomDiff = allSettings.misc.canvasPadding.bottom != defaultSettings.misc.canvasPadding.bottom;
		var canvasPaddingLeftDiff = allSettings.misc.canvasPadding.left != defaultSettings.misc.canvasPadding.left;
		if (canvasPaddingTopDiff || canvasPaddingRightDiff || canvasPaddingBottomDiff || canvasPaddingLeftDiff) {
			if (!finalObj.hasOwnProperty("misc")) { finalObj.misc = {}; }
			finalObj.misc.canvasPadding = {};

			if (canvasPaddingTopDiff) {
				finalObj.misc.canvasPadding.top = parseInt(allSettings.misc.canvasPadding.top, 10);
			}
			if (canvasPaddingRightDiff) {
				finalObj.misc.canvasPadding.right = parseInt(allSettings.misc.canvasPadding.right, 10);
			}
			if (canvasPaddingBottomDiff) {
				finalObj.misc.canvasPadding.bottom = parseInt(allSettings.misc.canvasPadding.bottom, 10);
			}
			if (canvasPaddingTopDiff) {
				finalObj.misc.canvasPadding.left = parseInt(allSettings.misc.canvasPadding.left, 10);
			}
		}

		var pieCenterOffsetXDiff = allSettings.misc.pieCenterOffset.x != defaultSettings.misc.pieCenterOffset.x;
		var pieCenterOffsetYDiff = allSettings.misc.pieCenterOffset.y != defaultSettings.misc.pieCenterOffset.y;
		if (pieCenterOffsetXDiff || pieCenterOffsetYDiff) {
			if (!finalObj.hasOwnProperty("misc")) { finalObj.misc = {}; }
			finalObj.misc.pieCenterOffset = {};
			if (pieCenterOffsetXDiff) {
				finalObj.misc.pieCenterOffset.x = parseInt(allSettings.misc.pieCenterOffset.x, 10);
			}
			if (pieCenterOffsetYDiff) {
				finalObj.misc.pieCenterOffset.y = parseInt(allSettings.misc.pieCenterOffset.y, 10);
			}
		}

		var miscPrefixDiff = allSettings.misc.cssPrefix != defaultSettings.misc.cssPrefix;
		if (miscPrefixDiff) {
			if (!finalObj.hasOwnProperty("misc")) { finalObj.misc = {}; }
			finalObj.misc.cssPrefix = allSettings.misc.cssPrefix;
		}

		var callbackOnloadDiff = allSettings.callbacks.onload != defaultSettings.callbacks.onload;
		var callbackOnmouseoverDiff = allSettings.callbacks.onMouseoverSegment != defaultSettings.callbacks.onMouseoverSegment;
		var callbackOnmouseoutDiff = allSettings.callbacks.onMouseoutSegment != defaultSettings.callbacks.onMouseoutSegment;
		var callbackOnclickDiff = allSettings.callbacks.onClickSegment != defaultSettings.callbacks.onClickSegment;

		if (callbackOnloadDiff || callbackOnmouseoverDiff || callbackOnmouseoutDiff || callbackOnclickDiff) {
			finalObj.callbacks = {};
			if (callbackOnloadDiff) {
				finalObj.callbacks.onload = allSettings.callbacks.onload;
			}
			if (callbackOnloadDiff) {
				finalObj.callbacks.onMouseoverSegment = allSettings.callbacks.onMouseoverSegment;
			}
			if (callbackOnloadDiff) {
				finalObj.callbacks.onMouseoutSegment = allSettings.callbacks.onMouseoutSegment;
			}
			if (callbackOnloadDiff) {
				finalObj.callbacks.onClickSegment = allSettings.callbacks.onClickSegment;
			}
		}

		return finalObj;
	};

	_init();

	return {
		render: _render
	};
});


/**
 * Contains all the logic for the actual generator.
 */
define([
	"constants",
	"mediator",
	"pageHelper",
	"startTab",
	"titleTab",
	"sizeTab",
	"dataTab",
	"labelsTab",
  "tooltipsTab",
	"footerTab",
	"effectsTab",
	"eventsTab",
	"miscTab",
	"generateTab",
	"examplePies",
	"hbs!generatorPageTemplate"
], function(C, mediator, pageHelper, startTab, titleTab, sizeTab, dataTab, labelsTab, tooltipsTab, footerTab,
    effectsTab, eventsTab, miscTab, generateTab, EXAMPLE_PIES, generatorPageTemplate) {
	"use strict";

	var _MODULE_ID = "generatorPage";
	var _isCreated = false;
	var _demoD3Pie = null;
	var _tabs = [
		"generator-start", "generator-title", "generator-size", "generator-data", "generator-labels", "generator-tooltips",
		"generator-footer", "generator-effects", "generator-events", "generator-misc", "generator-result"
	];
	var _currentTab = null;
	var _firstPageLoaded = false;
	var _firstPage;

	/**
	 * Our initialization function. Called on page load.
	 * @private
	 */
	var _init = function() {
		_addTabEventHandlers();

		var config = {
			hideMainContent: false
		};
		if (document.location.hash === "#generator-result") {
			config = {
				hideMainContent: true
			}
		}
		$("#generator").html(generatorPageTemplate(config));

		// now fade in the three sections: nav, main content & footer row
		$("#generatorTabs,#mainContent,#footerRow").hide().removeClass("hidden").fadeIn(400);

		// always initialize the sidebar with whatever's in the selected example (always first item right now)
		var index = pageHelper.getDemoPieChartIndex(EXAMPLE_PIES);
		_loadDemoPie(EXAMPLE_PIES[index]);

		// focus on the title field, just to be nice
		$("#pieTitle").focus();

		var subscriptions = {};
		subscriptions[C.EVENT.DEMO_PIE.LOAD]                  = _onRequestLoadDemoPie;
		subscriptions[C.EVENT.DEMO_PIE.RENDER.NO_ANIMATION]   = _renderWithNoAnimation;
		subscriptions[C.EVENT.DEMO_PIE.RENDER.WITH_ANIMATION] = _renderWithAnimation;
		subscriptions[C.EVENT.DEMO_PIE.RENDER.UPDATE_PROP]    = _updateProperty;
		subscriptions[C.EVENT.DEMO_PIE.SELECT_SEGMENT]        = _selectPieSegment;
		subscriptions[C.EVENT.PAGE.LOAD]                      = _onPageSelected;
		subscriptions[C.EVENT.PAGE.RESIZE]                    = _onPageResize;
		mediator.subscribe(_MODULE_ID, subscriptions);
	};

	/**
	 * Our main event handler assignment function. Bit klutzy - it would be nice to update this for some sort of
	 * binding, but it's simple & will do for now.
	 * @private
	 */
	var _addTabEventHandlers = function() {
		// general event handlers used in any old tab
		$(document).on("change", ".changeUpdateNoAnimation", _renderWithNoAnimation);
		$(document).on("keyup change", ".keyupUpdateNoAnimation", _onKeyupUpdateNoAnimation);
	};

	var _onKeyupUpdateNoAnimation = function(e) {
		var val = e.target.value;
		var isNumberField = $(e.target).hasClass("numbers");
		var isFloat = $(e.target).hasClass("floatingNum");

		// check it's not empty and contains only numbers
		if (isNumberField) {
			if (/[^\d]/.test(val) || val === "") {
				$(e.target).addClass("hasError");
				return;
			}
		}
		if (isFloat) {
			if (/[^\d\.]/.test(val) || val === "") {
				$(e.target).addClass("hasError");
				return;
			}
		}

		$(e.target).removeClass("hasError");
		_renderWithNoAnimation();
	};

	var _renderWithNoAnimation = function() {
		console.log( _getConfigObject());
		_renderPie(false, _getConfigObject());
	};

	var _renderWithAnimation = function() {
		_renderPie(true, _getConfigObject());
	};

	var _updateProperty = function(msg) {
		_demoD3Pie.updateProp(msg.data.prop, msg.data.value);
	};

	var _renderPie = function(includeStartAnimation, config) {

		// notify anyone that's interested that the data changed and we're going to do a re-render
		mediator.publish(_MODULE_ID, C.EVENT.DEMO_PIE.DATA_CHANGE, {
			includeStartAnimation: includeStartAnimation,
			config: config
		});

		// if we don't want to include the start animation, just override the values in the UI. This is useful for
		// automatically showing the user's latest changes to the pie settings and not having to see the animation
		// each and every time
		if (!includeStartAnimation) {
			config.effects.load.effect = "none";
		}

		if (_isCreated) {
			_demoD3Pie.destroy();
		}

		_demoD3Pie = new d3pie("generatorPieChart", config);
		_isCreated = true;
		var height = parseInt(config.size.canvasHeight, 10) + 2;
		var width  = parseInt(config.size.canvasWidth, 10) + 2;
		$("#generatorPieChartPad, #generatorPieChart").css({
			width: width, // + 2 for padding
			height: height
		});
	};

	/**
	 * Parses the generator fields and get the latest values.
	 * @private
	 */
	var _getConfigObject = function() {
		return {
			header:    titleTab.getTabData(),
			footer:    footerTab.getTabData(),
			size:      sizeTab.getTabData(),
			data:      dataTab.getTabData(),
			labels:    labelsTab.getTabData(),
			tooltips:  tooltipsTab.getTabData(),
			effects:   effectsTab.getTabData(),
			callbacks: eventsTab.getTabData(),
			misc:      miscTab.getTabData()
		};
	};

	var _onRequestLoadDemoPie = function(msg) {
		_loadDemoPie(EXAMPLE_PIES[msg.data.index]);
	};

	var _loadDemoPie = function(pieConfiguration) {
		var config = pieConfiguration.config;

		mediator.publish(_MODULE_ID, C.EVENT.DEMO_PIE.EXAMPLE_CHANGE, { config: config });

		// render the generator tabs
		startTab.render("#generator-start", config);
		titleTab.render("#generator-title", config);
		sizeTab.render("#generator-size", config, pieConfiguration.showCanvasOutline); // meh!
		dataTab.render("#generator-data", config);
		labelsTab.render("#generator-labels", config);
    tooltipsTab.render("#generator-tooltips", config);
		footerTab.render("#generator-footer", config);
		effectsTab.render("#generator-effects", config);
		eventsTab.render("#generator-events", config);
		miscTab.render("#generator-misc", config);
		generateTab.render("#generator-result", config);

		// render the pie!
		_renderWithAnimation();
	};

	var _selectPieSegment = function() {
		var openSegmentInfo = _demoD3Pie.getOpenSegment();
		if (openSegmentInfo === null) {
			_demoD3Pie.openSegment(0);
		} else {
			var nextIndex = openSegmentInfo.index + 1;

			// assumption here is that the dataset in the generator is an accurate reflection of what's in the
			// demo pie. It's reasonable. Or I screwed up somewhere.
			var data = dataTab.getTabData();
			if (nextIndex > data.length-1) {
				nextIndex -= data.length;
			}
			_demoD3Pie.openSegment(nextIndex);
		}
	};

	/**
	 * This is a little wonky, but acceptable. It anticipates that the generate-result tab will need the actual
	 * generated data set and posts it whenever the user selects that tab. Yeah, it's backward - the tab should
	 * retrieve it itself, but since it's loosely coupled, we'd need some sort of way for it to interact with the
	 * data found in this file, which would cause circular references with Require. So that's that.
	 * @private
	 */
	var _sendDemoPieData = function() {
		mediator.publish(_MODULE_ID, C.EVENT.DEMO_PIE.SEND_DATA, _getConfigObject());
	};

	/**
	 * Bloody awful function! This is called whenever the page/tab is changed. It got way out of control. Nav should
	 * be moved to a single, top-level area.
	 * @param msg
	 * @private
	 */
	var _onPageSelected = function(msg) {
		if (!_firstPageLoaded) {
			if (msg.data.pageHash !== "generator-result") {
				_firstPage = msg.data.page;
			}
			_firstPageLoaded = true;
		}
		if (msg.data.page !== "generator") {
			return;
		}

		var pageHash = msg.data.pageHash;
		var tab = (_currentTab) ? _currentTab : "generator-start";
		if ($.inArray(pageHash, _tabs) !== -1) {
			tab = pageHash;
		}

		if (pageHash === "generator-result") {
			_sendDemoPieData();
			$("#sidebar,#pieChartDiv").addClass("fadeOut");
			setTimeout(function() {
				$("#sidebar,#pieChartDiv").addClass("hidden").removeClass("fadeOut");
				$("#generator-result").removeClass("hidden fadeOut").addClass("fadeIn");
			}, C.OTHER.PAGE_LOAD_SPEED);
		} else if (pageHash === "generator" && tab === "generator-result") {
			// do nothing. This happens when a user's on the generator-result tab, goes to
			// another page (not tab), then clicks back to the generator tab
		} else {
			// if the previous tab was generator-result
			if (_currentTab === "generator-result") {
				$("#generator-result").addClass("fadeOut");
				setTimeout(function() {
					$("#generator-result").addClass("hidden").removeClass("fadeOut");
					_renderWithNoAnimation();
					$("#sidebar,#pieChartDiv").removeClass("hidden fadeOut").addClass("fadeIn");
				}, C.OTHER.PAGE_LOAD_SPEED);
			}
		}

		if (msg.data.pageHash.match(/pie\d$/)) {
			var index = pageHelper.getDemoPieChartIndex(EXAMPLE_PIES);
			_loadDemoPie(EXAMPLE_PIES[index]);
		}

		var $generatorTabs = $("#generatorTabs");

		// now show the appropriate tab
		if (_currentTab === null) {
			$generatorTabs.find("a[href=#" + tab + "]").closest("li").addClass("active");
			$("#" + tab).removeClass("fadeOut hidden").addClass("fadeIn");
			_renderWithAnimation();
		} else {
			$generatorTabs.find("a[href=#" + _currentTab + "]").closest("li").removeClass("active");
			$generatorTabs.find("a[href=#" + tab + "]").closest("li").addClass("active");
			$("#" + _currentTab).removeClass("hidden fadeIn").addClass("fadeOut");

			// another klutzy workaround
			if (pageHash === "generator" && tab === "generator-result") {
				$("#" + tab).removeClass("hidden fadeOut");
			} else {
				(function(ct) {
					setTimeout(function() {
						$("#" + ct).addClass("hidden").removeClass("fadeOut");
						$("#" + tab).removeClass("hidden fadeOut").addClass("fadeIn");
					}, C.OTHER.PAGE_LOAD_SPEED);
				})(_currentTab);
			}
		}
		_currentTab = tab;
	};

	var _onPageResize = function() {
		_demoD3Pie.redraw();
	};

	mediator.register(_MODULE_ID);

	return {
		init: _init
	}
});


/*! Lazy Load XT v1.1.0 2016-01-12
 * http://ressio.github.io/lazy-load-xt
 * (C) 2016 RESS.io
 * Licensed under MIT */

(function ($, window, document, undefined) {
    // options
    var lazyLoadXT = 'lazyLoadXT',
        dataLazied = 'lazied',
        load_error = 'load error',
        classLazyHidden = 'lazy-hidden',
        docElement = document.documentElement || document.body,
    //  force load all images in Opera Mini and some mobile browsers without scroll event or getBoundingClientRect()
        forceLoad = (window.onscroll === undefined || !!window.operamini || !docElement.getBoundingClientRect),
        options = {
            autoInit: true, // auto initialize in $.ready
            selector: 'img[data-src]', // selector for lazyloading elements
            blankImage: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
            throttle: 99, // interval (ms) for changes check
            forceLoad: forceLoad, // force auto load all images

            loadEvent: 'pageshow', // check AJAX-loaded content in jQueryMobile
            updateEvent: 'load orientationchange resize scroll touchmove focus', // page-modified events
            forceEvent: 'lazyloadall', // force loading of all elements

            //onstart: null,
            oninit: {removeClass: 'lazy'}, // init handler
            onshow: {addClass: classLazyHidden}, // start loading handler
            onload: {removeClass: classLazyHidden, addClass: 'lazy-loaded'}, // load success handler
            onerror: {removeClass: classLazyHidden}, // error handler
            //oncomplete: null, // complete handler

            //scrollContainer: undefined,
            checkDuplicates: true
        },
        elementOptions = {
            srcAttr: 'data-src',
            edgeX: 0,
            edgeY: 0,
            visibleOnly: true
        },
        $window = $(window),
        $isFunction = $.isFunction,
        $extend = $.extend,
        $data = $.data || function (el, name) {
            return $(el).data(name);
        },
        elements = [],
        topLazy = 0,
    /*
     waitingMode=0 : no setTimeout
     waitingMode=1 : setTimeout, no deferred events
     waitingMode=2 : setTimeout, deferred events
     */
        waitingMode = 0;

    $[lazyLoadXT] = $extend(options, elementOptions, $[lazyLoadXT]);

    /**
     * Return options.prop if obj.prop is undefined, otherwise return obj.prop
     * @param {*} obj
     * @param {*} prop
     * @returns *
     */
    function getOrDef(obj, prop) {
        return obj[prop] === undefined ? options[prop] : obj[prop];
    }

    /**
     * @returns {number}
     */
    function scrollTop() {
        var scroll = window.pageYOffset;
        return (scroll === undefined) ? docElement.scrollTop : scroll;
    }

    /**
     * Add new elements to lazy-load list:
     * $(elements).lazyLoadXT() or $(window).lazyLoadXT()
     *
     * @param {object} [overrides] override global options
     */
    $.fn[lazyLoadXT] = function (overrides) {
        overrides = overrides || {};

        var blankImage = getOrDef(overrides, 'blankImage'),
            checkDuplicates = getOrDef(overrides, 'checkDuplicates'),
            scrollContainer = getOrDef(overrides, 'scrollContainer'),
            forceShow = getOrDef(overrides, 'show'),
            elementOptionsOverrides = {},
            prop;

        // empty overrides.scrollContainer is supported by both jQuery and Zepto
        $(scrollContainer).on('scroll', queueCheckLazyElements);

        for (prop in elementOptions) {
            elementOptionsOverrides[prop] = getOrDef(overrides, prop);
        }

        return this.each(function (index, el) {
            if (el === window) {
                $(options.selector).lazyLoadXT(overrides);
            } else {
                var duplicate = checkDuplicates && $data(el, dataLazied),
                    $el = $(el).data(dataLazied, forceShow ? -1 : 1);

                // prevent duplicates
                if (duplicate) {
                    queueCheckLazyElements();
                    return;
                }

                if (blankImage && el.tagName === 'IMG' && !el.src) {
                    el.src = blankImage;
                }

                // clone elementOptionsOverrides object
                $el[lazyLoadXT] = $extend({}, elementOptionsOverrides);

                triggerEvent('init', $el);

                elements.push($el);
                queueCheckLazyElements();
            }
        });
    };

    /**
     * Process function/object event handler
     * @param {string} event suffix
     * @param {jQuery} $el
     */
    function triggerEvent(event, $el) {
        var handler = options['on' + event];
        if (handler) {
            if ($isFunction(handler)) {
                handler.call($el[0]);
            } else {
                if (handler.addClass) {
                    $el.addClass(handler.addClass);
                }
                if (handler.removeClass) {
                    $el.removeClass(handler.removeClass);
                }
            }
        }

        $el.trigger('lazy' + event, [$el]);

        // queue next check as images may be resized after loading of actual file
        queueCheckLazyElements();
    }

    /**
     * Trigger onload/onerror handler
     * @param {Event} e
     */
    function triggerLoadOrError(e) {
        triggerEvent(e.type, $(this).off(load_error, triggerLoadOrError));
    }

    /**
     * Load visible elements
     * @param {bool} [force] loading of all elements
     */
    function checkLazyElements(force) {
        if (!elements.length) {
            return;
        }

        force = force || options.forceLoad;

        topLazy = Infinity;

        var viewportTop = scrollTop(),
            viewportHeight = window.innerHeight || docElement.clientHeight,
            viewportWidth = window.innerWidth || docElement.clientWidth,
            i,
            length;

        for (i = 0, length = elements.length; i < length; i++) {
            var $el = elements[i],
                el = $el[0],
                objData = $el[lazyLoadXT],
                removeNode = false,
                visible = force || $data(el, dataLazied) < 0,
                topEdge;

            // remove items that are not in DOM
            if (!$.contains(docElement, el)) {
                removeNode = true;
            } else if (force || !objData.visibleOnly || el.offsetWidth || el.offsetHeight) {

                if (!visible) {
                    var elPos = el.getBoundingClientRect(),
                        edgeX = objData.edgeX,
                        edgeY = objData.edgeY;

                    topEdge = (elPos.top + viewportTop - edgeY) - viewportHeight;

                    visible = (topEdge <= viewportTop && elPos.bottom > -edgeY &&
                        elPos.left <= viewportWidth + edgeX && elPos.right > -edgeX);
                }

                if (visible) {
                    $el.on(load_error, triggerLoadOrError);

                    triggerEvent('show', $el);

                    var srcAttr = objData.srcAttr,
                        src = $isFunction(srcAttr) ? srcAttr($el) : el.getAttribute(srcAttr);

                    if (src) {
                        el.src = src;
                    }

                    removeNode = true;
                } else {
                    if (topEdge < topLazy) {
                        topLazy = topEdge;
                    }
                }
            }

            if (removeNode) {
                $data(el, dataLazied, 0);
                elements.splice(i--, 1);
                length--;
            }
        }

        if (!length) {
            triggerEvent('complete', $(docElement));
        }
    }

    /**
     * Run check of lazy elements after timeout
     */
    function timeoutLazyElements() {
        if (waitingMode > 1) {
            waitingMode = 1;
            checkLazyElements();
            setTimeout(timeoutLazyElements, options.throttle);
        } else {
            waitingMode = 0;
        }
    }

    /**
     * Queue check of lazy elements because of event e
     * @param {Event} [e]
     */
    function queueCheckLazyElements(e) {
        if (!elements.length) {
            return;
        }

        // fast check for scroll event without new visible elements
        if (e && e.type === 'scroll' && e.currentTarget === window) {
            if (topLazy >= scrollTop()) {
                return;
            }
        }

        if (!waitingMode) {
            setTimeout(timeoutLazyElements, 0);
        }
        waitingMode = 2;
    }

    /**
     * Initialize list of hidden elements
     */
    function initLazyElements() {
        $window.lazyLoadXT();
    }

    /**
     * Loading of all elements
     */
    function forceLoadAll() {
        checkLazyElements(true);
    }

    /**
     * Initialization
     */
    $(document).ready(function () {
        triggerEvent('start', $window);

        $window
            .on(options.updateEvent, queueCheckLazyElements)
            .on(options.forceEvent, forceLoadAll);

        $(document).on(options.updateEvent, queueCheckLazyElements);

        if (options.autoInit) {
            $window.on(options.loadEvent, initLazyElements);
            initLazyElements(); // standard initialization
        }
    });

})(window.jQuery || window.Zepto || window.$, window, document);

(function ($) {
    var options = $.lazyLoadXT;

    options.selector += ',video,iframe[data-src]';
    options.videoPoster = 'data-poster';

    $(document).on('lazyshow', 'video', function (e, $el) {
        var srcAttr = $el.lazyLoadXT.srcAttr,
            isFuncSrcAttr = $.isFunction(srcAttr),
            changed = false;

        $el.attr('poster', $el.attr(options.videoPoster));
        $el.children('source,track')
            .each(function (index, el) {
                var $child = $(el),
                    src = isFuncSrcAttr ? srcAttr($child) : $child.attr(srcAttr);
                if (src) {
                    $child.attr('src', src);
                    changed = true;
                }
            });

        // reload video
        if (changed) {
            this.load();
        }
    });

})(window.jQuery || window.Zepto || window.$);


/*jslint browser:true, plusplus:true, vars:true */
/*jshint browser:true, jquery:true */

/* SIMPLE VERSION IS NO LONGER SUPPORTED. USE jquery.lazyloadxt.js INSTEAD. */

(function ($, window, document) {
    'use strict';

    // options
    var options = {
            selector: 'img',
            srcAttr: 'data-src',
            classNojs: 'lazy',
            edgeX: 0,
            edgeY: 0,
            throttle: 99,
            visibleOnly: true,
            loadEvent: 'pageshow', // check AJAX-loaded content in jQueryMobile
            updateEvent: 'load orientationchange resize scroll' // page-modified events
        },
        $window = $(window),
        elements = [],
        viewportTop,
        viewportBottom,
        viewportLeft,
        viewportRight,
        topLazy = 0,
    /*
     waitingMode=0 : no setTimeout
     waitingMode=1 : setTimeout, no deferred events
     waitingMode=2 : setTimeout, deferred events
     */
        waitingMode = 0;

    $.lazyLoadXT = $.extend(options, $.lazyLoadXT);

    /**
     * Add new elements to lazy-load list:
     * $(elements).lazyLoadXT() or $(window).lazyLoadXT()
     */
    $.fn.lazyLoadXT = function () {
        this.each(function () {
            if (this === window) {
                $(options.selector).lazyLoadXT();
                return;
            }

            var $el = $(this);

            // prevent duplicates
            if ($el.data('lazied')) {
                return;
            }

            $el
                .data('lazied', 1)
                .removeClass(options.classNojs);

            elements.unshift($el); // push it in the first position as we iterate elements in reverse order
        });

        // queue next check as images may be resized after loading of actual file
        queueCheckLazyElements();

        return this;
    };

    /**
     * Save visible viewport boundary to viewportXXX variables
     */
    function calcViewport() {
        var scrollTop = $window.scrollTop(),
            scrollLeft = window.pageXOffset || 0,
            edgeX = options.edgeX,
            edgeY = options.edgeY;

        viewportTop = scrollTop - edgeY;
        viewportBottom = scrollTop + (window.innerHeight || $window.height()) + edgeY;
        viewportLeft = scrollLeft - edgeX;
        viewportRight = scrollLeft + (window.innerWidth || $window.width()) + edgeX;
    }

    /**
     * Load visible elements
     */
    function checkLazyElements() {
        if (!elements.length) {
            return;
        }

        topLazy = Infinity;
        calcViewport();

        var i = elements.length - 1,
            srcAttr = options.srcAttr;
        for (; i >= 0; i--) {
            var $el = elements[i],
                el = $el[0];

            // remove items that are not in DOM
            if (!$.contains(document.documentElement, el)) {
                elements.splice(i, 1);
            } else if (!options.visibleOnly || el.offsetWidth > 0 || el.offsetHeight > 0) {
                var offset = $el.offset(),
                    elTop = offset.top,
                    elLeft = offset.left;

                if ((elTop < viewportBottom) && (elTop + $el.height() > viewportTop) &&
                    (elLeft < viewportRight) && (elLeft + $el.width() > viewportLeft)) {

                    var src = $el.attr(srcAttr);
                    if (src) {
                        el.src = src;
                    }

                    elements.splice(i, 1);
                } else {
                    if (elTop < topLazy) {
                        topLazy = elTop;
                    }
                }
            }
        }
    }

    /**
     * Run check of lazy elements after timeout
     */
    function timeoutLazyElements() {
        if (waitingMode > 1) {
            waitingMode = 1;
            checkLazyElements();
            setTimeout(timeoutLazyElements, options.throttle);
        } else {
            waitingMode = 0;
        }
    }

    /**
     * Queue check of lazy elements because of event e
     * @param {Event} [e]
     */
    function queueCheckLazyElements(e) {
        if (!elements.length) {
            return;
        }

        // fast check for scroll event without new visible elements
        if (e && e.type === 'scroll') {
            calcViewport();
            if (topLazy >= viewportBottom) {
                return;
            }
        }

        if (!waitingMode) {
            setTimeout(timeoutLazyElements, 0);
        }

        waitingMode = 2;
    }

    /**
     * Initialize list of hidden elements
     */
    function initLazyElements() {
        $(window).lazyLoadXT();
    }

    /**
     * Initialization
     */
    $(document).ready(function () {
        $window
            .on(options.loadEvent, initLazyElements)
            .on(options.updateEvent, queueCheckLazyElements);

        initLazyElements(); // standard initialization
    });

})(window.jQuery || window.Zepto, window, document);


ngApp.controller('LayoutCtrl', ['$scope', '$http', '$rootScope', 'apiAdapter', 'helpers', function ($scope, $http, $rootScope, apiAdapter, helpers) {
	$scope.vm = {
		actorTags: "",
		createSessionFromTemplate: createSessionFromTemplate,
		createTestSession: createTestSession,
		editSession: editSession,
		environments: null,
		editTemplate: editTemplate,
		isLoadingTemplateInfo: true,
		isLoadingTestInfo: true,
		iterations: "1",
		moment: moment,
		replaySession: replaySession,
		selectedEnvironment: null,
		selectedTemplate: null,
		selectedTestsCount: 0,
		sessionLabel: null,
		testParams: null,
		testClick: testClick,
		templates: [],
		tests: [],
		testSessions: null
	};

	/** Stores a reference to the test that was last clicked, to support
	 * shift+click when selecting tests for a session. */
	var lastClickedTest;

	init();

	/** Check the release version in GitHub, compare it with the installed
	 * version and notify the user if a new version is available. */
	function checkVersion() {
		var nextVersionCheckTimestamp = parseInt(Cookies.get('nextVersionCheckTimestamp')) || 0;

		if (!nextVersionCheckTimestamp || (Date.now() >= nextVersionCheckTimestamp)) {
			$http.get('/api/build-info')
				.then(function success(res) {
					var currentVersion = parseVersion(res.data && res.data.version);

					$http.get('https://api.github.com/repos/mcdcorp/opentest/releases')
						.then(
							function success(res) {
								var eightDaysLater = Date.now() + (8 * 24 * 60 * 60 * 1000);
								Cookies.set('nextVersionCheckTimestamp', eightDaysLater);

								var latestVersionStr = res.data && res.data[0] && res.data[0].tag_name;
								var latestVersionUrl = res.data && res.data[0] && res.data[0].html_url;
								var latestVersion = parseVersion(latestVersionStr);
								if (latestVersion && (compareVersions(latestVersion, currentVersion) === 1)) {
									$.notify(
										{
											message:
												'A new OpenTest version is now available: ' + latestVersionStr + '. ' +
												'You should always stay on the latest version to benefit from new features and security updates.'
										}, {
											type: 'info',
											delay: 0,
											placement: { from: 'bottom' }
										})
								}
							},
							function error(res) {
								var oneHourLater = Date.now() + (60 * 60 * 1000);
								Cookies.set('nextVersionCheckTimestamp', oneHourLater);
							});
				});
		}
	}

	function compareVersions(v1, v2) {
		for (var i = 0; i < v1.length; ++i) {
			if (v2.length == i) {
				return 1;
			}

			if (v1[i] == v2[i]) {
				continue;
			} else if (v1[i] > v2[i]) {
				return 1;
			} else {
				return -1;
			}
		}

		if (v1.length != v2.length) {
			return -1;
		}

		return 0;
	}

	function createSessionFromTemplate(templateName) {
		$('#create-from-template').modal('hide');

		var reqBody = {
			"template": {
				"name": templateName.name,
				"path": templateName.path
			}
		};

		$http.post('/api/session', reqBody)
			.then(
				function success() {
					$rootScope.$broadcast('sessionCreated');
				},
				function error(res) {
					console.log(res);
				});
	}

	function createTestSession() {
		var tests = $scope.vm.tests.filter(function (t) { return t.selected; });

		if (!tests.length) { return; }

		var testInfos = tests.map(function (test) {
			return {
				name: test.name,
				path: test.path
			};
		});

		var actorTags = [];

		if ($scope.vm.actorTags) {
			if (typeof $scope.vm.actorTags === 'string') {
				actorTags = $scope.vm.actorTags
					.split(",");
			} else if ($scope.vm.actorTags.length) {
				actorTags = $scope.vm.actorTags;
			}
		}

		// Trim space around tags and filter
		// out any empty tags, if any
		actorTags = actorTags
			.map(function (tag) {
				return tag.trim();
			})
			.filter(function (t) {
				return String(t).trim().length > 0;
			});

		var reqBody = {
			actorTags: actorTags,
			environment: $scope.vm.selectedEnvironment || '',
			maxIterations: $('#session-iterations').val(),
			sessionLabel: $scope.vm.sessionLabel,
			tests: testInfos
		};

		$http.post('/api/session', reqBody)
			.then(
				function success() {
					$rootScope.$broadcast('sessionCreated');
				},
				function error(res) {
					console.log(res);
				});
	}

	function editSession(uiSession) {
		apiAdapter.getSession(uiSession.id)
			.then(function (session) {
				session = session || uiSession;
				$scope.vm.sessionLabel = apiAdapter.computeDuplicateSessionLabel(session.label || session.sessionLabel);

				// Remove the iteration number from the end of the test name
				// for data-driven tests
				$scope.vm.selectedTests = session.tests.map(function (test) {
					var match = test.name.match(/^(.*) \[\d+\]$/);
					if (match) {
						var originalTestName = match[1];
						test.name = originalTestName;
					}
					return test;
				});

				// Remove the duplicate names that will appear for test
				// sessions containing data-driven tests
				var uniqueTestNames = [];
				$scope.vm.selectedTests =
					$scope.vm.selectedTests.filter(function (test) {
						var curatedPath = (test.path || '').replace(/^\/|\/$/g, '');
						var testFullName = curatedPath + '/' + test.name;
						if (uniqueTestNames.indexOf(testFullName) < 0) {
							uniqueTestNames.push(testFullName);
							return true;
						}
					});
				$scope.vm.iterations = (session.maxIterations || 1).toString();
				$scope.vm.actorTags = session.actorTags;
				$scope.vm.selectedEnvironment = session.environment || null;
				$('#create-session').modal('show');
			})
			.catch(function (err) {
				console.log(err);
			});
	}

	function editTemplate() {
		$('#create-from-template').modal('hide');

		if ($scope.vm.selectedTemplate) {
			var session = {
				sessionLabel: $scope.vm.selectedTemplate.sessionLabel,
				iterations: $scope.vm.selectedTemplate.iterations,
				actorTags: $scope.vm.selectedTemplate.actorTags,
				tests: $scope.vm.selectedTemplate.tests
			};
			editSession(session);
		}
	}

	function init() {
		$('#create-session').on('shown.bs.modal', function () {
			$scope.vm.isLoadingTestInfo = true;
			$scope.vm.selectedTestsCount = 0;
			refreshEnvironments();
			refreshTests();
		});

		$('#create-session').on('hidden.bs.modal', function () {
			$scope.vm.isLoadingTestInfo = true;
			$scope.vm.tests = [];
			helpers.safeApply($scope);
		});

		$('#create-from-template').on('shown.bs.modal', function () {
			$scope.vm.selectedTemplate = null;
			$scope.vm.isLoadingTemplateInfo = true;
			refreshTemplates();
		});

		$('#create-from-template').on('hidden.bs.modal', function () {
			$scope.vm.isLoadingTemplateInfo = true;
			$scope.vm.templates = [];
			helpers.safeApply($scope);
		});

		setupVersionCheck();
	}

	/** Parses a semantic version string into an array of three integers. */
	function parseVersion(versionString) {
		if (typeof versionString !== 'string') {
			return null;
		}

		var versionRegexMatch = versionString.match(/v?(\d+)\.(\d+)\.(\d+)/i);
		if (versionRegexMatch) {
			return [parseInt(versionRegexMatch[1]), parseInt(versionRegexMatch[2]), parseInt(versionRegexMatch[3])];
		} else {
			return null;
		}
	}

	function refreshSelectedTestsCount() {
		$scope.vm.selectedTestsCount = $scope.vm.tests.filter(function (test) { return test.selected; }).length;
	}

	function refreshEnvironments() {
		$scope.vm.templates = [];
		$http.get('/api/environments')
			.then(
				function success(res) {
					if (res.data && res.data.length) {
						$scope.vm.environments = res.data;
					} else {
						$scope.vm.environments = null;
					}
				},
				function error(res) {
					console.log(res.data);
				});
	}

	function refreshTemplates() {
		$scope.vm.templates = [];
		$http.get('/api/templates')
			.then(
				function success(res) {
					$scope.vm.templates = res.data;
					$scope.vm.templates.sort(firstBy('path').thenBy('name'));
					$scope.vm.isLoadingTemplateInfo = false;
				},
				function error(res) {
					console.log(res.data);
				});
	}

	function refreshTests() {
		$scope.vm.tests = [];
		$http.get('/api/tests')
			.then(
				function success(res) {
					$scope.vm.isLoadingTestInfo = false;
					$scope.vm.tests = res.data;

					$scope.vm.tests.sort(firstBy('path').thenBy('name'));

					if ($scope.vm.selectedTests) {
						$scope.vm.selectedTests.forEach(function (selectedTest) {
							var selectedTestPath = (selectedTest.path || '').replace(/^\/|\/$/g, '');
							$scope.vm.tests.forEach(function (test) {
								var currentTestPath = test.path.replace(/^\/|\/$/g, '');
								if ((test.name === selectedTest.name) && (currentTestPath === selectedTestPath)) {
									test.selected = true;
								}
							});
						});
						$scope.vm.selectedTests = null;
					}

					refreshSelectedTestsCount();
				},
				function error(res) {
					console.log(res.data);
				});
	}

	function replaySession(uiSession) {
		apiAdapter.getSession(uiSession.id)
			.catch(function (err) {
				console.log(err);
			})
			.then(function (session) {
				var template = session;
				template.sessionLabel = apiAdapter.computeDuplicateSessionLabel(session.label);

				$http.post('/api/session', template)
					.then(
						function success() {
							$rootScope.$broadcast('sessionCreated');
						},
						function error(res) {
							console.log(res);
						});
			});
	}

	/** Setup a timer to check the latest release version in GitHub and
	 * compare it with the installed version. */
	function setupVersionCheck() {
		setInterval(checkVersion, 15 * 60 * 1000);
	}

	function testClick(event, test) {
		if (event.shiftKey && lastClickedTest) {
			var previousClickedTestIndex = $scope.vm.filteredTests.indexOf(lastClickedTest);
			var clickedTestIndex = $scope.vm.filteredTests.indexOf(test);

			if (previousClickedTestIndex >= 0 && clickedTestIndex >= 0) {
				var firstTestIndex = Math.min(previousClickedTestIndex, clickedTestIndex);
				var lastTestIndex = Math.max(previousClickedTestIndex, clickedTestIndex);

				for (var testIndex = firstTestIndex; testIndex <= lastTestIndex; ++testIndex) {
					if ($scope.vm.filteredTests[testIndex]) {
						$scope.vm.filteredTests[testIndex].selected = test.selected;
					}
				}
			}
		}

		lastClickedTest = test;
		refreshSelectedTestsCount();
	}
}]);


/**
 * This file is loaded automatically in the JS interpreter that the test actor
 * uses to evaluate action arguments, all the functions declared here will be
 * available to call in test definitions.
 */

var $global = this;

/**
 * Utility function for string formatting.
 * Usage:
 *     format("{0}, {1}!", "Hello", "world") // "Hello, world!"
 */
function $format(text) {
    var args = arguments;
    var arg;
    var argIndex;

    return text.replace(
        /\{(\d+)\}/g,
        function (match, index) {
            argIndex = Number(index) + 1;
            arg = args[argIndex];
            switch (typeof arg) {
                case 'number':
                case 'string': return args[argIndex];
                case 'object': return JSON.stringify(args[argIndex]);
                case 'undefined': return match;
                default: return args[argIndex];
            }
        });
}

/**
 * Returns a random integer number between min and max.
 * Usage:
 *     $randomInt(1, 100); // 87
 */
function $randomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1) + min);
}

var $random = $randomInt;

/**
 * Returns a random string with the specified length by randomly
 * selecting characters from the pool provided. If the character
 * pool is not provided, the default pool will contain all numbers
 * and letters (both uppercase and lowercase).
 * Usage:
 *     $randomString(5); // 2U8cA
 *     $randomString(5, "ABC123"); // AB2AC
 */
function $randomString(length, characterPool)
{
    if (typeof characterPool !== 'string') {
        characterPool = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    }

    var text = "";
    for (var i = 0; i < length; i++) {
        text += characterPool.charAt(Math.floor(Math.random() * characterPool.length));
    }

    return text;
}

/**
 * Create and return an array of integers.
 * Usage:
 *     $range(5, 3); // [5,6,7]
 *     $range(5);    // [0,1,2,3,4]
 */
function $range(start, length) {
    if (arguments.length === 1) {
        length = arguments[0];
        start = 0;
    }
    return Array.apply(null, Array(length)).map(function (_, index) {
        return index + start;
    });
}


const deepmerge = require('deepmerge');
const Cell = require('./cell.js');
const Row = require('../row/row.js');
const Comment = require('../classes/comment');
const Column = require('../column/column.js');
const Style = require('../style/style.js');
const utils = require('../utils.js');
const util = require('util');

const validXmlRegex = /[\u0009\u000a\u000d\u0020-\uD7FF\uE000-\uFFFD]/u;

/**
 * The list of valid characters is
 * #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
 *
 * We need to test codepoints numerically, instead of regex characters above 65536 (0x10000),
 */
function removeInvalidXml(str) {
    return Array.from(str).map(c => {
        const cp = c.codePointAt(0);
        if (cp >= 65536 && cp <= 1114111) {
            return c
        } else if (c.match(validXmlRegex)) {
            return c;
        } else {
            return '';
        }
    }).join('');
}

function stringSetter(val) {
    let logger = this.ws.wb.logger;

    if (typeof (val) !== 'string') {
        logger.warn('Value sent to String function of cells %s was not a string, it has type of %s',
            JSON.stringify(this.excelRefs),
            typeof (val));
        val = '';
    }
    val = removeInvalidXml(val);

    if (!this.merged) {
        this.cells.forEach((c) => {
            c.string(this.ws.wb.getStringIndex(val));
        });
    } else {
        let c = this.cells[0];
        c.string(this.ws.wb.getStringIndex(val));
    }
    return this;
}

function complexStringSetter(val) {
    if (!this.merged) {
        this.cells.forEach((c) => {
            c.string(this.ws.wb.getStringIndex(val));
        });
    } else {
        let c = this.cells[0];
        c.string(this.ws.wb.getStringIndex(val));
    }
    return this;
}

function numberSetter(val) {
    if (val === undefined || parseFloat(val) !== val) {
        throw new TypeError(util.format('Value sent to Number function of cells %s was not a number, it has type of %s and value of %s',
            JSON.stringify(this.excelRefs),
            typeof (val),
            val
        ));
    }
    val = parseFloat(val);

    if (!this.merged) {
        this.cells.forEach((c, i) => {
            c.number(val);
        });
    } else {
        var c = this.cells[0];
        c.number(val);
    }
    return this;
}

function booleanSetter(val) {
    if (val === undefined || typeof (val.toString().toLowerCase() === 'true' || ((val.toString().toLowerCase() === 'false') ? false : val)) !== 'boolean') {
        throw new TypeError(util.format('Value sent to Bool function of cells %s was not a bool, it has type of %s and value of %s',
            JSON.stringify(this.excelRefs),
            typeof (val),
            val
        ));
    }
    val = val.toString().toLowerCase() === 'true';

    if (!this.merged) {
        this.cells.forEach((c, i) => {
            c.bool(val.toString());
        });
    } else {
        var c = this.cells[0];
        c.bool(val.toString());
    }
    return this;
}

function formulaSetter(val) {
    if (typeof (val) !== 'string') {
        throw new TypeError(util.format('Value sent to Formula function of cells %s was not a string, it has type of %s', JSON.stringify(this.excelRefs), typeof (val)));
    }
    if (this.merged !== true) {
        this.cells.forEach((c, i) => {
            c.formula(val);
        });
    } else {
        var c = this.cells[0];
        c.formula(val);
    }

    return this;
}

function dateSetter(val) {
    let thisDate = new Date(val);
    if (isNaN(thisDate.getTime())) {
        throw new TypeError(util.format('Invalid date sent to date function of cells. %s could not be converted to a date.', val));
    }
    if (this.merged !== true) {
        this.cells.forEach((c, i) => {
            c.date(thisDate);
        });
    } else {
        var c = this.cells[0];
        c.date(thisDate);
    }
    const dtStyle = new Style(this.ws.wb, {
        numberFormat: '[$-409]' + this.ws.wb.opts.dateFormat
    });
    return styleSetter.bind(this)(dtStyle);
}

function styleSetter(val) {
    let thisStyle;
    if (val instanceof Style) {
        thisStyle = val.toObject();
    } else if (val instanceof Object) {
        thisStyle = val;
    } else {
        throw new TypeError(util.format('Parameter sent to Style function must be an instance of a Style or a style configuration object'));
    }

    let borderEdges = {};
    if (thisStyle.border && thisStyle.border.outline) {
        borderEdges.left = this.firstCol;
        borderEdges.right = this.lastCol;
        borderEdges.top = this.firstRow;
        borderEdges.bottom = this.lastRow;
    }

    this.cells.forEach((c) => {
        if (thisStyle.border && thisStyle.border.outline) {
            let thisCellsBorder = {};
            if (c.row === borderEdges.top && thisStyle.border.top) {
                thisCellsBorder.top = thisStyle.border.top;
            }
            if (c.row === borderEdges.bottom && thisStyle.border.bottom) {
                thisCellsBorder.bottom = thisStyle.border.bottom;
            }
            if (c.col === borderEdges.left && thisStyle.border.left) {
                thisCellsBorder.left = thisStyle.border.left;
            }
            if (c.col === borderEdges.right && thisStyle.border.right) {
                thisCellsBorder.right = thisStyle.border.right;
            }
            thisStyle.border = thisCellsBorder;
        }

        if (c.s === 0) {
            let thisCellStyle = this.ws.wb.createStyle(thisStyle);
            c.style(thisCellStyle.ids.cellXfs);
        } else {
            let curStyle = this.ws.wb.styles[c.s];
            let newStyleOpts = deepmerge(curStyle.toObject(), thisStyle);
            let mergedStyle = this.ws.wb.createStyle(newStyleOpts);
            c.style(mergedStyle.ids.cellXfs);
        }
    });
    return this;
}

function hyperlinkSetter(url, displayStr, tooltip) {
    this.excelRefs.forEach((ref) => {
        displayStr = typeof displayStr === 'string' ? displayStr : url;
        this.ws.hyperlinkCollection.add({
            location: url,
            display: displayStr,
            tooltip: tooltip,
            ref: ref
        });
    });
    stringSetter.bind(this)(displayStr);
    return styleSetter.bind(this)({
        font: {
            color: 'Blue',
            underline: true
        }
    });
}

function commentSetter(comment, options) {
    if (this.merged !== true) {
        this.cells.forEach((c, i) => {
            this.ws.comments[c.r] = new Comment(c.r, comment, options)
        });
    } else {
        var c = this.cells[0];
        this.ws.comments[c.r] = new Comment(c.r, comment, options)
    }
    return this;
}

function mergeCells(cellBlock) {
    let excelRefs = cellBlock.excelRefs;
    if (excelRefs instanceof Array && excelRefs.length > 0) {
        excelRefs.sort(utils.sortCellRefs);

        let cellRange = excelRefs[0] + ':' + excelRefs[excelRefs.length - 1];
        let rangeCells = excelRefs;

        let okToMerge = true;
        cellBlock.ws.mergedCells.forEach((cr) => {
            // Check to see if currently merged cells contain cells in new merge request
            let curCells = utils.getAllCellsInExcelRange(cr);
            let intersection = utils.arrayIntersectSafe(rangeCells, curCells);
            if (intersection.length > 0) {
                okToMerge = false;
                cellBlock.ws.wb.logger.error(`Invalid Range for: ${cellRange}. Some cells in this range are already included in another merged cell range: ${cr}.`);
            }
        });
        if (okToMerge) {
            cellBlock.ws.mergedCells.push(cellRange);
        }
    } else {
        throw new TypeError(util.format('excelRefs variable sent to mergeCells function must be an array with length > 0'));
    }
}

/**
 * @class cellBlock
 */
class cellBlock {

    constructor() {
        this.ws;
        this.cells = [];
        this.excelRefs = [];
        this.merged = false;
    }

    get matrix() {
        let matrix = [];
        let tmpObj = {};
        this.cells.forEach((c) => {
            if (!tmpObj[c.row]) {
                tmpObj[c.row] = [];
            }
            tmpObj[c.row].push(c);
        });
        let rows = Object.keys(tmpObj);
        rows.forEach((r) => {
            tmpObj[r].sort((a, b) => {
                return a.col - b.col;
            });
            matrix.push(tmpObj[r]);
        });
        return matrix;
    }

    get firstRow() {
        let firstRow;
        this.cells.forEach((c) => {
            if (c.row < firstRow || firstRow === undefined) {
                firstRow = c.row;
            }
        });
        return firstRow;
    }

    get lastRow() {
        let lastRow;
        this.cells.forEach((c) => {
            if (c.row > lastRow || lastRow === undefined) {
                lastRow = c.row;
            }
        });
        return lastRow;
    }

    get firstCol() {
        let firstCol;
        this.cells.forEach((c) => {
            if (c.col < firstCol || firstCol === undefined) {
                firstCol = c.col;
            }
        });
        return firstCol;
    }

    get lastCol() {
        let lastCol;
        this.cells.forEach((c) => {
            if (c.col > lastCol || lastCol === undefined) {
                lastCol = c.col;
            }
        });
        return lastCol;
    }
}

/**
 * Module repesenting a Cell Accessor
 * @alias Worksheet.cell
 * @namespace
 * @func Worksheet.cell
 * @desc Access a range of cells in order to manipulate values
 * @param {Number} row1 Row of top left cell
 * @param {Number} col1 Column of top left cell
 * @param {Number} row2 Row of bottom right cell (optional)
 * @param {Number} col2 Column of bottom right cell (optional)
 * @param {Boolean} isMerged Merged the cell range into a single cell
 * @returns {cellBlock}
 */
function cellAccessor(row1, col1, row2, col2, isMerged) {
    let theseCells = new cellBlock();
    theseCells.ws = this;

    row2 = row2 ? row2 : row1;
    col2 = col2 ? col2 : col1;

    if (row2 > this.lastUsedRow) {
        this.lastUsedRow = row2;
    }

    if (col2 > this.lastUsedCol) {
        this.lastUsedCol = col2;
    }

    for (let r = row1; r <= row2; r++) {
        for (let c = col1; c <= col2; c++) {
            let ref = `${utils.getExcelAlpha(c)}${r}`;
            if (!this.cells[ref]) {
                this.cells[ref] = new Cell(r, c);
            }
            if (!this.rows[r]) {
                this.rows[r] = new Row(r, this);
            }
            if (this.rows[r].cellRefs.indexOf(ref) < 0) {
                this.rows[r].cellRefs.push(ref);
            }

            theseCells.cells.push(this.cells[ref]);
            theseCells.excelRefs.push(ref);
        }
    }
    if (isMerged) {
        theseCells.merged = true;
        mergeCells(theseCells);
    }

    return theseCells;
}

/**
 * @alias cellBlock.string
 * @func cellBlock.string
 * @param {String} val Value of String
 * @returns {cellBlock} Block of cells with attached methods
 */
cellBlock.prototype.string = function (val) {
    if (val instanceof Array) {
        return complexStringSetter.bind(this)(val);
    } else {
        return stringSetter.bind(this)(val);
    }
};

/**
 * @alias cellBlock.style
 * @func cellBlock.style
 * @param {Object} style One of a Style instance or an object with Style parameters
 * @returns {cellBlock} Block of cells with attached methods
 */
cellBlock.prototype.style = styleSetter;

/**
 * @alias cellBlock.number
 * @func cellBlock.number
 * @param {Number} val Value of Number
 * @returns {cellBlock} Block of cells with attached methods
 */
cellBlock.prototype.number = numberSetter;

/**
 * @alias cellBlock.bool
 * @func cellBlock.bool
 * @param {Boolean} val Value of Boolean
 * @returns {cellBlock} Block of cells with attached methods
 */
cellBlock.prototype.bool = booleanSetter;

/**
 * @alias cellBlock.formula
 * @func cellBlock.formula
 * @param {String} val Excel style formula as string
 * @returns {cellBlock} Block of cells with attached methods
 */
cellBlock.prototype.formula = formulaSetter;

/**
 * @alias cellBlock.date
 * @func cellBlock.date
 * @param {Date} val Value of Date
 * @returns {cellBlock} Block of cells with attached methods
 */
cellBlock.prototype.date = dateSetter;

/**
 * @alias cellBlock.link
 * @func cellBlock.link
 * @param {String} url Value of Hyperlink URL
 * @param {String} displayStr Value of String representation of URL
 * @param {String} tooltip Value of text to display as hover
 * @returns {cellBlock} Block of cells with attached methods
 */
cellBlock.prototype.link = hyperlinkSetter;

cellBlock.prototype.comment = commentSetter;

module.exports = cellAccessor;


'use strict';

var _ = require('lodash');
var crypto = require('crypto');
var base32 = require('thirty-two');
var mail = require('../../email');
var models = require('../../models');
var User = models.User;
var InviteCode = models.InviteCode;

module.exports = {
  add: add,
  del: del
};

function updateInvite(invite, collaboration, done) {
  var found = _.find(invite.collaborations, function (c) {
    return c.project === collaboration.project;
  });
  // There is already an outstanding invite to this user, just push these additional perms onto the collaborations
  // list and send another email. We only push if they have not already been added to the invite.
  if (!found) {
    invite.update({$push: {collaborations: collaboration}}, function (err) {
      if (err) return done(err);
      // Invite updated, should probably send another email to recipient.
      return done(null, false, false);
    });
  } else {
    return done(null, false, true);
  }
}

function sendInvite(inviter, email, collaboration, done) {
  var random = crypto.randomBytes(5).toString('hex');
  var invite_code = base32.encode(random);
  var invite = new InviteCode({
    code: invite_code,
    emailed_to: email,
    created_timestamp: new Date(),
    collaborations: [collaboration]
  });

  invite.save(function (err) {
    if (err) return done(err);
    // Invite created, send email to recipient.
    mail.sendInviteCollaboratorNewUser(inviter, email, invite_code, collaboration.project);
    return done(null, false, false);
  });

}

// done(err, userExisted, inviteExisted)
function add(project, email, accessLevel, inviter, done) {
  User.findOne({email: email}, function (err, user) {
    if (err) {
      return done(err);
    }

    if (user) {
      var p = _.find(user.projects, function (p) {
        return p.name === project.toLowerCase();
      });
      if (p) {
        return done('user already a collaborator', true);
      }
      User.update({email: email}, {
        $push: {
          'projects': {
            name: project.toLowerCase(),
            display_name: project,
            access_level: accessLevel
          }
        }
      }, function (err) {
        if (err) return done(err, true);
        done(null, true);
      });
    } else {
      var collaboration = {
        project: project,
        invited_by: inviter._id,
        access_level: accessLevel
      };
      InviteCode.findOne({emailed_to: email, consumed_timestamp: null}, function (err, invite) {
        if (err) return done(err);
        if (invite) {
          return updateInvite(invite, collaboration, done);
        }
        sendInvite(inviter, email, collaboration, done);
      });
    }
  });
}

function del(project, email, done) {
  User.update({email: email, 'projects.name': project.toLowerCase()},
    {$pull: {'projects': {'name': project.toLowerCase()}}}, done);
}


'use strict';

var utils = require('./utils');
var models = require('./models');

var Project = models.Project;
var User = models.User;

module.exports = {
  allProjects: allProjects
};

// Get a sanitized listing of all projects, along with the users who have access
function allProjects(done) {
  User.find({}, function (err, users) {
    if (err) return done(err);

    Project.find()
      .sort({_id: -1})
      .exec(function (err, projects) {
        if (err) return done(err);
        done(null, projects.map(function (project) {
          project = utils.sanitizeProject(project);
          project.created_date = utils.timeFromId(project._id);
          project.users = [];
          for (var i = 0; i < users.length; i++) {
            if ('undefined' !== typeof users[i].projects[project.name]) {
              project.users.push({
                email: users[i].email,
                access: users[i].projects[project.name]
              });
            }
          }
          return project;
        }));
      });
  });
}


'use strict';

var fs = require('fs');
var path = require('path');
var everypaas = require('everypaas');
var _ = require('lodash');
var debug = require('debug')('strider:config');
var pjson = require('../package.json');
var hasGithub = pjson && pjson.dependencies['strider-github'];

var envDefaults = {
  host: process.env.HOST || '0.0.0.0',
  port: process.env.PORT || 3000,
  server_name: 'http://localhost',

  db_uri: everypaas.getMongodbUrl() || 'mongodb://localhost:27017/strider-foss',

  smtp_host: '',
  smtp_port: 587,
  smtp_user: '',
  smtp_pass: '',
  smtp_from: 'Strider <noreply@stridercd.com>',
  smtp_secure: '',

  enablePty: false,
  extpath: 'node_modules',
  session_secret: '8L8BudMkqBUqrz',

  github_app_id: '',
  github_secret: '',

  cors: false,
  body_parser_limit: false,
  ldap: false,

  jobsQuantityOnPage: {
    enabled: process.env.JOBS_QUANTITY_ON_PAGE_ENABLED || true,
    default: process.env.JOBS_QUANTITY_ON_PAGE_DEFAULT || 20,
    min: process.env.JOBS_QUANTITY_ON_PAGE_MIN || 1,
    max: process.env.JOBS_QUANTITY_ON_PAGE_MAX || 100
  }
};

envDefaults.server_name = `${envDefaults.server_name}:${envDefaults.port}`;

var defaults = _.extend({
  // Logging configuration
  logging: {
    exitOnError: true,
    file_enabled: false,
    console: {
      // Log everything
      level: 0,
      colorize: true,
      timestamp: true
    },
    console_enabled: true
  },
  viewpath: path.join(__dirname, 'views')
}, envDefaults);

module.exports = {
  defaults: defaults,
  deprecated: deprecated,
  smtp: smtp,
  filterEnv: filterEnv,
  addPlugins: addPlugins,
  camel: camel,
  getConfig: getConfig
};

// main function. Get the config, using rc
function getConfig() {
  process.env = filterEnv(deprecated(process.env), envDefaults);
  var rc = require('rc')('strider', defaults);

  if (!rc.smtp) rc.smtp = smtp(rc);
  if (!rc.smtp) rc.stubSmtp = true;

  rc.ldap = getConfigByName('ldap');

  addPlugins(rc, process.env);

  // BACK COMPAT until we get strider config into plugins...
  if (hasGithub) {
    rc.plugins.github = rc.plugins.github || {};
    rc.plugins.github.hostname = rc.server_name;
  }

  debug(rc);

  return rc;
}

function getConfigByName(filname) {
  var configPath = path.join(__dirname, '..', `${filname}.json`);
  if (fs.existsSync(configPath)) {
    return require(configPath);
  } else {
    return false;
  }
}

function camel(words) {
  return words[0] + words.slice(1)
    .map(function (word) {
      return word[0].toUpperCase() + word.slice(1);
    }).join('');
}

function addPlugins(rc, env) {
  var parts;
  if (!rc.plugins) rc.plugins = {};
  for (var key in env) {
    if (!key.match(/^plugin_/i)) continue;
    parts = key.toLowerCase().split('_');
    if (parts.length === 2) {
      try {
        rc.plugins[parts[1]] = JSON.parse(env[key]);
      } catch (e) {
        console.warn('Ignoring config because it\'s not valid JSON: ', key, env[key]);
      }
      continue;
    }
    if (!rc.plugins[parts[1]]) rc.plugins[parts[1]] = {};
    rc.plugins[parts[1]][camel(parts.slice(2))] = env[key];
  }
}

// Filter process.env.FOO to process.env.strider_foo for rc's benefit
function filterEnv(env, defaults) {
  var res = {};
  for (var k in env) {
    if (defaults[k.toLowerCase()] !== undefined) {
      res[`strider_${k.toLowerCase()}`] = env[k];
    } else {
      res[k] = env[k];
    }
  }
  return res;
}

function deprecated(env) {
  var nenv = _.extend({}, env);
  if (env.APP_ID) {
    console.warn('WARNING: You are using APP_ID to configure Github OAuth application id.');
    console.warn('This name has been deprecated. Please use PLUGIN_GITHUB_APP_ID instead.\n');
    nenv.PLUGIN_GITHUB_APP_ID = env.APP_ID;
  }
  if (env.APP_SECRET) {
    console.warn('WARNING: You are using APP_SECRET to configure Github OAuth application secret.');
    console.warn('This name has been deprecated. Please use PLUGIN_GITHUB_APP_SECRET instead.\n');
    nenv.PLUGIN_GITHUB_APP_SECRET = env.APP_SECRET;
  }
  if (env.GITHUB_APP_ID) {
    console.warn('WARNING: You are using GITHUB_APP_ID to configure Github OAuth application id.');
    console.warn('This name has been deprecated. Please use PLUGIN_GITHUB_APP_ID instead.\n');
    nenv.PLUGIN_GITHUB_APP_ID = env.GITHUB_APP_ID;
  }
  if (env.GITHUB_SECRET) {
    console.warn('WARNING: You are using GITHUB_SECRET to configure Github OAuth application secret.');
    console.warn('This name has been deprecated. Please use PLUGIN_GITHUB_APP_SECRET instead.\n');
    nenv.PLUGIN_GITHUB_APP_SECRET = env.GITHUB_SECRET;
  }
  if (env.GITHUB_API_ENDPOINT) {
    console.warn('WARNING: You are using GITHUB_API_ENDPOINT to configure Github API base URL.');
    console.warn('This name has been deprecated. Please use PLUGIN_GITHUB_API_BASE instead.\n');
    nenv.PLUGIN_GITHUB_API_BASE = env.GITHUB_API_ENDPOINT;
  }
  return nenv;
}

function smtp(rc) {
  if (!rc.smtp_host) {
    return;
  }

  var options = {
    host: rc.smtp_host,
    port: rc.smtp_port,
    from: rc.smtp_from,
    secure: rc.smtp_secure
  };

  if (rc.smtp_user) {
    options.auth = {
      user: rc.smtp_user,
      pass: rc.smtp_pass
    };
  }

  return options;
}