Utilities =
{
    _parse: function(str, sep)
    {
        var output = {};
        str.split(sep).forEach(function(part) {
            var item = part.split("=");
            var value = decodeURIComponent(item[1]);
            if (value[0] == "'" )
                output[item[0]] = value.substr(1, value.length - 2);
            else
                output[item[0]] = value;
          });
        return output;
    },

    parseParameters: function()
    {
        return this._parse(window.location.search.substr(1), "&");
    },

    parseArguments: function(str)
    {
        return this._parse(str, " ");
    },

    extendObject: function(obj1, obj2)
    {
        for (var attrname in obj2)
            obj1[attrname] = obj2[attrname];
        return obj1;
    },

    copyObject: function(obj)
    {
        return this.extendObject({}, obj);
    },

    mergeObjects: function(obj1, obj2)
    {
        return this.extendObject(this.copyObject(obj1), obj2);
    },

    createClass: function(classConstructor, classMethods)
    {
        classConstructor.prototype = classMethods;
        return classConstructor;
    },

    createSubclass: function(superclass, classConstructor, classMethods)
    {
        classConstructor.prototype = Object.create(superclass.prototype);
        classConstructor.prototype.constructor = classConstructor;
        if (classMethods)
            Utilities.extendObject(classConstructor.prototype, classMethods);
        return classConstructor;
    },

    createElement: function(name, attrs, parentElement)
    {
        var element = document.createElement(name);

        for (var key in attrs)
            element.setAttribute(key, attrs[key]);

        parentElement.appendChild(element);
        return element;
    },

    createSVGElement: function(name, attrs, xlinkAttrs, parentElement)
    {
        const svgNamespace = "http://www.w3.org/2000/svg";
        const xlinkNamespace = "http://www.w3.org/1999/xlink";

        var element = document.createElementNS(svgNamespace, name);

        for (var key in attrs)
            element.setAttribute(key, attrs[key]);

        for (var key in xlinkAttrs)
            element.setAttributeNS(xlinkNamespace, key, xlinkAttrs[key]);

        parentElement.appendChild(element);
        return element;
    },

    browserPrefix: function()
    {
        // Get the HTML element's CSSStyleDeclaration
        var styles = window.getComputedStyle(document.documentElement, '');

        // Convert the styles list to an array
        var stylesArray = Array.prototype.slice.call(styles);

        // Concatenate all the styles in one big string
        var stylesString = stylesArray.join('');

        // Search the styles string for a known prefix type, settle on Opera if none is found.
        var prefixes = stylesString.match(/-(moz|webkit|ms)-/) || (styles.OLink === '' && ['', 'o']);

        // prefixes has two elements; e.g. for webkit it has ['-webkit-', 'webkit'];
        var prefix = prefixes[1];

        // Have 'O' before 'Moz' in the string so it is matched first.
        var dom = ('WebKit|O|Moz|MS').match(new RegExp(prefix, 'i'))[0];

        // Return all the required prefixes.
        return {
            dom: dom,
            lowercase: prefix,
            css: '-' + prefix + '-',
            js: prefix[0].toUpperCase() + prefix.substr(1)
        };
    },

    setElementPrefixedProperty: function(element, property, value)
    {
        element.style[property] = element.style[this.browserPrefix().js + property[0].toUpperCase() + property.substr(1)] = value;
    },

    stripNonASCIICharacters: function(inputString)
    {
        return inputString.replace(/[ .,]/g, '');
    },

    convertObjectToQueryString: function(object)
    {
        var queryString = [];
        for (var property in object) {
            if (object.hasOwnProperty(property))
                queryString.push(encodeURIComponent(property) + "=" + encodeURIComponent(object[property]));
        }
        return "?" + queryString.join("&");
    },

    convertQueryStringToObject: function(queryString)
    {
        queryString = queryString.substring(1);
        if (!queryString)
            return null;

        var object = {};
        queryString.split("&").forEach(function(parameter) {
            var components = parameter.split("=");
            object[components[0]] = components[1];
        });
        return object;
    },

    progressValue: function(value, min, max)
    {
        return (value - min) / (max - min);
    },

    lerp: function(value, min, max)
    {
        return min + (max - min) * value;
    },

    toFixedNumber: function(number, precision)
    {
        if (number.toFixed)
            return Number(number.toFixed(precision));
        return number;
    }
};

