const VnObject = require('./object');
const JsonException = require('./json-exception');

/**
 * Handler for JSON rest connections.
 */
module.exports = new Class({
	Extends: VnObject,

	_connected: false,
	_requestsCount: 0,
	token: null,

	/**
	 * Initilizes the connection object.
	 */
	initialize() {
		VnObject.prototype.initialize.call(this);
		this.fetchToken();
	},

	fetchToken() {
		let token = null;
	
		if (sessionStorage.getItem('vnToken'))
			token = sessionStorage.getItem('vnToken');
		if (localStorage.getItem('vnToken'))
			token = localStorage.getItem('vnToken');
			
		this.token = token;
	},
	
	clearToken() {
		this.token = null;
		localStorage.removeItem('vnToken');
		sessionStorage.removeItem('vnToken');
	},

	/**
	 * Opens the connection to the REST service.
	 *
	 * @param {String} user The user name
	 * @param {String} password The user password
	 * @param {Boolean} remember Specifies if the user should be remembered
	 * @return {Promise} Resolved when operation is done
	 */
	async open(user, pass, remember) {
		let params;

		if (user !== null && user !== undefined) {
			params = {
				user,
				password: pass
			};
		} else
			params = null;

		try {
			const json = await this.post('Accounts/login', params);

			const storage = remember ? localStorage : sessionStorage;
			storage.setItem('vnToken', json.token);

			this.token = json.token;
			this._connected = true;
			this.emit('openned');
		} catch(err) {
			this._closeClient();
			throw err;
		}
	},

	/**
	 * Closes the connection to the REST service.
	 *
	 * @return {Promise} Resolved when operation is done
	 */
	async close() {
		const token = this.token;
		this._closeClient();
		this.emit('closed');

		if (token) {
			const config = {
				headers: {'Authorization': token}
			};
			await this.send('user/logout', null, config);
			await this.post('Accounts/logout', null, config);
		}
	},

	
	_closeClient() {
		this._connected = false;
		this.clearToken();
	},

	/**
	 * Executes the specified REST service with the given params and calls
	 * the callback when response is received.
	 *
	 * @param {String} url The service path
	 * @param {Object} params The params to pass to the service
	 * @return {Object} The parsed JSON response
	 */
	async send(url, params) {
		if (!params) params = {};
		params.srv = `json:${url}`;
		return this.sendWithUrl('POST', '.', params);
	},
	
	async sendForm(form) {
		const params = {};
		const elements = form.elements;

		for (var i = 0; i < elements.length; i++)
		if (elements[i].name)
			params[elements[i].name] = elements[i].value;

		return this.sendWithUrl('POST', form.action, params);
	},
	
	async sendFormMultipart(form) {
		return this.request({
			method: 'POST',
			url: form.action,
			data: new FormData(form)
		});
	},

	async sendFormData(formData) {
		return this.request({
			method: 'POST',
			url: '',
			data: formData
		});
	},

	/*
	 * Called when REST response is received.
	 */
	async sendWithUrl(method, url, params, config) {
		config = Object.assign({}, config, {
			method,
			url,
			data: Vn.Url.makeUri(params)
		});
		config.headers = Object.assign({}, config.headers, {
			'Content-Type': 'application/x-www-form-urlencoded'
		});
		return this.request(config);
	},

	async get(url, config) {
		config = Object.assign({}, config, {
			method: 'GET',
			url: `api/${url}`
		});
		return this.request(config);
	},

	async post(url, data, config) {
		return this.requestData('POST', url, data, config);
	},

	async patch(url, data, config) {
		return this.requestData('PATCH', url, data, config);
	},

	async requestData(method, url, data, config) {
		config = Object.assign({}, config, {
			method,
			url: `api/${url}`,
			data: data && JSON.stringify(data),
		});
		
		config.headers = Object.assign({}, config.headers, {
			'Content-Type': 'application/json;charset=utf-8'
		});
		return this.request(config);
	},

	async request(config) {
		const request = new XMLHttpRequest();
		request.open(config.method, config.url, true);
		if (this.token)
			request.setRequestHeader('Authorization', this.token);

		const headers = config.headers;
		if (headers)
		for (const header in headers)
			request.setRequestHeader(header, headers[header]);

		const promise = new Promise((resolve, reject) => {
			request.onreadystatechange =
				() => this._onStateChange(request, resolve, reject);
		});

		request.send(config.data);

		this._requestsCount++;
		
		if (this._requestsCount === 1)
			this.emit('loading-changed', true);

		return promise;
	},
	
	_onStateChange(request, resolve, reject) {
		if (request.readyState !== 4)
			return;

		this._requestsCount--;
		
		if (this._requestsCount === 0)
			this.emit('loading-changed', false);

		let data = null;
		let error = null;
		try {
			if (request.status == 0) {
				const err = new JsonException();
				err.message = _('The server does not respond, please check your Internet connection');
				err.statusCode = request.status;
				throw err;
			}
		
			let contentType = null;
			
			try {
				contentType = request
					.getResponseHeader('Content-Type')
					.split(';')[0]
					.trim();
			} catch (err) {
				console.warn(err);
			}
				
			if (contentType != 'application/json') {
				const err = new JsonException();
				err.message = request.statusText;
				err.statusCode = request.status;
				throw err;
			}

			let json;
			let jsData;

			if (request.responseText)
				json = JSON.parse(request.responseText);
			if (json)
				jsData = json.data || json;

			if (request.status >= 200 && request.status < 300) {
				data = jsData;
			} else {
				let exception = jsData.exception;
				let error = jsData.error;
				let err = new JsonException();
			
				if (exception) {
					exception = exception
						.replace(/\\/g, '.')
						.replace(/Exception$/, 'Error')
						.replace(/^Vn\.Web\./, '');

					err.exception = exception;
					err.message = jsData.message;
					err.code = jsData.code;
					err.file = jsData.file;
					err.line = jsData.line;
					err.trace = jsData.trace;
					err.statusCode = request.status;
				} else if (error) {
					err.message = error.message;
					err.code = error.code;
					err.statusCode = request.status;
				} else {
					err.message = request.statusText;
					err.statusCode = request.status;
				}

				throw err;
			}
		} catch (e) {
			data = null;
			error = e;
		}

		if (error) {
			if (error.exception == 'SessionExpired')
				this.clearToken();

			this.emit('error', error);
			reject(error);
		} else
			resolve(data);
	}
});