/**
 * ArchDB Core Module
 * 
 * Contains the main application module with core functionality:
 * - App configuration and routing
 * - Filters for data formatting
 * - Factories for API services
 * - Core directives
 * - Resource definitions
 * - Core controllers and services
 */

var archDB = angular.module('archDB', ['ngResource', 'ngRoute', 'ngSanitize', '$strap.directives', 'ClientsModule', 'dashboard_v2', 'bookings', 'invoices', 'accounts', 'admin', 'StockModule', 'StatsModule', 'ui.calendar', 'ui.sortable', 'notificationsModule', 'daterangepicker', 'ui.bootstrap', 'ngFileupload', 'xeditable', 'infinite-scroll', 'gantt', 'ngTextcomplete', 'ui.select2', 'ui.keypress', 'timer', 'ngAudio', 'ngIdle', 'wu.masonry', 'mentio', 'monospaced.elastic', 'ui.grid', 'ui.grid.selection', 'ui.grid.pagination', 'ui.grid.cellNav', 'ui.grid.pinning', 'ui.grid.resizeColumns', 'ui.grid.edit', 'chart.js', 'oitozero.ngSweetAlert', 'ui.select', 'ThreeCXModule'], function ($routeProvider, $locationProvider) {
	//test
	// configure html5 to get links working on jsfiddle
	$locationProvider.html5Mode(false);

}).config(function ($interpolateProvider, $idleProvider, $keepaliveProvider) {
	$interpolateProvider.startSymbol('{[{').endSymbol('}]}');
	$idleProvider.idleDuration(120); // 2 minutes idle time
	$idleProvider.keepalive(false);
}).run(function (editableOptions, editableThemes) {
	editableOptions.theme = 'default'; // bootstrap2 theme. Can be also 'bs3', 'default'
	editableThemes['default'].submitTpl = '<button type="submit" class="btn btn-primary btn-small"><i class="fa fa-check"></i></button>';
	editableThemes['default'].cancelTpl = '<button type="button" class="btn btn-default btn-small" ng-click="$form.$cancel()"><i class="fa fa-times"></i></button>';
	
	// Set all calendars to start from Monday (1 = Monday, 0 = Sunday)
	moment.updateLocale('en', {
		week: {
			dow: 1 // Monday is the first day of the week
		}
	});
});

archDB.controller('TitleController', ['$scope', 'Page', function ($scope, Page) {
	$scope.Page = Page;
}]);

// ------------------------------------------------------------------------
//
//	FILTERS BELOW -->
//
// ------------------------------------------------------------------------

archDB.filter('fromNow', function () {
	return function (dateString) {
		if (!dateString) return "";

		return moment(dateString).fromNow();
	};
});

archDB.filter('diff', function () {
	return function (dateString, format) {
		if (!format)
			format = 'seconds';

		if (_.indexOf(['seconds', 'minutes', 'hours', 'days'], format) == -1)
			throw "The format you requested is invalid";

		return moment().diff(dateString, format);
	};
});

archDB.filter('console', function () {
	return function (object) {
		console.log(object);
	};
});

archDB.filter('calendar', function () {
	return function (dateString) {
		if (!dateString) return moment(new Date()).calendar();

		// Split timestamp into [ Y, M, D, h, m, s ]
		var t = dateString.split(/[- :]/);

		// Apply each element to the Date function
		var d = new Date(t[0], t[1] - 1, t[2], t[3], t[4], t[5]);
		return moment(d).calendar();
	};
});

archDB.filter('moment', function () {
	return function (dateString, format) {
		if (!format) format = "YYYY-MM-DD HH:mm:ss";

		return moment(dateString).format(format);
	};
});

archDB.filter('html', function () {
	return function (data) {

	};
});

archDB.filter('capitalize', function () {
	return function (string) {
		var capitalized = string.charAt(0).toUpperCase() +
			string.substring(1);

		return capitalized;
	};
});

archDB.filter('nl2br', function () {
	return function (text) {
		if (!text) return '';

		return text.replace(/\n/g, '<br/>');
	};
});

archDB.filter('taskUrl', function () {
	return function (task) {
		if (!task) return '';

		if (task.type == 'repair')
			return 'repairs/' + task.id;

		if (task.type == 'order')
			return 'orders/' + task.id;

		if (task.type == 'sale_lead')
			return 'sale-lead/' + task.id;

		return task.type + '/' + task.id;
	}
});

archDB.filter('bytes', function () {
	return function (bytes, precision) {
		if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) return '-';
		if (typeof precision === 'undefined') precision = 1;
		var units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
			number = Math.floor(Math.log(bytes) / Math.log(1024));
		return (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + ' ' + units[number];
	};
});

archDB.filter('slaTimer', function () {
	return function (seconds) {
		var duration = moment.duration(seconds, 'seconds');
		var minutes = Math.abs(duration.asMinutes());
		var hours = Math.floor(minutes / 60);
		minutes = Math.floor(minutes % 60);

		// add padding
		if (hours < 10) hours = '0' + hours;
		if (minutes < 10) minutes = '0' + minutes;

		return (seconds < 0) ? '-' + hours + ':' + minutes : hours + ':' + minutes;
	};
});

archDB.filter('range', function () {
	return function (input, total, start) {
		total = parseInt(total);
		start = (start) ? parseInt(start) : 0;
		for (var i = start; i < total; i++)
			input.push(i);
		return input;
	};
});

archDB.filter('dateAddition', function () {
	return function (input, measurement, unit, format) {
		if (!measurement) measurement = 'days';
		if (!unit) unit = 1;
		if (!format) format = "DD/MM/YYYY";

		return moment().add(measurement, unit).format(format);
	};
});

archDB.filter('exclude', function () {
	return function (input, index, value) {
		if (!input) return [];
		if (!index || typeof value == 'undefined') return input;

		return _.reject(input, function (el) { return el[index] == value });
	};
});

// ------------------------------------------------------------------------
//
//	FACTORIES BELOW -->
//
// ------------------------------------------------------------------------

archDB.factory('apiUrl', function () {
	// This service formats the api url througout the application
	return function (path) {
		return '/api/v1/' + path;
	};
});

archDB.factory('Messenger', function () {

	return function (message, type) {
		if (!message) return false;
		if (!type) type = 'success';

		Messenger().post({
			message: message,
			hideAfter: 2,
			type: type
		});
	};
});

archDB.factory('ErrorsHandler', function () {

	return function (res) {

		if (res.data && res.data.error)
			if (res.data.error.message)
				Messenger().post({
					message: res.data.error.message,
					hideAfter: 0,
					type: 'error',
					showCloseButton: true,
				});
			else
				Messenger().post({
					message: res.data.error,
					hideAfter: 0,
					type: 'error',
					showCloseButton: true,
				});

		if (res.data && res.data.error == '')
			Messenger().post({
				message: 'Something went wrong',
				hideAfter: 0,
				type: 'error',
				showCloseButton: true,
			});

		// Handle new validation error format from TaskController (with priority)
		if (res.data && res.data.success === false && res.data.message) {
			Messenger().post({
				message: res.data.message,
				hideAfter: 0,
				type: 'error',
				showCloseButton: true,
			});
		}
		// Only show field errors if there's no main message (avoid duplicate notifications)
		else if (res.data && res.data.errors) {
			var messages = '';

			// Handle both array and object formats
			if (Array.isArray(res.data.errors)) {
				// Array format: ["Error 1", "Error 2"]
				messages = res.data.errors.join("\n");
			} else if (typeof res.data.errors === 'object') {
				// Object format: {"field1": ["Error 1"], "field2": ["Error 2"]}
				var errorMessages = [];
				for (var field in res.data.errors) {
					if (res.data.errors.hasOwnProperty(field)) {
						var fieldErrors = res.data.errors[field];
						if (Array.isArray(fieldErrors)) {
							errorMessages = errorMessages.concat(fieldErrors);
						} else {
							errorMessages.push(fieldErrors);
						}
					}
				}
				messages = errorMessages.join("\n");
			} else {
				messages = res.data.errors.toString();
			}

			Messenger().post({
				message: messages,
				hideAfter: 0,
				type: 'error',
				showCloseButton: true,
				theme: 'air',
				extraClasses: 'messenger-fixed messenger-on-top messenger-on-right'
			});
		}
	};
});

