/**
 * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
 *
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 */

if (typeof window !== 'undefined') {
    (function () {
        // Console-polyfill. MIT license.
        // https://github.com/paulmillr/console-polyfill
        // Make it safe to do console.log() always.

        (window as any).console = window.console || {};
        const con = window.console;
        let prop, method;
        const empty = {};
        const dummy = function () {};
        const properties = 'memory'.split(',');
        const methods = (
            'assert,clear,count,debug,dir,dirxml,error,exception,group,' +
        'groupCollapsed,groupEnd,info,log,markTimeline,profile,profiles,profileEnd,' +
        'show,table,time,timeEnd,timeline,timelineEnd,timeStamp,trace,warn'
        ).split(',');

        while ((prop = properties.pop())) if (!(con as any)[prop]) (con as any)[prop] = empty;
        while ((method = methods.pop())) if (!(con as any)[method]) (con as any)[method] = dummy;
    })();
}

if (typeof window.HTMLCanvasElement !== 'undefined' && !window.HTMLCanvasElement.prototype.toBlob) {
    // http://code.google.com/p/chromium/issues/detail?id=67587#57
    Object.defineProperty(window.HTMLCanvasElement.prototype, 'toBlob', {

        value: function (callback: any, type: any, quality: any) {
            const bin = window.atob(this.toDataURL(type, quality).split(',')[1]);
            const len = bin.length;
            const len32 = len >> 2;
            const a8 = new Uint8Array(len);
            const a32 = new Uint32Array(a8.buffer, 0, len32);

            let j = 0;
            for (let i = 0; i < len32; i++) {
                a32[i] = (
                    bin.charCodeAt(j++) |
                    bin.charCodeAt(j++) << 8 |
                    bin.charCodeAt(j++) << 16 |
                    bin.charCodeAt(j++) << 24
                );
            }

            let tailLength = len & 3;

            while (tailLength--) {
                a8[j] = bin.charCodeAt(j++);
            }

            callback(new window.Blob([a8], { 'type': type || 'image/png' }));
        }

    });
}

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cbrt
Math.cbrt = Math.cbrt || function (x) {
    const y = Math.pow(Math.abs(x), 1 / 3);
    return x < 0 ? -y : y;
};

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sign
if (!Math.sign) {
    Math.sign = function (x) {
        // If x is NaN, the result is NaN.
        // If x is -0, the result is -0.
        // If x is +0, the result is +0.
        // If x is negative and not -0, the result is -1.
        // If x is positive and not +0, the result is +1.
        x = +x; // convert to a number
        if (x === 0 || isNaN(x)) {
            return Number(x);
        }
        return x > 0 ? 1 : -1;
    };
}

if (!Number.isInteger) {
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger
    Number.isInteger = function isInteger(nVal) {
        return typeof nVal === 'number' && isFinite(nVal) && nVal > -9007199254740992 && nVal < 9007199254740992 && Math.floor(nVal) === nVal;
    };
}

if (!Number.isNaN) {
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN
    Number.isNaN = function isNaN(value) {
        return value !== value; // eslint-disable-line no-self-compare
    };
}

if (!Object.assign) {
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
    Object.defineProperty(Object, 'assign', {

        enumerable: false,
        configurable: true,
        writable: true,

        value: function (target: any/* , firstSource */) {
            'use strict';
            if (target === undefined || target === null) { throw new TypeError('Cannot convert first argument to object'); }

            const to = Object(target);

            let hasPendingException = false;
            let pendingException;

            for (let i = 1; i < arguments.length; i++) {
                const nextSource = arguments[i];
                if (nextSource === undefined || nextSource === null) { continue; }

                const keysArray = Object.keys(Object(nextSource));
                for (let nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
                    const nextKey = keysArray[nextIndex];
                    try {
                        const desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
                        if (desc !== undefined && desc.enumerable) { to[nextKey] = nextSource[nextKey]; }
                    } catch (e) {
                        if (!hasPendingException) {
                            hasPendingException = true;
                            pendingException = e;
                        }
                    }
                }

                if (hasPendingException) { throw pendingException; }
            }

            return to;
        }

    });
}