Array.prototype.swap = function(i, j)
{
    var t = this[i];
    this[i] = this[j];
    this[j] = t;
    return this;
}

if (!Array.prototype.fill) {
    Array.prototype.fill = function(value) {
        if (this == null)
            throw new TypeError('Array.prototype.fill called on null or undefined');

        var object = Object(this);
        var len = parseInt(object.length, 10);
        var start = arguments[1];
        var relativeStart = parseInt(start, 10) || 0;
        var k = relativeStart < 0 ? Math.max(len + relativeStart, 0) : Math.min(relativeStart, len);
        var end = arguments[2];
        var relativeEnd = end === undefined ? len : (parseInt(end) || 0) ;
        var final = relativeEnd < 0 ? Math.max(len + relativeEnd, 0) : Math.min(relativeEnd, len);

        for (; k < final; k++)
            object[k] = value;

        return object;
    };
}

if (!Array.prototype.find) {
    Array.prototype.find = function(predicate) {
        if (this == null)
            throw new TypeError('Array.prototype.find called on null or undefined');
        if (typeof predicate !== 'function')
            throw new TypeError('predicate must be a function');

        var list = Object(this);
        var length = list.length >>> 0;
        var thisArg = arguments[1];
        var value;

        for (var i = 0; i < length; i++) {
            value = list[i];
            if (predicate.call(thisArg, value, i, list))
                return value;
        }
        return undefined;
    };
}

Array.prototype.shuffle = function()
{
    for (var index = this.length - 1; index >= 0; --index) {
        var randomIndex = Math.floor(Math.random() * (index + 1));
        this.swap(index, randomIndex);
    }
    return this;
}

Point = Utilities.createClass(
    function(x, y)
    {
        this.x = x;
        this.y = y;
    }, {

    // Used when the point object is used as a size object.
    get width()
    {
        return this.x;
    },

    // Used when the point object is used as a size object.
    get height()
    {
        return this.y;
    },

    // Used when the point object is used as a size object.
    get center()
    {
        return new Point(this.x / 2, this.y / 2);
    },

    str: function()
    {
        return "x = " + this.x + ", y = " + this.y;
    },

    add: function(other)
    {
        if(isNaN(other.x))
            return new Point(this.x + other, this.y + other);
        return new Point(this.x + other.x, this.y + other.y);
    },

    subtract: function(other)
    {
        if(isNaN(other.x))
            return new Point(this.x - other, this.y - other);
        return new Point(this.x - other.x, this.y - other.y);
    },

    multiply: function(other)
    {
        if(isNaN(other.x))
            return new Point(this.x * other, this.y * other);
        return new Point(this.x * other.x, this.y * other.y);
    },

    move: function(angle, velocity, timeDelta)
    {
        return this.add(Point.pointOnCircle(angle, velocity * (timeDelta / 1000)));
    },

    length: function() {
        return Math.sqrt( this.x * this.x + this.y * this.y );
    },

    normalize: function() {
        var l = Math.sqrt( this.x * this.x + this.y * this.y );
        this.x /= l;
        this.y /= l;
        return this;
    }
});

Utilities.extendObject(Point, {
    zero: new Point(0, 0),

    pointOnCircle: function(angle, radius)
    {
        return new Point(radius * Math.cos(angle), radius * Math.sin(angle));
    },

    pointOnEllipse: function(angle, radiuses)
    {
        return new Point(radiuses.x * Math.cos(angle), radiuses.y * Math.sin(angle));
    },

    elementClientSize: function(element)
    {
        var rect = element.getBoundingClientRect();
        return new Point(rect.width, rect.height);
    }
});

Insets = Utilities.createClass(
    function(top, right, bottom, left)
    {
        this.top = top;
        this.right = right;
        this.bottom = bottom;
        this.left = left;
    }, {

    get width()
    {
        return this.left + this.right;
    },

    get height()
    {
        return this.top + this.bottom;
    },

    get size()
    {
        return new Point(this.width, this.height);
    }
});