archDB.factory('Page', function () {
	var title = 'Seraph';
	return {
		getTitle: function () { return title; },
		setTitle: function (newTitle) { title = newTitle; }
	};
});

archDB.factory('Settings', function () {
	return {
		vat: 20,
		staff_percentage: 5
	};
});


/*
|--------------------------------------------------------------------------
| REST services
|--------------------------------------------------------------------------
|
| Here we declare a list of restful services that we can use throughout the
| application.
|
*/

archDB.factory('ResourceMethods', function () {

	return {
		'get': { method: 'GET' },
		'save': { method: 'POST' },
		'update': { method: 'PUT' },
		'query': { method: 'GET', isArray: true },
		'remove': { method: 'DELETE' },
		'delete': { method: 'DELETE' }
	};

});

archDB.factory('ResourcePrototype', function () {

	return {
		/**
		 * Backbone-style save() that inserts or updated the record based on the presence of an id.
		 */
		save: function (values, callback) {
			if (typeof values == 'function') {
				// If the first parameter is a function
				// then this is our callback
				callback = values;
			} else if (values) {
				angular.extend(this, values);
			}
			if (this.id) {
				return this.$update(callback);
			}
			return this.$save(callback);
		}
	};
});

// ------------------------------------------------------------------------
//
//	RESOURCES BELOW -->
//
// ------------------------------------------------------------------------

archDB.factory('User', ['apiUrl', '$resource', 'ResourceMethods', 'ResourcePrototype', function (apiUrl, $resource, methods, _prototype) {
	var User = $resource(apiUrl('users/:user'), { user: '@id' }, methods);
	User.getSearchUrl = function () { return apiUrl('users?name='); };
	angular.extend(User.prototype, _prototype);
	return User;
}]);

archDB.factory('Client', ['apiUrl', '$resource', 'ResourceMethods', 'ResourcePrototype', function (apiUrl, $resource, methods, _prototype) {
	var Client = $resource(apiUrl('clients/:client'), { client: '@id' }, methods);
	Client.getSearchUrl = function () { return apiUrl('clients?name='); };
	angular.extend(Client.prototype, _prototype);
	return Client;
}]);

archDB.factory('Customer', ['apiUrl', '$resource', 'ResourceMethods', 'ResourcePrototype', function (apiUrl, $resource, methods, _prototype) {
	var Customer = $resource(apiUrl('customers/:customer'), { customer: '@id' }, methods);
	Customer.getSearchUrl = function () { return apiUrl('customers?name='); };
	angular.extend(Customer.prototype, _prototype);
	return Customer;
}]);

archDB.factory('Site', ['apiUrl', '$resource', 'ResourceMethods', 'ResourcePrototype', function (apiUrl, $resource, methods, _prototype) {
	var Site = $resource(apiUrl('sites/:site'), { site: '@id' }, methods);
	angular.extend(Site.prototype, _prototype);
	return Site;
}]);

archDB.factory('SiteUser', ['apiUrl', '$resource', 'ResourceMethods', 'ResourcePrototype', function (apiUrl, $resource, methods, _prototype) {
	var SiteUser = $resource(apiUrl('sites/:site/users/:user'), { site: '@site_id', user: '@id' }, methods);
	angular.extend(SiteUser.prototype, _prototype);
	return SiteUser;
}]);

archDB.factory('Sla', ['apiUrl', '$resource', 'ResourceMethods', 'ResourcePrototype', function (apiUrl, $resource, methods, _prototype) {
	var Sla = $resource(apiUrl('sla/:sla'), { sla: '@id' }, methods);
	angular.extend(Sla.prototype, _prototype);
	return Sla;
}]);

archDB.factory('SlaLog', ['apiUrl', '$resource', 'ResourceMethods', 'ResourcePrototype', function (apiUrl, $resource, methods, _prototype) {
	var SlaLog = $resource(apiUrl('sla-logs/:sla_log'), { sla_log: '@id' }, methods);
	angular.extend(SlaLog.prototype, _prototype);
	return SlaLog;
}]);

archDB.factory('ActivityLog', ['apiUrl', '$resource', 'ResourceMethods', 'ResourcePrototype', function (apiUrl, $resource, methods, _prototype) {
	var ActivityLog = $resource(apiUrl('activity-logs/:activity_log'), { activity_log: '@id' }, methods);
	angular.extend(ActivityLog.prototype, _prototype);
	return ActivityLog;
}]);

archDB.factory('Follow', ['apiUrl', '$resource', 'ResourceMethods', 'ResourcePrototype', function (apiUrl, $resource, methods, _prototype) {
	var Follow = $resource(apiUrl('follow/:follow'), { follow: '@id' }, methods);
	angular.extend(Follow.prototype, _prototype);
	return Follow;
}]);
archDB.factory('Severity', ['apiUrl', '$resource', 'ResourceMethods', 'ResourcePrototype', function (apiUrl, $resource, methods, _prototype) {
	var Severity = $resource(apiUrl('severities/:severity'), { severity: '@id' }, methods);
	angular.extend(Severity.prototype, _prototype);
	return Severity;
}]);
/*
|--------------------------------------------------------------------------
| AngularJS Directives
|--------------------------------------------------------------------------
|
| A list of directives that are used throughout the application.
|
*/


archDB.directive('modal', function () {

	var defaults = {
		onOpen: function () { },
		onClose: function () { }
	};

	return {
		restrict: 'A',
		scope: {
			'options': '=modalOptions',			// a two way bind to the source data
			'open': '=modal',
			'classes': '@customClass'
		},
		controller: ['$scope', '$element', function ($scope, $element) {

			angular.extend(defaults, $scope.options || {});

			$scope.$watch('open', function () {
				if ($scope.open)
					$scope.showModal();
				else
					$scope.hideModal();
			});

			$scope.showModal = function () {
				$element.modal('show');
			};

			$scope.hideModal = function () {
				$element.modal('hide');
			};

			// Update the open value when the element is hidden
			$element.bind('hidden', function () {
				$scope.$apply(function () {
					$scope.open = false;
					if (defaults.onClose) defaults.onClose($element);
				});
			});

			$element.bind('shown', function () {
				$scope.$apply(function () {
					$scope.open = true;
					if (defaults.onOpen) defaults.onOpen($element);
				});
			});
		}],
		link: function (scope, element, attrs) {

			// add the modal class
			element.addClass('modal hide fade');

			// add custom css classes
			if (scope.classes) element.addClass(scope.classes);

			// Determines if the dialog is opened or closed
			defaults.show = (scope.open) ? true : false;

			// Create the modal
			element.modal(defaults);

		}
	};
});

/**
 * Resizes the iframe to the height of the content
 * @return directive
 */
archDB.directive('iframeAutoheight', function () {
	return {
		restrict: 'A',
		link: function (scope, element, attrs) {

			element.on('load', function () {
				$(this).height($(this).contents().outerHeight());
			});
		}
	};
});

/**
 * Typeahead for search clients and customers.
 * @return directive
 */