if (!String.prototype.startsWith) {
    /* ! https://mths.be/startswith v0.2.0 by @mathias */

    (function () {
        'use strict'; // needed to support `apply`/`call` with `undefined`/`null`
        const defineProperty = (function () {
        // IE 8 only supports `Object.defineProperty` on DOM elements
            let result;
            try {
                const object = {};
                const $defineProperty = Object.defineProperty;
                result = $defineProperty(object, object as any, object) && $defineProperty;
            } catch (error) {} // eslint-disable-line no-empty
            return result;
        }());
        const toString = {}.toString;
        const startsWith = function (this: any, search: any) {
            if (this === null) {
                throw TypeError();
            }
            const string = String(this);
            if (search && toString.call(search) === '[object RegExp]') {
                throw TypeError();
            }
            const stringLength = string.length;
            const searchString = String(search);
            const searchLength = searchString.length;
            const position = arguments.length > 1 ? arguments[1] : undefined;
            // `ToInteger`
            let pos = position ? Number(position) : 0;
            if (Number.isNaN(pos)) {
                pos = 0;
            }
            const start = Math.min(Math.max(pos, 0), stringLength);
            // Avoid the `indexOf` call if no match is possible
            if (searchLength + start > stringLength) {
                return false;
            }
            let index = -1;
            while (++index < searchLength) {
                if (string.charCodeAt(start + index) !== searchString.charCodeAt(index)) {
                    return false;
                }
            }
            return true;
        };
        if (defineProperty) {
            defineProperty(String.prototype, 'startsWith', {
                'value': startsWith,
                'configurable': true,
                'writable': true
            });
        } else {
        // eslint-disable-next-line no-extend-native
            String.prototype.startsWith = startsWith;
        }
    }());
}

if (!String.prototype.endsWith) {
    // eslint-disable-next-line no-extend-native
    String.prototype.endsWith = function (searchString, position) {
        const subjectString = this.toString();
        if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) {
            position = subjectString.length;
        }
        position -= searchString.length;
        const lastIndex = subjectString.indexOf(searchString, position);
        return lastIndex !== -1 && lastIndex === position;
    };
}

if (!String.prototype.repeat) {
    // eslint-disable-next-line no-extend-native
    String.prototype.repeat = function (count) {
        'use strict';
        if (this === null) {
            throw new TypeError('can\'t convert ' + this + ' to object');
        }
        let str = '' + this;
        count = +count;
        if (Number.isNaN(count)) {
            count = 0;
        }
        if (count < 0) {
            throw new RangeError('repeat count must be non-negative');
        }
        if (count === Infinity) {
            throw new RangeError('repeat count must be less than infinity');
        }
        count = Math.floor(count);
        if (str.length === 0 || count === 0) {
            return '';
        }
        // Ensuring count is a 31-bit integer allows us to heavily optimize the
        // main part. But anyway, most current (August 2014) browsers can't handle
        // strings 1 << 28 chars or longer, so:
        if (str.length * count >= 1 << 28) {
            throw new RangeError('repeat count must not overflow maximum string size');
        }
        let rpt = '';
        for (;;) {
            if ((count & 1) === 1) {
                rpt += str;
            }
            count >>>= 1;
            if (count === 0) {
                break;
            }
            str += str;
        }
        // Could we try:
        // return Array(count + 1).join(this);
        return rpt;
    };
}

if (!String.prototype.includes) {
    // eslint-disable-next-line no-extend-native
    String.prototype.includes = function (search, start) {
        'use strict';
        if (typeof start !== 'number') {
            start = 0;
        }

        if (start + search.length > this.length) {
            return false;
        } else {
            return this.indexOf(search, start) !== -1;
        }
    };
}

if (!Array.prototype.includes) {
    // eslint-disable-next-line no-extend-native
    Array.prototype.includes = function (searchElement /* , fromIndex */) {
        'use strict';
        if (this == null) {
            throw new TypeError('Array.prototype.includes called on null or undefined');
        }

        const O = Object(this);
        const len = parseInt(O.length, 10) || 0;
        if (len === 0) {
            return false;
        }
        const n = parseInt(arguments[1], 10) || 0;
        let k;
        if (n >= 0) {
            k = n;
        } else {
            k = len + n;
            if (k < 0) { k = 0; }
        }
        let currentElement;
        while (k < len) {
            currentElement = O[k];
            if (searchElement === currentElement ||
            (Number.isNaN(searchElement) && Number.isNaN(currentElement))
            ) {
                return true;
            }
            k++;
        }
        return false;
    };
}

