/* @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0 */

'use strict';
// Contains only auxiliary methods
// May be included and executed unlimited number of times without any consequences

// Polyfills for IE11
Array.prototype.find = Array.prototype.find || function (condition) {
    return this.filter(condition)[0];
};
Array.from = Array.from || function (source) {
    return Array.prototype.slice.call(source);
};
NodeList.prototype.forEach = NodeList.prototype.forEach || function (callback) {
    Array.from(this).forEach(callback);
};
String.prototype.includes = String.prototype.includes || function (searchString) {
    return this.indexOf(searchString) >= 0;
};
String.prototype.startsWith = String.prototype.startsWith || function (prefix) {
    return this.substr(0, prefix.length) === prefix;
};
Math.sign = Math.sign || function(x) {
    x = +x;
    if (!x) return x; // 0 and NaN
    return x > 0 ? 1 : -1;
};
if (!window.hasOwnProperty('HTMLDetailsElement') && !window.hasOwnProperty('mockHTMLDetailsElement')) {
    window.mockHTMLDetailsElement = true;
    const style = 'details:not([open]) > :not(summary) {display: none}';
    document.head.appendChild(document.createElement('style')).textContent = style;

    addEventListener('click', function (e) {
        if (e.target.nodeName !== 'SUMMARY') return;
        const details = e.target.parentElement;
        if (details.hasAttribute('open'))
            details.removeAttribute('open');
        else
            details.setAttribute('open', '');
    });
}

