/**
 * FH-Complete
 *
 * @package
 * @author
 * @copyright   Copyright (c) 2016 fhcomplete.org
 * @license GPLv3
 * @link	https://fhcomplete.org
 * @since	Version 1.0.0
 */

//--------------------------------------------------------------------------------------------------------------------
// Configs

// To see debug messages into the browser console set this parameter as true
const DEBUG = false;

// Default veil timeout (milliseconds)
const VEIL_TIMEOUT = 5000;

//--------------------------------------------------------------------------------------------------------------------
// Constants

// Success
const SUCCESS = 0;

// Properties present in a response
const CODE = "error";
const RESPONSE = "retval";

// HTTP method parameters
const HTTP_GET_METHOD = "GET";
const HTTP_POST_METHOD = "POST";

const REMOTE_CONTROLLER = "remoteController";

const FHC_CONTROLLER_ID = "fhc_controller_id";

/**
 * Definition and initialization of object FHC_AjaxClient
 */
var FHC_AjaxClient = {
	//------------------------------------------------------------------------------------------------------------------
	// Properties

	_veilCallersCounter: 0, // count the number of callers that want to activate the veil

	//------------------------------------------------------------------------------------------------------------------
	// Public methods

	/**
	 * Performs a call using the HTTP GET method
	 * controllerParameters is an object
	 * ajaxCallParameters is an object
	 */
	ajaxCallGet: function(remoteController, controllerParameters, ajaxCallParameters) {
		FHC_AjaxClient._ajaxCall(remoteController, controllerParameters, HTTP_GET_METHOD, ajaxCallParameters);
	},

	/**
	 * Performs a call using the HTTP POST method
	 * controllerParameters is an object
	 * ajaxCallParameters is an object
	 */
	ajaxCallPost: function(remoteController, controllerParameters, ajaxCallParameters) {
		FHC_AjaxClient._ajaxCall(remoteController, controllerParameters, HTTP_POST_METHOD, ajaxCallParameters);
	},

	/**
	 * Checks if the response is a success
	 */
	isSuccess: function(response) {
		var isSuccess = false;

		if (jQuery.type(response) == "object" && response.hasOwnProperty(CODE) && response.hasOwnProperty(RESPONSE))
		{
			if (response.error == SUCCESS)
			{
				isSuccess = true;
			}
		}

		return isSuccess;
	},

	/**
	 * Checks if the response is an error
	 */
	isError: function(response) {
		return !FHC_AjaxClient.isSuccess(response);
	},

	/**
	 * Checks if the response has data
	 */
	hasData: function(response) {
		var hasData = false;

		if (FHC_AjaxClient.isSuccess(response))
		{
			if ((jQuery.type(response.retval) == "object" && !jQuery.isEmptyObject(response.retval))
				|| (jQuery.isArray(response.retval) && response.retval.length > 0)
				|| (jQuery.type(response.retval) == "string" && response.retval.trim() != "")
				|| jQuery.type(response.retval) == "number")
			{
				hasData = true;
			}
		}

		return hasData;
	},

	/**
	 * Retrives data from response object
	 */
	getData: function(response) {
		var data = null;

		if (FHC_AjaxClient.hasData(response))
		{
			data = response.retval;
		}

		return data;
	},

	/**
	 * Retrives error message from response object
	 */
	getError: function(response) {
		var error = "Generic error";

		if (jQuery.type(response) == "object" && !jQuery.isEmptyObject(response) && response.hasOwnProperty(RESPONSE))
		{
			error = response.retval;
		}

		return error;
	},

	/**
	 * Retrives code from response object
	 */
	getCode: function(response) {
		var code = 1; // Generic error

		if (jQuery.type(response) == "object" && response.hasOwnProperty(CODE))
		{
			code = response.error;
		}

		return code;
	},

	/**
	 * Show a veil
	 */
	showVeil: function(veilTimeout) {
		if (typeof veilTimeout == "number")
		{
			FHC_AjaxClient._veilTimeout = veilTimeout;
		}
		else
		{
			FHC_AjaxClient._veilTimeout = VEIL_TIMEOUT;
		}
		FHC_AjaxClient._showVeil();
	},

	/**
	 * Hide a veil that was shown before
	 */
	hideVeil: function() {
		FHC_AjaxClient._hideVeil();
	},

	/**
	 * Retrives parameters from URL query string (HTTP GET parameters)
	 */
	getUrlParameter: function(sParam) {
		var sPageURL = decodeURIComponent(window.location.search.substring(1)),
			sURLVariables = sPageURL.split("&"),
			sParameterName,
			i;

		for (var i = 0; i < sURLVariables.length; i++)
		{
			sParameterName = sURLVariables[i].split("=");

			if (sParameterName[0] === sParam)
			{
				return sParameterName[1];
			}
		}
	},

	//------------------------------------------------------------------------------------------------------------------
	// Private methods

	/**
	 * Generate the router URI using the connection parameters
	 */
	_generateRouterURI: function(remoteController) {
		var uri = null;

		// Checks if global JS object FHC_JS_DATA_STORAGE_OBJECT exists
		if (typeof FHC_JS_DATA_STORAGE_OBJECT !== "undefined")
		{
			uri = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + "/" + remoteController;
		}

		return uri;
	},

	/**
	 * Method to print debug info after a controller has been called
	 */
	_printDebug: function(parameters, response, errorThrown) {

		if (DEBUG === true) // If global const DEBUG is true, but really true!
		{
			// Print info about called controller
			console.log("Called controller: " + parameters.remoteController);
			console.log("Call parameters:"); // parameters given to this call
			console.log(parameters);

			if (response != null) // if there is a response...
			{
				console.log("Controller Response:");
				console.log(response); // ...print it
			}
			if (errorThrown != null) // if there is a jQuery error...
			{
				console.log("jQuery error:");
				console.log(errorThrown); // ...print it
			}
			console.log("--------------------------------------------------------------------------------------------");
		}
	},

	/**
	 * Method to call if the ajax call has succeeded
	 */
	_onSuccess: function(response, textStatus, jqXHR) {

		FHC_AjaxClient._printDebug(this._data, response); // debug time!

		// Call the success callback saved in _successCallback property
		// NOTE: this is not referred to FHC_AjaxClient but to the ajax object
		this._successCallback(response);
	},

	/**
	 * Method to call if the ajax call has raised an error
	 */
	_onError: function(jqXHR, textStatus, errorThrown) {

		FHC_AjaxClient._printDebug(this._data, null, errorThrown); // debug time!

		// Call the error callback saved in _errorCallback property
		// NOTE: this is not referred to FHC_AjaxClient but to the ajax object
		this._errorCallback(jqXHR, textStatus, errorThrown);
	},

	/**
	 * Method to call after the ajax call has ended
	 */
	_onComplete: function(jqXHR, textStatus) {

		FHC_AjaxClient._printDebug(this._data, null, jqXHR.responseJSON); // debug time!

		// Call the complete callback if it was saved in the _completeCallback property
		// NOTE: this is not referred to FHC_AjaxClient but to the ajax object.
		//		It's known that it's a function because it was already checked before in the
		//		_checkAndGenerateAjaxParams method
		if (this.hasOwnProperty("_completeCallback"))
		{
			this._completeCallback(jqXHR, textStatus);
		}

		FHC_AjaxClient._hideVeil(); // finally hide the veil

		// If was required a page reload then do it!
		if (this._reloadPage === true)
		{
			// NOTE: to avoid any conflict with other ajaxs calls
			// Otherwise some browsers (eg. firefox) can prompt an error
			$(document).ajaxStop(function() { location.reload(); });
		}
	},

	/**
	 * If an error callback is not given, this is the default error callback that is used
	 * to display useful info about the occurred error. It uses the JQuery UI dialog
	 */
	_defaultErrorCallback: function(jqXHR, textStatus, errorThrown) {

		// Row table format
		var tableRowFormat = "<tr><td class=\"fhc-ajaxclient-error-td\"><b>%1s: </b></td><td>%2s</td></tr>";
		var strDivDialog = "<div id=\"fhc-ajaxclient-dialog\"><table>"; // dialog div and open the error table

		// If textStatus is usable then place it in the table
		if (textStatus != null) strDivDialog += tableRowFormat.replace(/%1s/g, "Error").replace(/%2s/g, textStatus);

		// If errorThrown is usable then place it in the table
		if (errorThrown != null) strDivDialog += tableRowFormat.replace(/%1s/g, "Error text").replace(/%2s/g, errorThrown);

		// If jqXHR.status is usable then place it in the table
		if (jqXHR != null && jqXHR.hasOwnProperty("status"))
		{
			strDivDialog += tableRowFormat.replace(/%1s/g, "HTTP status").replace(/%2s/g, jqXHR.status);
		}

		// If jqXHR.responseText is usable then place it in the table
		if (jqXHR != null && jqXHR.hasOwnProperty("responseText"))
		{
			strDivDialog += tableRowFormat.replace(/%1s/g, "HTTP response").replace(/%2s/g, jqXHR.responseText);
		}

		strDivDialog += "</table></div>"; // close table and div

		$(strDivDialog).appendTo("body"); // append the dialog div to the body

		// Dialog definition
		$("#fhc-ajaxclient-dialog").dialog({
			title: "Error occurred",
			dialogClass: "no-close",
			autoOpen: true,
			modal: true,
			resizable: false,
			height: "auto",
			width: 700,
			closeOnEscape: false,
			buttons: [{
				text: "Ok",
				click: function() {
					$(this).dialog("close");
				}
			}]
		});
	},

	/**
	 * Instantiate a new object and copy in it the properties from the parameter
	 */
	_cpObjProps: function(obj) {
		var returnObj = {};

		for (var prop in obj)
		{
			returnObj[prop] = obj[prop];
		}

		return returnObj;
	},

	/**
	 * Method to show the veil
	 */
	_showVeil: function() {
		if (FHC_AjaxClient._veilCallersCounter == 0)
		{
			$("<div class=\"fhc-ajaxclient-veil\"></div>").appendTo("body");
		}

		FHC_AjaxClient._veilCallersCounter++;
	},

	/**
	 * Method to hide the veil
	 */
	_hideVeil: function() {
		window.setTimeout(function() {
			if (FHC_AjaxClient._veilCallersCounter >= 0)
			{
				if (FHC_AjaxClient._veilCallersCounter > 0)
				{
					FHC_AjaxClient._veilCallersCounter--;
				}

				if (FHC_AjaxClient._veilCallersCounter == 0)
				{
					$(".fhc-ajaxclient-veil").remove();
				}
			}
		},
		this._veilTimeout);
	},

	/**
	 * Check if controllerParameters has a FileList of uploaded file(s).
	 *
	 * @param controllerParameters
	 * 			Example: {
	 * 				name1: value,
	 * 				name2: value,
	 * 				files: $(selector)[0].files --> this is the FileList
	 * 			}
	 * @returns {boolean}
	 * @private
	 */
	_hasFileList: function(controllerParameters)
	{
		// Loop through controllerParameters
		for (key in controllerParameters)
		{
			// When FileList is found ( parameter with uploaded file(s))
			if (controllerParameters[key] instanceof FileList)
			{
				return true;
			}
		}
	},

	/**
	 * Returns a FormData object. Useful for passing uploaded files via AJAX.
	 *
	 * @param controllerParameters
	 * @returns {FormData}
	 * @private
	 */
	_convertToFormDataObject: function(controllerParameters)
	{
		// The new FormData instance
		const formData = new FormData();

		// Loop through controllerParameters
		for (key in controllerParameters)
		{
			var value = controllerParameters[key];
			// When FileList is found ( parameter with uploaded file(s))
			if (value instanceof FileList)
			{
				// Loop through uploaded files
				//for (let file of value)
				//for (var file of value)
				for (var filekey in value)
				{
					var file = value[filekey];
					// Append file to FormData object (if more than 1 file, append as array)
					formData.append(value.length == 1 ? key : key + '[]', file);
				}
			}
			else
			{
				// For any other then FileList, just append to FormData object
				formData.append(key, value);
			}
		}
		return formData;
	},

	/**
	 * Checks call parameters, if they are present and are valid
	 * It generates and returns all the parameters needed to perform an ajax remote call
	 * NOTE: console.error is used here because those are not messages for the final user,
	 *		but for the web interface developer
	 */
	_checkAndGenerateAjaxParams: function(remoteController, controllerParameters, type, ajaxCallParameters) {

		var valid = true; // by default they are ok (we want to trust you, please do not betray it)

		// Returned parameters
		var ajaxParameters = {
			cache: false, // data are never cached by the browser
			dataType: "json", // always json!
			type: type // set HTTP method, GET or POST
		};

		// remoteController must be a NON-empty string
		if (typeof remoteController == "string" && remoteController.trim() != "")
		{
				// Is it possible to generate the URL
				if ((url = FHC_AjaxClient._generateRouterURI(remoteController)) != null)
				{
					ajaxParameters.url = url;
				}
				else // but it could fail
				{
					console.error("FHC_JS_DATA_STORAGE_OBJECT is not present");
					valid = false;
				}
		}
		else // otherwise is NOT possible to generate the URL
		{
			console.error("Invalid remoteController parameter");
			valid = false;
		}

		// controllerParameters must be an object
		if (typeof controllerParameters == "object")
		{
			// If controllerParameters contains uploaded file(s) as FileList
			if (controllerParameters !==  null && FHC_AjaxClient._hasFileList(controllerParameters))
			{
				// Convert controllerParameters to FormData object to easily pass uploaded files via AJAX
				var data = FHC_AjaxClient._convertToFormDataObject(controllerParameters);	// data is a FormData object now

				// Add options to tell jQuery not to process data or worry about content-type
				ajaxParameters.processData = false;
				ajaxParameters.contentType = false;
			}
			// Anything else
			else
			{
				// Copy the properties of controllerParameters into a new object
				var data = FHC_AjaxClient._cpObjProps(controllerParameters);
			}

			// fhc_controller_id is given if present
			data[FHC_CONTROLLER_ID] = FHC_AjaxClient.getUrlParameter(FHC_CONTROLLER_ID);

			// Stores them into ajaxParameters
			// NOTE: property data is not possible to get later,
			//		so the variable data is saved also in _data and it will be used later
			ajaxParameters.data = data;
			ajaxParameters._data = data;
		}
		else
		{
			console.error("Invalid controller parameters, must be an object");
			valid = false;
		}

		// Checks if ajaxCallParameters is an object
		if (typeof ajaxCallParameters == "object")
		{
			// If present, errorCallback must be a function
			if (ajaxCallParameters.hasOwnProperty("errorCallback"))
			{
				if (typeof ajaxCallParameters.errorCallback == "function")
				{
					ajaxParameters._errorCallback = ajaxCallParameters.errorCallback; // save as property the callback error
					ajaxParameters.error = FHC_AjaxClient._onError; // function to call if an error occurred
				}
				else
				{
					console.error("Invalid errorCallback, it must be a function");
					valid = false;
				}
			}
			else // if is not given then call the default errorCallback
			{
				ajaxParameters._errorCallback = FHC_AjaxClient._defaultErrorCallback; // save as property the callback error
				ajaxParameters.error = FHC_AjaxClient._onError; // function to call if an error occurred
			}

			// If present, successCallback must be a function
			if (ajaxCallParameters.hasOwnProperty("successCallback"))
			{
				if (typeof ajaxCallParameters.successCallback == "function")
				{
					ajaxParameters._successCallback = ajaxCallParameters.successCallback; // save as property the callback success
					ajaxParameters.success = FHC_AjaxClient._onSuccess; // function to call if succeeded
				}
				else
				{
					console.error("Invalid successCallback, it must be a function");
					valid = false;
				}
			}

			// If present, completeCallback must be a function
			if (ajaxCallParameters.hasOwnProperty("completeCallback"))
			{
				if (typeof ajaxCallParameters.completeCallback == "function")
				{
					ajaxParameters._completeCallback = ajaxCallParameters.completeCallback; // save as property the callback complete
				}
				else
				{
					console.error("Invalid completeCallback, it must be a function");
					valid = false;
				}
			}

			// If present, veilTimeout must be a number and cannot be less then 0 or greater then 60000
			if (ajaxCallParameters.hasOwnProperty("veilTimeout") && typeof ajaxCallParameters.veilTimeout == "number")
			{
				if (ajaxCallParameters.veilTimeout > 0 && ajaxCallParameters.veilTimeout < 60000)
				{
					ajaxParameters._veilTimeout = ajaxCallParameters.veilTimeout;
					ajaxParameters.beforeSend = FHC_AjaxClient._showVeil;
				}
				else if(ajaxCallParameters.veilTimeout == 0)
				{
					// veil is disabled
				}
				else
				{
					console.error("Invalid veilTimeout parameter, must be a number >= 0 and <= 60000");
					valid = false;
				}
			}
			else // is not present or the value is invalid
			{
				ajaxParameters._veilTimeout = VEIL_TIMEOUT;
				ajaxParameters.beforeSend = FHC_AjaxClient._showVeil;
			}

			// If present reloadPage has to be a boolean
			if (ajaxCallParameters.hasOwnProperty("reloadPage"))
			{
				if (typeof ajaxCallParameters.reloadPage != "boolean")
				{
					console.error("Invalid reloadPage parameter, must be a boolean");
					valid = false;
				}
				else
				{
					ajaxParameters._reloadPage = ajaxCallParameters.reloadPage;
				}
			}
			else // not present
			{
				ajaxParameters._reloadPage = false;
			}

			// Function to call after the ajax call is ended, is it here because it must be always called
			ajaxParameters.complete = FHC_AjaxClient._onComplete;
		}

		if (valid === false)
		{
			ajaxParameters = null;
		}

		return ajaxParameters;
	},

	/**
	 * Performs a call to the server were the CI PHP layer is running
	 * - remoteController: alias of the core controller to call
	 * - controllerParameters: parameters to give to the called controller
	 * - type: POST or GET HTTP method
	 * - ajaxCallParameters is an object and could contains:
	 *	- errorCallback: function to call after an error has been raised
	 *	- successCallback: function to call after succeeded
	 *	- veilTimeout: veil timeout
	 */
	_ajaxCall: function(remoteController, controllerParameters, type, ajaxCallParameters) {
		// Retrives the parameters for the ajax call
		var ajaxParameters = FHC_AjaxClient._checkAndGenerateAjaxParams(remoteController, controllerParameters, type, ajaxCallParameters);

		// Checks the given parameters if they are present and are valid
		if (ajaxParameters != null)
		{
			$.ajax(ajaxParameters); // ajax call
		}
	}
};