archDB.directive('clientSearch', function () {

	return {
		restrict: 'A',
		controller: ['$scope', '$element', 'ErrorsHandler', '$ClientsManager', 'Client', 'Customer', function ($scope, $element, ErrorsHandler, $ClientsManager, Client, Customer) {

			// ------------------------------------------------------------------------
			//
			//	SCOPE VARIABLES BELOW -->
			//
			// ------------------------------------------------------------------------

			var clientSource = new Bloodhound({
				datumTokenizer: function (datum) {
					return Bloodhound.tokenizers.whitespace(datum.name);
				},
				queryTokenizer: Bloodhound.tokenizers.whitespace,
				remote: {
					url: Client.getSearchUrl() + '%QUERY',
					wildcard: '%QUERY',
					transform: function (response) {
						// Add a 'type' field to each client
						if (response.data) {
							response.data.forEach(function (client) {
								client.type = 'client';
							});
						}
						return response.data;
					}
				}
			});

			var customerSource = new Bloodhound({
				datumTokenizer: function (datum) {
					return Bloodhound.tokenizers.whitespace(datum.name || datum.first_name);
				},
				queryTokenizer: Bloodhound.tokenizers.whitespace,
				remote: {
					url: Customer.getSearchUrl() + '%QUERY',
					wildcard: '%QUERY',
					transform: function (response) {
						// Add a 'name' field to each customer for displayKey
						if (response.data) {
							response.data.forEach(function (customer) {
								customer.name = customer.first_name + ' ' + customer.last_name;
								customer.type = 'customer';
							});
						}
						return response.data;
					}
				}
			});

			clientSource.initialize();
			customerSource.initialize();

			// The search options passed to typeahead
			$scope.options = [
				{
					name: 'clients',
					source: clientSource.ttAdapter(),
					displayKey: 'name',
					templates: {
						header: '<h6 class="typeahead-header">Clients</h6>'
					}
				},
				{
					name: 'customers',
					source: customerSource.ttAdapter(),
					displayKey: 'name',
					templates: {
						header: '<h6 class="typeahead-header">Customers</h6>'
					}
				}
			];

			$scope.$on('clients::changed', function (e, client) {
				$element.typeahead('val', client.name);
			});

			// ------------------------------------------------------------------------
			//
			//	FUNCTIONS BELOW THIS LINE -->
			//
			// ------------------------------------------------------------------------

			$scope.customerSelected = function (ev, datum) {
				// Handle both datum structures (datum.id and datum.data.id)
				var clientId = datum.data ? datum.data.id : datum.id;
				var clientName = datum.name || datum.value || (datum.first_name + ' ' + datum.last_name);

				// Update the typeahead value immediately
				$element.typeahead('val', clientName);

				// Use the proper type field that was set in the transform functions
				var isCustomer = datum.type === 'customer';

				if (!isCustomer) {
					// We need to retrieve the client using the show method
					Client.get({ client: clientId, site_users: true }, function (res) {
						if (res.success) {
							$ClientsManager.setClient(res.data, true); // true to broadcast the change
							$ClientsManager.setClientType('client');
							// Update the search field with the client name
							$scope.client_search = res.data.name;
							// Trigger Angular digest if needed
							if (!$scope.$$phase) {
								$scope.$apply();
							}
						}

					}, ErrorsHandler);
				} else {
					// We will retrieve a customer
					Customer.get({ customer: clientId }, function (res) {
						if (res.success && res.data) {
							$ClientsManager.setClient(res.data, true); // true to broadcast the change
							$ClientsManager.setClientType('customer');
							// Update the search field with the customer name
							$scope.client_search = res.data.first_name + ' ' + res.data.last_name;
							// Trigger Angular digest if needed
							if (!$scope.$$phase) {
								$scope.$apply();
							}
						}

					}, ErrorsHandler);
				}
			};

			$scope.client_search = $ClientsManager.getClient().name;

			// Set the currently selected client name
			$scope.init = function () {
				if ($ClientsManager.isSelected()) {
					$scope.client_search = $ClientsManager.getClient().name;
					$element.typeahead('val', $ClientsManager.getClient().name);
				}
			};
		}],
		link: function (scope, element, attrs) {
			// Create the typeahead element
			element.typeahead({ hint: false }, scope.options);

			// Set the currently selected client name
			scope.init();

			// Bind to the typeahead:selected event (this is the event used by typeahead.js)
			element.on('typeahead:selected', function (ev, datum) {
				ev.preventDefault();
				scope.customerSelected(ev, datum);
				if (!scope.$$phase) {
					scope.$apply();
				}
			});
		}
	};
});

/**
 * Typeahead for searching stock products.
 * @return directive
 */
archDB.directive('productSearch', function () {

	return {
		restrict: 'A',
		scope: {
			'callback': '&searchCallback',
			'autoinsert': '@searchAutoInsert',
			'clear': '@searchClear',
			'composite': '@searchComposite',
			'hide': '=',
			'model': '=ngModel',
			'discontinued': '='
		},
		require: 'ngModel',
		controller: ['$scope', '$element', 'ErrorsHandler', 'Stock', '$timeout', function ($scope, $element, ErrorsHandler, Stock, $timeout) {

			$scope.$watch('discontinued', function () {
				return;
			}, true);

			var productSource = new Bloodhound({
				datumTokenizer: function (datum) {
					return Bloodhound.tokenizers.whitespace(datum.name);
				},
				queryTokenizer: Bloodhound.tokenizers.whitespace,
				remote: {
					// original
					//                    url: Stock.getSearchUrl() + '%QUERY'+ ( $scope.composite != undefined ? '&composite='+ ( $scope.composite === 'true' ? 1 : 0 ) : ''),
					url: Stock.getSearchUrl(),
					wildcard: '%QUERY',
					prepare: function (query, settings) {
						settings.url += query + ($scope.composite != undefined ? '&composite=' + ($scope.composite === 'true' ? 1 : 0) : '') + ($scope.discontinued != undefined ? '&show_discontinued=' + $scope.discontinued : '');
						return settings;
					},

					transform: function (response) {
						// Auto insert if only one element is found
						if (response.data.length == 1 && $scope.autoinsert && $scope.barcode) {
							$scope.barcode = false;
							$scope.callback({ ev: null, data: response.data[0] });
							$scope.clearInput();
							return [];
						}

						return response.data;
					}
				}
			});

			productSource.initialize();

			$scope.options = {
				name: 'products_' + Math.floor(Math.random() * 99999) + 9999,
				source: productSource.ttAdapter(),
				limit: 15,
				display: function (item) {
					return item.name + ' - £' + item.retail_price + ' (In stock: ' + (item.unlimited ? 'unlimited' : item.quantity) + ')';
				}
			};

			$scope.clearInput = function () {
				if ($scope.clear)
					$element.typeahead('val', '').focus();
			};

			$scope.$watch('hide', function () {
				if ($scope.hide) {
					$element.hide();
				} else {
					$element.show();
				}
			});

		}],

		link: function (scope, element, attrs, model) {

			// Create the typeahead element
			element.typeahead({ hint: false }, scope.options);

			// Enlarge the element
			element.parent().addClass('col-xs-12');

			// Callback once the element is selected
			element.bind('typeahead:select', function (ev, item) {
				console.log('typeahead:select triggered with item:', item);
				ev.preventDefault();

				console.log('Calling callback with data:', item);
				scope.callback({ ev: ev, data: item });

				// Clear the element value
				console.log('Clearing input');
				scope.clearInput();

				if (!scope.$$phase) {
					console.log('Triggering digest cycle');
					scope.$apply();
				}

				return false;
			});

			// Log when product data is cleared
			element.on('keyup', function () {
				if (!element.val()) {
					console.log('Product input cleared, triggering callback with null data');
					scope.$apply(function () {
						model.$setViewValue('');
						scope.callback({ ev: null, data: null });
					});
				}
			});

			// This is a hack so that we only listen in one
			// direction for changes on the model
			var current_phrase;

			element.on('keydown', function (e) {
				if (e.which === 13 && /^[0-9]+$/.test(element.val())) {
					scope.barcode = true;
					e.preventDefault();
				}
			});

			element.on('keyup', function (e) {
				setTimeout(function () {
					current_phrase = element.val();
					scope.model = current_phrase;
					model.$setViewValue(current_phrase);
					if (!scope.$$phase) scope.$apply();
				}, 50);
			});

			scope.$watch('model', function (newValue) {
				if (typeof newValue != 'undefined' && newValue != current_phrase)
					element.typeahead('val', newValue);
			});
		}
	};
});

archDB.directive('eatClick', function () {
	return function (scope, element, attrs) {
		$(element).click(function (event) {
			event.preventDefault();
		});
	};
});

archDB.directive('eatPropagation', function () {
	return function (scope, element, attrs) {
		$(element).click(function (event) {
			event.stopImmediatePropagation();
			event.preventDefault();
		});
	};
});

