'use strict';
// Сервис для получения и обработки заказов
app.service('ordersService', ['$http', '$q', '$filter', '$interval', 'config',
	function ($http, $q, $filter, $interval, config) {
		// Типы заказов
		var logInfo = config.logConsole;
		var orderType = parseArray(config.orderType);
		var isOrdinaryType = orderType.indexOf('Ordinary') >= 0;
		var isPickupType = orderType.indexOf('Pickup') >= 0;
		var isCourierType = orderType.indexOf('Courier') >= 0;

		// Столы
		var showTables = parseArray(config.showTables);
		var hideTables = parseArray(config.hideTables);
		// Столы
		var showOrderSources = parseArray(config.showOrderSources);
		var hideOrderSources = parseArray(config.hideOrderSources);

		// Специальное имя продукта
		var specialProductNames = parseArray(config.specialProductName);

		// Схема работы с доставочным заказом
		var isKDSScheme = config.deliveryScheme == 'KDS';
		var isDeliveryScheme = !isKDSScheme;

		// Свойство времени окончания готовки
		//   (применяется только для не доставочных заказов)
		var completeTimeProperty = config.completeStatus >= 5 ? "ProcessingCompleteTime" : "Processing" + config.completeStatus + "BeginTime";

		// Типы статуса "Готовится"
		var inProgressStatusTypes = {
			deliverTime: 'DeliverTime',
			serveTime: 'ServeTime',
			cookingTime: 'CookingTime',
			remainingTime: 'RemainingTime',
			specialText: 'SpecialText'
		};

		// Типы статуса "Готовится"
		var completeStatusTypes = {
			deliverTime: 'DeliverTime',
			serveTime: 'ServeTime',
			cookingTime: 'CookingTime',
			remainingTime: 'RemainingTime',
			specialText: 'SpecialText'
		};

		// Статусы доставки
		var deliveryStatus = {
			unconfirmed: 0,
			new: 1,
			waiting: 2,
			onWay: 3,
			delivered: 4,
			closed: 5,
			cancelled: 6
		};

		// Статусы блюд базовых заказов
		var orderItemStatus = {
			added: 0,
			printedNotCooking: 1,
			cookingStarted: 2,
			cookingCompleted: 3,
			served: 4
		};

		// Названия типов заказов (для обратной совместимости)
		var orderTypes = {
			courier: 'Доставка курьером',
			pickup: 'Доставка самовывоз'
		};

		// Типы заказов
		var orderServiceTypes = {
			common: 0,
			deliveryByCourier: 1,
			deliveryByClient: 2
		};

		// Состояние заказа по информации KDS
		var orderStates = {
			QUEUED: 0, // Ожидает
			READY_FOR_COOKING: 1, // Пора готовить
			COOKING_4: 2, // Сборка
			COMPLETED: 3, // Выдача
			QUEUEDSERVED: 4 // Вручен
		}

		var cookingCompletedOrdersIds = [];
		// Получить заказы
		function getOrders() {
			log('getOrders');
			log(config);
			return getKitchenOrders().then(function (kitchenOrders) {
				log('getKitchenOrders');
				log(kitchenOrders);
				kitchenOrders = setDefaultState(kitchenOrders);
				return setStateFromExternalData(kitchenOrders).then(function (el) {
					kitchenOrders = filterKitchenOrders(kitchenOrders);
					return setDateFromExternalData(kitchenOrders).then(function (kitchenOrders) {
						return getDeliveries().then(function (deliveries) {
							return getCommonOrders(kitchenOrders, deliveries).then(function (orders) {

								log('getCommonOrders');
								log(kitchenOrders);
								log(deliveries);
								log(orders);
								return {
									orders: orders.filter(filterOrder),
									kitchenOrders: kitchenOrders,
									deliveries: deliveries
								}
							});
						});
					});
				});
			}).then(function (res) {
				log('resultOrders');
				log(res);
				// Обработка заказов из текущей кассовой смены
				var resultOrders = res.orders.reverse().map(function (order) {
					order.kitchenOrder = findSpecialOrder(order.Url, res.kitchenOrders, 'BaseOrder');
					order.delivery = findSpecialOrder(order.Url, res.deliveries, 'Order');
					if (order.kitchenOrder)
						order.State = order.kitchenOrder.State;
					else order.State = -1;
					return order;
				});
				var clientUrls = resultOrders
					.map(function (order) {
						if (order.delivery && (!order.Customers || !order.Customers.length) && order.delivery.Client && !order.delivery.Client.includes("00000000-0000-0000-0000-000000000000")) {
							return order.delivery.Client;
						}
					})
					.unique()
					.filter(function (clientUrl) {
						return clientUrl;
					});
				log('clientUrls');
				log(clientUrls);
				var courierUrls = resultOrders
					.map(function (order) {
						if (order.delivery && order.delivery.Courier && !order.delivery.Courier.includes("00000000-0000-0000-0000-000000000000")) {
							return order.delivery.Courier;
						}
					})
					.unique()
					.filter(function (courierUrl) {
						return courierUrl;
					});
				log('courierUrls');
				log(courierUrls);

				var clientReqs = clientUrls.map(function (clientUrl) {
					return $http.get(clientUrl).then(function (res) {
						return res && res.data;
					});
				});
				var courierReqs = courierUrls.map(function (courierUrl) {
					return $http.get(courierUrl).then(function (res) {
						return res && res.data;
					});
				});
				var kitchenUrls = res.kitchenOrders
					.map(function (kitchenOrder) {
						if (kitchenOrder.BaseOrder && !kitchenOrder.BaseOrder.includes("00000000-0000-0000-0000-000000000000")) {
							return kitchenOrder.BaseOrder;
						}
					})
					.unique()
					.filter(function (kitchenOrderUrl) {
						return kitchenOrderUrl;
					});
				log('kitchenUrls');
				log(kitchenUrls);
				var kitchenReqs = kitchenUrls.map(function (kitchenOrderUrl) {
					try {
						return getCommonOrder(kitchenOrderUrl).then(function (res) {
							return res;
						}).catch(() => false);
					} catch (e) {
						return Promise.resolve();
					}
				}).filter(el => el && el.Url);
				return $q.all(clientReqs).then(function (clients) {
					log('clientReqs');
					log(clients);
					return $q.all(courierReqs).then(function (couriers) {
						log('courierReqs');
						log(couriers);
						return $q.all(kitchenReqs).then(function (kitchenOrds) {
							log('kitchenReqs');
							log(kitchenOrds);
							resultOrders = resultOrders
								.map(function (order) {
									if (order.delivery) {
										if (order.delivery.Client) {
											order.delivery.ClientData = clients.filter(function (client) {
												return client && (client.url === order.delivery.Client || client.Url === order.delivery.Client);
											})[0];
										}
										if (order.delivery.Courier) {
											order.delivery.CourierData = couriers.filter(function (courier) {
												return courier && (courier.url === order.delivery.Courier || courier.Url === order.delivery.Courier);
											})[0];
										}
									}
									return order;
								})
								// Обработка кухонных заказов, которые остались с прошлых кассовых смен
								.concat(res.kitchenOrders.reverse().map(function (kitchenOrder) {
									log("findSpecialOrder");
									var order = findSpecialOrder(kitchenOrder.BaseOrder, kitchenOrds, 'Url');
									if (!!order) {
										if (!filterOrder(order)) {
											log("filterOrder false " + kitchenOrder.Number);
											return false;
										}
										return {
											Number: kitchenOrder.Number,
											TableNum: kitchenOrder.TableNum,
											OrderType: kitchenOrder.OrderType,
											OrderServiceType: kitchenOrder.OrderServiceType,
											kitchenOrder: kitchenOrder,
											State: kitchenOrder.State,
											ExternalNumber: order.externalNumber,
											OriginName: order.originName
										};
									} else {
										if (!config.showKitchenOrderFromTerminalGroup)
											return false;

										return {
											Number: kitchenOrder.Number,
											TableNum: kitchenOrder.TableNum,
											OrderType: kitchenOrder.OrderType,
											OrderServiceType: kitchenOrder.OrderServiceType,
											kitchenOrder: kitchenOrder,
											State: kitchenOrder.State,
											ExternalNumber: "",
											OriginName: ""
										};
									}
								}))
								.map(getResultOrder)
								.filter(function (resultOrder) { return resultOrder; })
								.sort(compareOrders);
							if (config.completedColumnSortType > 0 || config.cookingColumnSortType > 0) {
								
								let completedData = resultOrders.filter(el => el.completed);
								let notCompletedData = resultOrders.filter(el => !el.completed);
								completedData = completedDataSort(completedData);
								notCompletedData = notCompletedDataSort(notCompletedData);
								resultOrders = notCompletedData.concat(completedData);
							}

							log('resultOrders 2');
							log(resultOrders);
							// Количество приготовленных заказов
							resultOrders.lengthCompleted = resultOrders.reduce(function (sum, resultOrder) {
								return sum + resultOrder.completed;
							}, 0);
							return resultOrders;
						})
					})
				})
			});
		}
		// Получить сформированный рузультат для вывода на экран
		function getResultOrder(order) {
			if (!order.kitchenOrder && !order.delivery) { log("kitchenOrder or delivery " + order.Number); return; }
			if (!checkOrderType(order)) { log("checkOrderType " + order.Number); return; }
			if (!checkTable(order)) { log("checkTable " + order.Number); return; }
			if (!checkSource(order)) { log("checkSource " + order.Number); return; }

			if (!filterOrder(order)) { log("filterOrder " + order.Number); return; }

			var latestOrderItem = getOrderItem(order, true);
			if (!latestOrderItem) { log("latestOrderItem " + order.Number + (isKDSScheme || !order.IsDeliveryOrder) ); return; }
			var firstOrderItem = getOrderItem(order,false);

			var resultOrder = {
				number: getOrderNumber(order),
				num: order.Number,
				State: order.State,
				tableNum: order.TableNum,
				guestName: (order.Customers && order.Customers.length && (order.Customers[0].Name || (order.Customers[0].Client && order.Customers[0].Client.Name))) || latestOrderItem.guestName,
				existsSpecialProduct: latestOrderItem.existsSpecialProduct,
				get status() {
					return getStatus(latestOrderItem);
				},
				completed: latestOrderItem.completed,
				sended: false,
				showTime: order.kitchenOrder?.showTime ?? firstOrderItem.start,
				completedTime: order.kitchenOrder?.completedTime ?? latestOrderItem.finish,
				cookingStart: latestOrderItem.start,
				cookingFinish: latestOrderItem.finish,
				cookingTime: latestOrderItem.cookingTime,
				get remainingTime() {
					return latestOrderItem.remainingTime;
				},
				get remainingIsExpired() {
					return latestOrderItem.remainingIsExpired;
				},
				remainingIsExpiredImmediately: latestOrderItem.remainingIsExpiredImmediately
			};

			// Доставочный заказ имеет дополнительные поля
			var dlvr = order.delivery;
			if (dlvr) {
				resultOrder.sended = !!dlvr.SendTime;
				resultOrder.guestName = resultOrder.guestName || dlvr.ClientData && dlvr.ClientData.Name;
				resultOrder.guestPhone = dlvr.Phone && getPhoneFormat(dlvr.Phone);
				resultOrder.guestPhoneFull = dlvr.Phone;
				resultOrder.courierName = dlvr.CourierData && dlvr.CourierData.Name;
				resultOrder.deliverTime = dlvr.ExpectedDeliverTime && new Date(dlvr.ExpectedDeliverTime);
				resultOrder.sendTime = dlvr.SendTime && new Date(dlvr.SendTime);

				var onWayTime = Date.now() - new Date(dlvr.SendTime);
				if (onWayTime < 0) {
					onWayTime = 0;
				}
				resultOrder.onWayTime = dlvr.SendTime && getTimeSpan(onWayTime);
			}
			return resultOrder;
		}

		// Получить номер заказа, отформатированный согласно заданной конфигурации
		function getOrderNumber(order) {
			var orderNumber = order.Number;
			if (config.orderNumberEncryptAlgorithm > 0)
				orderNumber = GetEncryptedOrderNumber(order);

			orderNumber = orderNumber + '';

			if (config.externalOrderDisplaySettings && config.externalOrderDisplaySettings.active === true && order.ExternalNumber && order.OriginName) {
				var source = config.externalOrderDisplaySettings.sources.find((el) => el.name == order.OriginName);
				if (source) {
					orderNumber = pad(order.ExternalNumber, +source.displayOrderNumberLength, source.leadOrderNumberSymbol);
					orderNumber = source.displayShortName + orderNumber;
				} else if (config.externalOrderDisplaySettings.defaultDisplayOrderNumberLength > 0) {
					orderNumber = pad(order.ExternalNumber, +config.externalOrderDisplaySettings.defaultDisplayOrderNumberLength, config.externalOrderDisplaySettings.defaultLeadOrderNumberSymbol);
				}
			} else {
				orderNumber = pad(orderNumber, +config.orderNumberLength, config.leadOrderNumberSymbol);
			}
			return orderNumber;
		}

		function GetEncryptedOrderNumber(order) {
			if (config.orderNumberEncryptAlgorithm == 1) {
				var letters = ['E', 'T', 'P', 'A', 'H', 'K', 'X', 'C', 'B', 'Y'];
				var fullNumber = pad(order.Number.toString().slice(-3), 3, '0');
				var letter = letters[order.Number % letters.length];
				var num1 = (fullNumber[2] * 2 + fullNumber[1] * 6 + fullNumber[0] * 3 + 1) % 10;
				var num2 = (fullNumber[2] * 1 + fullNumber[1] * 7 + fullNumber[0] * 2 + 1) % 10;
				return letter + num1 + num2;
			} else if (config.orderNumberEncryptAlgorithm == 2) {
				if (order.delivery && order.delivery.Comment) {
					var matchResult = order.delivery.Comment.match(/(?<=[#])[^.]+(?=[.])/);
					if (matchResult && matchResult.length > 0) {
						return matchResult[0];
					}
				}
			}
		}

		function filterOrder(order) {
			if (!!order) {
				if (config.showOrderWithTab) {
					return true;
				}

				return order.TabName === null || order.TabName === undefined;
			}
			return false;
		}

		// Получить базовые заказы
		function getCommonOrders(kitchenOrders, deliveries) {
			var ordersReqs = kitchenOrders.map(function (kitchenOrder) { return kitchenOrder.BaseOrder; })
				.concat(deliveries.map(function (delivery) { return delivery.Order; }))
				.filter(function (orderUrl) { return !!orderUrl; })
				.filter(function (orderUrl) { return !orderUrl.includes("00000000-0000-0000-0000-000000000000"); })
				.unique().map(getCommonOrder);
			return $q.all(ordersReqs);
		}

		// Получить базовый заказ
		function getCommonOrder(orderUrl) {
			try {
				return $http.get(orderUrl)
					.then(function (res) {
						return res && res.data;
					}).catch(function () {
						return false;
					});
			} catch (er) {
				return Promise.resolve();
			}
		}

		// Получить кухонные заказы
		function getKitchenOrders() {
			return $http.get(config.webServiceUrl + '/api/kitchenorders')
				.then(function (res) {
					return res;
				});
		}

		function setDefaultState(kitchenOrders) {
			if (config.useOrderStatus && kitchenOrders) {
				if (cookingCompletedOrdersIds.length > 1000)
					cookingCompletedOrdersIds = cookingCompletedOrdersIds.slice(-500);

				kitchenOrders.data = kitchenOrders.data.map((el) => {
					if (cookingCompletedOrdersIds.includes(el.Id)) {
						el.State = orderStates.QUEUEDSERVED;
					} else {
						el.State = -1;
					}
					return el;
				});
			}
			return kitchenOrders;
		}

		function setStateFromExternalData(kitchenOrders) {
			var res = kitchenOrders.data.filter((el) => {
				return el.State !== orderStates.QUEUEDSERVED;
			}).map(setkitchenOrderExternalData);
			return $q.all(res);
		}

		function setDateFromExternalData(kitchenOrders) {
			if (config.cookingColumnSortType > 0 || config.completedColumnSortType > 0) {
				log(kitchenOrders);
				var res = kitchenOrders.filter((el) => {
					return el.State !== orderStates.QUEUEDSERVED;
				}).map(setDateKitchenOrderExternalData);
				return $q.all(res);
			}
			return Promise.resolve(kitchenOrders);
		}
		function setDateKitchenOrderExternalData(el) {
			return getDateFromExternalData(el).then(res =>
			{
				log(el.Id);
				log(res);
				if (res["states"]?.length > 0) {
					el.showTime = res["states"].find(x => x.state == config.showOrderStatus)?.changed
						?? res["states"].find(x => x.state > config.showOrderStatus)?.changed
						?? res["states"].slice(-1)[0]?.changed;

					el.completedTime = res["states"].find(x => x.state == config.completeOrderStatus)?.changed
						?? res["states"].find(x => x.state > config.completeOrderStatus)?.changed
						?? res["states"].slice(-1)[0]?.changed;
				}
				return el;
			});
		}

		function getDateFromExternalData(el) {
			return $http.get(config.webServiceUrl + '/api/externalData/?kitchenOrderId=' + el.Id + '&externalDataKey=Resto.Front.Api.KDSBalancePlugin.States')
				.then(function (res2) { return JSON.parse(res2.data); })
				.catch(function () {
					return Promise.resolve({ state: 4, deleted: true });
				});
		}

		function filterKitchenOrders(kitchenOrders) {
			return kitchenOrders.data.filter(function (kitchenOrder) {
				return kitchenOrder.Items.some(function (orderItem) {
					return filterOrderItem(kitchenOrder, orderItem)
						|| (orderItem.Modifiers && orderItem.Modifiers.some(function (modifier) {
							return filterOrderItem(kitchenOrder, modifier)
						}));
				});
			});
		}
		// Получить kitchenOrderExternalData и установить state заказа
		function setkitchenOrderExternalData(el) {
			if (!config.useOrderStatus)
				return Promise.resolve(el);

			return getkitchenOrderExternalData(el)
				.then(function (res2) {
					log(el.Id);
					log(res2);
					if (res2) {
						var kitchenExternalData = res2;
						if (kitchenExternalData && kitchenExternalData.state !== undefined) {
							let state = kitchenExternalData.state;
							log(kitchenExternalData.deleted === true);
							log(kitchenExternalData.clear === true);
							if (kitchenExternalData.deleted === true || kitchenExternalData.clear === true)
								state = orderStates.QUEUEDSERVED;
							el.State = state;
							if (state >= orderStates.QUEUEDSERVED)
								cookingCompletedOrdersIds.push(el.Id);
							return el;
						}
					}
				});
		}

		function getkitchenOrderExternalData(el) {
			return $http.get(config.webServiceUrl + '/api/externalData/?kitchenOrderId=' + el.Id + '&externalDataKey=Resto.Front.Api.KDSBalancePlugin.Data')
				.then(function (res2) { return JSON.parse(res2.data); })
				.catch(function () {
					return Promise.resolve({ state: 4, deleted: true });
				})
		}

		// Получить доставочные заказы
		function getDeliveries() {
			// Если в конфиге не выбрана доставка самовывозом либо доставка курьером,
			//   то грузить список доставок не имеет смысла.
			if (!isPickupType && !isCourierType) {
				return $q.resolve([]);
			}
			return $http.get(config.webServiceUrl + '/api/deliveries').then(function (res) {
				return res.data.filter(filterDelivery);
			});
		}

		// Отфильтровать доставочные заказы
		function filterDelivery(delivery) {
			return deliveryStatus.unconfirmed <= delivery.Status && delivery.Status <= deliveryStatus.onWay &&
				(!config.hideIfDeliveryTimeIsLaterThanMinutes || !delivery.ExpectedDeliverTime ||
					new Date(delivery.ExpectedDeliverTime) < (Date.now() + config.hideIfDeliveryTimeIsLaterThanMinutes * 60000));
		}

		// Поиск кухонного или доставочного заказа по базовому заказу
		function findSpecialOrder(orderUrl, specialOrders, orderProp) {
			var specialOrder = specialOrders.filter(function (specialOrder) {
				return specialOrder[orderProp] == orderUrl;
			})[0];
			var specialOrderPos = specialOrders.indexOf(specialOrder);
			if (specialOrderPos >= 0) {
				specialOrders.splice(specialOrderPos, 1);
			}
			return specialOrder;
		}

		// Фильтрация по типу заказа
		function checkOrderType(order) {
			// Для обратной совместимости
			if (order.OrderServiceType === undefined) {
				return isOrdinaryType && (order.OrderType != orderTypes.pickup && order.OrderType != orderTypes.courier)
					|| isPickupType && order.OrderType == orderTypes.pickup
					|| isCourierType && order.OrderType == orderTypes.courier;
			}

			return (isOrdinaryType && order.OrderServiceType == orderServiceTypes.common
				|| isPickupType && order.OrderServiceType == orderServiceTypes.deliveryByClient
				|| isCourierType && order.OrderServiceType == orderServiceTypes.deliveryByCourier)
				&& (!config.orderTypeNames
					|| !config.orderTypeNames.length
					|| config.orderTypeNames.indexOf(order.OrderType) >= 0);
		}

		// Фильтрация по столу
		function checkTable(order) {
			return (!showTables.length || showTables.some(function (tableNum) { return tableNum == order.TableNum; }))
				&& (!hideTables.length || !hideTables.some(function (tableNum) { return tableNum == order.TableNum; }));
		}

		// Фильтрация по источнику
		function checkSource(order) {
			if (!!order.OriginName) {
				return (!showOrderSources.length || showOrderSources.some(function (orderSource) { return orderSource.toLowerCase() == order.OriginName.toLowerCase(); }))
					&& (!hideOrderSources.length || !hideOrderSources.some(function (orderSource) { return orderSource.toLowerCase() == order.OriginName.toLowerCase(); }));
			}

			return true;
		}

		// Получение последнего блюда в заказе
		function getOrderItem(order, latest) {

			var orderItems = isKDSScheme || !order.IsDeliveryOrder ?
				(order.kitchenOrder ? order.kitchenOrder.Items : []) :
				order.Guests.reduce(function (allItems, guest) { return allItems.concat(guest.Items); }, []);
			orderItems = orderItems
				.filter(function (orderItem) {
					return filterOrderItem(order, orderItem) ||
						(orderItem.Modifiers && orderItem.Modifiers.some(function (modifier) { filterOrderItem(order, modifier); }));
				})
				.map(function (orderItem) {
					return createOrderItem(order, orderItem, orderItems);
				})
				.sort(compareOrderItems);

			var orderItem = latest ? orderItems[0] : orderItems.slice(-1)[0];
			if (orderItem) {
				orderItem.order = order;
				orderItem.guestName = getKitchenOrderGuestName(orderItems);
				orderItem.existsSpecialProduct = existsSpecialProduct(orderItems);
			}
			return orderItem;
		}

		// Получение имени гостя для кухонного заказа
		function getKitchenOrderGuestName(orderItems) {
			var guestNameFromOrderItem;
			if (config.guestNameProductNumber) {
				guestNameFromOrderItem = orderItems
					.filter(function (x) { return x.Product && x.Product.split('/').reverse()[0] == config.guestNameProductNumber; })
					.map(function (x) { return x.Comment; })[0];
			}
			return guestNameFromOrderItem || config.guestNameDefault || '';
		}

		// Проверить, содержится ли в имени блюд заказа специальное слово из настроек (specialProductName)
		function existsSpecialProduct(orderItems) {
			if (specialProductNames.length) {
				return orderItems
					.some(function (x) {
						return specialProductNames.some(function (y) {
							return (x.ProductName && x.ProductName.indexOf(y) >= 0) || (x.Name && x.Name.indexOf(y) >= 0);
						});
					});
			}
			return false;
		}

		// Отфильтровать блюда в заказе по статусам
		//   (применяется только для не доставочных заказов)
		function filterOrderItem(order, orderItem) {
			if (orderItem.Deleted)
				return false;

			if (config.useOrderStatus && order.State !== undefined) {
				return +config.showOrderStatus <= order.State &&
					order.State < +config.hideOrderStatus &&
					!isCompleteStatusTimeout(order, orderItem);
			}

			if (orderItem.ProcessingStatus === undefined) {
				return orderItemStatus.cookingStarted <= orderItem.Status &&
					(isCourierType || orderItem.Status < orderItemStatus.served)
					&&
					!isCompleteStatusTimeout(order, orderItem);
			}

			return config.showStatus <= orderItem.ProcessingStatus &&
				orderItem.ProcessingStatus < config.hideStatus &&
				!isCompleteStatusTimeout(order, orderItem);
		}

		// Получения признака, что заказ готов либо он пометился как готовый по таймауту
		function isCompleteStatusTimeout(order, orderItem) {
			if (!isCompleted(order, orderItem)) {
				return false;
			}
			if (!+config.completeStatusTimeout) {
				return false;
			}
			if (!orderItem[completeTimeProperty]) {
				return false;
			}
			var completeTime = new Date(orderItem[completeTimeProperty]);
			var timeout = +config.completeStatusTimeout * 1000;
			return (+completeTime + timeout) < Date.now();
		}

		// Получения признака, что заказ готов
		function isCompleted(order, orderItem) {
			if (config.useOrderStatus && order.State !== undefined)
				return order.State >= +config.completeOrderStatus;

			return orderItem.ProcessingStatus === undefined ?
				orderItem.Status >= orderItemStatus.cookingCompleted :
				orderItem.ProcessingStatus >= config.completeStatus;
		}

		// Конвертация блюд заказа
		function createOrderItem(order, orderItem, orderItems) {
			var result = {
				deleted: orderItem.Deleted,
				completed: isCompleted(order, orderItem)
			};

			var cookingStartTime = orderItem.EstimatedCookingBeginTime === undefined ?
				orderItem.CookingStartTime : orderItem.EstimatedCookingBeginTime;
			var cookingTimeStr = orderItem.CookingTime;

			// Сценарий одновременной подачи блюд
			if (orderItem.EstimatedCookingBeginTime === null) {
				var latestOrderItemInServeGroup = getLatestOrderItemInServeGroup(orderItems);
				if (latestOrderItemInServeGroup) {
					cookingStartTime = latestOrderItemInServeGroup.EstimatedCookingBeginTime;
					cookingTimeStr = latestOrderItemInServeGroup.CookingTime;
				}
			}

			if (cookingStartTime) {
				var start = new Date(cookingStartTime);
				var cookingTime = 0;
				if (cookingTimeStr) {
					var spl = cookingTimeStr.split(':');
					cookingTime = (+spl[0] * 360 + +spl[1] * 60 + +spl[2]) * 1000;
				}
				var finish = new Date(+start + cookingTime);

				result.start = start;
				result.finish = finish;
				result.cookingTime = getTimeSpan(cookingTime);
				Object.defineProperty(result, 'remainingTime', {
					get: function () {
						var remainingTime = getRemainingTime(orderItem, finish);
						return getTimeSpan(Math.abs(remainingTime));
					}
				});
				Object.defineProperty(result, 'remainingIsExpired', {
					get: function () {
						var remainingTime = getRemainingTime(orderItem, finish);
						return remainingTime < -60000;
					}
				});
				Object.defineProperty(result, 'remainingIsExpiredImmediately', {
					get: function () {
						var remainingTime = getRemainingTime(orderItem, finish);
						return remainingTime < 0;
					}
				});
			}
			return result;
		}

		// Получить блюдо с максимальным предполагаемым временем начала приготовления в сценарии одновременной подачи блюд
		function getLatestOrderItemInServeGroup(orderItems) {
			return orderItems.filter(function (orderItem) {
				return orderItem.ServeGroupNumber === orderItem.ServeGroupNumber && orderItem.EstimatedCookingBeginTime;
			})[0];
		}

		// Получить оставшееся время готовки
		function getRemainingTime(orderItem, finish) {
			var completeTime = orderItem[completeTimeProperty] || orderItem.CookingFinishTime;
			return finish - (completeTime ? new Date(completeTime) : Date.now());
		}

		// Получить временной промежуток
		//   (метод возвращает Date равный 01.01.2000 с
		//   прибавленным к нему временем в миллисекундах)
		function getTimeSpan(milliseconds) {
			var date = new Date(2000, 0);
			date.setTime(+date + milliseconds);
			return date;
		}

		// Сравнить блюда в заказе
		//   (deleted asc, completed asc, finish desc)
		function compareOrderItems(a, b) {
			return a.deleted - b.deleted || a.completed - b.completed || !!b.finish - !!a.finish || b.finish - a.finish;
		}

		// Получить статус заказа
		//   (вычисляется по последнему блюду в заказе)
		function getStatus(orderItem) {
			if (orderItem.completed) {
				// Проверка необходима для обратной совместимости с конфигом, когда в настройке completeStatusName прописывалась просто строка
				if (config.completeStatusName && config.completeStatusName.format && config.completeStatusName.type) {
					var format = config.completeStatusName.format;
					switch (config.completeStatusName.type) {
						case completeStatusTypes.remainingTime:
							return getRemainingTimeStatus(orderItem, format);
						case completeStatusTypes.specialText:
							return format;
						default:
							return '';
					}
				}
				return config.completeStatusName;
			} else {
				var format = config.inProgressStatusName.format;
				switch (config.inProgressStatusName.type) {
					case inProgressStatusTypes.serveTime:
						return $filter('date')(orderItem.finish, format);
					case inProgressStatusTypes.cookingTime:
						return $filter('date')(orderItem.cookingTime, format);
					case inProgressStatusTypes.remainingTime:
						return getRemainingTimeStatus(orderItem, format);
					case inProgressStatusTypes.specialText:
						return format;
					case inProgressStatusTypes.deliverTime:
						return orderItem.order.delivery && orderItem.order.delivery.ExpectedDeliverTime
							? $filter('date')(new Date(orderItem.order.delivery.ExpectedDeliverTime), format) : '';
					default:
						return '';
				}
			}
		}

		// Получить оставшееся время готовки для отображения
		function getRemainingTimeStatus(orderItem, format) {
			if (orderItem.remainingTime == undefined) {
				return '';
			}
			var waiting = +orderItem.remainingTime;
			// Оставшееся время готовки не должно уходить в минус
			if (orderItem.remainingIsExpiredImmediately || waiting < 0) {
				waiting = 0;
			}
			return $filter('date')(getTimeSpan(waiting), format);
		}

		// Получить номер телефона в формате *N-NN
		function getPhoneFormat(phone) {
			return '*' + phone.slice(-3, -2) + '-' + phone.slice(-2);
		}

		// Сравнить заказы
		//   (completed desc, deliverTime asc, number asc)
		function compareOrders(a, b) {
			return b.completed - a.completed || a.deliverTime - b.deliverTime || a.num - b.num;
		}

		function completedDataSort(data) {
			if (config.completedColumnSortType == 1) // Готов сортировать заказы по времени перехода в статус для отображения в этой зоне по возрастанию ("новый" заказ отображается в конце списка)
				return data.sort(dynamicSort("completedTime"));

			if (config.completedColumnSortType == 2)
				return data.sort(dynamicSort("-completedTime")); // Готов сортировать заказы по времени перехода в статус для отображения в этой зоне по убыванию  ("новый" заказ отображается в начале списка)

			return data;
		}

		function notCompletedDataSort(data) {
			if (config.cookingColumnSortType == 1) // зоне Готовится сортировать заказы по времени перехода в статус для отображения в этой зоне по возрастанию ("новый" заказ отображается в конце списка)
				return data.sort(dynamicSort("showTime"));

			if (config.cookingColumnSortType == 2)
				return data.sort(dynamicSort("-showTime")); // зоне Готовится сортировать заказы по времени перехода в статус для отображения в этой зоне по возрастанию ("новый" заказ отображается в конце списка)

			return data;
		}

		function dynamicSort(property) {
			var sortOrder = 1;
			if (property[0] === "-") {
				sortOrder = -1;
				property = property.substr(1);
			}
			return function (a, b) {
				var result = (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0;
				return result * sortOrder;
			}
		}

		// Обновить список готовых заказов, которые будут
		//   выводиться во всплывающем окне
		function updateSplashOrders(splashOrders, orders) {
			var completedOrders = orders.filter(function (order) {
				return order.completed;
			});
			for (var i = splashOrders.length - 1; i >= 0; i--) {
				var isServed = completedOrders.every(function (order) {
					return order.number != splashOrders[i].number
				});
				if (splashOrders[i].isShown && isServed) {
					splashOrders.splice(i, 1);
				}
			};
			completedOrders.forEach(function (order) {
				var isNewCompletedOrder = splashOrders.every(function (splashOrder) {
					return splashOrder.number != order.number;
				});
				if (isNewCompletedOrder) {
					splashOrders.push(order);
				}
			});
		}

		// Получить заказ для вывода во всплывающем окне
		function getSplashOrder(splashOrders) {
			var order = splashOrders.filter(function (splashOrder) {
				return !splashOrder.isShown;
			})[0];
			if (order) {
				order.isShown = true;
				return order;
			}
		}

		// Дополнить строку лидирующими символами
		//   (если длина строки больше требуемой, то исходная строка
		//   обрезается слева до необходимой длины)
		function pad(value, length, symbol) {
			value = value + '';
			var result = value.substr(value.length - length);
			if (value.length < length) {
				result = new Array(length - value.length + 1).join(symbol) + value;
			}
			return result;
		}

		function log(value) {
			if (logInfo)
				console.log(JSON.stringify(value));
		}

		// Преобразовать строку в массив (разделитель - символ ',')
		function parseArray(value) {
			return !value ? [] : value.split(',').map(function (item) {
				return item.trim();
			});
		}

		// Получить только уникальные элементы массива
		Array.prototype.unique = function () {
			var u = {}, a = [];
			for (var i = 0, l = this.length; i < l; ++i) {
				if (u.hasOwnProperty(this[i])) {
					continue;
				}
				a.push(this[i]);
				u[this[i]] = 1;
			}
			return a;
		};

		return {
			getOrders: getOrders,
			updateSplashOrders: updateSplashOrders,
			getSplashOrder: getSplashOrder
		};
	}]);