// Monstrous global variable for handy code
// Includes: clamp, xhr, storage.{get,set,remove}
window.helpers = window.helpers || {
    /**
     * https://en.wikipedia.org/wiki/Clamping_(graphics)
     * @param {Number} num Source number
     * @param {Number} min Low border
     * @param {Number} max High border
     * @returns {Number} Clamped value
     */
    clamp: function (num, min, max) {
        if (max < min) {
            var t = max; max = min; min = t; // swap max and min
        }

        if (max < num)
            return max;
        if (min > num)
            return min;
        return num;
    },

    /** @private */
    _xhr: function (method, url, options, callbacks) {
        const xhr = new XMLHttpRequest();
        xhr.open(method, url);

        // Default options
        xhr.responseType = 'json';
        xhr.timeout = 10000;
        // Default options redefining
        if (options.responseType)
            xhr.responseType = options.responseType;
        if (options.timeout)
            xhr.timeout = options.timeout;

        if (method === 'POST')
            xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

        // better than onreadystatechange because of 404 codes https://stackoverflow.com/a/36182963
        xhr.onloadend = function () {
            if (xhr.status === 200) {
                if (callbacks.on200) {
                    // fix for IE11. It doesn't convert response to JSON
                    if (xhr.responseType === '' && typeof(xhr.response) === 'string')
                        callbacks.on200(JSON.parse(xhr.response));
                    else
                        callbacks.on200(xhr.response);
                }
            } else {
                // handled by onerror
                if (xhr.status === 0) return;

                if (callbacks.onNon200)
                    callbacks.onNon200(xhr);
            }
        };

        xhr.ontimeout = function () {
            if (callbacks.onTimeout)
                callbacks.onTimeout(xhr);
        };

        xhr.onerror = function () {
            if (callbacks.onError)
                callbacks.onError(xhr);
        };

        if (options.payload)
            xhr.send(options.payload);
        else
            xhr.send();
    },
    /** @private */
    _xhrRetry: function(method, url, options, callbacks) {
        if (options.retries <= 0) {
            console.warn('Failed to pull', options.entity_name);
            if (callbacks.onTotalFail)
                callbacks.onTotalFail();
            return;
        }
        helpers._xhr(method, url, options, callbacks);
    },
    /**
     * @callback callbackXhrOn200
     * @param {Object} response - xhr.response
     */
    /**
     * @callback callbackXhrError
     * @param {XMLHttpRequest} xhr
     */
    /**
     * @param {'GET'|'POST'} method - 'GET' or 'POST'
     * @param {String} url - URL to send request to
     * @param {Object} options - other XHR options
     * @param {XMLHttpRequestBodyInit} [options.payload=null] - payload for POST-requests
     * @param {'arraybuffer'|'blob'|'document'|'json'|'text'} [options.responseType=json]
     * @param {Number} [options.timeout=10000]
     * @param {Number} [options.retries=1]
     * @param {String} [options.entity_name='unknown'] - string to log
     * @param {Number} [options.retry_timeout=1000]
     * @param {Object} callbacks - functions to execute on events fired
     * @param {callbackXhrOn200} [callbacks.on200]
     * @param {callbackXhrError} [callbacks.onNon200]
     * @param {callbackXhrError} [callbacks.onTimeout]
     * @param {callbackXhrError} [callbacks.onError]
     * @param {callbackXhrError} [callbacks.onTotalFail] - if failed after all retries
     */
     xhr: function(method, url, options, callbacks) {
        if (!options.retries || options.retries <= 1) {
            helpers._xhr(method, url, options, callbacks);
            return;
        }

        if (!options.entity_name) options.entity_name = 'unknown';
        if (!options.retry_timeout) options.retry_timeout = 1000;
        const retries_total = options.retries;
        let currentTry = 1;

        const retry = function () {
            console.warn('Pulling ' + options.entity_name + ' failed... ' + (currentTry++) + '/' + retries_total);
            setTimeout(function () {
                options.retries--;
                helpers._xhrRetry(method, url, options, callbacks);
            }, options.retry_timeout);
        };

        // Pack retry() call into error handlers
        callbacks._onError = callbacks.onError;
        callbacks.onError = function (xhr) {
            if (callbacks._onError)
                callbacks._onError(xhr);
            retry();
        };
        callbacks._onTimeout = callbacks.onTimeout;
        callbacks.onTimeout = function (xhr) {
            if (callbacks._onTimeout)
                callbacks._onTimeout(xhr);
            retry();
        };

        helpers._xhrRetry(method, url, options, callbacks);
    },

    /**
     * @typedef {Object} invidiousStorage
     * @property {(key:String) => Object} get
     * @property {(key:String, value:Object)} set
     * @property {(key:String)} remove
     */

    /**
     * Universal storage, stores and returns JS objects. Uses inside localStorage or cookies
     * @type {invidiousStorage}
     */
    storage: (function () {
        // access to localStorage throws exception in Tor Browser, so try is needed
        let localStorageIsUsable = false;
        try{localStorageIsUsable = !!localStorage.setItem;}catch(e){}

        if (localStorageIsUsable) {
            return {
                get: function (key) {
                    if (!localStorage[key]) return;
                    try {
                        return JSON.parse(decodeURIComponent(localStorage[key]));
                    } catch(e) {
                        // Erase non parsable value
                        helpers.storage.remove(key);
                    }
                },
                set: function (key, value) { localStorage[key] = encodeURIComponent(JSON.stringify(value)); },
                remove: function (key) { localStorage.removeItem(key); }
            };
        }

        // TODO: fire 'storage' event for cookies
        console.info('Storage: localStorage is disabled or unaccessible. Cookies used as fallback');
        return {
            get: function (key) {
                const cookiePrefix = key + '=';
                function findCallback(cookie) {return cookie.startsWith(cookiePrefix);}
                const matchedCookie = document.cookie.split('; ').find(findCallback);
                if (matchedCookie) {
                    const cookieBody = matchedCookie.replace(cookiePrefix, '');
                    if (cookieBody.length === 0) return;
                    try {
                        return JSON.parse(decodeURIComponent(cookieBody));
                    } catch(e) {
                        // Erase non parsable value
                        helpers.storage.remove(key);
                    }
                }
            },
            set: function (key, value) {
                const cookie_data = encodeURIComponent(JSON.stringify(value));

                // Set expiration in 2 year
                const date = new Date();
                date.setFullYear(date.getFullYear()+2);

                document.cookie = key + '=' + cookie_data + '; expires=' + date.toGMTString();
            },
            remove: function (key) {
                document.cookie = key + '=; Max-Age=0';
            }
        };
    })()
};

/* @license-end */