archDB.directive('tabdrop', function () {
	return function (scope, element, attrs) {
		element.tabdrop();
	};
});

archDB.directive('paginator', function () {

	var template =
		'<div class="row">' +
		'<div class="col-xs-6">' +
		'<div class="paginator_info">' +
		'<small>Showing {[{ paginator.getFrom() }]} to {[{ paginator.getTo() }]} of {[{ paginator.getTotalItems() }]} entries</small>' +
		'</div>' +
		'</div>' +
		'<div class="col-xs-6">' +
		'<nav class="paginator_pages pull-right">' +
		'<ul class="pagination">' +
		'<li ng-class="{disabled: ! paginator.hasPrev()}"><a href="#" ng-click="paginator.prevPage()" eat-click>Prev</a></li>' +
		'<li ng-if="showPages != \'false\'" ng-repeat="page in paginator.getVisiblePages()" ng-class="{disabled: paginator.getCurrentPage() == page || page == \'..\' || page == \'...\'}">' +
		'<a href="#" ng-click="paginator.setPage(page)" eat-click>{[{ page }]}</a>' +
		'</li>' +
		'<li ng-class="{disabled: ! paginator.hasNext()}"><a href="#" ng-click="paginator.nextPage()" eat-click>Next</a></li>' +
		'</ul>' +
		'</nav>' +
		'</div>' +
		'</div>';

	return {
		restrict: 'A',
		replace: true,
		template: template,
		scope: {
			'paginator': '=',
			'showPages': '@'
		}
	};
});


/**
 * Directive that creates a search box for stock items.
 * @return directive
 */
archDB.directive('lookupStock', function () {

	return {
		restrict: 'A',
		require: 'ngModel',
		scope: {
			'stockSelected': '&',
			'model': '=ngModel'
		},
		template: '<input autocomplete="off" ng-keypress="updated($event)">',
		replace: true,
		controller: ['$scope', '$element', 'ErrorsHandler', 'Stock', function ($scope, $element, ErrorsHandler, Stock) {

			var suggestion = new Bloodhound({
				datumTokenizer: Bloodhound.tokenizers.whitespace,
				queryTokenizer: Bloodhound.tokenizers.whitespace,
				remote: {
					url: Stock.getSearchUrl() + '%QUERY',
					wildcard: '%QUERY',
					transform: function (response) {
						return response.data;
					}
				}
			});

			suggestion.initialize();

			$scope.dataset = {
				name: 'products',
				displayKey: 'name',
				source: suggestion.ttAdapter()
			};

			$scope.updated = function (ev) {
				$scope.model = $element.val();
			};

			$scope.$watch('model', function () {
				// Fix for when the form is cleared
				if (!$scope.model) $element.typeahead('val', '').focus();
			});

		}],
		link: function (scope, element, attrs) {

			// Create the typeahead element
			element.typeahead({ hint: false }, scope.dataset);

			element.on('change', function () {
				scope.$apply(function () {
					scope.model = element.val();
				});
			});

			element.on('typeahead:selected', function (e, item) {
				if (scope.stockSelected) scope.stockSelected({ data: item.data });

				scope.$apply(function () {
					scope.model = element.val();
				});
			});

		}
	};
});

/**
 * Checks the current page url, and if it matches with the one supplied
 * in the attribute then it adds the class selected.
 */
archDB.directive('pageHighlighter', ['$location', function ($location) {

	return {
		restrict: 'A',
		link: function ($scope, $element, $attrs) {

			var cssClass = ($attrs.customClass) ? $attrs.customClass : 'active';

			$scope.$on('$routeChangeSuccess', function (ev, currentRoute, previousRoute) {

				if ($attrs.exactMatch) {
					if ($location.path() == $attrs.pageHighlighter)
						$element.addClass(cssClass);
					else
						$element.removeClass(cssClass);
				}
				else {
					if ($location.path().substr(0, $attrs.pageHighlighter.length) == $attrs.pageHighlighter)
						$element.addClass(cssClass);
					else
						$element.removeClass(cssClass);
				}
			});
		}
	};
}]);

/**
 * DEPRECATED: To be removed once the original dashboard is removed!!!
 *
 * Displays the client name.
 * Requires the client json object.
 */
archDB.directive('clientName', function () {

	return {
		restrict: 'A',
		scope: {
			'data': '=clientName'
		},
		link: function (scope, element, attrs) {
			if (!scope.data) return;

			if (scope.data.hasOwnProperty('first_name')) {
				element.text(scope.data.first_name + ' ' + scope.data.last_name);
			} else {
				element.text(scope.data.name);
			}

		}
	};
});

archDB.directive('notification', function () {

	return {
		restrict: 'A',
		controller: ['$scope', 'Notification', 'ErrorsHandler', '$timeout', 'Messenger', '$DesktopNotification', function ($scope, Notification, ErrorsHandler, $timeout, Messenger, $DesktopNotification) {

			/**
			 * Total unread notifications.
			 *
			 * @type integer
			 */
			$scope.unread = 0;

			/**
			 * Last message id that have been displayed
			 */
			$scope.lastId = 0;

			/**
			 * Total notifications.
			 *
			 * @type integer
			 */
			$scope.total = 0;

			/**
			 * A reference to the ticker that refreshes the notifications.
			 */
			var cancelRefresh;

			/**
			 * Determines if this is the first time notifications run.
			 *
			 * @type Boolean
			 */
			var firstRun = true;

			/**
			 * Retrieves the total number of notifications and
			 * unread notifications from the server.
			 *
			 * @return void
			 */
			$scope.CountNotifications = function () {
				Notification.get({ count: true }, function (res) {
					$scope.total = res.total;
					$scope.unread = res.unread.length;
					$scope.notifications = res.unread;
					$scope.flashNewMessages(res.unread);

				}, ErrorsHandler);
			};

			/**
			 * Marks all notifications as read
			 *
			 * @return null
			 */
			$scope.markAsRead = function () {
				Notification.get({ markAsRead: true }, function (response) {
					$scope.unread = 0;
				}, ErrorsHandler);
			};

			/**
			 * Clear all notifications (mark as read) from sidebar
			 *
			 * @return null
			 */
			$scope.clearAllNotifications = function () {
				Notification.get({ clearAll: true }, function (response) {
					$scope.notifications = [];
					$scope.unread = 0;
					Messenger("All notifications marked as read.");
				}, ErrorsHandler);
			};

			/**
			 * Displays the content of the messages for a few seconds to the user.
			 *
			 * @param  Array   messages - all unread messages
			 * @return Void
			 */
			$scope.flashNewMessages = function (messages) {

				var sorted = _.sortBy(messages, function (el) { return el.id; });

				if (firstRun) {
					if (sorted.length > 0) $scope.lastId = sorted[sorted.length - 1].id;
					firstRun = false;
					return false;
				}

				_.each(sorted, function (el) {
					if (el.id > $scope.lastId) {
						Messenger(el.message);
						$DesktopNotification.show('Notification', el.message);
						$scope.lastId = el.id;
					}
				});
			};

			$scope.redirect = function (notification) {
				switch (notification.related_type) {
					case 'repair': return window.location.href = '/tasks/repairs/' + notification.related_id;
					case 'support': return window.location.href = '/tasks/support/' + notification.related_id;
					case 'onsite': return window.location.href = '/tasks/onsite/' + notification.related_id;
					case 'order': return window.location.href = '/tasks/orders/' + notification.related_id;
					case 'message': return window.location.href = '/messages/' + notification.related_id;
				}
			};

			/**
			 * Watches for when the directive gets destroyed,
			 * and cancels the ticker.
			 *
			 * @return void
			 */
			$scope.$on('$destroy', function (e) {
				$timeout.cancel(cancelRefresh);
			});

			// Start the autorefresh
			cancelRefresh = $timeout(function NotificationsRefresh() {
				$scope.CountNotifications();
				cancelRefresh = $timeout(NotificationsRefresh, 600 * 1000); // 45 seconds refresh
			}, 5);

		}]
	};
});