// Production steps of ECMA-262, Edition 6, 22.1.2.1
// Reference: https://people.mozilla.org/~jorendorff/es6-draft.html#sec-array.from
if (!Array.from) {
    Array.from = (function () {
        const toStr = Object.prototype.toString;
        const isCallable = function (fn: any) {
            return typeof fn === 'function' || toStr.call(fn) === '[object Function]';
        };
        const toInteger = function (value: any) {
            const number = Number(value);
            if (isNaN(number)) { return 0; }
            if (number === 0 || !isFinite(number)) { return number; }
            return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number));
        };
        const maxSafeInteger = Math.pow(2, 53) - 1;
        const toLength = function (value: any) {
            const len = toInteger(value);
            return Math.min(Math.max(len, 0), maxSafeInteger);
        };

        // The length property of the from method is 1.
        return function from(this: any, arrayLike: any/* , mapFn, thisArg */) {
        // 1. Let C be the this value.
            const C = this;

            // 2. Let items be ToObject(arrayLike).
            const items = Object(arrayLike);

            // 3. ReturnIfAbrupt(items).
            if (arrayLike == null) {
                throw new TypeError('Array.from requires an array-like object - not null or undefined');
            }

            // 4. If mapfn is undefined, then let mapping be false.
            const mapFn = arguments.length > 1 ? arguments[1] : void undefined;
            let T;
            if (typeof mapFn !== 'undefined') {
                // 5. else
                // 5. a If IsCallable(mapfn) is false, throw a TypeError exception.
                if (!isCallable(mapFn)) {
                    throw new TypeError('Array.from: when provided, the second argument must be a function');
                }

                // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined.
                if (arguments.length > 2) {
                    T = arguments[2];
                }
            }

            // 10. Let lenValue be Get(items, "length").
            // 11. Let len be ToLength(lenValue).
            const len = toLength(items.length);

            // 13. If IsConstructor(C) is true, then
            // 13. a. Let A be the result of calling the [[Construct]] internal method of C with an argument list containing the single item len.
            // 14. a. Else, Let A be ArrayCreate(len).
            const A = isCallable(C) ? Object(new C(len)) : new Array(len);

            // 16. Let k be 0.
            let k = 0;
            // 17. Repeat, while k < len… (also steps a - h)
            let kValue;
            while (k < len) {
                kValue = items[k];
                if (mapFn) {
                    A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k);
                } else {
                    A[k] = kValue;
                }
                k += 1;
            }
            // 18. Let putStatus be Put(A, "length", len, true).
            A.length = len;
            // 20. Return A.
            return A;
        };
    }());
}

if (typeof window !== 'undefined') {
    (function () {
        // http://paulirish.com/2011/requestanimationframe-for-smart-animating/
        // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating

        // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel

        // MIT license

        let lastTime = 0;
        const vendors = ['ms', 'moz', 'webkit', 'o'];

        for (let x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
            window.requestAnimationFrame = (
                (window as any)[vendors[x] + 'RequestAnimationFrame']
            );

            window.cancelAnimationFrame = (
                (window as any)[vendors[x] + 'CancelAnimationFrame'] ||
                (window as any)[vendors[x] + 'CancelRequestAnimationFrame']
            );
        }

        if (!window.requestAnimationFrame) {
            window.requestAnimationFrame = function (callback/* , element */) {
                const currTime = new Date().getTime();
                const timeToCall = Math.max(0, 16 - (currTime - lastTime));

                const id = window.setTimeout(function () {
                    const time = currTime + timeToCall;
                    callback(time);
                }, timeToCall);

                lastTime = currTime + timeToCall;

                return id;
            };
        }

        if (!window.cancelAnimationFrame) {
            window.cancelAnimationFrame = function (id) {
                clearTimeout(id);
            };
        }
    }());
}

