(function () {

	'use strict';

	angular.module('uniflash').directive('ufMemoryBrowser', memoryBrowser);

	memoryBrowser.$inject = ['$window', '$rootScope', '$log'];

	function memoryBrowser($window, $rootScope, $log) {

		let directive = {
			restrict: 'E',
			controller: MemoryBrowser,
			controllerAs: 'vm',
			templateUrl: 'memory/memory_browser.template.html',
			scope: {},
			link: (scope, ele, attr, ctrl) => {
				let browserWindowEle = angular.element($window);
				let browserWindow = browserWindowEle[0];
				browserWindowEle.bind('resize', () => {
					let viewHeight = browserWindow['innerHeight'] / 2;
					ctrl.viewHeight = viewHeight;
					$log.debug('window resized, viewHeight = ' + viewHeight);
				});

				scope.$on('$destroy', () => {
					browserWindowEle.unbind('resize');
				});

				let consoleViewEle = angular.element(document.getElementById('console-view'));

				scope.$on('$destroy', $rootScope.$on('ds.opStarted', () => {
					ctrl.dsOpInProgress = true;
				}));

				scope.$on('$destroy', $rootScope.$on('ds.opFinished', () => {
					ctrl.dsOpInProgress = false;
				}));

			}
		};
		return directive;
	}

	MemoryBrowser.$inject = ['$uibModal', '$q', '$scope', '$rootScope', '$window', '$log',
		'ufDS', 'ufDsProgressDialog', 'ufConsole', 'ufContext', 'ticFileDialogs', 'ticExecutionPlatform', 'ticAnalyticsSrv', 'ufMemoryRanges'];

	function MemoryBrowser($uibModal, $q, $scope, $rootScope, $window, $log,
		ufDS, ufDsProgressDialog, ufConsole, ufContext: uniflash.Context, ticFileDialogs, ticExecutionPlatform, ticAnalyticsSrv, ufMemoryRanges) {
		let vm = this;

		let coreName = ufContext.getCurrentCore().name;
		//TODO: grab these from some service that knows the target

		let isC28 = coreName.match(/.*c28.*/i);
		let is430 = coreName.match(/.*msp430.*/i);
		let currentValTypeLength = isC28 ? 2 : 4;
		let currentMAUSize = isC28 ? 2 : 1;
		let currentWordSize = (isC28 || is430) ? 2 : 4;
		let currentCellSize = currentValTypeLength / currentMAUSize;

		let newAddr;
		let dsConfigObj;

		vm.format = 'TISTYLEHEX';
		vm.valType = 'INT'; //'CHAR'; //should be inferred

		vm.bufferCount = 150; //rows to read before the target addr 
		vm.cols = 16; // default to max num of cols
		vm.rows = [];

		vm.isInit = false;
		vm.showInitOverlay = true;
		vm.viewHeight = 450;
		vm.curPage = '';
		vm.ufMemoryRanges = ufMemoryRanges;
		vm.selectedMemoryRange = "0";

		vm.updateTableWithPageInfo = (pageInfo) => {
			currentValTypeLength = pageInfo.wordSize / 8;
			currentMAUSize = pageInfo.mauSize / 8;
			currentCellSize = currentValTypeLength / currentMAUSize;
			$log.debug('currentValTypeLength = ' + currentValTypeLength +
				' currentMAUSize = ' + currentMAUSize +
				' currentCellSize = ' + currentCellSize);
		};

		function getPageInfo(page) {
			let ret;
			angular.forEach(vm.pageInfoList, (pageInfo) => {
				if (pageInfo.name === page) {
					ret = pageInfo;
				}
			});

			return ret;
		}

		vm.init = function () {
			ticAnalyticsSrv.record('InitMemoryBrowser');

			let initializedDeferred = $q.defer();
			vm.initializing = initializedDeferred.promise;
			vm.showInitOverlay = false;

			//just running an empty operation will do nothing, but will setup the dialog handlers and console handlers in the process.
			ufDsProgressDialog.configureDS();

			vm.core = ufDS.getDSCore(coreName);
			vm.core.getTargetInfo()
				.then((ret) => {
					$log.debug('InitMemoryBrowser ' + JSON.stringify(ret));
					vm.pageInfoList = ret.pageInfoList;
					angular.forEach(vm.pageInfoList, (pageInfo) => {
						if (pageInfo.isDefault) {
							vm.curPage = pageInfo.name;
							vm.updateTableWithPageInfo(getPageInfo(vm.curPage));
						}
					});
				})
				.then(() => {
					return vm.goToMem('0x0');
				})
				.then(function () {
					let cellEle = angular.element(document.getElementsByClassName(vm.cellClass));
					vm.cellEleWidth = cellEle ? cellEle[0].clientWidth : null;

					vm.isInit = true;
					initializedDeferred.resolve();
				})
				.catch(function () {
					vm.isInit = false;
					vm.showInitOverlay = true;
					initializedDeferred.reject();
				});

			vm.initializing.then(() => {
				updateTableDimensions();
			});
		};

		//width control the number of columns
		vm.memoryBrowserId = 'memory-browser';
		vm.cellClass = 'memory-browser-cell';
		function updateTableDimensions() {
			let memoryBrowserEle = angular.element(document.getElementById(vm.memoryBrowserId));
			let memoryBrowserEleWidth = (
				memoryBrowserEle
				&& memoryBrowserEle[0]
				&& memoryBrowserEle[0].clientWidth) ? memoryBrowserEle[0].clientWidth : null;

			if (vm.cellEleWidth && memoryBrowserEleWidth) {
				let bestFitCols = Math.floor(memoryBrowserEleWidth / vm.cellEleWidth);// - 1; //minus the address col
				if (bestFitCols >= 16) {
					reRenderTable(16);
				} else if (bestFitCols > 8 && bestFitCols < 16) {
					reRenderTable(8);
				} else if (bestFitCols <= 8) {
					reRenderTable(4);
				}
			}
		};

		function reRenderTable(cols, force?) {
			if (force || (vm.cols !== cols && vm.scrollInfo && !(vm.currentTopRow === null))) {
				$log.debug('reRenderTable: ' + cols);
				let curCols = vm.cols;
				let skippedRows = Math.round(vm.scrollInfo.scrollTop / vm.scrollInfo.rowHeight);
				let curTopLeftAddr = (vm.currentTopRow === undefined || vm.currentTopRow === null) ?
					0 : (vm.currentTopRow + skippedRows) * curCols * currentCellSize;

				if (!vm.dsOpInProgress) {
					if (cols) {
						vm.cols = cols;
					}
					vm.goToMem('0x' + curTopLeftAddr.toString(16));
				} else {
					$log.debug('reRenderTable skipped');
				}
			}
		}

		vm.refreshTable = function () {
			reRenderTable(null, true);
		};

		vm.toolTipDirection = function (cellAddr) {
			let curCols = vm.cols;
			let skippedRows = Math.round(vm.scrollInfo.scrollTop / vm.scrollInfo.rowHeight);
			let curTopLeftAddr = (vm.currentTopRow === undefined || vm.currentTopRow === null) ?
				0 : (vm.currentTopRow + skippedRows) * curCols * currentCellSize;

			let valsPerRow = curCols * currentCellSize;
			let lowBound = (curTopLeftAddr - 2 * valsPerRow) > 0 ? curTopLeftAddr - valsPerRow : 0;
			let highBound = curTopLeftAddr + valsPerRow;
			if (lowBound <= cellAddr && cellAddr < highBound) {
				return 'bottom';
			} else {
				return 'top';
			}
		};

		angular.element($window).on('resize', updateTableDimensions);

		vm.updateScrollInfo = function (info) {
			vm.scrollInfo = info;
		};

		function generateDummyVals() {
			let errArray = [];
			let valArray = [];
			for (let i = 0; i < vm.bufferCount * vm.cols; i++) {
				let val = '';
				for (let j = 0; j < currentValTypeLength; j++) {
					val += '??';
				}
				valArray.push(val);
				errArray.push('Memory browser not initialized.');
			}

			let ret: { values; indexedErrors; } = { values: valArray, indexedErrors: errArray };
			return ret;
		}

		function generateRows(startAddr, values, highlight?: boolean, length?: number) {
			let valArray = values.values;
			let errsArray = values.indexedErrors;
			let rows = [];
			//rows are addr in the first col followed by a number of values defined by cols
			let assertMustBeTrue = (valArray.length % vm.cols) === 0;
			if (!assertMustBeTrue) {
				$log.error('generateRows assert failed! must be zero => ' + valArray.length % vm.cols);
			}
			let numOfRows = (valArray.length === 0) ? 0 : (valArray.length / vm.cols);
			//go through row by row, each has an cell to display the starting address of the row.
			for (let i = 0; i < numOfRows; i++) {
				let newRow: { addrStr?; cells?} = {};
				let newRowRawAddr = startAddr + i * vm.cols * currentCellSize;
				newRow.addrStr = intToHexStr(newRowRawAddr);
				let rowVals = valArray.splice(0, vm.cols);
				newRow.cells = [];
				for (let j = 0; j < rowVals.length; j++) {
					let cell: { addr?; addrStr?; style?; val?; err?; } = {};
					let cellRawAddress = newRowRawAddr + j * currentCellSize;
					cell.addr = cellRawAddress;
					cell.addrStr = intToHexStr(cellRawAddress);
					if (highlight && cellMatch(cellRawAddress, length)) {
						cell.style = {
							'background-color': 'rgba(17, 136, 153, 0.05)',
							'font-weight': '500',
							'color': '#888',
						};
					}
					cell.val = rowVals[j];
					cell.err = errsArray[i * vm.cols + j];
					newRow.cells.push(cell);
				}

				rows.push(newRow);
			}

			return rows;
		}

		function generateDummyRows() {
			let dummyVals = generateDummyVals();
			let startAddr = 0;
			//rows are addr in the first col followed by a number of values defined by cols
			let rows = generateRows(startAddr, dummyVals);

			vm.isAtTopLimit = true;
			return rows;
		}


		vm.rows = generateDummyRows();

		function findAddrGivenRowIdx(rowIdx) {
			return rowIdx * vm.cols * currentCellSize;
		}

		function findRowIdxGivenAddr(addr) {
			return Math.floor(addr / vm.cols / currentCellSize);
		}

		vm.readMem = function (rowIdx, rowCount, page) {
			$log.debug('memory browser: readMem: rowIdx=' + rowIdx + ', rowCount=' + rowCount + ', page=' + page);
			if (vm.dsOpInProgress) {
				return $q.reject('dsOpInProgress, readMem skipped');
			}

			vm.updateTableWithPageInfo(getPageInfo(vm.curPage));

			let numOfValsToRead = rowCount * vm.cols;
			let addr_decimal = findAddrGivenRowIdx(rowIdx);
			if (page) {
				vm.curPage = page;
				vm.initializing = vm.core.readMemory(addr_decimal + '@' + vm.curPage, vm.valType, numOfValsToRead, vm.format);
			} else {
				vm.initializing = vm.core.readMemory(addr_decimal, vm.valType, numOfValsToRead, vm.format);
			}
			return vm.initializing.then((ret) => {
				vm.showInitOverlay = false;
				return ret;
			});
		};

		function findLoadingTopRowIdx(addr) {
			let topRowIdx = findRowIdxGivenAddr(addr) - vm.bufferCount;
			topRowIdx = (topRowIdx < 0) ? 0 : topRowIdx;
			return topRowIdx;
		}

		function findBottomRowIdx(addr) {
			let botRowIdx = findRowIdxGivenAddr(addr) + vm.bufferCount;
			botRowIdx = (findAddrGivenRowIdx(botRowIdx) >= 0xFFFFFFFF) ? findRowIdxGivenAddr(0xFFFFFFFF) : botRowIdx;
			return botRowIdx;
		}

		function validateAddressRegex() {
			return /^0x(#)?([0-9a-fA-F]{1,8})$/i;
		}

		function validLocationRegex() {
			return /^0x(#)?([0-9a-fA-F]{1,8})(@.+)?$/i;
		}

		vm.validateAddressRegex = validateAddressRegex();
		vm.validLocationRegex = validLocationRegex();

		vm.goToMemHighlight = function (addrStr) {
			return vm.goToMem(addrStr, { highlight: true });
		};

		vm.goToMemRange = function () {
			const curMemoryRange = this.ufMemoryRanges.getMemoryRange(parseInt(vm.selectedMemoryRange));
			return vm.goToMem(curMemoryRange.address, { highlight: true }, curMemoryRange.length);
		}

		vm.goToMem = function (addrStr, options?, length?: number) {
			$log.debug('memory browser: goToMem: addrStr=' + addrStr + (options) ? ', options=' + JSON.stringify(options) : '');
			if (!addrStr || !addrStr.match(vm.validateAddressRegex)) {
				let err = 'invalid hexadecimal: ' + addrStr;
				$log.error('memory browser: goToMem: ' + err);
				vm.isAddrInputPopoverOpen = true;
				return $q.reject(err);
			} else if (vm.dsOpInProgress) {
				let err = 'vm.dsOpInProgress, goToMem skipped';
				$log.error('memory browser: goToMem: ' + err);
				return $q.reject(err);
			}

			vm.isAddrInputPopoverOpen = false;
			newAddr = parseInt(addrStr, 16);
			let currentCenterRow = findRowIdxGivenAddr(newAddr);
			let currentTopRow = findLoadingTopRowIdx(newAddr);
			let topRowIdxAddr = findAddrGivenRowIdx(currentTopRow);
			let rowCount = findBottomRowIdx(newAddr) - currentTopRow;

			// let page = (addrStr.indexOf('@') > 0) ? addrStr.substring(addrStr.indexOf('@')) : '';
			return vm.readMem(currentTopRow, rowCount, vm.curPage)
				.then(function (values) {
					if (options && options.highlight) {
						return generateRows(topRowIdxAddr, values, options.highlight, length);
					} else {
						return generateRows(topRowIdxAddr, values);
					}
				})
				.then(function (rows) {
					vm.currentTopRow = currentTopRow;
					vm.currentCenterRow = currentCenterRow;
					vm.isAtTopLimit = isAtTop();
					vm.isAtBottomLimit = isAtBottom();
					vm.rows = rows;
					$rootScope.$emit('memory_browser.afterMemoryLoad');
					return;
				})
				.catch(function (e) {
					$log.error('readMem failed:' + e);
				});
		};

		vm.loadUp = function () {
			$log.debug('memory browser: loadUp');
			let currentCenterRow = findLoadingTopRowIdx(findAddrGivenRowIdx(vm.currentCenterRow));
			let currentTopRow = findLoadingTopRowIdx(findAddrGivenRowIdx(currentCenterRow));
			let newRowCountRead = currentCenterRow - currentTopRow;
			return vm.readMem(currentTopRow, newRowCountRead)
				.then(function (values) {
					return generateRows(findAddrGivenRowIdx(currentTopRow), values);
				})
				.then(function (newRows) {
					vm.rows.splice(vm.rows.length - newRows.length, newRows.length);
					vm.currentTopRow = currentCenterRow - newRows.length;
					vm.currentCenterRow = currentCenterRow;
					vm.isAtTopLimit = isAtTop();
					vm.isAtBottomLimit = isAtBottom();
					vm.rows = newRows.concat(vm.rows);
				})
				.catch(function (e) {
					$log.error('readMem failed:' + e);
				});
		};

		vm.loadDown = function () {
			$log.debug('memory browser: loadDown');
			let oldCenterRow = vm.currentCenterRow;
			let currentCenterRow = findBottomRowIdx(findAddrGivenRowIdx(oldCenterRow));
			let newRowCountRead = currentCenterRow - oldCenterRow;

			return vm.readMem(currentCenterRow, newRowCountRead)
				.then(function (values) {
					return generateRows(findAddrGivenRowIdx(currentCenterRow), values);
				})
				.then(function (newRows) {
					vm.rows.splice(0, vm.rows.length - vm.bufferCount);
					vm.currentTopRow = oldCenterRow;
					vm.currentCenterRow = currentCenterRow;
					vm.isAtTopLimit = isAtTop();
					vm.isAtBottomLimit = isAtBottom();
					vm.rows = vm.rows.concat(newRows);
				})
				.catch(function (e) {
					$log.error('readMem failed:' + e);
				});
		};

		function isAtTop() {
			let topRow = findLoadingTopRowIdx(findAddrGivenRowIdx(vm.currentCenterRow));
			return (topRow === 0);
		}

		function isAtBottom() {
			let botRow = findBottomRowIdx(findAddrGivenRowIdx(vm.currentCenterRow));
			let botRowLastAddr = findAddrGivenRowIdx(botRow) + vm.cols * currentCellSize;
			if (botRowLastAddr >= 0xFFFFFFFF) {
				return true;
			}
			return false;
		}

		function cellMatch(cellAddr, length: number) {
			if (length) {
				return (cellAddr >= newAddr) && (cellAddr < newAddr + length);
			} else {
				return (cellAddr <= newAddr) && (cellAddr + currentCellSize > newAddr);
			}
		}

		function intToHexStr(int) {
			return '0x' + int.toString(16).toUpperCase();
		}

		function hexStrToInt(str) {
			if (str && str.match(vm.validateAddressRegex)) {
				return parseInt(str, 16);
			} else {
				return undefined;
			}
		}

		vm.cellTootip = function (cell) {
			let ret = [];
			if (cell.err) {
				ret.push(cell.err);
			} else {
				for (let i = 0; i < currentCellSize; i++) {
					let addr = intToHexStr(parseInt(cell.addrStr, 16) + i);
					let tooltipAddrVal;
					if (currentMAUSize === 1) {
						tooltipAddrVal = (parseInt(cell.val, 16) >> (i * 8)) & 0xFF;
						ret.push(addr + ': ' + ('0' + tooltipAddrVal.toString(16).toUpperCase()).slice(-2));
					} else if (currentMAUSize === 2) {
						tooltipAddrVal = parseInt(cell.val, 16);
						ret.push(addr + ': ' + ('000' + tooltipAddrVal.toString(16).toUpperCase()).slice(-4));
					}
				}
			}
			return ret;
		};

		vm.export = function (core, pageInfoList) {
			let modalInstance = $uibModal.open({
				templateUrl: 'memory/memory_export.html',
				controllerAs: 'vm',
				controller: OpenExportDialogController
			});

			function OpenExportDialogController() {
				let vm = this;
				vm.title = 'Export Memory';
				vm.format = 'BIN';
				vm.exportConfig = {};
				vm.exportConfig.ranges = [];
				vm.mauSizeInBits = 8 * currentMAUSize;
				vm.isNW = function () {
					return ticExecutionPlatform.getPlatform() === ti_common.PlatformType.NW;
				};
				vm.getExtension = function () {
					if (vm.format === 'BIN') {
						return '.bin';
					} else {
						return '.out';
					}
				};
				vm.browseExportFile = function () {
					return ticFileDialogs.saveFile('Select Export File', 'exported-image', vm.getExtension())
						.then((file) => {
							if (file) {
								vm.path = file.path;
							}
							return file;
						});
				};
				vm.exportToCloudStorage = function () {
					modalInstance.close();
				};
				vm.validateAddressRegex = validateAddressRegex();

				vm.pageInfoList = pageInfoList;
				angular.forEach(pageInfoList, (pageInfo) => {
					if (pageInfo.isDefault) {
						vm.curPage = pageInfo.name;
					}
				});

				function setPageToRanges(addrRanges) {
					let retRanges = [];
					angular.forEach(addrRanges, (range) => {
						let locationRange = {
							location: range.address + '@' + vm.curPage,
							length: getWordsCountInput(range.length)
						};
						retRanges.push(locationRange);
					});

					return retRanges;
				}

				function getWordsCountInput(byteCountNumber: number) {
					if (byteCountNumber) {
						const wordSize = Math.ceil(byteCountNumber * currentMAUSize / currentWordSize);
						return wordSize;
					} else {
						return undefined;
					}
				}

				vm.exportToLocal = function () {
					let exportPromise;
					let defaultPath = vm.isNW() ? undefined : vm.path;
					if (vm.format === 'BIN') {
						let range = {
							location: hexStrToInt(vm.addrInput) + '@' + vm.curPage,
							length: getWordsCountInput(Number(vm.bytesCountInput))
						};
						$log.debug('exportAsBin: range=' + JSON.stringify(range));
						exportPromise = core.exportMemoryAsBin(range, vm.mauSizeInBits, defaultPath);
					} else {
						let ranges = setPageToRanges(vm.exportConfig.ranges);
						$log.debug('exportAsCoff: ranges=' + JSON.stringify(ranges));
						exportPromise = core.exportMemoryAsCoff(ranges, defaultPath);
					}

					return exportPromise
						.then((res) => {
							if (vm.isNW()) {
								if (vm.path) {
									const fs = require('fs');
									fs.createReadStream(res.path).pipe(fs.createWriteStream(vm.path));
									ufConsole.addMessage(ufConsole.msgTypes.STATUS, 'Memory exported to: ' + vm.path + ', ' + res.length * currentWordSize + 'B of binary data.');
								} else {
									ufConsole.addMessage(ufConsole.msgTypes.STATUS, 'Please provide an export file path.');
								}
							} else {
								ufConsole.addMessage(ufConsole.msgTypes.STATUS, 'Memory exported to: ' + res.path + ', ' + res.length * currentWordSize + 'B of binary data.');
							}
						})
						.finally(() => {
							modalInstance.close();
						});
				};

				vm.showTable = function () {
					return vm.format === 'COFF';
				};

				vm.addRange = function (addr, len) {
					if (validateRange(addr, len)) {
						vm.exportConfig.ranges.push({
							address: hexStrToInt(addr),
							length: Number(len)
						});
						vm.addrInput = undefined;
						vm.bytesCountInput = undefined;
					}
				};

				vm.removeRange = function (range) {
					for (let i = 0; i < vm.exportConfig.ranges.length; i++) {
						if (vm.exportConfig.ranges[i].address === range.address) {
							vm.exportConfig.ranges.splice(i, 1);
							return;
						}
					}
				};

				function isInt(n) {
					if (!n) {
						return n === 0;
					}

					return n % 1 === 0;
				}

				function validateRange(addr, length) {
					let addrNum = hexStrToInt(addr);
					let lenNum = Number(length);

					let addrValid = (addrNum === undefined) ? false : isInt(addrNum) && addrNum >= 0;
					let lenValid = isInt(lenNum) && lenNum > 0;

					vm.msg = '';
					if (vm.bytesCountInput && !addrValid) {
						vm.msg += 'Address must be a valid hexadecimal number. eg. 0x12345678';
					}
					if (vm.bytesCountInput && !lenValid) {
						vm.msg += 'Length must be an integer larger than zero. ';
					}
					let ret = lenValid && addrValid;
					return ret;
				}

				vm.readyToExport = function () {
					if (vm.format === 'COFF') {
						return (vm.exportConfig.ranges.length > 0);
					} else {
						return validateRange(vm.addrInput, vm.bytesCountInput);
					}
				};

				vm.quickGuide = function () {
					return (vm.format === 'COFF') ?
						'Export an array of memory ranges to an .out file in COFF format.' :
						'Export a single piece of continous memory range to a .bin file in binary byte stream.';
				};
			}

			return modalInstance.result;
		};
	}

})();