archDB.directive('stripHtml', function () {
	return {
		link: function ($scope, $element, $attrs) {
			var div = angular.element('<div/>').html($attrs['stripHtml']);
			$element.text(div.text());
		}
	};
});

archDB.directive('confirmOnExit', ['$location', '$rootScope', function ($location, $rootScope) {

	return {
		link: function ($scope, elem, attrs) {
			var formName = (attrs.name) ? attrs.name : 'myForm';
			var check = true;

			window.onbeforeunload = function () {
				if ($scope[formName].$dirty) {
					return "Your form has not been saved, do you want to stay on the page?";
				}
			};

			$rootScope.$on('$locationChangeStart', function (event, next, current) {
				if ($scope[formName].$dirty && check) {
					if (!confirm("Your have not saved the form, do you want to leave this page?")) {
						event.preventDefault();
					} else {
						check = false;
					}
				}
			});
		}
	};
}]);

archDB.directive('archChange', ['$parse', function ($parse) {
	return {
		link: function ($scope, $element, $attrs) {
			var fn = $parse($attrs['archChange']);

			$element.on('change', function (ev) {
				fn($scope, { event: ev });
				if (!$scope.$$phase) $scope.$apply();
			});
		}
	};
}]);

archDB.directive('numbersOnly', function () {
	return {
		require: 'ngModel',
		link: function (scope, element, attrs, modelCtrl) {
			modelCtrl.$parsers.push(function (inputValue) {
				// this next if is necessary for when using ng-required on your input.
				// In such cases, when a letter is typed first, this parser will be called
				// again, and the 2nd time, the value will be undefined
				if (inputValue === undefined) return '';
				var transformedInput = inputValue.replace(/[^0-9.]/g, '');
				if (transformedInput != inputValue) {
					modelCtrl.$setViewValue(transformedInput);
					modelCtrl.$render();
				}

				return transformedInput;
			});
		}
	};
});

archDB.directive('summernote', function () {
	return {
		require: '?ngModel',
		link: function ($scope, $element, $attrs, $ctrl) {
			$element.summernote({
				height: $attrs['summernoteHeight'] ? $attrs['summernoteHeight'] : false,
				onblur: function () {
					$ctrl.$setViewValue($element.code());
					if (!$scope.$$phase) $scope.$apply();
				}
			});

			$scope.$watch($attrs.ngModel, function (newValue, oldValue) {
				if ($element.code() != newValue) $element.code(newValue);
			});
		}
	};
});

archDB.directive('dropdownCheckbox', function () {
	return {
		restrict: 'CA',
		link: function (scope, element, attrs) {
			element.on('click', function (e) {
				e.stopPropagation();
			});
		}
	};
});

archDB.directive('textcomplete', ['Textcomplete', 'Client', 'Customer', '$timeout', function (Textcomplete, Client, Customer, $timeout) {
	return {
		restrict: 'EA',
		scope: {
			staff: '=',
			tags: '=',
			message: '='
		},
		replace: true,
		template: '<textarea ng-model="message"></textarea>',
		link: function (scope, iElement, iAttrs) {

			var clients_timeout;
			if (!scope.tags) scope.tags = {};
			var date = [{ name: 'Today', value: moment() }, { name: 'Tommorrow', value: moment().add('days', 1) }, { name: moment().add('days', 2).format('dddd'), value: moment().add('days', 2) }];
			var tasks = [{ name: 'Repair', id: 'repair' }, { name: 'Support', id: 'support' }, { name: 'Onsite', id: 'onsite' }, { name: 'Message', id: 'message' }, { name: 'Order', id: 'order' }, { name: 'Sale Lead', id: 'sale_lead' }, { name: 'RMA', id: 'rma' }];

			// Generate times
			var times = [];
			for (var i = 0; i < 24; i++) {
				for (var j = 0; j < 60; j += 15) {
					var time = (i < 10) ? '0' + i : i;
					time += ':';
					time += (j < 10) ? '0' + j : j;
					times.push(time);
				}
			}

			var textcomplete = new Textcomplete(iElement, {
				date: {
					match: /\B\$(\w*)$/,
					search: function (term, callback) {
						callback($.map(date, function (mention) {
							return mention.name.toLowerCase().indexOf(term.toLowerCase()) === 0 ? mention : null;
						}));
					},
					index: 1,
					replace: function (mention) {
						scope.tags.date = mention;
						return '$' + mention.name + ' ';
					},
					template: function (mention) {
						return mention.name;
					}
				},
				time: {
					match: /\B\!(\w*)$/,
					search: function (term, callback) {
						callback($.map(times, function (mention) {
							return mention.indexOf(term) === 0 ? mention : null;
						}));
					},
					index: 1,
					replace: function (mention) {
						scope.tags.time = mention;
						return '!' + mention + ' ';
					}
				},
				staff: {
					match: /\B@(\w*\s*\w*)$/,
					search: function (term, callback) {
						callback($.map(scope.staff, function (mention) {
							return mention.name.toLowerCase().indexOf(term.toLowerCase()) === 0 ? mention : null;
						}));
					},
					index: 1,
					replace: function (mention) {
						scope.tags.staff = mention;
						return '@' + mention.name + ' ';
					},
					template: function (mention) {
						return mention.name;
					}
				},
				task: {
					match: /\B\^(\w*\s*\w*)$/,
					search: function (term, callback) {
						callback($.map(tasks, function (mention) {
							return mention.name.toLowerCase().indexOf(term.toLowerCase()) === 0 ? mention : null;
						}));
					},
					index: 1,
					replace: function (mention) {
						scope.tags.task = mention;
						return '^' + mention.name + ' ';
					},
					template: function (mention) {
						return mention.name;
					}
				},
				client: {
					match: /\B#(\w*\s*\w*)$/,
					search: function (term, callback) {
						var first = true;
						var data = [];

						if (clients_timeout) $timeout.cancel(clients_timeout);

						clients_timeout = $timeout(function () {
							Client.get({ name: term, limit: 5, site_users: true }, function (response) {
								data = (first) ? response.data : callback(data.concat(response.data));
								first = false;
							}, function () {
								if (first) first = false; else callback(data);
							});

							Customer.get({ name: term, limit: 5 }, function (response) {
								data = (first) ? response.data : callback(data.concat(response.data));
								first = false;
							}, function () {
								if (first) first = false; else callback(data);
							});
						}, 100);

					},
					index: 1,
					replace: function (mention) {
						scope.tags.client = mention;
						return '#' + mention.name + ' ';
					},
					template: function (mention) {
						return mention.name;
					}
				}
			});

			iElement.parent().css('display', 'block');
		}
	}
}]);

archDB.directive('profilePhotoUpload', ['apiUrl', '$parse', 'ErrorsHandler', '$http', function (apiUrl, $parse, ErrorsHandler, $http) {

	return {
		scope: {
			userData: '='
		},
		link: function ($scope, $element, $attrs) {
			var pendingFile = null;

			// Listen for file selection
			$element.on('change', function(e) {
				var file = e.target.files[0];
				if (file) {
					pendingFile = file;
					// Preview the image
					var reader = new FileReader();
					reader.onload = function(e) {
						$scope.$apply(function() {
							$scope.userData.profile_photo_url = e.target.result;
						});
					};
					reader.readAsDataURL(file);
				}
			});

			// Listen for form submission from parent scope
			$scope.$on('uploadProfilePhoto', function(event, callback) {
				if (pendingFile) {
					var formData = new FormData();
					formData.append('files', pendingFile);

					$http.post(apiUrl('users/profile-photo'), formData, {
						headers: {
							'Content-Type': undefined,
							'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
						},
						transformRequest: angular.identity
					}).then(function(response) {
						// Update the profile photo URL from server response
						if (response.data && response.data.url) {
							$scope.userData.profile_photo_url = response.data.url;
							// Also update in parent scope if accessible
							if ($scope.$parent && $scope.$parent.userData) {
								$scope.$parent.userData.profile_photo_url = response.data.url;
							}
						}
						pendingFile = null;
						if (callback) callback();
					}, function(error) {
						ErrorsHandler(error);
						if (callback) callback();
					});
				} else {
					if (callback) callback();
				}
			});
		}
	};
}]);

