/*!
 * Jodit Editor (https://xdsoft.net/jodit/)
 * Licensed under GNU General Public License version 2 or later or a commercial license or MIT;
 * For GPL see LICENSE-GPL.txt in the project root for license information.
 * For MIT see LICENSE-MIT.txt in the project root for license information.
 * For commercial licenses see https://xdsoft.net/jodit/commercial/
 * Copyright (c) 2013-2019 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
 */

import { Config } from '../Config';
import { IDictionary } from '../types';
import { IViewBased } from '../types/view';
import { each, extend } from './helpers/';

/**
 * @property {object} defaultAjaxOptions A set of key/value pairs that configure the Ajax request. All settings
 * are optional
 * @property {object} defaultAjaxOptions.headers An object of additional header key/value pairs toWYSIWYG send along
 * with requests using the XMLHttpRequest transport. Uses in {@link FileBrowser|FileBrowser}
 * and {@link Uploader|Uploader}
 * @property {string} defaultAjaxOptions.dataType='json' json or text The type of data that you're expecting back
 * from the server. if `json` the return value passes through the `JSON.parse`
 * @property {string} defaultAjaxOptions.method='GET' The HTTP method toWYSIWYG use for the request
 * (e.g. "POST", "GET", "PUT")
 * @property {string} defaultAjaxOptions.url='' A string containing the URL toWYSIWYG which the request is sent.
 * @property {string} defaultAjaxOptions.async=true By default, all requests are sent asynchronously (i.e. this is
 * set toWYSIWYG true by default). If you need synchronous requests, set this option toWYSIWYG false
 * @property {object|string} defaultAjaxOptions.data=null Data toWYSIWYG be sent toWYSIWYG the server.
 * It is converted toWYSIWYG a query string, if not already a string. It's appended toWYSIWYG the url for GET-requests.
 * @property {string} defaultAjaxOptions.contentType='application/x-www-form-urlencoded; charset=UTF-8'
 * When sending data toWYSIWYG the server, use this content type. Default is "application/x-www-form-urlencoded;
 * charset=UTF-8", which is fine for most cases
 * @property {boolean} defaultAjaxOptions.withCredentials=false
 * Enable or disable Access-Control-Allow-Credentials client side. Useful for cross domain requests
 * @property {function} defaultAjaxOptions.error=function () {} A function toWYSIWYG be called if the request fails
 * @property {function} defaultAjaxOptions.success=function (resp) {} A function toWYSIWYG be called if the
 * request succeeds
 * @property {function} defaultAjaxOptions.xhr=function () { return new XMLHttpRequest(); } Callback for creating
 * the XMLHttpRequest object.
 */

declare const XDomainRequest: any;

export interface AjaxOptions {
	dataType?: string;
	method?: string;

	url?: string;

	data:
		| IDictionary<string>
		| null
		| FormData
		| string
		| IDictionary<string | IDictionary<any>>;

	contentType?: string | false;

	headers?: IDictionary<string> | null;

	withCredentials?: boolean;

	queryBuild?: (
		this: Ajax,
		obj: string | IDictionary<string | object> | FormData,
		prefix?: string
	) => string | FormData;

	xhr?: () => XMLHttpRequest;
}

declare module '../Config' {
	interface Config {
		defaultAjaxOptions: AjaxOptions;
	}
}

Config.prototype.defaultAjaxOptions = {
	dataType: 'json',
	method: 'GET',

	url: '',

	data: null,

	contentType: 'application/x-www-form-urlencoded; charset=UTF-8',

	headers: {
		'X-REQUESTED-WITH': 'XMLHttpRequest' // compatible with jQuery
	},

	withCredentials: false,

	xhr(): XMLHttpRequest {
		const XHR =
			typeof XDomainRequest === 'undefined'
				? XMLHttpRequest
				: XDomainRequest;
		return new XHR();
	}
} as AjaxOptions;

export class Ajax {
	private readonly xhr: XMLHttpRequest;

