Ext.ns("Terrasoft.integration");
Ext.ns("Terrasoft.integration.telephony");
Ext.ns("Terrasoft.integration.telephony.finesse");

//region Class: FinesseCtiProvider

/**
 * The provider class to the Finesse service.
 */
Ext.define("Terrasoft.integration.telephony.finesse.FinesseCtiProvider", {
	extend: "Terrasoft.BaseCtiProvider",
	alternateClassName: "Terrasoft.FinesseCtiProvider",
	singleton: true,

	//region Properties: Private

	/**
  * Our phone number.
  * @private
  * @type {String}
  */
	deviceId: "",

	/**
  * The array of licenses required for integration.
  * @private
  * @type {String[]}
  */
	licInfoKeys: ["BPMonlineFinesseConnector.Use"],

	/**
  * The telephony service address on the application server.
  * @private
  * @type {String}
  */
	msgUtilServiceUrl: Terrasoft.workspaceBaseUrl + "/ServiceModel/MsgUtilService.svc/",

	/**
  * The name of the method of entering the user's telephony session.
  * @private
  * @type {String}
  */
	loginMethodName: "LogInMsgServer",

	/**
  * The name of the method for storing information about the call.
  * @private
  * @type {String}
  */
	updateCallMethodName: "UpdateCall",

	/**
  * Cisco Finesse server domain.
  * @private
  * @type {String}
  */
	domain: "",

	/**
  * ID of the user agent.
  * @private
  * @type {String}
  */
	agentId: "",

	/**
  * user password.
  * @private
  * @type {String}
  */
	password: "",

	/**
  * The reason for the "Not ready" status by default.
  * @private
  * @type {String}
  */
	defaultNotReadyReasonCode: "",

	/**
  * The object of integration with Cisco Finesse via BOSH connection.
  * @private
  * @type {Object}
  */
	boshClient: null,

	/**
  * A flag that the Finesse user is detected by the jabber connection of Bosh.
  * @type {Boolean}
  */
	isBoshPresenceInit: false,

	/**
  * Virtual directory for url redirect to Finesse server.
  * @private
  * @type {String}
  */
	finessePath: "/finesse",

	/**
  * The path to Finesse web services.
  * @private
  * @type {String}
  */
	finesseApiPath: "/finesse/api",

	/**
  * The value of the Content-Type header for non-GET queries to Finesse.
  * @private
  * @type {String}
  */
	notGetFinesseRequestsContentType: "application/xml",

	/**
  * The value of the Accept header for non-GET requests to Finesse.
  * @private
  * @type {String}
  */
	notGetFinesseRequestsAccept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",

	/**
  * Message when there is no jabberwerx library.
  * @private
  * @type {String}
  */
	jabberLibNotFoundErrorMessage: "jabberwerx library not found.",

	/**
  * Message template for Finesse error.
  * @private
  * @type {String}
  */
	finesseErrorMessageTemplate: "Finesse error. Detail info: {0}",

	/**
  * The error code of the wrong BOSH authorization.
  * @private
  * @type {String}
  */
	jabberNotAuthorizedErrorCode: "not-authorized",

	/**
  * The name of the jabber client.
  * @private
  * @type {String}
  */
	jabberClientName: "cisco",

	/**
  * Actibve calls.
  * @private
  * @type {Terrasoft.Collection}
  */
	activeCalls: null,

	/**
  * Id of the active call.
  * @private
  * @type {String}
  */
	activeCallId: "",

	/**
  * User status.
  * @private
  * @type {String}
  */
	userState: "",

	/**
  * The code of the reason of user status.
  * @private
  * @type {String}
  */
	userStateReasonCode: "",

	/**
  * The last reason code for the user status is "Not ready".
  * @private
  * @type {String}
  */
	lastNotReadyStateReasonCode: "",

	/**
  * The phone number of the user who initiated the transfer of the call to the current agent.
  * @private
  * @type {String}
  */
	transferInitiator: "",

	/**
  * The types of calls for which information about the number is determined using the mediaProperties property.
  * @private
  * @type {Terrasoft.FinesseCallType[]}
  */
	callTypesWithMediaProperties: [Terrasoft.FinesseCallType.AGENT_INSIDE, Terrasoft.FinesseCallType.CONSULT],

	//endregion

	//region Methods: Private

	/*jshint camelcase: false */
	/**
  * Converts an xml string to a json object.
  * @param {String} xmlData Xml string for conversion.
  * @return {Object} Converted json object.
  */
	xmlToJson: function (xmlData) {
		var x2Js = new Ext.global.X2JS();
		return x2Js.xml_str2json(xmlData);
	},
	/*jshint camelcase: true */

	/*jshint camelcase: false */
	/**
  * Converts the json object to an xml string.
  * @param {Object} jsonObject Json-object for conversion.
  * @return {String} String xml.
  */
	jsonToXml: function (jsonObject) {
		var x2Js = new Ext.global.X2JS();
		return x2Js.json2xml_str(jsonObject);
	},
	/*jshint camelcase: true */

	/**
  * Send ajax request to Finesse server.
  * @private
  * @param {String} url Path to the Finesse method.
  * @param {String} method The http request method.
  * @param {Function} onSuccess (optional) Callback function on successful execution of the request.
  * @param {Function} onError (optional) Callback function if the request fails.
  * @param {Object} scope (optional) The context for executing the callback functions.
  * @param {Object} apiMethodConfig (optional) Json is the Api Finesse method object.
  */
	sendFinesseRequest: function (url, method, onSuccess, onError, scope, apiMethodConfig) {
		var finesseUrl = this.finesseApiPath + url;
		var credentials = Ext.global.jabberwerx.util.crypto.b64Encode(this.agentId + ":" + this.password);
		var requestOptions = {
			url: finesseUrl,
			method: method,
			headers: {
				Authorization: "Basic " + credentials
			},
			success: onSuccess,
			failure: onError,
			scope: scope
		};
		if (method !== "GET") {
			requestOptions.headers["Content-Type"] = this.notGetFinesseRequestsContentType;
			requestOptions.headers.Accept = this.notGetFinesseRequestsAccept;
		}
		if (apiMethodConfig) {
			requestOptions.xmlData = this.jsonToXml(apiMethodConfig);
		}
		Ext.Ajax.request(requestOptions);
	},

	/**
  * Method of opening a connection.
  * @private
  * @param {Object} config Connection parameters.
  */
	connect: function (config) {
		this.agentId = config.AgentID;
		this.password = config.Password;
		this.domain = config.Domain;
		this.deviceId = config.Extension;
		this.createBoshConnection();
	},

	/**
  * Creates a connection to receive events via a BOSH connection.
  * @private
  */
	createBoshConnection: function () {
		var jabberwerx = Ext.global.jabberwerx;
		if (Ext.isEmpty(jabberwerx)) {
			this.logError(this.jabberLibNotFoundErrorMessage);
			return;
		}
		var boshClient = this.boshClient;
		if (!boshClient) {
			boshClient = new jabberwerx.Client(this.jabberClientName);
			this.boshClient = boshClient;
			boshClient.Scope = this;
			jabberwerx._config.unsecureAllowed = true;
			var scope = this;
			boshClient.event("clientStatusChanged").bind(function (event) {
				scope.onBoshStatusChanged(event);
			});
			boshClient.event("presenceReceived").bind(function (event) {
				scope.onBoshPresenceReceived(event);
			});
			boshClient.event("messageReceived").bindWhen("event[xmlns=\"http://jabber.org/protocol/pubsub#event\"] items item notification", function (event) {
				scope.onFinesseEvent(event);
			});
		} else if (!boshClient.isConnected()) {
			boshClient.cancelReconnect();
			boshClient.disconnect();
		}
		var jabberId = this.agentId + "@" + this.domain;
		var boshParameters = {
			httpBindingURL: this.finessePath + "/http-bind",
			errorCallback: function (error) {
				scope.onBoshClientError(error);
			},
			successCallback: function (boshClient) {
				scope.onBoshClientConnected(boshClient);
			}
		};
		boshClient.connect(jabberId, this.password, boshParameters);
	},

	/**
  * It works when the connection status of BOSCH changes.
  * @param {Object} event The jabber event object.
  */
	onBoshStatusChanged: function (event) {
		switch (event.data.next) {
			case Terrasoft.BoshConnectionStatus.CONNECTED:
				this.log("Bosh client connected");
				this.checkToSendConnected();
				break;
			case Terrasoft.BoshConnectionStatus.DISCONNECTED:
				if (event.data.error) {
					this.logError("Bosh client disconnected. Reason: BOSH client error");
				} else {
					this.log("Bosh client disconnected successfully");
				}
				this.logoutFinesse();
				break;
			default:
				break;
		}
	},

	/**
  * Activates when a user enters the jabber channel.
  * @param {Object} event The jabber event object.
  */
	onBoshPresenceReceived: function (event) {
		this.log("Jabber presence received.");
		var presence = event.data;
		var from = presence.getFrom();
		if (!this.isBoshPresenceInit) {
			var index = from.indexOf("@");
			if (index > 0) {
				var user = from.substr(0, index);
				if (user === "finesse") {
					this.isBoshPresenceInit = true;
				}
			}
			this.checkToSendConnected();
		}
	},

	/**
  * Verifies that the bosh jabber client is connected and the user Finesse has been connected. If so, it sends an event about the successful connection to Finesse.
  */
	checkToSendConnected: function () {
		if (this.boshClient.isConnected() && this.isBoshPresenceInit) {
			this.loginFinesse();
		}
	},

	/**
  * The method of entering the user's telephony session.
  * @private
  * @param {String} url address of the LogInMsgServer method on the application server.
  * @param {Object} jsonData parameters for logging into the session.
  */
	loginMsgService: function (url, jsonData) {
		Terrasoft.AjaxProvider.request({
			url: url,
			scope: this,
			callback: this.onLoginMsgService,
			jsonData: jsonData
		});
	},

	/**
  * Authorizes the user on the Finesse server.
  */
	loginFinesse: function () {
		var url = "/User/" + this.agentId;
		var apiMethodConfig = {
			User: {
				extension: this.deviceId,
				state: Terrasoft.FinesseAgentState.LOGIN
			}
		};
		this.sendFinesseRequest(url, "PUT", function () {
			this.fireEvent("initialized");
		}, function (response) {
			var message = Ext.String.format("onSignInErrorHandler({0})", Terrasoft.encode(response.responseText));
			this.fireEvent("rawMessage", message);
		}, this, apiMethodConfig);
	},

	/**
  * Exits from Finesse.
  */
	logoutFinesse: function () {
		this.setFinesseAgentState(Terrasoft.FinesseAgentState.LOGOUT, "", function (response) {
			var disconnectReason = response ? response.responseText : "";
			this.fireEvent("disconnected", disconnectReason);
		});
	},

	/**
  * Changes the user state of Finesse.
  * @param {String} stateCode The code for the new state.
  * @param {String} reasonCode The reason code for the state.
  * @param {Function} callback (optional) Callback function.
  */
	setFinesseAgentState: function (stateCode, reasonCode, callback) {
		var url = "/User/" + this.agentId;
		var apiMethodConfig = {
			User: {
				state: stateCode,
				reasonCodeId: reasonCode
			}
		};
		this.sendFinesseRequest(url, "PUT", function (response) {
			if (response.status === Terrasoft.HttpStatusCode.ACCEPTED) {
				this.log("setUserState success");
				this.userStateChanged(stateCode, reasonCode);
			}
			if (Ext.isFunction(callback)) {
				callback.call(this, response);
			}
		}, function (response) {
			this.logError("setUserState error. Response = {0}", Terrasoft.encode(response.responseText));
			if (Ext.isFunction(callback)) {
				callback.call(this, response);
			}
		}, this, apiMethodConfig);
	},

	/**
  * Queries the status of the user Finesse.
  */
	getFinesseAgentState: function () {
		var url = "/User/" + this.agentId;
		this.sendFinesseRequest(url, "GET", function (response) {
			if (Ext.isEmpty(response.responseXML)) {
				return;
			}
			var xmlData = response.responseXML.xml;
			if (Ext.isEmpty(xmlData)) {
				return;
			}
			var userInfo = this.xmlToJson(xmlData);
			if (userInfo && userInfo.User && !Ext.isEmpty(userInfo.User.state)) {
				this.userStateChanged(userInfo.User.state);
			}
		}, function (response) {
			this.logError("getUserState error. Response = {0}", Terrasoft.encode(response.responseText));
		}, this);
	},

	/**
  * Make an outgoing Finesse call.
  * @param {String} targetAddress The number to which the call is made.
  * @param {Function} callback Callback function.
  */
	makeFinesseCall: function (targetAddress, callback) {
		var url = "/User/" + this.agentId + "/Dialogs";
		var apiMethodConfig = {
			Dialog: {
				requestedAction: Terrasoft.FinesseDialogAction.MAKE_CALL,
				toAddress: targetAddress,
				fromAddress: this.deviceId
			}
		};
		this.sendFinesseRequest(url, "POST", function (response) {
			this.log("Make call successful.");
			if (Ext.isFunction(callback)) {
				callback.call(this, response);
			}
		}, function (response) {
			this.logError("makeCall error. status = {0}", Terrasoft.encode(response.responseText));
			if (Ext.isFunction(callback)) {
				callback.call(this, response);
			}
		}, this, apiMethodConfig);
	},

	/**
  * Make a consultation call Finesse.
  * @param {String} callId Call id.
  * @param {String} targetAddress The number to which the call is made.
  * @param {Function} callback Callback function.
  */
	makeFinesseConsultCall: function (callId, targetAddress, callback) {
		var url = "/Dialog/" + callId;
		var requestedAction = Terrasoft.FinesseDialogAction.CONSULT_CALL;
		var apiMethodConfig = {
			Dialog: {
				requestedAction: requestedAction,
				toAddress: targetAddress,
				targetMediaAddress: this.deviceId
			}
		};
		this.sendFinesseRequest(url, "PUT", function (response) {
			this.log("{0} action succeed.", requestedAction);
			if (Ext.isFunction(callback)) {
				callback.call(this, response);
			}
		}, function (response) {
			this.logError("{0} action error. response: {1}.", requestedAction, Terrasoft.encode(response.responseText));
			if (Ext.isFunction(callback)) {
				callback.call(this, response);
			}
		}, this, apiMethodConfig);
	},

	/**
  * Make a quick transfer of the Finesse call.
  * @param {String} callId Call id.
  * @param {String} targetAddress The number to which the call is made.
  * @param {Function} callback Callback function.
  */
	makeBlindTransferCall: function (callId, targetAddress, callback) {
		var url = "/Dialog/" + callId;
		var requestedAction = Terrasoft.FinesseDialogAction.TRANSFER_SST;
		var apiMethodConfig = {
			Dialog: {
				requestedAction: requestedAction,
				toAddress: targetAddress,
				targetMediaAddress: this.deviceId
			}
		};
		this.sendFinesseRequest(url, "PUT", function (response) {
			this.log("{0} action succeed.", requestedAction);
			if (Ext.isFunction(callback)) {
				callback.call(this, response);
			}
		}, function (response) {
			this.logError("{0} action error. response: {1}.", requestedAction, Terrasoft.encode(response.responseText));
			if (Ext.isFunction(callback)) {
				callback.call(this, response);
			}
		}, this, apiMethodConfig);
	},

	/**
  * Performs the Finesse action for an active call.
  * @param {String} callId Call id.
  * @param {Terrasoft.FinesseDialogAction} requestedAction Finesse action.
  * @param {Function} callback Callback function.
  */
	takeFinesseAction: function (callId, requestedAction, callback) {
		var url = "/Dialog/" + callId;
		var apiMethodConfig = {
			Dialog: {
				requestedAction: requestedAction,
				targetMediaAddress: this.deviceId
			}
		};
		this.sendFinesseRequest(url, "PUT", function (response) {
			this.log("{0} action succeed.", requestedAction);
			if (Ext.isFunction(callback)) {
				callback.call(this, response);
			}
		}, function (response) {
			this.logError("{0} action error. response: {1}.", requestedAction, Terrasoft.encode(response.responseText));
			if (Ext.isFunction(callback)) {
				callback.call(this, response);
			}
		}, this, apiMethodConfig);
	},

	/**
  * Gets the available operation with a call from the possible Finesse actions.
  * @private
  * @param {Object} action The Finesse action.
  * @return {Terrasoft.CallFeaturesSet} Available operation.
  */
	getCallFeatureFromAction: function (action) {
		switch (action) {
			case Terrasoft.FinesseDialogAction.MAKE_CALL:
				return Terrasoft.CallFeaturesSet.CAN_DIAL;
			case Terrasoft.FinesseDialogAction.ANSWER:
				return Terrasoft.CallFeaturesSet.CAN_ANSWER;
			case Terrasoft.FinesseDialogAction.HOLD:
				return Terrasoft.CallFeaturesSet.CAN_HOLD;
			case Terrasoft.FinesseDialogAction.RETRIEVE:
				return Terrasoft.CallFeaturesSet.CAN_UNHOLD;
			case Terrasoft.FinesseDialogAction.DROP:
				return Terrasoft.CallFeaturesSet.CAN_DROP;
			case Terrasoft.FinesseDialogAction.SEND_DTMF:
				return Terrasoft.CallFeaturesSet.CAN_DTMF;
			case Terrasoft.FinesseDialogAction.CONSULT_CALL:
				return Terrasoft.CallFeaturesSet.CAN_MAKE_CONSULT_CALL;
			case Terrasoft.FinesseDialogAction.TRANSFER:
				return Terrasoft.CallFeaturesSet.CAN_COMPLETE_TRANSFER;
			case Terrasoft.FinesseDialogAction.TRANSFER_SST:
				return Terrasoft.CallFeaturesSet.CAN_BLIND_TRANSFER;
			default:
				return Terrasoft.CallFeaturesSet.CAN_NOTHING;
		}
	},

	/**
  * Receives the call status by the state of the call in Finesse.
  * @private
  * @param {Object} state Call status in Finesse.
  * @param {Object} stateCause The reason for the call status in Finesse.
  * @return {Terrasoft.GeneralizedCallState} Call status.
  */
	getCallStateFromFinesseState: function (state, stateCause) {
		var callState;
		switch (state) {
			case Terrasoft.FinesseDialogState.INITIATING:
				callState = Terrasoft.GeneralizedCallState.ALERTING;
				break;
			case Terrasoft.FinesseDialogState.INITIATED:
				callState = Terrasoft.GeneralizedCallState.ALERTING;
				break;
			case Terrasoft.FinesseDialogState.ALERTING:
				callState = Terrasoft.GeneralizedCallState.ALERTING;
				break;
			case Terrasoft.FinesseDialogState.ACTIVE:
				callState = Terrasoft.GeneralizedCallState.CONNECTED;
				break;
			case Terrasoft.FinesseDialogState.HELD:
				callState = Terrasoft.GeneralizedCallState.HOLDED;
				break;
			case Terrasoft.FinesseDialogState.FAILED:
				callState = stateCause === Terrasoft.FinesseDialogStateCause.BUSY ? Terrasoft.GeneralizedCallState.BUSY : Terrasoft.GeneralizedCallState.ALERTING;
				break;
			default:
				callState = Terrasoft.GeneralizedCallState.NONE;
		}
		return callState;
	},

	/**
  * Processes the Finesse dialog object.
  * @private
  * @param {Object} dialog The Finesse dialog object.
  * @return {Terrasoft.integration.telephony.Call} Gets a callback object or null if the Finesse dialog object should be ignored.
  */
	processFinesseDialog: function (dialog) {
		var activeCalls = this.activeCalls;
		var callId = dialog.id;
		var call = activeCalls.find(callId);
		if (!Ext.isEmpty(call)) {
			call.oldState = call.state;
		} else {
			if (dialog.state === Terrasoft.FinesseDialogState.DROPPED) {
				this.log(Terrasoft.Resources.Telephony.CommonMessages.FinesseDialogObjectIgnoredReasonDroppped);
				return null;
			}
			call = this.createNewCall(dialog);
			this.activeCalls.add(callId, call);
		}
		call.timeStamp = new Date();
		this.setCallParticipants(call, dialog);
		var oldCallFeaturesSet = call.callFeaturesSet;
		var callParticipants = Ext.isArray(dialog.participants.Participant) ? dialog.participants.Participant : [dialog.participants.Participant];
		var thisParticipantSearchResult = Terrasoft.findItem(callParticipants, { mediaAddress: this.deviceId });
		if (Ext.isEmpty(thisParticipantSearchResult)) {
			this.log(Terrasoft.Resources.Telephony.CommonMessages.FinesseDialogObjectIgnoredReasonParticipantMissing);
			return null;
		}
		call.callFeaturesSet = Terrasoft.CallFeaturesSet.CAN_NOTHING;
		var thisParticipant = thisParticipantSearchResult.item;
		var actions = thisParticipant.actions.action;
		if (!Ext.isEmpty(actions)) {
			actions = Ext.isArray(actions) ? actions : [actions];
			for (var i = 0; i < actions.length; i++) {
				/*jshint bitwise:false */
				call.callFeaturesSet |= this.getCallFeatureFromAction(actions[i]);
				/*jshint bitwise:true */
			}
		}
		if (oldCallFeaturesSet !== call.callFeaturesSet) {
			call.isCallFeaturesSetChanged = true;
		}
		var state = thisParticipant.state;
		var stateCause = thisParticipant.stateCause;
		call.state = this.getCallStateFromFinesseState(state, stateCause);
		if (state === Terrasoft.FinesseDialogState.FAILED) {
			call.stateCause = stateCause || "";
			this.userStateChanged(call.stateCause, null, call.callFeaturesSet);
		}
		if (state === Terrasoft.FinesseDialogState.DROPPED || state === Terrasoft.FinesseDialogState.WRAP_UP) {
			this.setRedirectionProperties(call, dialog);
			activeCalls.remove(call);
			if (activeCalls.isEmpty() && state === Terrasoft.FinesseDialogState.DROPPED) {
				/*jshint bitwise:false */
				call.callFeaturesSet |= Terrasoft.CallFeaturesSet.CAN_DIAL;
				/*jshint bitwise:true */
			}
		}
		return call;
	},

	/**
  * Creates a new call object.
  * @private
  * @param {Object} dialog The Finesse call object.
  * @return {Terrasoft.integration.telephony.Call} The call object.
  */
	createNewCall: function (dialog) {
		var callId = dialog.id;
		var call = Ext.create("Terrasoft.integration.telephony.Call");
		call.id = callId;
		call.deviceId = this.deviceId;
		call.ctiProvider = this;
		call.direction = this.getCallDirection(dialog);
		return call;
	},

	/**
  * Defines whether the call was distributed to the operator via the automated call system (ACD).
  * @private
  * @param {Terrasoft.integration.telephony.finesse.FinesseCallType} callType Ring type Finesse.
  * @return {Boolean}
  */
	isAcdIn: function (callType) {
		return callType === Terrasoft.FinesseCallType.ACD_IN || callType === Terrasoft.FinesseCallType.PREROUTE_ACD_IN;
	},

	/**
  * Returns the direction of the call.
  * @private
  * @param {Object} dialog The Finesse call object.
  * @return {Terrasoft.CallDirection} callDirection Direction of the call.
  */
	getCallDirection: function (dialog) {
		var mediaProperties = dialog.mediaProperties;
		var callType = mediaProperties.callType;
		if (this.isAcdIn(callType)) {
			return Terrasoft.CallDirection.IN;
		}
		var callDirection;
		if (callType !== Terrasoft.FinesseCallType.CONSULT_OFFERED && callType !== Terrasoft.FinesseCallType.TRANSFER) {
			callDirection = this.deviceId === dialog.toAddress || this.deviceId === mediaProperties.DNIS ? Terrasoft.CallDirection.IN : Terrasoft.CallDirection.OUT;
		} else {
			callDirection = Ext.isEmpty(dialog.toAddress) ? Terrasoft.CallDirection.OUT : Terrasoft.CallDirection.IN;
		}
		return callDirection;
	},

	/**
  * Sets who calls whom.
  * @private
  * @param {Terrasoft.integration.telephony.Call} call Object of the call.
  * @param {Object} dialog Object of the Finesse call.
  */
	setCallParticipants: function (call, dialog) {
		var oldCallInfo = {
			callerId: call.callerId,
			calledId: call.calledId
		};
		var mediaProperties = dialog.mediaProperties;
		var callType = mediaProperties.callType;
		if (Ext.isEmpty(call.callerId) || Ext.isEmpty(call.calledId)) {
			var isMediaPropertiesCalledId = callType === Terrasoft.FinesseCallType.OUT || this.callTypesWithMediaProperties.indexOf(callType) !== -1 && call.direction === Terrasoft.CallDirection.OUT;
			if (isMediaPropertiesCalledId) {
				call.callerId = this.deviceId;
				call.calledId = mediaProperties.dialedNumber;
			} else if (this.isAcdIn(callType)) {
				call.callerId = dialog.fromAddress;
				call.calledId = mediaProperties.DNIS || this.deviceId;
			} else if (callType !== Terrasoft.FinesseCallType.CONFERENCE) {
				call.callerId = dialog.fromAddress;
				call.calledId = dialog.toAddress;
			}
		}
		if (callType === Terrasoft.FinesseCallType.CONSULT_OFFERED || callType === Terrasoft.FinesseCallType.TRANSFER) {
			this.updateCallAfterConsultationFinished(call, dialog.participants.Participant);
		}
		call.isInfoChanged = oldCallInfo.callerId !== call.callerId || oldCallInfo.calledId !== call.calledId;
	},

	/**
  * Sets from whom and to whom the call was transferred.
  * @private
  * @param {Terrasoft.integration.telephony.Call} call The call object.
  * @param {Object} dialog The Finesse call object.
  */
	setRedirectionProperties: function (call, dialog) {
		var activeCalls = this.activeCalls;
		var mediaProperties = dialog.mediaProperties;
		var callType = mediaProperties.callType;
		if (callType === Terrasoft.FinesseCallType.TRANSFER && activeCalls.getCount() === 2) {
			var activeCallId = this.activeCallId;
			var isActiveCall = call.id === activeCallId;
			var currentCall = isActiveCall ? call : activeCalls.find(activeCallId);
			var consultCall = isActiveCall ? activeCalls.getByIndex(1) : call;
			if (currentCall && consultCall) {
				currentCall.redirectingId = consultCall.calledId;
				currentCall.redirectionId = consultCall.callerId;
			}
		}
	},

	/**
  * Updates the call data after the consultant calls the consultant.
  * @private
  * @param {Terrasoft.integration.telephony.Call} call The call object.
  * @param {Object []} participants An array of participants in the conversation.
  */
	updateCallAfterConsultationFinished: function (call, participants) {
		if (!Ext.isArray(participants) || participants.length !== 2) {
			return;
		}
		var deviceId = this.deviceId;
		var opponent = _.reject(participants, function (participant) {
			return participant.mediaAddress === deviceId;
		});
		var opponentNumber = call.direction === Terrasoft.CallDirection.OUT ? call.calledId : call.callerId;
		if (!Ext.isEmpty(opponent) && opponent.length === 1 && opponent[0].mediaAddress !== opponentNumber) {
			opponentNumber = opponent[0].mediaAddress;
			if (call.direction === Terrasoft.CallDirection.OUT) {
				call.calledId = opponentNumber;
			} else {
				call.callerId = opponentNumber;
			}
		}
	},

	/**
  * Generates a call message based on the status change.
  * @private
  * @param {Terrasoft.integration.telephony.Call} call Call.
  */
	fireCallEventByState: function (call) {
		var event;
		if (call.state !== call.oldState) {
			switch (call.state) {
				case Terrasoft.GeneralizedCallState.ALERTING:
					event = "callStarted";
					break;
				case Terrasoft.GeneralizedCallState.CONNECTED:
					event = "commutationStarted";
					break;
				case Terrasoft.GeneralizedCallState.HOLDED:
					event = "hold";
					break;
				case Terrasoft.GeneralizedCallState.NONE:
					event = "callFinished";
					break;
				default:
					break;
			}
		} else if (call.isInfoChanged) {
			event = "callInfoChanged";
		} else if (call.id === this.activeCallId && call.isCallFeaturesSetChanged) {
			this.fireLineStateChangedEvent(call.id, call.callFeaturesSet);
		}
		call.isInfoChanged = false;
		call.isCallFeaturesSetChanged = false;
		if (!Ext.isEmpty(event)) {
			this.fireCallEvent(call, event);
		}
	},

	/**
  * Sends a message about changing the status of the call.
  * @private
  * @param {Terrasoft.integration.telephony.Call} call Call.
  * @param {String} event Call event.
  */
	fireCallEvent: function (call, event) {
		this.fireEvent(event, call);
	},

	/**
  * Sends a message about the change of the line state.
  * @private
  * @param {String} callId Call id.
  * @param {Terrasoft.CallFeaturesSet} callFeaturesSet The set of available operations on the call.
  */
	fireLineStateChangedEvent: function (callId, callFeaturesSet) {
		this.fireEvent("lineStateChanged", {
			callId: callId,
			callFeaturesSet: callFeaturesSet
		});
	},

	/**
  * Processing of the event when the BOSH connection is successfully created.
  * @private
  * @param {Object} boshClient The BOSH connection object.
  */
	onBoshClientConnected: function (boshClient) {
		var scope = boshClient.Scope;
		scope.log("Bosh Client Connected");
		this.boshClient.sendPresence();
		scope.fireEvent("rawMessage", "Connected");
	},

	/**
  * Processing of the event when a BOSH connection error occurs.
  * @private
  * @param {Object} error The error object is BOSH.
  */
	onBoshClientError: function (error) {
		var errorXmlData = error.xml;
		var errorInfo = {
			internalErrorCode: errorXmlData,
			data: Terrasoft.Resources.Telephony.Exception.UnknownBoshException,
			source: "Finesse(Bosh)",
			errorType: Terrasoft.MsgErrorType.GENERAL_ERROR
		};
		if (!Ext.isEmpty(errorXmlData)) {
			var errorConfig = this.xmlToJson(errorXmlData);
			Terrasoft.each(errorConfig.error, function (item, index) {
				if (index === this.jabberNotAuthorizedErrorCode) {
					errorInfo.errorType = Terrasoft.MsgErrorType.AUTHENTICATION_ERROR;
					errorInfo.data = Terrasoft.Resources.Telephony.Exception.NotAuthorizedException;
				}
			}, this);
		}
		this.fireEvent("error", errorInfo);
	},

	/**
  * Processing of the event when an event occurs on the Cisco Finesse server side.
  * @private
  * @param {Object} event Connection object BOSH.
  */
	onFinesseEvent: function (event) {
		var eventTextContent = Ext.global.jQuery(event.selected).text();
		this.fireEvent("rawMessage", eventTextContent);
		var jsonEventContent = this.xmlToJson(eventTextContent);
		var eventContent = Terrasoft.decode(jsonEventContent);
		if (Ext.isEmpty(eventContent) || !eventContent.Update) {
			return;
		}
		var data = eventContent.Update.data;
		if (data.user && !Ext.isEmpty(data.user.state) && this.agentId === data.user.loginId) {
			this.userStateChanged(data.user.state, data.user.reasonCodeId);
		}
		if (!Ext.isEmpty(data.dialogs) || !Ext.isEmpty(data.dialog)) {
			this.dialogStateChanged(eventContent);
		}
		if (data.apiErrors) {
			var apiError = data.apiErrors.apiError;
			this.onFinesseError(apiError);
		}
	},

	/**
  * Triggers when you add a new call to the active list.
  * @private
  */
	onActiveCallAdded: function () {
		if (this.activeCalls.getCount() === 1) {
			var activeCall = this.activeCalls.getByIndex(0);
			this.activeCallId = activeCall.id;
		}
	},

	/**
  * Trggers when you delete a call from the active list.
  * @private
  */
	onActiveCallRemoved: function () {
		var activeCallsCount = this.activeCalls.getCount();
		if (activeCallsCount === 0) {
			this.activeCallId = "";
			this.fireEvent("lineStateChanged", { callFeaturesSet: Terrasoft.CallFeaturesSet.CAN_DIAL });
		} else if (Ext.isEmpty(this.activeCalls.find(this.activeCallId))) {
			var activeCall = this.activeCalls.getByIndex(0);
			this.activeCallId = activeCall.id;
		}
	},

	/**
  * Triggers when you clear the active calls list.
  * @private
  */
	onActiveCallsCleared: function () {
		this.activeCallId = "";
	},

	/**
  * Returns when saving information about a call to the database.
  * @private
  * @param {Object} request Instance of the request.
  * @param {Boolean} success Indicates a successful server response.
  * @param {Object} response Server response.
  */
	onUpdateDbCall: function (request, success, response) {
		var callDatabaseUid = Terrasoft.decode(response.responseText);
		if (success && Terrasoft.isGUID(callDatabaseUid)) {
			var updateCall = Terrasoft.decode(request.jsonData);
			var callId = updateCall.id;
			var call = this.activeCalls.find(callId);
			if (!Ext.isEmpty(call)) {
				call.databaseUId = callDatabaseUid;
				this.fireEvent("callSaved", call);
			}
		} else {
			this.fireEvent("rawMessage", "Update Call error");
			var errorInfo = {
				internalErrorCode: null,
				data: response.responseText,
				source: "App server",
				errorType: Terrasoft.MsgErrorType.COMMAND_ERROR
			};
			this.fireEvent("error", errorInfo);
		}
	},

	/**
  * Called when the user status is changed.
  * @private
  * @param {String} userState Agent status.
  * @param {String} [userStateReasonCode] Agent reason code for the agent.
  * @param {Number} callFeaturesSet Available call operations (a set of bitwise enumeration flags
  * {@link Terrasoft.CallFeaturesSet}).
  */
	userStateChanged: function (userState, userStateReasonCode, callFeaturesSet) {
		var stateChanged = this.userState !== userState || this.userStateReasonCode !== userStateReasonCode;
		this.userState = userState;
		this.userStateReasonCode = userStateReasonCode;
		if (userState && stateChanged) {
			this.fireEvent("agentStateChanged", {
				userState: userState,
				userStateReasonCode: userStateReasonCode
			});
			this.lastNotReadyStateReasonCode = userState === Terrasoft.FinesseAgentState.NOT_READY ? userStateReasonCode : this.lastNotReadyStateReasonCode;
			/*TODO: СС-62 Активация функциональных кнопок по состоянию пользователя/агента.
   Получать от сервера Finesse список возможный операций для пользователя, а не опираться на статус NOT_READY.
   Если сервер не дает такой информации, то предусмотреть метод, который даст список доступных операций осно-
   вываясь на любом переданном ему статусе.*/
			if (this.activeCalls.getCount() === 0) {
				var currentCallFeaturesSet = !Ext.isEmpty(callFeaturesSet) ? callFeaturesSet : Terrasoft.CallFeaturesSet.CAN_NOTHING;
				var featureSet = userState === Terrasoft.FinesseAgentState.NOT_READY ? Terrasoft.CallFeaturesSet.CAN_DIAL : currentCallFeaturesSet;
				this.fireEvent("lineStateChanged", { callFeaturesSet: featureSet });
			}
		}
	},

	/**
  * Called when the call status changes.
  * @private
  * @param {Object} eventContent The event object of the Finesse library.
  */
	dialogStateChanged: function (eventContent) {
		var dialog = eventContent.Update.data.dialog;
		if (!Ext.isEmpty(eventContent.Update.data.dialogs)) {
			var dialogs = eventContent.Update.data.dialogs;
			////TODO реализовать корректную обработку если есть несколько  звонков (например консультационный)

			dialog = Ext.isArray(dialogs) ? dialogs[0] : dialogs.Dialog;
		}
		if (Ext.isEmpty(dialog)) {
			return;
		}
		if (dialog.mediaType !== Terrasoft.FinesseDialogMediaType.VOICE) {
			return;
		}
		var call = this.processFinesseDialog(dialog);
		if (Ext.isEmpty(call)) {
			return;
		}
		this.updateDbCall(call, this.onUpdateDbCall);
		this.fireCallEventByState(call, dialog);
	},

	/**
  * Processes an error from the Finesse server.
  * @private
  * @param {Object} apiError The Finesse error object.
  */
	onFinesseError: function (apiError) {
		this.logError(this.finesseErrorMessageTemplate, Terrasoft.encode(apiError));
		var errorInfo = {
			internalErrorCode: apiError.errorType,
			data: Terrasoft.Resources.Telephony.Exception.UnknownFinesseException,
			source: "Finesse",
			errorType: Terrasoft.MsgErrorType.GENERAL_ERROR
		};
		switch (apiError.errorType) {
			case Terrasoft.FinesseErrorType.DEVICE_BUSY:
				errorInfo.data = Terrasoft.Resources.Telephony.Exception.ExtensionBusyByAnotherUser;
				errorInfo.errorType = Terrasoft.MsgErrorType.OPEN_CONNECTION_ERROR;
				this.fireEvent("error", errorInfo);
				this.closeConnection();
				break;
			case Terrasoft.FinesseErrorType.INVALID_DEVICE:
				errorInfo.data = Terrasoft.Resources.Telephony.Exception.InvalidDevice;
				errorInfo.errorType = Terrasoft.MsgErrorType.OPEN_CONNECTION_ERROR;
				this.fireEvent("error", errorInfo);
				this.closeConnection();
				break;
			default:
				this.fireEvent("error", errorInfo);
				break;
		}
	},

	/**
  * Handle the event when logging in to the user's telephony session.
  * @private
  * @param {Object} request Instance of the request.
  * @param {Boolean} success Indicates a successful server response.
  * @param {Object} response Server response
  */
	onLoginMsgService: function (request, success, response) {
		if (success && Terrasoft.isGUID(response.responseText.replace(/\"/g, ""))) {
			var sysSettingCode = "SysMsgFinesseDefaulNotReadyReasonCode";
			Terrasoft.SysSettings.querySysSettings([sysSettingCode], function (settings) {
				this.defaultNotReadyReasonCode = settings.SysMsgFinesseDefaulNotReadyReasonCode;
				if (!Ext.isEmpty(this.defaultNotReadyReasonCode)) {
					this.connect(this.initialConfig.connectionConfig);
				} else {
					var errorMsg = Ext.String.format(Terrasoft.Resources.Telephony.Exception.DefaultNotReadyReasonCodeNotSetFromSysSettings, sysSettingCode);
					this.logError(errorMsg);
				}
			}, this);
		} else {
			this.fireEvent("rawMessage", "LogInMsgService error");
			var errorInfo = {
				internalErrorCode: null,
				data: response.responseText,
				source: "App server",
				errorType: Terrasoft.MsgErrorType.AUTHENTICATION_ERROR
			};
			this.fireEvent("error", errorInfo);
		}
	},

	//endregion

	//region Methods: Public
	/**
  * Creates a provider for the Finesse service.
  */
	constructor: function () {
		this.callParent(arguments);
		this.activeCalls = Ext.create("Terrasoft.Collection");
		this.activeCalls.on("add", this.onActiveCallAdded, this);
		this.activeCalls.on("remove", this.onActiveCallRemoved, this);
		this.activeCalls.on("clear", this.onActiveCallsCleared, this);
	},

	/**
  * The method of initializing the connection.
  * @override
  */
	init: function () {
		this.callParent(arguments);
		var callback = function () {
			this.loginMsgService(this.msgUtilServiceUrl + this.loginMethodName, {
				"LicInfoKeys": this.licInfoKeys,
				"UserUId": Terrasoft.SysValue.CURRENT_USER.value
			});
		}.bind(this);
		var configuration = Terrasoft.configuration;
		if (configuration && configuration.RootSchemaDescriptors && configuration.RootSchemaDescriptors.jabberwerx) {
			require(["jQuery", "x2js"], function () {
				require(["jqueryMigrate"], function () {
					//Save original function toJSON() in order to cancel overwriting it in jabber library.
					var originalDateToJsonFunc = Date.prototype.toJSON;
					require(["jabberwerx"], function () {
						/* jshint freeze:false */
						Date.prototype.toJSON = originalDateToJsonFunc;
						/* jshint freeze:true */
						callback();
					});
				});
			});
		} else {
			callback();
		}
	},

	/**
  * @inheritdoc Terrasoft.BaseCtiProvider#closeConnection.
  */
	closeConnection: function () {
		this.boshClient.disconnect();
	},

	/**
  * @inheritdoc Terrasoft.BaseCtiProvider#makeCall
  */
	makeCall: function (targetAddress) {
		this.makeFinesseCall(targetAddress);
	},

	/**
  * @inheritdoc Terrasoft.BaseCtiProvider#answerCall
  */
	answerCall: function (call) {
		this.takeFinesseAction(call.id, Terrasoft.FinesseDialogAction.ANSWER);
	},

	/**
  * @inheritdoc Terrasoft.BaseCtiProvider#holdCall
  */
	holdCall: function (call) {
		var finesseAction = call.state === Terrasoft.GeneralizedCallState.HOLDED ? Terrasoft.FinesseDialogAction.RETRIEVE : Terrasoft.FinesseDialogAction.HOLD;
		this.takeFinesseAction(call.id, finesseAction);
	},

	/**
  * @inheritdoc Terrasoft.BaseCtiProvider#dropCall
  */
	dropCall: function (call) {
		this.takeFinesseAction(call.id, Terrasoft.FinesseDialogAction.DROP);
	},

	/**
  * @inheritdoc Terrasoft.BaseCtiProvider#makeConsultCall
  */
	makeConsultCall: function (call, targetAddress) {
		this.makeFinesseConsultCall(call.id, targetAddress);
	},

	/**
  * @inheritdoc Terrasoft.BaseCtiProvider#transferCall
  */
	transferCall: function (currentCall) {
		this.takeFinesseAction(currentCall.id, Terrasoft.FinesseDialogAction.TRANSFER);
	},

	/**
  * @inheritdoc Terrasoft.BaseCtiProvider#cancelTransfer
  */
	cancelTransfer: function (currentCall, consultCall) {
		this.dropCall(consultCall);
		this.holdCall(currentCall);
	},

	/**
  * @inheritdoc Terrasoft.BaseCtiProvider#blindTransferCall
  */
	blindTransferCall: function (call, targetAddress) {
		this.makeBlindTransferCall(call.id, targetAddress);
	},

	/**
  * @inheritdoc Terrasoft.BaseCtiProvider#setUserState
  */
	setUserState: function (code, reason, callback) {
		if (code === Terrasoft.FinesseAgentState.NOT_READY) {
			reason = Ext.isEmpty(reason) || reason === Terrasoft.FinesseAgentStateReason.UNKNOWN ? this.defaultNotReadyReasonCode : reason;
		}
		this.setFinesseAgentState(code, reason, callback);
	},

	/**
  * @inheritdoc Terrasoft.BaseCtiProvider#setWrapUpUserState
  */
	setWrapUpUserState: function (isWrapUpActive, callback) {
		if (!isWrapUpActive) {
			var stateCode, reasonCode;
			if (this.userState === Terrasoft.FinesseAgentState.WORK_READY) {
				stateCode = Terrasoft.FinesseAgentState.READY;
				reasonCode = null;
			} else if (this.userState === Terrasoft.FinesseAgentState.WORK) {
				stateCode = Terrasoft.FinesseAgentState.NOT_READY;
				reasonCode = this.lastNotReadyStateReasonCode;
			} else {
				if (Ext.isFunction(callback)) {
					callback.call(this);
				}
				return;
			}
			this.setUserState(stateCode, reasonCode, callback);
		}
	},

	/**
  * @inheritdoc Terrasoft.BaseCtiProvider#queryUserState
  */
	queryUserState: function () {
		this.getFinesseAgentState();
	},

	/**
  * @inheritdoc Terrasoft.BaseCtiProvider#sendDtmf
  */
	sendDtmf: function () {},

	/**
  * @inheritdoc Terrasoft.BaseCtiProvider#queryActiveCallSnapshot
  */
	queryActiveCallSnapshot: function () {},

	/**
  * @inheritdoc Terrasoft.BaseCtiProvider#queryLineState
  */
	queryLineState: function () {},

	/**
  * @inheritdoc Terrasoft.BaseCtiProvider#getCapabilities
  */
	getCapabilities: function () {
		/*jshint bitwise:false */
		var callCapabilities = Terrasoft.CallFeaturesSet.CAN_RECALL | Terrasoft.CallFeaturesSet.CAN_DIAL | Terrasoft.CallFeaturesSet.CAN_DROP | Terrasoft.CallFeaturesSet.CAN_HOLD | Terrasoft.CallFeaturesSet.CAN_UNHOLD | Terrasoft.CallFeaturesSet.CAN_COMPLETE_TRANSFER | Terrasoft.CallFeaturesSet.CAN_BLIND_TRANSFER | Terrasoft.CallFeaturesSet.CAN_MAKE_CONSULT_CALL | Terrasoft.CallFeaturesSet.CAN_DTMF;
		var agentCapabilities = Terrasoft.AgentFeaturesSet.CAN_NOTHING;
		/*jshint bitwise:true */
		return {
			callCapabilities: callCapabilities,
			agentCapabilities: agentCapabilities
		};
	}

	/*jshint bitwise:true */
	//endregion

});

//endregion