archDB.directive('optionsDisabled', ['$parse', function ($parse) {
	var disableOptions = function (scope, attr, element, data, fnDisableIfTrue) {
		// refresh the disabled options in the select element.
		angular.forEach(element.find("option"), function (value, index) {
			var elem = angular.element(value);
			if (elem.val() != "") {
				var locals = {};
				locals[attr] = data[index];
				elem.attr("disabled", fnDisableIfTrue(scope, locals));
			}
		});
	};
	return {
		priority: 0,
		require: 'ngModel',
		link: function (scope, iElement, iAttrs, ctrl) {
			// parse expression and build array of disabled options
			var expElements = iAttrs.optionsDisabled.match(/^\s*(.+)\s+for\s+(.+)\s+in\s+(.+)?\s*/);
			var attrToWatch = expElements[3];
			var fnDisableIfTrue = $parse(expElements[1]);
			scope.$watch(attrToWatch, function (newValue, oldValue) {
				if (newValue)
					disableOptions(scope, expElements[2], iElement, newValue, fnDisableIfTrue);
			}, true);
			// handle model updates properly
			scope.$watch(iAttrs.ngModel, function (newValue, oldValue) {
				var disOptions = $parse(attrToWatch)(scope);
				if (newValue)
					disableOptions(scope, expElements[2], iElement, disOptions, fnDisableIfTrue);
			});
		}
	};
}]);

archDB.directive('ngConfirmClick', ['SweetAlert', function (SweetAlert) {
	return {
		link: function (scope, element, attr) {
			var msg = attr.ngConfirmClick || "Are you sure?";
			var clickAction = attr.confirmedClick;
			element.bind('click', function (event) {
				if (attr.disabled) {
					return;
				}

				SweetAlert.swal({
					title: msg,
					type: "warning",
					showCancelButton: true,
					confirmButtonColor: "#DD6B55",
					confirmButtonText: "Yes"
				}, function (confirmed) {
					if (confirmed) {
						scope.$eval(clickAction);
						if (!scope.$$phase) scope.$apply();
					}
				});
			});
		}
	};
}]);

archDB.directive('ngHtml', function () {
	return {
		link: function (scope, element, attr) {
			element.html(attr.ngHtml);
		}
	};
});

archDB.directive('nanoscroller', ['$interval', function ($interval) {
	return {
		link: function (scope, element, attr) {
			element.nanoScroller({
				iOSNativeScrolling: true,
				disableResize: true,
				preventPageScrolling: true
			});

			// Set an interval to autoresize the scroller
			$interval(function () {
				element.nanoScroller();
			}, 1000);
		}
	};
}]);



archDB.directive('addCommand', function () {

	return {
		restrict: 'A',
		scope: {
			'commands': '=',
			'command': '='
		},
		replace: true,
		templateUrl: "modals/add_command.html",
		controller: function ($scope) {
			alert("hello");
		}
	};
});

archDB.directive('tooltip', ['$compile', function ($compile) {
	return {
		restrict: 'EA',
		link: function (scope, element, attrs) {
			// Forward to UI Bootstrap tooltip
			if (attrs.tooltip) {
				element.attr('uib-tooltip', attrs.tooltip);
				element.attr('tooltip-placement', attrs.tooltipPlacement || 'top');
				element.attr('tooltip-animation', attrs.tooltipAnimation !== undefined ? attrs.tooltipAnimation : true);
				element.attr('tooltip-popup-delay', attrs.tooltipPopupDelay || 300);
				element.attr('tooltip-trigger', attrs.tooltipTrigger || 'mouseenter');
				element.attr('tooltip-append-to-body', attrs.tooltipAppendToBody !== undefined ? attrs.tooltipAppendToBody : true);
				
				// Remove the old tooltip attribute to prevent conflicts
				element.removeAttr('tooltip');
				
				// Recompile the element with the new attributes
				$compile(element)(scope);
			}
		}
	}
}]);

archDB.directive('focusable', ['$timeout', function ($timeout) {
	return {
		restrict: 'AC',
		link: function (scope, element, attrs) {
			$timeout(function () {
				element.focus();
			}, 100);
		}
	};
}]);

archDB.directive('datepickerPopup', function () {
	return {
		restrict: 'EAC',
		require: 'ngModel',
		link: function (scope, element, attr, controller) {
			//remove the default formatter from the input directive to prevent conflict
			controller.$formatters.shift();
		}
	}
});


/**
 * A list of services
 */

archDB.factory('$ClientsManager', ['$rootScope', function ($rootScope) {
	var $this = this;
	var client = {};
	var client_type = 'client'; // client or customer
	var data = {};

	return {
		// We are exposing the internal data
		// for binding purposes
		client: client,
		client_type: client_type,
		data: data,

		// Functions
		setClient: function (new_client, broadcast) {
			client = angular.copy(new_client || {});
			if (new_client && new_client.type) client_type = new_client.type;
			if (broadcast === true) $rootScope.$broadcast('clients::changed', client);
		},
		getClient: function () {
			return client;
		},
		setClientType: function (new_type) {
			client_type = new_type;
		},
		getClientType: function () {
			return client_type;
		},
		isCustomer: function () {
			return client_type == 'customer';
		},
		isClient: function () {
			return client_type == 'client';
		},
		isSelected: function () {
			return (client.id) ? true : false;
		},
		unify: function (site, site_user) {
			data = angular.extend(data, {
				name: client.name,
				address: (site) ? site.address : client.address,
				address2: (site) ? site.address2 : client.address2,
				postcode: (site) ? site.postcode : client.postcode,
				town: (site) ? null : client.town,
				county: (site) ? null : client.county,
				phone: (site_user) ? site_user.phone : client.phone,
				phone2: (site_user) ? null : client.phone2,
				mobile: (site_user) ? site_user.mobile : client.mobile,
				email: (site_user) ? site_user.email : client.email

			});

			return data;
		},
		getAddress: function (site) {
			var output = [];

			if (site) {
				if (site.address) output.push(site.address);
				if (site.address2) output.push(site.address2);
				if (site.postcode) output.push(site.postcode);
			}
			else {
				if (data.address) output.push(data.address);
				if (data.address2) output.push(data.address2);
				if (data.town) output.push(data.town);
				if (data.county) output.push(data.county);
				if (data.postcode) output.push(data.postcode);
			}

			return output.join(', ');
		}
	};
}]);

/**
 * Stores and manipulates data that is related to paginating a table or page.
 * Each injection of the paginator creates a new separate class, so you can have
 * multiple instances of the paginator on the same page.
 */