Insets.elementPadding = function(element)
{
    var styles = window.getComputedStyle(element);
    return new Insets(
        parseFloat(styles.paddingTop),
        parseFloat(styles.paddingRight),
        parseFloat(styles.paddingBottom),
        parseFloat(styles.paddingTop));
}

UnitBezier = Utilities.createClass(
    function(point1, point2)
    {
        // First and last points in the Bézier curve are assumed to be (0,0) and (!,1)
        this._c = point1.multiply(3);
        this._b = point2.subtract(point1).multiply(3).subtract(this._c);
        this._a = new Point(1, 1).subtract(this._c).subtract(this._b);
    }, {

    epsilon: 1e-5,
    derivativeEpsilon: 1e-6,

    solve: function(x)
    {
        return this.sampleY(this.solveForT(x));
    },

    sampleX: function(t)
    {
        return ((this._a.x * t + this._b.x) * t + this._c.x) * t;
    },

    sampleY: function(t)
    {
        return ((this._a.y * t + this._b.y) * t + this._c.y) * t;
    },

    sampleDerivativeX: function(t)
    {
        return(3 * this._a.x * t + 2 * this._b.x) * t + this._c.x;
    },

    solveForT: function(x)
    {
        var t0, t1, t2, x2, d2, i;

        for (t2 = x, i = 0; i < 8; ++i) {
            x2 = this.sampleX(t2) - x;
            if (Math.abs(x2) < this.epsilon)
                return t2;
            d2 = this.sampleDerivativeX(t2);
            if (Math.abs(d2) < this.derivativeEpsilon)
                break;
            t2 = t2 - x2 / d2;
        }

        t0 = 0;
        t1 = 1;
        t2 = x;

        if (t2 < t0)
            return t0;
        if (t2 > t1)
            return t1;

        while (t0 < t1) {
            x2 = this.sampleX(t2);
            if (Math.abs(x2 - x) < this.epsilon)
                return t2;
            if (x > x2)
                t0 = t2;
            else
                t1 = t2;
            t2 = (t1 - t0) * .5 + t0;
        }

        return t2;
    }
});

SimplePromise = Utilities.createClass(
    function()
    {
        this._chainedPromise = null;
        this._callback = null;
    }, {

    then: function (callback)
    {
        if (this._callback)
            throw "SimplePromise doesn't support multiple calls to then";

        this._callback = callback;
        this._chainedPromise = new SimplePromise;

        if (this._resolved)
            this.resolve(this._resolvedValue);

        return this._chainedPromise;
    },

    resolve: function (value)
    {
        if (!this._callback) {
            this._resolved = true;
            this._resolvedValue = value;
            return;
        }

        var result = this._callback(value);
        if (result instanceof SimplePromise) {
            var chainedPromise = this._chainedPromise;
            result.then(function (result) { chainedPromise.resolve(result); });
        } else
            this._chainedPromise.resolve(result);
    }
});

