(function () {
	/*global JSZip, TICloudAgent*/
	'use strict';

	angular.module('uniflash').service('ufFilePackageCreator', filePackageCreator);


	filePackageCreator.$inject = ['ufSession', 'ufPropertiesValue', 'ufPrograms', 'ufProgess', 'ticAgentAPI', '$http', '$q', '$log'];


	function filePackageCreator(ufSession, ufPropertiesValue, ufPrograms, ufProgess, ticAgentAPI, $http, $q, $log) {
		let installerOS = getOSType();
		let includeSettings = false;
		let includeImages = false;
		let includeDrivers = false;
		let includeSessions = false;
		let deviceStr = '';
		let isMSP = false;

		let curCCXMLFileName = '';
		let curSettingsFileName = '';
		let cores = [];
		let curOperations = '';
		let progressDialog;

		let cliScriptName = 'dslite';
		let extensionName = [];
		extensionName['win'] = '.bat';
		extensionName['linux'] = '.sh';
		extensionName['osx'] = '.sh';
		let defaultMode = 'flash';

		const quote = (getOSType() === 'win') ? "\"" : "\\\"";

		function getOSType() {
			return TICloudAgent.getOS();
		}

		function setOSType(type) {
			if (type === 'win' || type === 'osx' || type === 'linux') {
				installerOS = type;
			}
		}

		function setPackageOptions(images, settings, drivers, sessions) {
			includeSettings = settings;
			includeImages = images;
			includeDrivers = drivers;
			includeSessions = sessions;
		}

		function setDevice(device) {
			deviceStr = device.toLowerCase();
			isMSP = ((deviceStr.substring(0, 6) === 'msp430') ||
				(deviceStr.substring(0, 5) === 'cc430') ||
				(deviceStr.substring(0, 5) === 'rf430'));
		}

		function blob2abAsync(blob) {
			let arrayBuffer;
			let deferred = $q.defer();
			let fileReader = new FileReader();
			fileReader.onload = function () {
				arrayBuffer = this.result;
				deferred.resolve(arrayBuffer);
			};
			fileReader.readAsArrayBuffer(blob);
			return deferred.promise;
		}

		function addFileToZip(zipFileContents, file, fileContents, filePermissions?) {
			let deferred = $q.defer();
			$log.debug('adding ' + file + ' ' + fileContents + ' to zipFileContents');
			let options: any = {};
			if (filePermissions) {
				options.unixPermissions = filePermissions;
			}

			if (fileContents instanceof Blob) {
				blob2abAsync(fileContents)
					.then((arrayBuffer) => {
						zipFileContents.push({
							fileName: file,
							contents: arrayBuffer,
							options: options
						});
						deferred.resolve();
					});
			} else if (fileContents instanceof ArrayBuffer) {
				zipFileContents.push({
					fileName: file,
					contents: fileContents,
					options: options
				});
				deferred.resolve();
			} else if (fileContents instanceof Uint8Array) {
				zipFileContents.push({
					fileName: file,
					contents: fileContents.buffer,
					options: options
				});
				deferred.resolve();
			} else if (typeof fileContents === 'string') {
				// string look the same as array buffer here just because the api is set to this way
				// but just keep them separated in case any handling is needed for any of these types and 
				// to show that they are really different
				zipFileContents.push({
					fileName: file,
					contents: fileContents,
					options: options
				});
				deferred.resolve();
			} else {
				deferred.reject('unknown file contents type.');
			}

			return deferred.promise;
		}

		function getChmodVal(octalStr) {
			return octalStr ? parseInt(octalStr, 8) : null;
		}

		function downloadError(file, err) {
			throw new Error('Error fetching ' + file + ', ' + err.status + ' ' + err.statusText + '.');
		}


		function getFileContentResponse(path: string, contentType?: ti_common.file.ContentType, version?: string): angular.IPromise<any> {
			if (!contentType) {
				contentType = ti_common.file.ContentType.Text;
			}


			if (!version) {
				version = "LATEST";
			}

			return ticAgentAPI.getFileContent(installerOS, path, contentType, version);
		}

		function downloadFile(file, version, zipFileContents) {
			$log.debug('Fetching ' + file);
			return getFileContentResponse(file, ti_common.file.ContentType.Blob, version)
				.then((response) => {
					let permissionsStr = response.headers()['x-ticloudagent-permissions'];

					// Do not set permissions on osx symbolic link for now due to a bug with getting symlink content
					if (permissionsStr === '120755') {
						permissionsStr = {};
					}
					let permissions = getChmodVal(permissionsStr);

					return addFileToZip(zipFileContents, file, response.data, permissions)
				})
				.catch((err) => {
					downloadError(file, err);
				});

		}


		function getSupportingFiles() {
			$log.debug('Getting install info... ');
			let ccxmlContent = ufSession.getCCXML();
			return (() => {
				return ticAgentAPI.getSupportingFiles(ccxmlContent, installerOS, 64);
			})()
				.then((response) => {
					return JSON.parse(response.data);
				});
		}


		function getFilePackage() {
			return getSupportingFiles()
				.then((installInfo) => {
					return getFilePackageImpl(installInfo.files);
				});
		}

		function addSettingsFiles(zipFileContents) {
			if (includeSettings) {
				let settings = ufPropertiesValue.saveValuesAsJson(true);
				curSettingsFileName = 'user_files/settings/' + 'generated.ufsettings';
				return addFileToZip(zipFileContents, curSettingsFileName, settings);
			} else {
				return $q.when();
			}
		}

		function getImagesStr(core) {
			let programList = core.programs;
			let curImage = '';
			// generate the imagePath command
			_.each(programList, function (curProgram) {
				if (curProgram.isSelected) {
					curImage += quote + 'user_files/images/' + curProgram.file.name;
					if (curProgram.isBinary) {
						curImage += ',';
						curImage +=	(curProgram.loadAddress === undefined || curProgram.loadAddress.length === 0) ? '0x0' : curProgram.loadAddress;
					}
					curImage += quote + ' ';
				}
			});
			return curImage;
		}

		function addImageFiles(zipFileContents, cores) {
			if (includeImages) {
				return $q.all(_.map(cores, (core: any) => {
					let programList = core.programs;
					// go through each of the programs in the list, get the contents, and add it to the zip
					return processArray(programList, function (curProgram) {
						if (curProgram.isSelected) {
							return curProgram.file.getContents(ti_common.file.ContentType.Blob)
								.then((content) => {
									return addFileToZip(zipFileContents, 'user_files/images/' + curProgram.file.name, content);
								});
						} else {
							return $q.when();
						}
					});
				}));
			} else {
				return $q.when();
			}
		}

		// helper function for serially evaluating all items in the collection before returning 
		function processArray(collection, predicate) {
			let index = 0;

			function processNext() {
				if (index < collection.length) {
					// process the current item and then move to the next item when done
					return predicate(collection[index++]).then(() => {
						processNext();
					});
				} else {
					// all done
					return $q.resolve();
				}
			}

			return processNext();
		}

		function addCCXMLFile(zipFileContents) {
			let ccxml = ufSession.getCCXML();
			curCCXMLFileName = 'user_files/configs/' + ufSession.getDeviceXMLFile() + '.ccxml';
			return addFileToZip(zipFileContents, curCCXMLFileName, ccxml);
		}

		function addLaunchScript(zipFileContents, cores) {
			return $q.all(_.map(cores, (core: any) => {
				let fileName = cliScriptName;
				let debuggableCores = ufSession.queryConfig().debuggableCores;
				// UNIFLASH-1144, only create 2 scripts for new sessions
				let finalFileName = (debuggableCores && debuggableCores.length > 1) ? cliScriptName + '-' + core.name : cliScriptName;
				fileName += extensionName[installerOS];
				finalFileName += extensionName[installerOS];

				let coreCounter: number;
				if (debuggableCores) { 
					coreCounter = _.findIndex(debuggableCores, (debuggableCore: uniflash.target_config.Core) => {
						return debuggableCore.name === core.name;
					});
				}
				/////////////////////////////
				$log.debug('addLaunchScript ' + finalFileName);
				//this will work because the template file is hosted in the app itself
				return $http.get('file_package_creator/templates/' + installerOS + '/' + fileName)
					.then((response) => {

						// update name of the ccxml file
						let generatedCommand = '-c ' + curCCXMLFileName;
						let additionals = '';
						// update settings information
						if (includeSettings) {
							generatedCommand += ' -l ' + curSettingsFileName;
						}
						
						generatedCommand += ' -s VerifyAfterProgramLoad='+quote+'No verification'+quote;
						// figure out the default mode to use
						if (ufSession.isSerial()) {
							defaultMode = 'serial';
						} else {
							defaultMode = 'flash';
						}

						// workaround for the awr/iwr16 core case
						if ((ufSession.getDeviceXMLFile().indexOf('awr16') !== -1) || (ufSession.getDeviceXMLFile().indexOf('iwr16') !== -1)) {
							generatedCommand += ' -n 1';
						} 
						else if (coreCounter) {
							additionals = '-n ' + coreCounter;
						}

						// no longer used, but this is an example of how to use the 'additionals'
						// if ((ufSession.getDeviceXMLFile().indexOf('cc26') !== -1) ||
						// 	(ufSession.getDeviceXMLFile().indexOf('cc13') !== -1)) {
						// 	additionals = '--lprf';
						// }

						// update images information
						let curImagesStr = getImagesStr(core);
						if (includeImages && curImagesStr) {
							if (defaultMode === 'serial') {
								generatedCommand += ' -e -f ' + curImagesStr;
							} else {
								generatedCommand += ' -e -f -v ' + curImagesStr;
							}
						}

						// replace in the file data string
						let fileData = response.data.replace('%%generatedCommand%%', generatedCommand);
						fileData = fileData.replace('%%defaultMode1%%', defaultMode);
						fileData = fileData.replace('%%additionals%%', additionals);

						return addFileToZip(zipFileContents, finalFileName, fileData, 33279);
					})
					.catch((err) => {
						downloadError(finalFileName, err);
					});
			}));
		}

		function addDriverScript(zipFileContents) {
			let osArc = 64;
			if (installerOS === 'win') {
				let fileName = 'one_time_setup.bat';
				let driverInfo;
				let dpinstFileName;
				return getSupportingFiles()
					.then((installInfo) => {
						driverInfo = installInfo.drivers;
						if (!driverInfo) {
							return;
						}

						dpinstFileName = 'dpinst_' + osArc + '_eng.exe';

						return getFileContentResponse('ticloudagent/setup/' + dpinstFileName, ti_common.file.ContentType.Blob);
					})
					.then((dpInstResponse) => {
						return addFileToZip(zipFileContents, dpinstFileName, dpInstResponse.data, null);
					})
					.then(() => {
						//this will work because the template file is hosted in the app itself
						return $http.get('file_package_creator/templates/' + installerOS + '/' + fileName);
					})
					.then((templateResponse) => {
						return (_.template(templateResponse.data))({ drivers: driverInfo, osArch: osArc });
					})
					.then((scriptData) => {
						return addFileToZip(zipFileContents, fileName, scriptData, null);
					})
					.catch((err) => {
						downloadError('one_time_setup.bat', err);
					});
			} else if (installerOS === 'linux') {
				let fileName = 'one_time_setup.sh';

				return $http.get('file_package_creator/templates/' + installerOS + '/' + fileName)
					.then((scriptData) => {
						return addFileToZip(zipFileContents, fileName, scriptData.data, 33279);
					})
					.catch((err) => {
						downloadError('one_time_setup.sh', err);
					});
			}
		}

		function addExtraFiles(zipFileContents) {
			if (isMSP && (installerOS === 'win')) {
				let fileName = 'ccs_base/DebugServer/drivers/MSP430Flasher.exe';
				return $http({
					url: 'bin/' + installerOS + '/MSP430Flasher.exe',
					method: 'GET',
					responseType: 'blob'
				}).then((response) => {
					return blob2abAsync(response.data)
						.then((arrayBuffer) => {
							return addFileToZip(zipFileContents, fileName, arrayBuffer, 33279);
						});
				})
					.catch((err) => {
						downloadError('MSP430Flasher.exe', err);
					});
			}
		}

		function getFilePackageImpl(files) {
			let zipFileContents = [];
			let deferred = $q.defer();

			ufProgess.createProgress('Generating standalone package').then((newProgressDialog) => {
				progressDialog = newProgressDialog;
				let initUpdate = {
					id: -1,
					name: 'Please wait...',
					subActivity: 'Getting the list of files to package.',
					isFirstUpdate: true,
					isComplete: false
				};
				progressDialog.handleEvent(initUpdate);
			});

			$log.debug('Fetching ' + files.length + ' files');
			let isCancelled = false;
			let cancelationDeferred = $q.defer();
			let downloads = [];
			let index = 0;
			curCCXMLFileName = '';
			curSettingsFileName = '';
			cores = [];
			curOperations = '';

			let cancel = () => { //should return a promise
				$log.info('Cancelling generate package.');
				isCancelled = true;
				return cancelationDeferred.promise;
			};
			let downloadNext = () => {
				if (!isCancelled && files.length > index) { //&& !deferred.promise.isRejected()) {
					let file = files[index++];
					return downloadFile(file.name, file.version, zipFileContents)
						.then(() => {
							let percentageDone = Math.floor(index * 100 / files.length);
							let progressMsg = 'Fetching ' + index + '/' + files.length + ': ' + file.name;
							deferred.notify({
								message: progressMsg,
								percent: percentageDone
							});
							if (progressDialog) {
								progressDialog.handleEvent({
									subActivity: progressMsg,
									percent: percentageDone / 2,
									isCancellable: true
								}, cancel);
							}
							return downloadNext();
						});
				}
			};

			// We only download 10 at a time.  Our server seems to handle 
			// downloading all of them at once, but if we scale up the number
			// of users...?  Plus, it doesn't seem to give much benefit to 
			// download all at once...

			for (let i = 0; i < 10; ++i) {
				downloads.push(downloadNext());
			}

			$q.all(downloads)
				.then(() => {
					if (isCancelled) {
						$log.debug('Generate package was cancelled.');
					} else {
						cores = ufPrograms.getAllGUICoresData();
						return addSettingsFiles(zipFileContents)
							.then(() => {
								return addImageFiles(zipFileContents, cores);
							})
							.then(() => {
								return addCCXMLFile(zipFileContents);
							})
							.then(() => {
								return addLaunchScript(zipFileContents, cores);
							})
							.then(() => {
								return addDriverScript(zipFileContents);
							})
							.then(() => {
								return addExtraFiles(zipFileContents);
							})
							.then(() => {
								deferred.resolve(zipFileContents);
							});
					}
				})
				.catch((err) => {
					deferred.reject(err);
					progressDialog.close();
				})
				.finally(() => {
					cancelationDeferred.resolve();
				});
			return deferred.promise;
		}

		function getProgess() {
			return progressDialog;
		}

		return {
			getFilePackage: getFilePackage,
			getFiles: getFilePackageImpl,
			getOSType: getOSType,
			setOSType: setOSType,
			setPackageOptions: setPackageOptions,
			setDevice: setDevice,
			getProgess: getProgess
		};
	}
})();