archDB.factory('$paginator', ['$rootScope', function ($rootScope) {

	var Paginator = function () {
		this.limit = 20;
		this.total_items = 0;
		this.total_pages = 0;
		this.current_page = 1;
		this.from = 0;
		this.to = 0;
		this.pages_to_show = 4;
		this.edges = 2;
		this.onPageChange = function (page, limit) { };
		this.onLimitChange = function (page, limit) { };
	};

	Paginator.prototype.getLimit = function () {
		return this.limit;
	};

	Paginator.prototype.getCurrentPage = function () {
		return this.current_page;
	};

	Paginator.prototype.getTotalItems = function () {
		return this.total_items;
	};

	Paginator.prototype.getTotalPages = function () {
		return this.total_pages;
	};

	Paginator.prototype.getFrom = function () {
		return this.from;
	};

	Paginator.prototype.getTo = function () {
		return this.to;
	};

	Paginator.prototype.setLimit = function (value) {
		this.limit = value;
		this.onLimitChange(this.current_page, this.limit);
	};

	Paginator.prototype.setCurrentPage = function (value) {
		this.current_page = value;
	};

	Paginator.prototype.setTotalItems = function (value) {
		this.total_items = value;
	};

	Paginator.prototype.setTotalPages = function (value) {
		this.total_pages = value;
	};

	Paginator.prototype.setFrom = function (value) {
		this.from = value;
	};

	Paginator.prototype.setTo = function (value) {
		this.to = value;
	};

	Paginator.prototype.setFromResponse = function (response) {
		if (response.hasOwnProperty('last_page')) this.setTotalPages(response.last_page);
		if (response.hasOwnProperty('current_page')) this.setCurrentPage(response.current_page);
		if (response.hasOwnProperty('total')) this.setTotalItems(response.total);
		if (response.hasOwnProperty('from')) this.setFrom(response.from);
		if (response.hasOwnProperty('to')) this.setTo(response.to);
	};

	Paginator.prototype.setOnPageChange = function (value) {
		if (typeof value == 'function')
			this.onPageChange = value;
	};

	Paginator.prototype.setOnLimitChange = function (value) {
		if (typeof value == 'function')
			this.onLimitChange = value;
	};

	Paginator.prototype.getVisiblePages = function () {
		var pages = [], i;

		if (!this.total_pages) return pages;

		var half_pages = this.pages_to_show / 2;
		var interval = {
			start: Math.ceil(this.current_page > half_pages ? Math.max(Math.min(this.current_page - half_pages, (this.total_pages - this.pages_to_show)), 0) : 0),
			end: Math.ceil(this.current_page > half_pages ? Math.min(this.current_page + half_pages, this.total_pages) : Math.min(this.pages_to_show, this.total_pages))
		};

		// Generate the start edges
		if (interval.start > 0 && this.edges > 0) {
			var end = Math.min(this.edges, interval.start);
			for (i = 0; i < end; i++) {
				pages.push(i + 1);
			}
			if ((this.edges < interval.start) && (interval.start - this.edges != 1)) {
				pages.push('..');
			}
			else if (interval.start - this.edges == 1) {
				pages.push(this.edges + 1);
			}
		}

		// Generate interval links
		for (i = interval.start; i < interval.end; i++) {
			pages.push(i + 1);
		}

		// Generate end edges
		if (interval.end < this.total_pages && this.edges > 0) {
			if (this.total_pages - this.edges > interval.end && (this.total_pages - this.edges - interval.end != 1)) {
				pages.push('...');
			}
			else if (this.total_pages - this.edges - interval.end == 1) {
				pages.push(++interval.end);
			}
			var begin = Math.max(this.total_pages - this.edges, interval.end);
			for (i = begin; i < this.total_pages; i++) {
				pages.push(i + 1);
			}
		}

		return pages;
	};

	Paginator.prototype.setPage = function (value) {
		if (value < 0 && value > this.total_pages)
			return false;

		this.current_page = value;
		this.onPageChange(this.current_page, this.limit);
	};

	Paginator.prototype.nextPage = function () {
		if (!this.hasNext())
			return false;

		this.setPage(++this.current_page);
	};

	Paginator.prototype.prevPage = function () {
		if (!this.hasPrev())
			return false;

		this.setPage(--this.current_page);
	};

	Paginator.prototype.hasNext = function () {
		return (this.current_page < this.total_pages);
	};

	Paginator.prototype.hasPrev = function () {
		return (this.current_page > 1);
	};

	return new Paginator();
}]);

archDB.factory('keyboardManager', ['$window', '$timeout', function ($window, $timeout) {
	var keyboardManagerService = {};

	var defaultOpt = {
		'type': 'keydown',
		'propagate': false,
		'inputDisabled': false,
		'target': $window.document,
		'keyCode': false
	};
	// Store all keyboard combination shortcuts
	keyboardManagerService.keyboardEvent = {}
	// Add a new keyboard combination shortcut
	keyboardManagerService.bind = function (label, callback, opt) {
		var fct, elt, code, k;
		// Initialize opt object
		opt = angular.extend({}, defaultOpt, opt);
		label = label.toLowerCase();
		elt = opt.target;
		if (typeof opt.target == 'string') elt = document.getElementById(opt.target);

		fct = function (e) {
			e = e || $window.event;

			// Disable event handler when focus input and textarea
			if (opt['inputDisabled']) {
				var elt;
				if (e.target) elt = e.target;
				else if (e.srcElement) elt = e.srcElement;
				if (elt.nodeType == 3) elt = elt.parentNode;
				if (elt.tagName == 'INPUT' || elt.tagName == 'TEXTAREA') return;
			}

			// Find out which key is pressed
			if (e.keyCode) code = e.keyCode;
			else if (e.which) code = e.which;
			var character = String.fromCharCode(code).toLowerCase();

			if (code == 188) character = ","; // If the user presses , when the type is onkeydown
			if (code == 190) character = "."; // If the user presses , when the type is onkeydown

			var keys = label.split("+");
			// Key Pressed - counts the number of valid keypresses - if it is same as the number of keys, the shortcut function is invoked
			var kp = 0;
			// Work around for stupid Shift key bug created by using lowercase - as a result the shift+num combination was broken
			var shift_nums = {
				"`": "~",
				"1": "!",
				"2": "@",
				"3": "#",
				"4": "$",
				"5": "%",
				"6": "^",
				"7": "&",
				"8": "*",
				"9": "(",
				"0": ")",
				"-": "_",
				"=": "+",
				";": ":",
				"'": "\"",
				",": "<",
				".": ">",
				"/": "?",
				"\\": "|"
			};
			// Special Keys - and their codes
			var special_keys = {
				'esc': 27,
				'escape': 27,
				'tab': 9,
				'space': 32,
				'return': 13,
				'enter': 13,
				'backspace': 8,

				'scrolllock': 145,
				'scroll_lock': 145,
				'scroll': 145,
				'capslock': 20,
				'caps_lock': 20,
				'caps': 20,
				'numlock': 144,
				'num_lock': 144,
				'num': 144,

				'pause': 19,
				'break': 19,

				'insert': 45,
				'home': 36,
				'delete': 46,
				'end': 35,

				'pageup': 33,
				'page_up': 33,
				'pu': 33,

				'pagedown': 34,
				'page_down': 34,
				'pd': 34,

				'left': 37,
				'up': 38,
				'right': 39,
				'down': 40,

				'f1': 112,
				'f2': 113,
				'f3': 114,
				'f4': 115,
				'f5': 116,
				'f6': 117,
				'f7': 118,
				'f8': 119,
				'f9': 120,
				'f10': 121,
				'f11': 122,
				'f12': 123
			};
			// Some modifiers key
			var modifiers = {
				shift: {
					wanted: false,
					pressed: e.shiftKey ? true : false
				},
				ctrl: {
					wanted: false,
					pressed: e.ctrlKey ? true : false
				},
				alt: {
					wanted: false,
					pressed: e.altKey ? true : false
				},
				meta: { //Meta is Mac specific
					wanted: false,
					pressed: e.metaKey ? true : false
				}
			};
			// Foreach keys in label (split on +)
			for (var i = 0, l = keys.length; k = keys[i], i < l; i++) {
				switch (k) {
					case 'ctrl':
					case 'control':
						kp++;
						modifiers.ctrl.wanted = true;
						break;
					case 'shift':
					case 'alt':
					case 'meta':
						kp++;
						modifiers[k].wanted = true;
						break;
				}

				if (k.length > 1) { // If it is a special key
					if (special_keys[k] == code) kp++;
				} else if (opt['keyCode']) { // If a specific key is set into the config
					if (opt['keyCode'] == code) kp++;
				} else { // The special keys did not match
					if (character == k) kp++;
					else {
						if (shift_nums[character] && e.shiftKey) { // Stupid Shift key bug created by using lowercase
							character = shift_nums[character];
							if (character == k) kp++;
						}
					}
				}
			}

			if (kp == keys.length &&
				modifiers.ctrl.pressed == modifiers.ctrl.wanted &&
				modifiers.shift.pressed == modifiers.shift.wanted &&
				modifiers.alt.pressed == modifiers.alt.wanted &&
				modifiers.meta.pressed == modifiers.meta.wanted) {
				$timeout(function () {
					callback(e);
				}, 1);

				if (!opt['propagate']) { // Stop the event
					// e.cancelBubble is supported by IE - this will kill the bubbling process.
					e.cancelBubble = true;
					e.returnValue = false;

					// e.stopPropagation works in Firefox.
					if (e.stopPropagation) {
						e.stopPropagation();
						e.preventDefault();
					}
					return false;
				}
			}

		};
		// Store shortcut
		keyboardManagerService.keyboardEvent[label] = {
			'callback': fct,
			'target': elt,
			'event': opt['type']
		};
		//Attach the function with the event
		if (elt.addEventListener) elt.addEventListener(opt['type'], fct, false);
		else if (elt.attachEvent) elt.attachEvent('on' + opt['type'], fct);
		else elt['on' + opt['type']] = fct;
	};
	// Remove the shortcut - just specify the shortcut and I will remove the binding
	keyboardManagerService.unbind = function (label) {
		label = label.toLowerCase();
		var binding = keyboardManagerService.keyboardEvent[label];
		delete (keyboardManagerService.keyboardEvent[label])
		if (!binding) return;
		var type = binding['event'],
			elt = binding['target'],
			callback = binding['callback'];
		if (elt.detachEvent) elt.detachEvent('on' + type, callback);
		else if (elt.removeEventListener) elt.removeEventListener(type, callback, false);
		else elt['on' + type] = false;
	};
	//
	return keyboardManagerService;
}]);