if (Function.prototype.name === undefined && Object.defineProperty !== undefined) {
    // Missing in IE9-11.
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name

    // eslint-disable-next-line no-extend-native
    Object.defineProperty(Function.prototype, 'name', {

        get: function () {
            return this.toString().match(/^\s*function\s*(\S*)\s*\(/)[1];
        }

    });
}

if (typeof window !== 'undefined') {
    if (window.performance === undefined) {
        /* global self */
        (window as any).performance = {};
    }

    if (window.performance.now === undefined) {
        (function () {
            const start = Date.now();

            window.performance.now = function () {
                return Date.now() - start;
            };
        })();
    }
}

if (Object.defineProperty !== undefined) {
    // Missing in IE < 13
    // MIT license
    // Copyright (c) 2016 Financial Times
    // https://github.com/Financial-Times/polyfill-service
    if (Number.MAX_SAFE_INTEGER === undefined) {
        Object.defineProperty(Number, 'MAX_SAFE_INTEGER', {
            enumerable: false,
            configurable: false,
            writable: false,
            value: Math.pow(2, 53) - 1
        });
    }
    if (Number.MIN_SAFE_INTEGER === undefined) {
        Object.defineProperty(Number, 'MIN_SAFE_INTEGER', {
            enumerable: false,
            configurable: false,
            writable: false,
            value: -(Math.pow(2, 53) - 1)
        });
    }
}

if (!Object.entries) {
    Object.entries = function (obj: any) {
        const ownProps = Object.keys(obj);
        let i = ownProps.length;
        const resArray = new Array(i); // preallocate the Array
        while (i--)
            resArray[i] = [ownProps[i], obj[ownProps[i]]];

        return resArray;
    };
}

// from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
// https://tc39.github.io/ecma262/#sec-array.prototype.find
if (!Array.prototype.find) {
    Object.defineProperty(Array.prototype, 'find', {
        value: function (predicate: any) {
            // 1. Let O be ? ToObject(this value).
            if (this == null) {
                throw TypeError('"this" is null or not defined');
            }

            const o = Object(this);

            // 2. Let len be ? ToLength(? Get(O, "length")).
            const len = o.length >>> 0;

            // 3. If IsCallable(predicate) is false, throw a TypeError exception.
            if (typeof predicate !== 'function') {
                throw TypeError('predicate must be a function');
            }

            // 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
            const thisArg = arguments[1];

            // 5. Let k be 0.
            let k = 0;

            // 6. Repeat, while k < len
            while (k < len) {
                // a. Let Pk be ! ToString(k).
                // b. Let kValue be ? Get(O, Pk).
                // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
                // d. If testResult is true, return kValue.
                const kValue = o[k];
                if (predicate.call(thisArg, kValue, k, o)) {
                    return kValue;
                }
                // e. Increase k by 1.
                k++;
            }

            // 7. Return undefined.
            return undefined;
        },
        configurable: true,
        writable: true
    });
}

// from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill
if (!Array.prototype.fill) {
    Object.defineProperty(Array.prototype, 'fill', {
        value: function (value: any) {

            // Steps 1-2.
            if (this == null) {
                throw new TypeError('this is null or not defined');
            }

            const O = Object(this);

            // Steps 3-5.
            const len = O.length >>> 0;

            // Steps 6-7.
            const start = arguments[1];
            const relativeStart = start >> 0;

            // Step 8.
            let k = relativeStart < 0 ?
                Math.max(len + relativeStart, 0) :
                Math.min(relativeStart, len);

            // Steps 9-10.
            const end = arguments[2];
            const relativeEnd = end === undefined ?
                len : end >> 0;

            // Step 11.
            const finalValue = relativeEnd < 0 ?
                Math.max(len + relativeEnd, 0) :
                Math.min(relativeEnd, len);

            // Step 12.
            while (k < finalValue) {
                O[k] = value;
                k++;
            }

            // Step 13.
            return O;
        }
    });
}

if (!Array.prototype.copyWithin) {
    Object.defineProperty(Array.prototype, 'copyWithin', {
        value: function (target: any, start: any/* , end*/) {
            // Steps 1-2.
            if (this == null) {
                throw new TypeError('this is null or not defined');
            }

            const O = Object(this);

            // Steps 3-5.
            const len = O.length >>> 0;

            // Steps 6-8.
            const relativeTarget = target >> 0;

            let to = relativeTarget < 0 ?
                Math.max(len + relativeTarget, 0) :
                Math.min(relativeTarget, len);

            // Steps 9-11.
            const relativeStart = start >> 0;

            let from = relativeStart < 0 ?
                Math.max(len + relativeStart, 0) :
                Math.min(relativeStart, len);

            // Steps 12-14.
            const end = arguments[2];
            const relativeEnd = end === undefined ? len : end >> 0;

            const final = relativeEnd < 0 ?
                Math.max(len + relativeEnd, 0) :
                Math.min(relativeEnd, len);

            // Step 15.
            let count = Math.min(final - from, len - to);

            // Steps 16-17.
            let direction = 1;

            if (from < to && to < (from + count)) {
                direction = -1;
                from += count - 1;
                to += count - 1;
            }

            // Step 18.
            while (count > 0) {
                if (from in O) {
                    O[to] = O[from];
                } else {
                    delete O[to];
                }

                from += direction;
                to += direction;
                count--;
            }

            // Step 19.
            return O;
        },
        configurable: true,
        writable: true
    });
}

if (!Int8Array.prototype.copyWithin) {
    Object.defineProperty(Int8Array.prototype, 'copyWithin', { value: Array.prototype.copyWithin });
}

if (!Int16Array.prototype.copyWithin) {
    Object.defineProperty(Int16Array.prototype, 'copyWithin', { value: Array.prototype.copyWithin });
}

if (!Int32Array.prototype.copyWithin) {
    Object.defineProperty(Int32Array.prototype, 'copyWithin', { value: Array.prototype.copyWithin });
}

if (!Uint8Array.prototype.copyWithin) {
    Object.defineProperty(Uint8Array.prototype, 'copyWithin', { value: Array.prototype.copyWithin });
}

if (!Uint16Array.prototype.copyWithin) {
    Object.defineProperty(Uint16Array.prototype, 'copyWithin', { value: Array.prototype.copyWithin });
}

if (!Uint32Array.prototype.copyWithin) {
    Object.defineProperty(Uint32Array.prototype, 'copyWithin', { value: Array.prototype.copyWithin });
}

if (!Float32Array.prototype.copyWithin) {
    Object.defineProperty(Float32Array.prototype, 'copyWithin', { value: Array.prototype.copyWithin });
}

if (!Float64Array.prototype.copyWithin) {
    Object.defineProperty(Float64Array.prototype, 'copyWithin', { value: Array.prototype.copyWithin });
}