const $i = document.getElementById.bind(document);
const $c = document.getElementsByClassName.bind(document);
const $q = document.querySelector.bind(document);
const $qa = document.querySelectorAll.bind(document);
const $t = document.createTextNode.bind(document);

const $r = function(element) {
    if(element && element.parentNode)
        element.parentNode.removeChild(element);
};

const $ri = function(name) {
    $r($i(name));
};

const $rq = function(query) {
    $r($q(query));
};

const $ib = function(ref, elem) {
    ref.parentNode.insertBefore(elem, ref);
};

const $rc = function(element) {
    while(element.lastChild)
        element.removeChild(element.lastChild);
};

const $e = function(info, attrs, child, created) {
    info = info || {};

    if(typeof info === 'string') {
        info = {tag: info};
        if(attrs)
            info.attrs = attrs;
        if(child)
            info.child = child;
        if(created)
            info.created = created;
    }

    const elem = document.createElement(info.tag || 'div');

    if(info.attrs) {
        const attrs = info.attrs;

        for(let key in attrs) {
            const attr = attrs[key];
            if(attr === undefined || attr === null)
                continue;

            switch(typeof attr) {
                case 'function':
                    if(key.substring(0, 2) === 'on')
                        key = key.substring(2).toLowerCase();
                    elem.addEventListener(key, attr);
                    break;

                case 'object':
                    if(attr instanceof Array) {
                        if(key === 'class')
                            key = 'classList';

                        const prop = elem[key];
                        let addFunc = null;

                        if(prop instanceof Array)
                            addFunc = prop.push.bind(prop);
                        else if(prop instanceof DOMTokenList)
                            addFunc = prop.add.bind(prop);

                        if(addFunc !== null) {
                            for(let j = 0; j < attr.length; ++j)
                                addFunc(attr[j]);
                        } else {
                            if(key === 'classList')
                                key = 'class';
                            elem.setAttribute(key, attr.toString());
                        }
                    } else {
                        for(const attrKey in attr)
                            elem[key][attrKey] = attr[attrKey];
                    }
                    break;

                case 'boolean':
                    if(attr)
                        elem.setAttribute(key, '');
                    break;

                default:
                    if(key === 'className')
                        key = 'class';
                    elem.setAttribute(key, attr.toString());
                    break;
            }
        }
    }

    if(info.child) {
        let children = info.child;

        if(!Array.isArray(children))
            children = [children];

        for(const child of children) {
            switch(typeof child) {
                case 'string':
                    elem.appendChild($t(child));
                    break;

                case 'object':
                    if(child instanceof Element)
                        elem.appendChild(child);
                    else if(child.getElement) {
                        const childElem = child.getElement();
                        if(childElem instanceof Element)
                            elem.appendChild(childElem);
                        else
                            elem.appendChild($e(child));
                    } else
                        elem.appendChild($e(child));
                    break;

                default:
                    elem.appendChild($t(child.toString()));
                    break;
            }
        }
    }

    if(info.created)
        info.created(elem);

    return elem;
};
const $er = (type, props, ...children) => $e({ tag: type, attrs: props, child: children });

const $ar = function(array, index) {
    array.splice(index, 1);
};
const $ari = function(array, item) {
    let index;
    while(array.length > 0 && (index = array.indexOf(item)) >= 0)
        $ar(array, index);
};
const $arf = function(array, predicate) {
    let index;
    while(array.length > 0 && (index = array.findIndex(predicate)) >= 0)
        $ar(array, index);
};

const $as = function(array) {
    if(array.length < 2)
        return;

    for(let i = array.length - 1; i > 0; --i) {
        let j = Math.floor(Math.random() * (i + 1)),
            tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }
};

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';

        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(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) => {
            let responseHeaders = undefined;

            xhr.onload = ev => resolve({
                status: xhr.status,
                body: () => xhr.response,
                text: () => xhr.responseText,
                headers: () => {
                    if(responseHeaders !== undefined)
                        return responseHeaders;

                    responseHeaders = new Map;

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

                    return responseHeaders;
                },
                xhr: xhr,
                ev: ev,
            });

            xhr.onabort = ev => reject({
                abort: true,
                xhr: xhr,
                ev: ev,
            });

            xhr.onerror = ev => reject({
                abort: false,
                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),
    };
})();