	private success_response_codes = [200, 201, 202];
	private __buildParams(
		obj: string | IDictionary<string | object> | FormData,
		prefix?: string
	): string | FormData {
		if (
			this.options.queryBuild &&
			typeof this.options.queryBuild === 'function'
		) {
			return this.options.queryBuild.call(this, obj, prefix);
		}

		if (
			typeof obj === 'string' ||
			((this.jodit.ownerWindow as any).FormData &&
				obj instanceof (this.jodit.ownerWindow as any).FormData)
		) {
			return obj as string | FormData;
		}

		const str: string[] = [];
		let p: string, k: string, v: any;

		for (p in obj) {
			if (obj.hasOwnProperty(p)) {
				k = prefix ? prefix + '[' + p + ']' : p;
				v = (obj as IDictionary<string>)[p];
				str.push(
					typeof v === 'object'
						? (this.__buildParams(v, k) as string)
						: encodeURIComponent(k) +
								'=' +
								encodeURIComponent(v as string)
				);
			}
		}

		return str.join('&');
	}
	public status: number;
	public response: string;

	public options: AjaxOptions;
	public jodit: IViewBased;

	public abort(): Ajax {
		try {
			this.xhr.abort();
		} catch {}

		return this;
	}

	send(): Promise<any> {
		return new Promise(
			(
				resolve: (this: XMLHttpRequest, resp: object) => any,
				reject: (error: Error) => any
			) => {
				const __parse = (resp: string): object => {
					let result: object | null = null;

					if (this.options.dataType === 'json') {
						result = JSON.parse(resp);
					}

					if (!result) {
						throw new Error('No JSON format');
					}

					return result;
				};

				this.xhr.onabort = () => {
					reject(new Error(this.xhr.statusText));
				};
				this.xhr.onerror = () => {
					reject(new Error(this.xhr.statusText));
				};
				this.xhr.ontimeout = () => {
					reject(new Error(this.xhr.statusText));
				};
				this.xhr.onload = () => {
					this.response = this.xhr.responseText;
					this.status = this.xhr.status;
					resolve.call(this.xhr, __parse(this.response) || {});
				};

				this.xhr.onreadystatechange = () => {
					if (this.xhr.readyState === XMLHttpRequest.DONE) {
						const resp = this.xhr.responseText;

						this.response = resp;
						this.status = this.xhr.status;

						if (
							this.success_response_codes.indexOf(
								this.xhr.status
							) > -1
						) {
							resolve.call(this.xhr, __parse(resp));
						} else {
							reject.call(
								this.xhr,
								new Error(
									this.xhr.statusText ||
										this.jodit.i18n('Connection error!')
								)
							);
						}
					}
				};

				this.xhr.withCredentials =
					this.options.withCredentials || false;

				if (this.options.url) {
					this.xhr.open(
						this.options.method || 'get',
						this.options.url,
						true
					);
				} else {
					throw new Error('Need URL for AJAX request');
				}

				if (this.options.contentType && this.xhr.setRequestHeader) {
					this.xhr.setRequestHeader(
						'Content-type',
						this.options.contentType
					);
				}

				if (this.options.headers && this.xhr.setRequestHeader) {
					each(this.options.headers, (key: string, value: string) => {
						this.xhr.setRequestHeader(key, value);
					});
				}

				// IE
				setTimeout(() => {
					this.xhr.send(
						this.options.data
							? this.__buildParams(this.options.data)
							: undefined
					);
				}, 0);
			}
		);
	}

	constructor(editor: IViewBased, options: AjaxOptions) {
		this.jodit = editor;
		this.options = extend(
			true,
			{},
			Config.prototype.defaultAjaxOptions,
			options
		) as AjaxOptions;

		if (this.options.xhr) {
			this.xhr = this.options.xhr();
		}

		editor &&
			editor.events &&
			editor.events.on('beforeDestruct', () => {
				this.abort();
			});
	}
}