archDB.factory('socket', ['$rootScope', '$http', 'apiUrl', function ($rootScope, $http, apiUrl) {

	var socket,
		object = {
			on: function () { },
			emit: function () { }
		};

	// We wrap the connection in a try catch block in case it fails...
	// so that the app will continue loading
	try {
		// Create the socket but do not connect unti we retrieve the logged in user details
		socket = io.connect('//' + window.location.host + ':8080', { 'auto connect': false });;

		// Fetch the logged in user details and connect
		$http.get(apiUrl('me'), { async: false, params: { channels: true } }).success(function (response) {
			socket.socket.options['query'] = $.param(response);
			socket.socket.connect();
		});

	} catch (err) {
		if (window.console) console.log(err);
		return object;
	}

	object.on = function (eventName, callback) {
		socket.on(eventName, function () {
			var args = arguments;
			$rootScope.$apply(function () {
				callback.apply(socket, args);
			});
		});
	};

	object.emit = function (eventName, data, callback) {
		socket.emit(eventName, data, function () {
			var args = arguments;
			$rootScope.$apply(function () {
				if (callback) {
					callback.apply(socket, args);
				}
			});
		})
	};

	return object;
}]);

archDB.service('$DesktopNotification', ['$rootScope', 'ngAudio', '$AuthUser', function ($rootScope, ngAudio, $AuthUser) {
	var self = this;

	self.notify = window.notify;
	self.isSupported = self.notify.isSupported;

	$rootScope.$on('$AuthUser loaded', function (response) {
		if (response.mute_sounds == '1') {
			ngAudio.toggleMuteAll(); // mute all sounds
		}
	});

	self.hasPermission = function () {
		return self.notify.permissionLevel() == self.notify.PERMISSION_GRANTED ? true : false;
	};

	self.requestPermission = function () {
		self.notify.requestPermission(function () {
			// The browser should display a popup asking the user for permission
			$rootScope.$apply();
		});
	};

	// Functions
	self.show = function (title, body, icon, play_sound) {
		if (!title) title = 'Notification';
		if (!body) body = 'Example';
		if (!icon) icon = 'alert.ico';

		if (!self.hasPermission && self.isSupported)
			self.requestPermission();

		self.notify.createNotification(title, { body: body, icon: icon });
		if (play_sound) ngAudio.play('/audio/notification.mp3');
	};
}]);

archDB.service('$AuthUser', ['apiUrl', '$http', '$rootScope', function (apiUrl, $http, $rootScope) {
	var self = this;

	self.data = {};
	self.loaded = false;

	self.load = function () {
		$http.get(apiUrl('me')).success(function (response) {
			self.data = response;
			self.loaded = true;
			$rootScope.$broadcast('$AuthUser loaded', self.data);
		});
	};

	self.getUser = function () {
		return self.data;
	};

	// Load the user when initialised
	self.load();
}]);

archDB.service('TaskTypes', [function () {
	var self = this;

	self.types = [
		{
			id: 'repair',
			name: 'Repair'
		},
		{
			id: 'support',
			name: 'Remote Support'
		},
		{
			id: 'onsite',
			name: 'Onsite'
		},
		{
			id: 'sale_lead',
			name: 'Sale Lead'
		},
		{
			id: 'order',
			name: 'Order'
		}
	];

	self.getTypes = function () {
		return self.types;
	}

	self.getType = function (id) {
		return _.find(self.types, function (el) { return el.id == id; });
	};
}]);

archDB.service('helpers', [function () {

	this.addZeroNumberPadding = function (number) {
		return (number < 10) ? '0' + number : number;
	};

}]);

/**
 * Menu search controller
 */

archDB.controller('MenuSearchCtrl', ['$scope', '$timeout', '$http', 'ErrorsHandler', function ($scope, $timeout, $http, ErrorsHandler) {

	/**
	 * Determine if there are any results
	 *
	 * @type Boolean
	 */
	$scope.hasResults = false;

	/**
	 * Contains all the results
	 *
	 * @type Object
	 */
	$scope.results = {};

	/**
	 * This is true during a search.
	 *
	 * @type Boolean
	 */
	$scope.searching = false;

	/**
	 * The search timeout object,
	 * that can be used to cancel a request.
	 *
	 * @type Timeout
	 */
	var searchTimeout;

	/**
	 * Minimum number of characters before
	 * sending an ajax request.
	 *
	 * @type Integer
	 */
	var minLength = 2;

	/**
	 * The delay between the requests
	 * in milliseconds.
	 *
	 * @type Integer
	 */
	var delay = 300;

	/**
	 * Hides the results
	 *
	 * @return void
	 */
	$scope.hideResults = function () {
		$scope.hasResults = false;
	};

	/**
	 * Performs the ajax request.
	 *
	 * @param  String query
	 * @return Void
	 */
	$scope.search = function (query) {
		if (searchTimeout) $timeout.cancel(searchTimeout);
		if (query.length < minLength) { $scope.hideResults(); return false; }

		$scope.searching = true;

		searchTimeout = $timeout(function () {
			$http.get('/search', { params: { q: query } }).success(function (response) {
				$scope.results = response;
				$scope.searching = false;
				$scope.hasResults = true;
			}).error(ErrorsHandler);
		}, delay);
	};

}]);
/**
 * Settings Controller
 */

archDB.controller('SettingsCtrl', ['$scope', 'User', 'ErrorsHandler', '$uibModalInstance', 'user', function ($scope, User, ErrorsHandler, $uibModalInstance, user) {

	/**
	 * Form data
	 *
	 * @type Object
	 */
	$scope.data = user;

	/**
	 * The update was successfull
	 *
	 * @type Boolean
	 */
	$scope.success = false;

	/**
	 * Updates the form
	 *
	 * @return Void
	 */
	$scope.update = function () {
		// First upload the photo if there's one pending, then update user data
		$scope.$broadcast('uploadProfilePhoto', function() {
			// After photo upload completes, update the user data
			User.update($scope.data, function (response) {
				$scope.data = response;
				$scope.success = true;
			}, ErrorsHandler);
		});
	};

	$scope.cancel = function () {
		$uibModalInstance.dismiss('cancel');
	};
}]);
archDB.controller('SidebarCtrl', ['$scope', '$DesktopNotification', '$uibModal', function ($scope, $DesktopNotification, $uibModal) {

	$scope.notifications = $DesktopNotification;

	/**
	 * Displays the modal that contains the
	 * edit user form.
	 *
	 * @return null
	 */
	$scope.showSettings = function () {
		var modalInstance = $uibModal.open({
			controller: 'SettingsCtrl',
			templateUrl: 'modals/settings.html',
			resolve: {
				user: function () {
					return $scope.user;
				}
			}
		});
	};

}]);

