(function () {
	"use strict";
	// Adjust jQuery prefix to avoid namespace object.
	jQuery.noConflict();
	var $ = {
		/**
		 * Initialize everything
		 */
		initialize: function () {
			// Set source of audio element.
			$.Audio.Element.src = $.Audio.Track;
			// Call load method of audio element (required by mobile browsers)
			$.Audio.Element.load();
			// Wait for it to load sufficiently before firing subsequent initialize functions (required to create AudioNodes)
			$.Audio.Element.addEventListener("canplay", function() {
				$.Audio.initialize();
				$.Visualiser.initialize();
				$.Controls.initialize();
			});
			// On track end stop animation loop and clear Levels array to avoid persistent final frame.
			$.Audio.Element.addEventListener("ended", function() {clearTimeout($.Visualiser.animationLoopTimeOutId);});
			$.Audio.Element.addEventListener("ended", $.Audio.clearLevels);
		},
		/**
		 * Audio related...
		 */
		Audio: {
			Track: "song.mp3",
			Element: document.createElement("audio"),
			Context: new (window.AudioContext || window.webAudioContext || window.webkitAudioContext)(),
			/**
			* Initialize Audio related values
			*/
			initialize: function () {
				// Create Audio node
				$.Audio.Source = $.Audio.Context.createMediaElementSource($.Audio.Element);
				// Create Analyser node and set fftSize
				$.Audio.Analyser = $.Audio.Context.createAnalyser();
				$.Audio.Analyser.fftSize = 2048;
				// Connect audio source node to analyser node and output destination
				$.Audio.Source.connect($.Audio.Analyser);
				$.Audio.Source.connect($.Audio.Context.destination);
				// Create data array to pass through analyser node
				$.Audio.Data = new Uint8Array(1);
				// Create Levels array to store a data discreet value for each shape instance
				$.Audio.Levels = [];
				// Call $.Audio.clearLevels() once to initialize $.Audio.Levels array
				$.Audio.clearLevels();
			},
			/**
			* Clears Levels array (fills with 0.1 values to clear screen)
			*/
			clearLevels: function () {
				var Index;
				for (Index = 0; Index < $.Visualiser.NumShapes; Index += 1) {
					$.Audio.Levels[Index] = 0.1;
				}
			},
			/**
			* Update Levels array.
			*/
			updateLevels: function () {
				var Index, Volume;
				// Get current Time Domain value from Analyser node.
				$.Audio.Analyser.getByteTimeDomainData($.Audio.Data);
				// Add amplitude value for current frame to the end of the array. -128 centers the 0db value around zero, (as opposed to 128 of 256).
				// Use Math.abs to negate a negative values, and *0.25 to scale resulting values appropriately (for line width drawing).
				$.Audio.Levels.push(Math.abs($.Audio.Data[0]-128)*0.25);
				// Trim the oldest value from the front of the array
				$.Audio.Levels.splice(0, 1);
			}
		},
		/**
		 * Visualiser Related...
		 */
		Visualiser: {
			Shape1: true,
			Sides1: 4,
			Shape2: false,
			Sides2: 8,
			Perspective: 0,
			ColorCycle: true,
			Hyper: false,
			StepInc: 24,
			NumShapes: 48,
			FPS: 24,
			Canvas: document.getElementById("mainCanvas"),
			/**
			* Initialize Visualiser related items
			*/
			initialize: function () {
				var Index;
				// Create canvas context & set x/y values
				$.Visualiser.Context = $.Visualiser.Canvas.getContext("2d");
				$.Visualiser.XCenter = $.Visualiser.Canvas.width / 2;
				$.Visualiser.YCenter = $.Visualiser.Canvas.height / 2;
				// Set cycle frequency, phase, center point and modulation width for sine waves
				$.Visualiser.ColorFreq = 0.036;
				$.Visualiser.ColorPhase1 = 2; // phase..
				$.Visualiser.ColorPhase2 = 4; // shift..
				$.Visualiser.ColorPhase3 = 0; // ...with these
				$.Visualiser.ColorCenter = 128;
				$.Visualiser.ColorWidth = 127; // reducing this causes desaturation
				// Initialize Colors array and create counter for cycling
				$.Visualiser.Colors = [];
				for (Index = 0; Index < $.Visualiser.NumShapes; Index += 1) {
					$.Visualiser.Colors[Index] = "rgba(0,0,0,1.0)";
				}
				$.Visualiser.ColorCounter = 0;
			},
			/**
			* Main loop function to play animation
			*/
			animationLoop: function () {
				$.Visualiser.animationLoopTimeOutId = setTimeout(function() {
					requestAnimationFrame($.Visualiser.animationLoop);
					$.Audio.updateLevels();
					$.Visualiser.animateFrame();
				}, 1000 / $.Visualiser.FPS);
			},
			/**
			* Draw a single frame of the visualisation
			*/
			animateFrame: function () {
				var Index, LineWidth1, LineWidth2, Size, Shade;
				if ($.Visualiser.ColorCycle == true) {
					// Update Colors array
					$.Visualiser.updateColours();
				}
				// Clear previous frame
				$.Visualiser.Context.fillRect(0, 0, $.Visualiser.Canvas.width, $.Visualiser.Canvas.height);
				// for loop for each shape instance (per frame)
				for (Index = 1; Index <= $.Visualiser.NumShapes; Index += 1) {
					// Map corresponding value from amplitude data array to path width for current shape instance, 
					// if 'Hyper' is on the multiply by inverse of array position
					if ($.Visualiser.Hyper == true) {
						LineWidth1 = $.Audio.Levels[$.Audio.Levels.length - Index]*($.Visualiser.NumShapes-Index);
					} else {
						LineWidth1 = $.Audio.Levels[$.Audio.Levels.length - Index];
					}
					// Multiply path width value by 0.25 for use in accompanying white 'highlight' instance
					LineWidth2 = LineWidth1*0.25;
					// Increase size value for each concentric shape instance (based on current perspective mode).
					if ($.Visualiser.Perspective == 0) {
						Size = $.Visualiser.StepInc * Index;
					} else {
						Size = Index * Index;
					}
					// Traverse Color array to reference appropriate color shade for currunt shape instance
					Shade = $.Visualiser.Colors[$.Visualiser.Colors.length - Index];
					// Draw current concentric shape instance(s)
					if ($.Visualiser.Shape1) {
						$.Visualiser.drawPolygon(LineWidth2, $.Visualiser.Sides1, Size, "rgba(256,256,256,0.6)");
						$.Visualiser.drawPolygon(LineWidth1, $.Visualiser.Sides1, Size, Shade);
					}
					if ($.Visualiser.Shape2) {
						$.Visualiser.drawPolygon(LineWidth2, $.Visualiser.Sides2, Size, "rgba(256,256,256,0.6)");
						$.Visualiser.drawPolygon(LineWidth1, $.Visualiser.Sides2, Size, Shade);
					}
				}
			},
			/**
			* Function to draw a single polygon shape instance
			*/
			drawPolygon: function (Width, NumberOfSides, Size, Shade) {
				var Index;
				$.Visualiser.Context.beginPath();
				$.Visualiser.Context.moveTo($.Visualiser.XCenter + Size * Math.cos(0), $.Visualiser.YCenter + Size * Math.sin(0));
				for (Index = 1; Index <= NumberOfSides; Index += 1) {
					$.Visualiser.Context.lineTo($.Visualiser.XCenter + Size * Math.cos(Index * 2 * Math.PI / NumberOfSides),
												$.Visualiser.YCenter + Size * Math.sin(Index * 2 * Math.PI / NumberOfSides));
				}
				$.Visualiser.Context.lineWidth = Width;
				$.Visualiser.Context.strokeStyle = Shade;
				$.Visualiser.Context.closePath();
				$.Visualiser.Context.stroke();
			},
			/**
			* Update Colours array.
			*/
			updateColours: function () {
				// Get current colour value from cycle and add to end of array.
				$.Visualiser.Colors.push($.Visualiser.getColor($.Visualiser.ColorFreq, $.Visualiser.ColorPhase1, $.Visualiser.ColorPhase2, 
																$.Visualiser.ColorPhase3, $.Visualiser.ColorCenter, $.Visualiser.ColorWidth));
				// Trim the oldest value from the front of the array
				$.Visualiser.Colors.splice(0, 1);
			},
			/**
			* Generates a single colour step in the colour cycle
			*/
			getColor: function (frequency, phase1, phase2, phase3, center, width) {
				var r = Math.sin(frequency*$.Visualiser.ColorCounter + phase1) * width + center;
				var g = Math.sin(frequency*$.Visualiser.ColorCounter + phase2) * width + center;
				var b = Math.sin(frequency*$.Visualiser.ColorCounter + phase3) * width + center;
				$.Visualiser.ColorCounter++;
				return "rgba("+Math.round(r)+","+Math.round(g)+","+Math.round(b)+",0.5)";
			}
		},
		/**
		 * Control related...
		 */
		Controls: {
			Message: "",
			/**
			* Initialize Control related items
			*/
			initialize: function () {
				var Buttons, Index;
				// Add event listeners for main control elements on page
				document.getElementById("play").addEventListener("click", $.Controls.play);
				document.getElementById("pause").addEventListener("click", $.Controls.pause);
				document.getElementById("rewind").addEventListener("click", $.Controls.rewind);
				document.getElementById("info").addEventListener("click", $.Controls.info);
				document.getElementById("preset1").addEventListener("click", function(){$.Controls.restorePreset(1)});
				document.getElementById("preset2").addEventListener("click", function(){$.Controls.restorePreset(2)});
				document.getElementById("perspectiveMode").addEventListener("click", $.Controls.switchPerspective);
				document.getElementById("shape1Active").addEventListener("click", $.Controls.shape1Active);
				document.getElementById("shape1Up").addEventListener("click", function(){$.Controls.shape1Sides("up")});
				document.getElementById("shape1Down").addEventListener("click", function(){$.Controls.shape1Sides("down")});
				document.getElementById("shape2Active").addEventListener("click", $.Controls.shape2Active);
				document.getElementById("shape2Up").addEventListener("click", function(){$.Controls.shape2Sides("up")});
				document.getElementById("shape2Down").addEventListener("click", function(){$.Controls.shape2Sides("down")});
				document.getElementById("colorCycleActive").addEventListener("click", $.Controls.colorCycleActive);
				document.getElementById("hyperActive").addEventListener("click", $.Controls.hyperActive);
				// Add event listeners to fade controls in/out on hover
				Buttons = document.querySelectorAll("div.btn-group");
				for (Index = 0; Index < Buttons.length; Index++) {
					Buttons[Index].addEventListener("mouseenter", function() { jQuery("#controls").stop().animate({opacity:1}, 350) });
					Buttons[Index].addEventListener("mouseleave", function() { $.Controls.fadeOut(3000) });
				// Fade out controls by default after initial load
				$.Controls.fadeOut(6000);
				}
				// Event Listener for info panel close button
				document.getElementById("closeInfo").addEventListener("click", $.Controls.info);
			},
			/**
			* Play Button
			*/
			play: function () {
				if ($.Audio.Element.paused) {
					$.Audio.Context.resume();
					$.Audio.Element.play();
					clearTimeout($.Visualiser.animationLoopTimeOutId);
					$.Visualiser.animationLoop();
					$.Controls.consoleWrite("Play");
				}
			},
			/**
			* Pause Button
			*/
			pause: function () {
				if (!$.Audio.Element.paused) {
					$.Audio.Element.pause();
					clearTimeout($.Visualiser.animationLoopTimeOutId);
					$.Controls.consoleWrite("Pause");
				}
			},
			/**
			* Reset Button
			*/
			rewind: function () {
				$.Audio.Element.currentTime = 0;
				$.Audio.clearLevels();
				$.Visualiser.ColorCounter = 0
				$.Visualiser.Context.clearRect(0, 0, $.Visualiser.Canvas.width, $.Visualiser.Canvas.height);
				$.Controls.consoleWrite("Restart");
			},
			/**
			* Info Button
			*/
			info: function () {
				jQuery("#infoPanel").fadeToggle();
			},
			/**
			* Preset Buttons
			*/
			restorePreset: function (num) {
				if (num == 1) {
					$.Visualiser.Sides1 = 4;
					$.Visualiser.Sides2 = 8;
					$.Visualiser.Perspective = 0;
					$.Visualiser.Shape2 = true;
					$.Visualiser.Hyper = false;
					$.Visualiser.animateFrame();
					$.Controls.consoleWrite("Preset 1");
				}
				if (num == 2) {
					$.Visualiser.Sides1 = 32;
					$.Visualiser.Sides2 = 4;
					$.Visualiser.Perspective = 1;
					$.Visualiser.Shape2 = true;
					$.Visualiser.Hyper = true;
					$.Visualiser.animateFrame();
					$.Controls.consoleWrite("Preset 2");
				}
			},
			/**
			* Zoom Button
			*/
			switchPerspective: function () {
				$.Visualiser.Perspective = ($.Visualiser.Perspective == 0) ? 1: 0;
				$.Visualiser.animateFrame();
				$.Controls.Message = ($.Visualiser.Perspective == 0) ? "Zoom: Off": "Zoom: On<br />...less effective at lower volumes.";
				$.Controls.consoleWrite($.Controls.Message);
			},
			/**
			* Shape 1 Buttons: Show/Hide, +/- Sides.
			*/
			shape1Active: function () {
				$.Visualiser.Shape1 = ($.Visualiser.Shape1 == true) ? false: true;
				$.Visualiser.animateFrame();
				$.Controls.Message = ($.Visualiser.Shape1 == true) ? "Shape 1: Visible": "Shape 1: Hidden";
				$.Controls.consoleWrite($.Controls.Message);
			},
			shape1Sides: function (change) {
				if ($.Visualiser.Sides1 <= 30 && change == "up") {
					$.Visualiser.Sides1 += 2;
				} else if ($.Visualiser.Sides1 >= 6 && change == "down") {
					$.Visualiser.Sides1 -= 2;
				}
				$.Visualiser.animateFrame();
				$.Controls.Message = "Shape 1 Sides: " + $.Visualiser.Sides1;
				if ($.Visualiser.Sides1 == 4) $.Controls.Message += " (min value)";
				if ($.Visualiser.Sides1 == 32) $.Controls.Message += " (max value)";
				$.Controls.consoleWrite($.Controls.Message);
			},
			/**
			* Shape 2 Buttons: Show/Hide, +/- Sides.
			*/
			shape2Active: function () {
				$.Visualiser.Shape2 = ($.Visualiser.Shape2 == true) ? false: true;
				$.Visualiser.animateFrame();
				$.Controls.Message = ($.Visualiser.Shape2 == true) ? "Shape 2: Visible": "Shape 2: Hidden";
				$.Controls.consoleWrite($.Controls.Message);
			},
			shape2Sides: function (change) {
				if ($.Visualiser.Sides2 <= 30 && change == "up") {
					$.Visualiser.Sides2 += 2;
				} else if ($.Visualiser.Sides2 >= 6 && change == "down") {
					$.Visualiser.Sides2 -= 2;
				}
				$.Visualiser.animateFrame();
				$.Controls.Message = "Shape 2 Sides: " + $.Visualiser.Sides2;
				if ($.Visualiser.Sides2 == 4) $.Controls.Message += " (min value)";
				if ($.Visualiser.Sides2 == 32) $.Controls.Message += " (max value)";
				$.Controls.consoleWrite($.Controls.Message);
			},
			/**
			* Hue Button: Cycle/Freeze
			*/
			colorCycleActive: function () {
				$.Visualiser.ColorCycle = ($.Visualiser.ColorCycle == true) ? false: true;
				$.Controls.Message = ($.Visualiser.ColorCycle == true) ? "Hue: Cycle": "Hue: Freeze";
				$.Controls.consoleWrite($.Controls.Message);
			},
			/**
			* Hyper Button: On/Off
			*/
			hyperActive: function () {
				$.Visualiser.Hyper = ($.Visualiser.Hyper == true) ? false: true;
				$.Visualiser.animateFrame();
				$.Controls.Message = ($.Visualiser.Hyper == true) ? "Hyper: On": "Hyper: Off";
				$.Controls.consoleWrite($.Controls.Message);
			},
			/**
			* Fade Out Button Controls
			*/
			fadeOut: function (duration) {
				jQuery("#controls").stop().animate({opacity:0.4},duration);
			},
			/**
			* Write Button Feedback To Console
			*/
			consoleWrite: function (message) {
				jQuery("#console").prepend('<li><span><i class="fa fa-terminal"></i> ' + message + '</span></li>');
				jQuery("#console li").fadeOut(11000);
			}
		}
	}
	$.initialize();
}());