#include csrf.js

const $x = (function() {
    const send = function(method, url, options, body) {
        if(options === undefined)
            options = {};
        else if(typeof options !== 'object')
            throw 'options must be undefined or an object';

        Object.freeze(options);

        const xhr = new XMLHttpRequest;
        const requestHeaders = new Map;

        if('headers' in options && typeof options.headers === 'object')
            for(const name in options.headers)
                if(options.headers.hasOwnProperty(name))
                    requestHeaders.set(name.toLowerCase(), options.headers[name]);

        if(options.csrf)
            requestHeaders.set('x-csrf-token', MszCSRF.token);

        if(typeof options.download === 'function') {
            xhr.onloadstart = ev => options.download(ev);
            xhr.onprogress = ev => options.download(ev);
            xhr.onloadend = ev => options.download(ev);
        }

        if(typeof options.upload === 'function') {
            xhr.upload.onloadstart = ev => options.upload(ev);
            xhr.upload.onprogress = ev => options.upload(ev);
            xhr.upload.onloadend = ev => options.upload(ev);
        }

        if(options.authed)
            xhr.withCredentials = true;

        if(typeof options.timeout === 'number')
            xhr.timeout = options.timeout;

        if(typeof options.type === 'string')
            xhr.responseType = options.type;

        if(typeof options.abort === 'function')
            options.abort(() => xhr.abort());

        if(typeof options.xhr === 'function')
            options.xhr(() => xhr);

        if(typeof body === 'object') {
            if(body instanceof URLSearchParams) {
                requestHeaders.set('content-type', 'application/x-www-form-urlencoded');
            } else if(body instanceof FormData) {
                // content-type is implicitly set
            } else if(body instanceof Blob || body instanceof ArrayBuffer || body instanceof DataView) {
                if(!requestHeaders.has('content-type'))
                    requestHeaders.set('content-type', 'application/octet-stream');
            } else if(!requestHeaders.has('content-type')) {
                const bodyParts = [];
                for(const name in body)
                    if(body.hasOwnProperty(name))
                        bodyParts.push(encodeURIComponent(name) + '=' + encodeURIComponent(body[name]));
                body = bodyParts.join('&');
                requestHeaders.set('content-type', 'application/x-www-form-urlencoded');
            }
        }

        return new Promise((resolve, reject) => {
            xhr.onload = ev => {
                const headers = (headersString => {
                    const headers = new Map;

                    const raw = headersString.trim().split(/[\r\n]+/);
                    for(const name in raw)
                        if(raw.hasOwnProperty(name)) {
                            const parts = raw[name].split(': ');
                            headers.set(parts.shift(), parts.join(': '));
                        }

                    return headers;
                })(xhr.getAllResponseHeaders());

                if(options.csrf && headers.has('x-csrf-token'))
                    MszCSRF.token = headers.get('x-csrf-token');

                resolve({
                    get ev() { return ev; },
                    get xhr() { return xhr; },

                    get status() { return xhr.status; },
                    get headers() { return headers; },
                    get body() { return xhr.response; },
                    get text() { return xhr.responseText; },
                });
            };

            xhr.onerror = ev => reject({
                xhr: xhr,
                ev: ev,
            });

            xhr.open(method, url);
            for(const [name, value] of requestHeaders)
                xhr.setRequestHeader(name, value);
            xhr.send(body);
        });
    };

    return {
        send: send,
        get: (url, options, body) => send('GET', url, options, body),
        post: (url, options, body) => send('POST', url, options, body),
        delete: (url, options, body) => send('DELETE', url, options, body),
        patch: (url, options, body) => send('PATCH', url, options, body),
        put: (url, options, body) => send('PUT', url, options, body),
    };
})();