var Heap = Utilities.createClass(
    function(maxSize, compare)
    {
        this._maxSize = maxSize;
        this._compare = compare;
        this._size = 0;
        this._values = new Array(this._maxSize);
    }, {

    // This is a binary heap represented in an array. The root element is stored
    // in the first element in the array. The root is followed by its two children.
    // Then its four grandchildren and so on. So every level in the binary heap is
    // doubled in the following level. Here is an example of the node indices and
    // how they are related to their parents and children.
    // ===========================================================================
    //              0       1       2       3       4       5       6
    // PARENT       -1      0       0       1       1       2       2
    // LEFT         1       3       5       7       9       11      13
    // RIGHT        2       4       6       8       10      12      14
    // ===========================================================================
    _parentIndex: function(i)
    {
        return i > 0 ? Math.floor((i - 1) / 2) : -1;
    },

    _leftIndex: function(i)
    {
        var leftIndex = i * 2 + 1;
        return leftIndex < this._size ? leftIndex : -1;
    },

    _rightIndex: function(i)
    {
        var rightIndex = i * 2 + 2;
        return rightIndex < this._size ? rightIndex : -1;
    },

    // Return the child index that may violate the heap property at index i.
    _childIndex: function(i)
    {
        var left = this._leftIndex(i);
        var right = this._rightIndex(i);

        if (left != -1 && right != -1)
            return this._compare(this._values[left], this._values[right]) > 0 ? left : right;

        return left != -1 ? left : right;
    },

    init: function()
    {
        this._size = 0;
    },

    top: function()
    {
        return this._size ? this._values[0] : NaN;
    },

    push: function(value)
    {
        if (this._size == this._maxSize) {
            // If size is bounded and the new value can be a parent of the top()
            // if the size were unbounded, just ignore the new value.
            if (this._compare(value, this.top()) > 0)
                return;
            this.pop();
        }
        this._values[this._size++] = value;
        this._bubble(this._size - 1);
    },

    pop: function()
    {
        if (!this._size)
            return NaN;

        this._values[0] = this._values[--this._size];
        this._sink(0);
    },

    _bubble: function(i)
    {
        // Fix the heap property at index i given that parent is the only node that
        // may violate the heap property.
        for (var pi = this._parentIndex(i); pi != -1; i = pi, pi = this._parentIndex(pi)) {
            if (this._compare(this._values[pi], this._values[i]) > 0)
                break;

            this._values.swap(pi, i);
        }
    },

    _sink: function(i)
    {
        // Fix the heap property at index i given that each of the left and the right
        // sub-trees satisfies the heap property.
        for (var ci = this._childIndex(i); ci != -1; i = ci, ci = this._childIndex(ci)) {
            if (this._compare(this._values[i], this._values[ci]) > 0)
                break;

            this._values.swap(ci, i);
        }
    },

    str: function()
    {
        var out = "Heap[" + this._size + "] = [";
        for (var i = 0; i < this._size; ++i) {
            out += this._values[i];
            if (i < this._size - 1)
                out += ", ";
        }
        return out + "]";
    },

    values: function(size) {
        // Return the last "size" heap elements values.
        var values = this._values.slice(0, this._size);
        return values.sort(this._compare).slice(0, Math.min(size, this._size));
    }
});

Utilities.extendObject(Heap, {
    createMinHeap: function(maxSize)
    {
        return new Heap(maxSize, function(a, b) { return b - a; });
    },

    createMaxHeap: function(maxSize) {
        return new Heap(maxSize, function(a, b) { return a - b; });
    }
});

var SampleData = Utilities.createClass(
    function(fieldMap, data)
    {
        this.fieldMap = fieldMap || {};
        this.data = data || [];
    }, {

    get length()
    {
        return this.data.length;
    },

    addField: function(name, index)
    {
        this.fieldMap[name] = index;
    },

    push: function(datum)
    {
        this.data.push(datum);
    },

    sort: function(sortFunction)
    {
        this.data.sort(sortFunction);
    },

    slice: function(begin, end)
    {
        return new SampleData(this.fieldMap, this.data.slice(begin, end));
    },

    forEach: function(iterationFunction)
    {
        this.data.forEach(iterationFunction);
    },

    createDatum: function()
    {
        return [];
    },

    getFieldInDatum: function(datum, fieldName)
    {
        if (typeof datum === 'number')
            datum = this.data[datum];
        return datum[this.fieldMap[fieldName]];
    },

    setFieldInDatum: function(datum, fieldName, value)
    {
        if (typeof datum === 'number')
            datum = this.data[datum];
        return datum[this.fieldMap[fieldName]] = value;
    },

    at: function(index)
    {
        return this.data[index];
    },

    toArray: function()
    {
        var array = [];

        this.data.forEach(function(datum) {
            var newDatum = {};
            array.push(newDatum);

            for (var fieldName in this.fieldMap) {
                var value = this.getFieldInDatum(datum, fieldName);
                if (value !== null && value !== undefined)
                    newDatum[fieldName] = value;
            }
        }, this);

        return array;
    }
});
