export interface IRestApiRejection {
    message : string;
    status : number;
    xhr : XMLHttpRequest;
    error : Error | null;
    type : string | null;
    content : any | null;
}

interface IAjaxRequest {
    ajaxGet(url : string, queryObject? : any) : Promise<any>;
    ajaxPost(url : string, postBody : any, queryObject? : any, type? : 'text' | 'json') : Promise<any>;
    ajaxPostForm(url : string, formData? : FormData, queryObject? : any) : Promise<any>;
    ajaxPut(url : string, postBody : any, queryObject? : any) : Promise<any>;
    ajaxDelete(url : string, queryObject? : any) : Promise<any>;
}

export class AjaxRequest implements IAjaxRequest {
    private XMLHttpRequest : typeof XMLHttpRequest;

    constructor(xmlHttpRequest : typeof XMLHttpRequest) {
        this.XMLHttpRequest = xmlHttpRequest;
    }

    public ajaxGet(url : string, queryObject? : any) : Promise<any> {
        return new Promise<any>((resolve, reject) => {
            const xhr = new this.XMLHttpRequest();
            const cacheBustingQueryObject = (queryObject || {}); // IE 11 caches GET requests
            cacheBustingQueryObject._cache_busting_arg_ = +new Date(); // this parameter name must be in the backend's excluded parameter set (see api_utils.py)

            xhr.open('GET', url + this.buildQueryString(cacheBustingQueryObject));
            xhr.withCredentials = true;
            xhr.onload = () => {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    try {
                        return resolve(JSON.parse(xhr.response));
                    } catch (e) {
                        const rejection : IRestApiRejection = {
                            message: e.message,
                            status: xhr.status,
                            xhr,
                            error: e,
                            type: null,
                            content: null,
                        };
                        // tslint:disable-next-line:no-console
                        console.error('Failure to parse response', xhr);
                        return reject(rejection);
                    }
                } else {
                    return this.handleXhrOnLoadCaughtException(reject, xhr);
                }
            };
            xhr.onerror = (e) => {
                return this.handleXhrOnLoadCaughtException(reject, xhr);
            };
            xhr.send();
        });
    }

    public ajaxPost(url : string, postBody : any, queryObject? : any, type? : 'text' | 'json') : Promise<any> {
        return new Promise<any>((resolve, reject) => {
            const xhr = new this.XMLHttpRequest();
            xhr.open('POST', url + this.buildQueryString(queryObject));
            xhr.withCredentials = true;
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.setRequestHeader('X-CSRFToken', (window as any).CSRF_TOKEN);
            xhr.onload = () => {
                if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 201)) {
                    if (type === 'text') {
                        return resolve(xhr.response);
                    }
                    try {
                        return resolve(JSON.parse(xhr.response));
                    } catch (e) {
                        const rejection : IRestApiRejection = {
                            message: e.message,
                            status: xhr.status,
                            xhr,
                            error: e,
                            type: null,
                            content: null,
                        };
                        // tslint:disable-next-line:no-console
                        console.error('Failure to parse response', xhr);
                        return reject(rejection);
                    }
                } else {
                    return this.handleXhrOnLoadCaughtException(reject, xhr);
                }
            };
            xhr.onerror = (e) => {
                return this.handleXhrOnLoadCaughtException(reject, xhr);
            };
            xhr.send(JSON.stringify(postBody || undefined));
        });
    }

    public ajaxPostForm(url : string, formData? : FormData, queryObject? : any) : Promise<any> {
        // Lesman 4/24/2017
        // NOTE: you do not set the Content-Type on this request,
        // the browser will automatically encode the form data and
        // set the appropriate Content-Type. The encoding is browser
        // specific so you MUST allow the browser to set the header
        return new Promise<any>((resolve, reject) => {
            const xhr = new this.XMLHttpRequest();
            xhr.open('POST', url + this.buildQueryString(queryObject));
            xhr.withCredentials = true;
            xhr.setRequestHeader('X-CSRFToken', (window as any).CSRF_TOKEN);
            xhr.onload = () => this.handleXhrOnLoad(resolve, reject, xhr);
            xhr.onerror = (e) => {
                return this.handleXhrOnLoadCaughtException(reject, xhr);
            };
            xhr.send(formData);
        });
    }

    public ajaxPut(url : string, postBody : any, queryObject? : any) : Promise<any> {
        return new Promise<any>((resolve, reject) => {
            const xhr = new this.XMLHttpRequest();
            xhr.open('PUT', url + this.buildQueryString(queryObject));
            xhr.withCredentials = true;
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.setRequestHeader('X-CSRFToken', (window as any).CSRF_TOKEN);
            xhr.onload = () => this.handleXhrOnLoad(resolve, reject, xhr);
            xhr.onerror = (e) => {
                return this.handleXhrOnLoadCaughtException(reject, xhr);
            };
            xhr.send(JSON.stringify(postBody || undefined));
        });
    }

    public ajaxDelete(url : string, queryObject? : any) : Promise<any> {
        return new Promise<any>((resolve, reject) => {
            const xhr = new this.XMLHttpRequest();
            xhr.open('DELETE', url + this.buildQueryString(queryObject));
            xhr.withCredentials = true;
            xhr.setRequestHeader('X-CSRFToken', (window as any).CSRF_TOKEN);
            xhr.onload = () => this.handleXhrOnLoad(resolve, reject, xhr);
            xhr.onerror = (e) => {
                return this.handleXhrOnLoadCaughtException(reject, xhr);
            };
            xhr.send();
        });
    }

    private buildQueryString(queryObject : any) {
        const queryParameters : Array<string> = [];
        let queryString = '';
        Object.keys(queryObject || {}).forEach((key : string) => {
            queryParameters.push(key + '=' + encodeURIComponent(queryObject[key]));
        });
        if (queryParameters.length > 0) {
            queryString = '?' + queryParameters.join('&');
        }
        return queryString;
    }

    private handleXhrOnLoad(resolve : (value : any) => void, reject : (reason : any) => void, xhr : XMLHttpRequest) : void {
        if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 204)) {
            if (xhr.responseType === '' && xhr.responseText === '') {
                return resolve(undefined);
            }
            if (xhr.responseType === 'text') {
                return resolve(xhr.response);
            }
            try {
                const responseJSON = JSON.parse(xhr.response);

                if (typeof(responseJSON.success) !== 'undefined' && !responseJSON.success) {
                    if (responseJSON.error === 'user_required') {
                        window.location.reload();
                    }
                }

                return resolve(responseJSON);
            } catch (e) {
                const rejection : IRestApiRejection = {
                    message: e.message,
                    status: xhr.status,
                    xhr,
                    error: e,
                    type: null,
                    content: null,
                };
                // tslint:disable-next-line:no-console
                console.error('Failed to parse response', xhr);
                return reject(rejection);
            }
        } else {
            return this.handleXhrOnLoadCaughtException(reject, xhr);
        }
    }

    private handleXhrOnLoadCaughtException(reject : (reason : any) => void, xhr : XMLHttpRequest) : void {
        if (xhr.status === 401) {
            window.location.reload();
        }

        if (xhr.responseType === 'text' || xhr.responseType === '') {
            const rejection : IRestApiRejection = {
                message: xhr.responseText || xhr.statusText,
                status: xhr.status,
                xhr,
                error: null,
                type: null,
                content: null,
            };
            return reject(rejection);
        }
        try {
            const responseJSON = JSON.parse(xhr.response);

            const rejection : IRestApiRejection = {
                message: responseJSON.message || xhr.statusText,
                status: xhr.status,
                xhr,
                error: null,
                type: responseJSON.exception_type,
                content: responseJSON.content,
            };
            return reject(rejection);
        } catch (e) {
            const rejection : IRestApiRejection = {
                message: e.message,
                status: xhr.status,
                xhr,
                error: e,
                type: null,
                content: null,
            };
            // tslint:disable-next-line:no-console
            console.error('Failed to parse response', xhr);
            return reject(rejection);
        }
    }
}
