//SIO-FIO (socket.io file.io)
//uploads files to the server without a bunch of mess
//todo: downloads files from the server without a bunch of mess

(function(scope, name, factory){

		if (typeof define === "function" && define.amd) define(name, factory);
		else scope[name] = factory();
	
	}(this, "SioFio", function(){
		return function(socket){
				
			"use strict";
			var self = this;

			// Check for compatibility
			if (!window.File || !window.FileReader){
				throw new Error("Socket.IO File Upload: Browser Not Supported");
			}

			// Private and Public Variables
			var callbacks = {}, uploadedFiles = [], readyCallbacks = [];
			self.fileInputElementId = "siofio_input";
			self.useText = false;
			self.serializedOctets = false;
			self.useBuffer = false;

			var _dispatch = function(eventName, properties){
				var event = document.createEvent("Event");
				event.initEvent(eventName, false, false);
				for (var prop in properties){
					if (properties.hasOwnProperty(prop)){
						event[prop] = properties[prop];
					}
				}
				return self.dispatchEvent(event);
			};

			var _listenedReferences = [];
			var _listenTo = function(object, eventName, callback, bubble){
				object.addEventListener(eventName, callback, bubble);
				_listenedReferences.push(arguments);
			};
			var _stopListeningTo = function(object, eventName, callback, bubble){
				if (object.removeEventListener){
					object.removeEventListener(eventName, callback, bubble);
				}
			};
			var _stopListening = function(){
				for (var i = _listenedReferences.length - 1; i >= 0; i--) {
					_stopListeningTo.apply(this, _listenedReferences[i]);
				};
				_listenedReferences = [];
			};

			var _loadOne = function(file, opts){
			
				// First check for file size
				if (self.maxFileSize !== null && file.size > self.maxFileSize){
					_dispatch("error", {
						file: file,
						message: "Attempt by client to upload file exceeding the maximum file size",
						code: 1
					});
					return;
				}

				// Dispatch an event to listeners and stop now if they don't want to upload
				var eventResult = _dispatch("start", {
					file: file
				});
				if (!eventResult) return;

				// Scope variables
				var reader = new FileReader();
				var transmitPos = 0;
				var id = uploadedFiles.length;
				var useText = self.useText;
				var newName;
					
				uploadedFiles.push(file);

				// Private function to handle transmission of file data
				var transmitPart = function(loaded){
				
					var content;
					var isBase64 = false;
					
					if (useText){
						content = reader.result.slice(transmitPos, loaded);
					}
					else {
						try {
							var uintArr = new Uint8Array(reader.result, transmitPos, loaded);
							// Support the transmission of serialized ArrayBuffers
							if (self.serializedOctets) content = uintArr;
							else if (self.useBuffer) content = uintArr.buffer;
							else {
								isBase64 = true;
								content = _uint8ArrayToBase64(uintArr);
							}
						}
						catch (error){
							socket.emit("siofio_done", {
								id: id,
								interrupt: true
							});
							return;
						}
					}
					
					socket.emit("siofio_progress", {
						id: id,
						start: transmitPos,
						end: loaded,
						content: content,
						base64: isBase64
					});
					
					transmitPos = loaded;
					
				};

				// Listen to the "progress" event.  Transmit parts of files as they are ready.
				_listenTo(reader, "progress", function(event){
					// would call transmitPart(event.loaded) here
				});

				// When the file is fully loaded, tell the server.
				_listenTo(reader, "load", function(event){
					transmitPart(event.loaded);
					socket.emit("siofio_done", {
						id: id
					});
					_dispatch("load", {
						file: file,
						reader: reader,
						name: newName
					});
				});

				// Listen for an "error" event.  Stop the transmission if one is received.
				_listenTo(reader, "error", function(){
					socket.emit("siofio_done", {
						id: id,
						interrupt: true
					});
				});

				// Do the same for the "abort" event.
				_listenTo(reader, "abort", function(){
					socket.emit("siofio_done", {
						id: id,
						interrupt: true
					});
				});

				// Transmit the "start" message to the server.
				socket.emit("siofio_start", {
					name: file.name,
					mtime: file.lastModifiedDate,
					opts: opts || window.upload_opts || {},
					encoding: useText ? "text" : "octet",
					id: id
				});

				// To avoid a race condition, wait for server saying it's ready.
				var readyCallback;
				
				if (useText){
					readyCallback = function(_newName){
						reader.readAsText(file);
						newName = _newName;
					};
				}
				else {
					readyCallback = function(_newName){
						reader.readAsArrayBuffer(file);
						newName = _newName;
					};
				}
				
				readyCallbacks.push(readyCallback);

			};

			var _load = function(files, opts){
				for (var i=0; i<files.length; i++){
					_loadOne(files[i], opts);
				}
			};

			var _getInputElement = function(){
				var input = document.getElementById(self.fileInputElementId);
				if (!input){
					input = document.createElement("input");
					input.setAttribute("type", "file");
					input.setAttribute("id", self.fileInputElementId);
					input.style.display = "none";
					document.body.appendChild(input);
				}
				return input;
			};

			var _removeInputElement = function(){
				var input = document.getElementById(self.fileInputElementId);
				if (input){
					input.parentNode.removeChild(input);
				}
			};

			var _baseFileSelectCallback = function(files, opts, callback){
				if (files.length === 0) return;
				for (var i=0; i<files.length; i++){
					if (opts) files[i].opts = opts;
					if (callback) files[i].callback = callback;
				}
				var eventResult = _dispatch("choose", {
					files: files
				});
				if (eventResult) _load(files, opts);
			};

			var _fileSelectCallback = function(event){
				event.preventDefault();
				var files = event.target.files || event.dataTransfer.files;
				_baseFileSelectCallback(files);
			};

			this.uploadFiles = function(files, opts, callback){
				if (files){
					_baseFileSelectCallback(files, opts, callback);
				}
			};
			
			this.uploadFile = function(file, opts, callback){
				if (file){
					_baseFileSelectCallback([file], opts, callback);
				}
			};

			this.listenOnSubmit = function(submitButton, input){
				if (!input.files) return;
				_listenTo(submitButton, "click", function(){ _baseFileSelectCallback(input.files); }, false);
			};

			this.listenOnArraySubmit = function(submitButton, array){
				for (var index in array){
					this.listenOnSubmit(submitButton, array[index]);
				}
			};

			this.listenOnInput = function(input){
				if (!input.files) return;
				_listenTo(input, "change", _fileSelectCallback, false);
			};

			this.listenOnDrop = function(div){
				_listenTo(div, "dragover", function(event){
					event.preventDefault();
				}, false);
				_listenTo(div, "drop", _fileSelectCallback);
			};

			this.prompt = function(){
				var inpt = _getInputElement();
				_listenTo(inpt, "change", _fileSelectCallback, false);
				var evnt = document.createEvent("MouseEvents");
				evnt.initMouseEvent("click", true, true, window,
					0, 0, 0, 0, 0, false, false, false, false, 0, null);
				inpt.dispatchEvent(evnt);
			};

			this.destroy = function(){
				_stopListening();
				_removeInputElement();
			};

			this.addEventListener = function(eventName, callback){
				if (!callbacks[eventName]) callbacks[eventName] = [];
				callbacks[eventName].push(callback);
			};

			this.removeEventListener = function(eventName, callback){
				if (!callbacks[eventName]) return false;
				for (var i=0; i<callbacks[eventName].length; i++){
					if (callbacks[eventName][i] === callback){
						callbacks[eventName].splice(i, 1);
						return true;
					}
				}
				return false;
			};

			this.dispatchEvent = function(evnt){
				var eventCallbacks = callbacks[evnt.type];
				if (!eventCallbacks) return true;
				var retVal = true;
				for (var i=0; i<eventCallbacks.length; i++){
					var callbackResult = eventCallbacks[i](evnt);
					if (callbackResult === false){
						retVal = false;
					}
				}
				return retVal;
			};

			var _uint8ArrayToBase64 = function(bytes){
				var i, len = bytes.buffer.byteLength, base64 = "",
					chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

				for (i = 0; i < len; i+=3){
					base64 += chars[bytes[i] >> 2];
					base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
					base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
					base64 += chars[bytes[i + 2] & 63];
				}

				if ((len % 3) === 2){
					base64 = base64.substring(0, base64.length - 1) + "=";
				}
				else if (len % 3 === 1){
					base64 = base64.substring(0, base64.length - 2) + "==";
				}

				return base64;
			};

			socket.on("siofio_ready", function(data){
				readyCallbacks[data.id](data.name);
			});
			socket.on("siofio_complete", function(data){
				var file = uploadedFiles[data.id];
				_dispatch("complete", {
					file: file,
					response: data.response,
					success: data.success
				});
				if (file.callback) file.callback({
					file: file,
					response: data.response,
					success: data.success
				});
			});
			socket.on("siofio_error", function(data){
				var file = uploadedFiles[data.id];
				_dispatch("error", {
					file: uploadedFiles[data.id],
					message: data.message,
					code: 0
				});
				if (file.callback) file.callback({
					file: file,
					response: data.message,
					success: false
				});
			});
		 }
	})
);
