'use strict';

/*@
Ox.$ <f> Generic HTML element, mimics jQuery
    value <s|h|w|?> tagname, selector, html element, `window`, or `document`
        Passing a tagname ('<tagname>') creates an element, passing a selector
        ('tagname', '.classname' or '#id') selects an element.
    (value) -> <o> Element object
    > Ox.$('<div>').addClass('red').hasClass('red')
    true
    > Ox.$('<div>').addClass('red').removeClass('red').hasClass('red')
    false
    > Ox.$('<div>').addClass('red').addClass('red')[0].className
    'red'
    > Ox.$('<a>').append(Ox.$('<b>')).children('b').length
    1
    > Ox.$('<b>').appendTo(Ox.$('<a>')).parents('a').length
    1
    > Ox.$('<div>').attr({id: 'red'}).attr('id')
    'red'
    > Ox.$('<div>').attr({id: 'red'}).removeAttr('id').attr('id')
    void 0
    > Ox.$('<div>').css({color: 'red'}).css('color')
    'red'
    > Ox.$('<div>').html('red').html()
    'red'
    > Ox.$('<div>').html('red').empty().html()
    ''
    > !!Ox.$('<div>').on({click: function(e) { Ox.test(e.type, 'click'); }}).trigger('click')
    true
    > Ox.$('<input>').val('red').val()
    'red'
@*/
Ox.$ = Ox.element = function $(value) {

    var elements = Ox.isArray(value) ? value // array of elements
            : Ox.isNodeList(value) ? Ox.slice(value) // nodelist
            : !Ox.isString(value) ? [value] // window, document or element
            : value[0] == '<' ? [document.createElement(value.slice(1, -1))]
            : Ox.slice(document.querySelectorAll(value)),
        mousewheelEvents = ['wheel', 'mousewheel'],
        originalMousewheelEvents = 'onwheel' in document ? ['wheel']
            : ['mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll'],
        previousDisplay;

    function getElements($other) {
        return $other.forEach ? $other
            : Ox.range($other.length).map(function(index) {
                return $other[index];
            });
    }

    function normalizeEvents(args) {
        var ret = {};
        Ox.forEach(Ox.makeObject(args), function(callback, event) {
            if (Ox.contains(mousewheelEvents, event)) {
                originalMousewheelEvents.forEach(function(event) {
                    ret[event] = callback;
                });
            } else {
                ret[event] = callback;
            }
        });
        return ret;
    }

    return elements.length ? Ox.extend(
        Ox.zipObject(Ox.range(elements.length), elements
    ), {

        /*@
        add <f> Adds another DOM object to this DOM object
            (other) -> This DOM object
            other <o> Other DOM object
        @*/
        add: function add($other) {
            elements = Ox.unique(elements.concat($other.elements()));
            this.length = elements.length;
            return this;
        },

        /*@
        addClass <f> Adds a class name to all elements
            (className) -> <o> This DOM object
            className <s> Class name
        @*/
        addClass: function addClass(string) {
            string = Ox.clean(string);
            elements.forEach(function(element) {
                element.className = Ox.unique(((
                    element.className ? element.className + ' ' : ''
                ) + string).split(' ')).join(' ');
            });
            return this;
        },

        /*@
        append <f> Appends one or more DOM objects to this DOM object
            (object[, object[, ...]]) -> <o> This DOM object
            element <o> Another DOM object
        @*/
        append: function append() {
            var $others = Ox.slice(arguments);
            elements.forEach(function(element) {
                $others.forEach(function($other) {
                    getElements($other).forEach(function(otherElement) {
                        element.appendChild(otherElement);
                    });
                });
            });
            return this;
        },

        /*@
        appendTo <f> Appends this DOM object to another DOM object
            (object) -> <o> This DOM object
            object <o> Another DOM object
        @*/
        appendTo: function appendTo($other) {
            getElements($other).forEach(function(otherElement) {
                elements.forEach(function(element) {
                    otherElement.appendChild(element);
                });
            });
            return this;
        },

        /*@
        attr <f> Gets or sets an attribute for all elements
            (key) -> <s> Value
            (key, value) -> <o> This DOM object
            ({key0: value0, key1: value1, ...}) -> <o> This DOM object
            key <s> Attribute name
            value <s> Attribute value
        @*/
        attr: function attr() {
            var args = arguments, ret;
            if (args.length == 1 && Ox.isString(args[0])) {
                ret = this[0].getAttribute
                    ? this[0].getAttribute(args[0])
                    : void 0;
                // fixme: why exactly is this needed?
                return ret === null ? void 0 : ret;
            } else {
                args = Ox.makeObject(args);
                elements.forEach(function(element) {
                    Ox.forEach(args, function(value, key) {
                        if (
                            element.setAttribute
                            && !Ox.contains([false, null, void 0], value)
                        ) {
                            element.setAttribute(key, value);
                        }
                    });
                });
                return this;
            }
        },

        /*@
        children <f> Returns the unique list of children of all elements
            ([selector]) -> <[h]> Children
            selector <s|'*'> CSS selector
        @*/
        children: function children(selector) {
            var children = Ox.unique(Ox.flatten(elements.map(function(element) {
                return Ox.slice(element.childNodes);
            })));
            return Ox.$(selector ? children.filter(function(child) {
                return Ox.$(child).is(selector);
            }) : children);
        },

        /*@
        css <f> Gets or sets a CSS attribute for all elements
            (key) -> <s> Value
            (key, value) -> <o> This DOM object
            ({key0: value0, key1: value1, ...}) -> <o> This DOM object
            key <s> Attribute name
            value <s> Attribute value
        @*/
        css: function css() {
            var args = arguments;
            if (args.length == 1 && Ox.isString(args[0])) {
                return elements[0].style[args[0]];
            } else {
                elements.forEach(function(element) {
                    Ox.forEach(Ox.makeObject(args), function(value, key) {
                        element.style[key] = value;
                    });
                });
                return this;
            }
        },

        /*@
        data <f> Gets or sets data
            () -> <o> All data
            (key) -> <s> Value
            (key, value) -> <o> This DOM object
            ({key0: value0, key1: value1, ...}) -> <o> This DOM object
            key <s> Property
            value <*> Value
        @*/
        data: function data() {
            var args;
            if (arguments.length == 1 && Ox.isString(arguments[0])) {
                return element.getAttribute('data-' + arguments[0]);
            } else {
                args = Ox.makeObject(arguments);
                elements.forEach(function(element) {
                    Ox.forEach(args, function(value, key) {
                        element.setAttribute('data-' + key, value);
                    });
                });
                return this;
            }
        },

        /*@
        elements <a> All elements
        @*/
        elements: elements,

        /*@
        eq <f> Reduces the list of elements to the one at the given index
            () -> <o> This DOM object
        @*/
        eq: function eq() {
            var that = this;
            Ox.loop(1, this.length, function(index) {
                delete that[index];
            });
            this.elements = [this.elements[index]];
            this.length = 1;
            return this;
        },

        /*@
        empty <f> Empties the inner HTML of all elements
            () -> <o> This DOM object
        @*/
        empty: function empty() {
            return this.html('');
        },

        /*@
        every <f> Tests if every element satisfies a given condition
            (test) -> True if every element passes the test
            test <f> Test function
        @*/
        every: function every() {
            return Array.prototype.every.apply(elements, arguments);
        },

        /*@
        filter <f> Filters all elements by a given condition
            (test) -> Array of matching elements
            test <f> Test function
        @*/
        filter: function filter() {
            return Array.prototype.filter.apply(elements, arguments);
        },

        /*@
        find <f> Find all descendant elements matching a CSS selector
            ([selector]) -> <[h]> Elements
            selector <s|'*'> CSS selector
        @*/
        find: function find(selector) {
            return Ox.$(Ox.unique(Ox.flatten(elements.map(function(element) {
                return Ox.slice(element.querySelectorAll(selector || '*'));
            }))));
        },

        /*@
        forEach <f> Loops over all elements
            (iterator) -> This DOM object
            iterator <f> Iterator function
        @*/
        forEach: function forEach() {
            Array.prototype.forEach.apply(elements, arguments);
            return this;
        },

        /*@
        hasClass <f> Returns true if this element has a given class
            (className) -> <b> True if this element has the class
            className <s> Class name
        @*/
        hasClass: function hasClass(string) {
            return elements.some(function(element) {
                return Ox.contains(element.className.split(' '), string);
            });
        },

        /*@
        height <f> Returns the height of the first element
            () -> <n> Height in px
        @*/
        height: function height() {
            return elements[0][
                elements[0] == document ? 'height'
                : elements[0] == window ? 'innerHeight'
                : 'offsetHeight'
            ];
        },

        /*@
        hide <f> Hides all elements
            () -> <o> This DOM object
        @*/
        hide: function hide() {
            previousDisplay = this.css('display');
            return this.css({display: 'none'});
        },

        /*@
        html <f> Gets or sets the innerHTML of all elements
            () -> <s> The inner HTML
            (html) -> <o> This DOM object
            html <s> The inner HTML
        @*/
        html: function html(string) {
            var html = '';
            if (arguments.length == 0) {
                elements.forEach(function(element) {
                    html += element.innerHTML;
                })
                return html;
            } else {
                elements.forEach(function(element) {
                    element.innerHTML = string;
                });
                return this;
            }
        },

        /*@
        insertAfter <f> Inserts this DOM object after another DOM object
            (object) -> <o> This DOM object
            object <o> Another DOM object
        @*/
        insertAfter: function insertAfter($other) {
            var nextSibling = $other[0].nextSibling;
            elements.forEach(function(element) {
                $other[0].parentNode.insertBefore(element, nextSibling);
            })
            return this;
        },

        /*@
        insertBefore <f> Inserts this DOM object before another DOM object
            (object) -> <o> This DOM object
            object <o> Another DOM object
        @*/
        insertBefore: function insertBefore($other) {
            elements.forEach(function(element) {
                $other[0].parentNode.insertBefore(element, $other[0]);
            });
            return this;
        },

        /*@
        is <f> Tests if any element matches a CSS selector
            (selector) -> <b> True if the element matches the selector
            selector <s> CSS selector
        @*/
        is: function is(selector) {
            return elements.some(function(element) {
                var parent = element.parentNode;
                if (!parent) {
                    parent = document.createElement('div');
                    parent.appendChild(element);
                }
                return Ox.contains(parent.querySelectorAll(selector), element);
            });
        },

        /*@
        length <n> Number of elements
        @*/
        length: elements.length,

        /*@
        map <f> Transforms all elements
            (iterator) -> [] Transformed elements
            iterator <f> Iterator function
        @*/
        map: function map() {
            return Array.prototype.map.apply(elements, arguments);
        },

        /*@
        next <f> Returns the unique list of siblings directly after all elements
            () -> <[h]> Siblings
        @*/
        next: function next() {
            return Ox.$(Ox.unique(Ox.filter(elements.map(function(element) {
                return element.nextSibling;
            }))));
        },

        /*@
        nextAll <f> Returns the unique list of siblings after all elements
            () -> <[h]> Siblings
        @*/
        nextAll: function nextAll() {
            var siblings = [];
            elements.forEach(function(element) {
                var sibling = element;
                while (true) {
                    sibling = sibling.nextSibling;
                    if (!sibling) {
                        break;
                    }
                    siblings.push(sibling);
                }
            });
            return Ox.$(Ox.unique(siblings));
        },

        /*@
        off <f> Unbinds a callback from an event
            (event) -> <o> This DOM object (unbinds all callbacks)
            (event, callback) -> <o> This DOM object
            ({event0: callback0, event1: callback1, ...}) -> <o> This DOM object
            event <s> Event name
            callback <f> Callback function
        @*/
        off: function off(event, callback) {
            var args = normalizeEvents(arguments);
            elements.forEach(function(element) {
                Ox.forEach(args, function(callback, event) {
                    if (callback) {
                        element.removeEventListener(event, callback, false);
                    } else {
                        element['on' + event] = null;
                    }
                });
            });
            return this;
        },

        /*@
        on <f> Binds a callback to an event
            (event, callback) -> <o> This DOM object
            ({event0: callback0, event1: callback1, ...}) -> <o> This DOM object
            event <s> Event name
            callback <f> Callback function
                e <o> Event properties
        @*/
        on: function on() {
            var args = normalizeEvents(arguments);
            elements.forEach(function(element) {
                Ox.forEach(args, function(callback, event) {
                    element.addEventListener(event, callback, false);
                });
            });
            return this;
        },

        /*@
        one <f> Binds a callback to an event and unbinds it on first invocation
            (event, callback) -> <o> This DOM object
            ({event0: callback0, event1: callback1, ...}) -> <o> This DOM object
            event <s> Event name
            callback <f> Callback function
                e <o> Event properties
        @*/
        one: function one(events) {
            var args = Ox.slice(arguments), that = this;
            Ox.forEach(normalizeEvents(arguments), function(callback, event) {
                that.on(event, function fn() {
                    that.off(event, fn);
                    return callback.apply(that, args);
                });
            });
            return this;
        },

        /*@
        parent <f> Returns the unique list of parents of all elements
            () -> <[h]> Parent elements
        @*/
        parent: function parent() {
            return Ox.$(Ox.unique(Ox.compact(elements.map(function(element) {
                return element.parentNode;
            }))));
        },

        /*@
        parents <f> Returns the unique list of all ancestors of all elements
            ([selector]) -> <[h]> Ancestor elements
            selector <s|'*'> CSS selector
        @*/
        parents: function parents(selector) {
            var parents = [];
            Ox.reverse(elements).forEach(function(element) {
                var parent = element;
                while (true) {
                    parent = parent.parentNode;
                    if (!parent || parent == document) {
                        break;
                    }
                    parents.unshift(parent);
                }
            });
            parents = Ox.unique(parents);
            return Ox.$(selector ? parents.filter(function(parent) {
                return Ox.$(parent).is(selector);
            }) : parents);
        },

        /*@
        prepend <f> Prepends one or more DOM objects to this DOM object
            (object[, object[, ...]]) -> <o> DOM object
            object <o> Another DOM objectt
        @*/
        prepend: function prepend() {
            var $others = Ox.slice(arguments).reverse();
            elements.forEach(function(element) {
                var parent = element.parentNode;
                $others.forEach(function($other) {
                    getElements($other).forEach(function(otherElement) {
                        parent.insertBefore(otherElement, parent.firstChild);
                    });
                });
            });
            return this;
        },

        /*@
        prependTo <f> Prepends this DOM object to another DOM object
            (object) -> <o> This DOM object
            object <o> Another DOM object
        @*/
        prependTo: function prependTo($other) {
            getElements($other).forEach(function(otherElement) {
                var firstChild = otherElement.firstChild
                elements.forEach(function(element) {
                    otherElement.insertBefore(element, firstChild);
                });
            });
            return this;
        },

        /*@
        prev <f> Returns the unique list of siblings directly before all elements
            () -> <[h]> Siblings
        @*/
        prev: function prev() {
            return Ox.$(Ox.unique(Ox.filter(elements.map(function(element) {
                return element.previousSibling;
            }))));
        },

        /*@
        prevAll <f> Returns the unique list of siblings before all elements
            () -> <[h]> Siblings
        @*/
        prevAll: function prevAll() {
            var siblings = [];
            Ox.reverse(elements).forEach(function(element) {
                var sibling = element;
                while (true) {
                    sibling = sibling.previousSibling;
                    if (!sibling) {
                        break;
                    }
                    siblings.unshift(sibling);
                }
            });
            return Ox.$(Ox.unique(siblings));
        },

        /*@
        reduce <f> Applies `reduce` to all elements
        @*/
        reduce: function reduce() {
            return Array.prototype.reduce.apply(elements, arguments);
        },

        /*@
        remove <f> Removes all element from the DOM
            () -> <o> This DOM object
        @*/
        remove: function remove() {
            elements.forEach(function(element) {
                if (element.parentNode) {
                    element.parentNode.removeChild(element);
                }
            });
            return this;
        },

        /*@
        removeAttr <f> Removes an attribute from all elements
            (key) -> <o> This DOM object
            ([key0, key1, ...]) -> <o> This DOM object
            key <s> The attribute
        @*/
        removeAttr: function removeAttr() {
            var keys = Ox.makeArray(arguments);
            elements.forEach(function(element) {
                keys.forEach(function(key) {
                    element.removeAttribute(key);
                });
            });
            return this;
        },

        /*@
        removeClass <f> Removes a class name from all elements
            (className) -> <o> This DOM object
            className <s> Class name
        @*/
        removeClass: function removeClass(string) {
            var classNames = Ox.clean(string).split(' ');
            elements.forEach(function(element) {
                element.className = element.className.split(' ')
                    .filter(function(className) {
                        return !Ox.contains(classNames, className)
                    })
                    .join(' ');
            });
            return this;
        },

        /*@
        replace <f> Replaces another DOM object with this DOM object
            (object) -> <o> This DOM object
            object <o> Another DOM object
        @*/
        replace: function replace($other) {
            getElements($other).forEach(function(otherElement) {
                var parent = otherElement.parentNode,
                    sibling = otherElement.nextSibling;
                if (parent) {
                    parent.removeChild(otherElement);
                    elements.forEach(function(element) {
                        parent.insertBefore(element, sibling)
                    });
                }
            });
            return this;
        },

        /*@
        replaceWith <f> Replaces this DOM object with another DOM object
            (object) -> <o> This DOM object
            object <o> Another DOM object
        @*/
        replaceWith: function replaceWith($other) {
            elements.forEach(function(element) {
                var parent = element.parentNode,
                    sibling = element.nextSibling;
                if (parent) {
                    parent.removeChild(element);
                    getElements($other).forEach(function(otherElement) {
                        parent.insertBefore(otherElement, sibling);
                    });
                }
            });
            return this;
        },

        /*@
        show <f> Shows all elements
            () -> This DOM object
        @*/
        show: function show() {
            return this.css({display: previousDisplay || 'block'});
        },

        /*@
        siblings <f> Returns all siblings of all elements
            ([selector]) -> <[h]> Siblings
            selector <s|'*'> CSS selector
        @*/
        siblings: function siblings(selector) {
            var siblings = Ox.unique(elements.map(function(element) {
                return Ox.filter(
                    element.parentNode.childNodes,
                    function(sibling) {
                        return sibling !== element;
                    }
                );
            }));
            return Ox.$(selector ? siblings.filter(function(sibling) {
                return Ox.$(sibling).is(selector);
            }) : siblings);
        },

        /*@
        some <f> Tests if some elements satisfy a given condition
            (test) -> True if some elements pass the test
            test <f> Test function
        @*/
        some: function some() {
            return Array.prototype.some.apply(elements, arguments);
        },

        /*@
        text <f> Gets or sets the text contents of all elements
            () -> <s> The text contents
            (text) -> <o> This DOM object
            text <s> The text contents
        @*/
        text: function text(string) {
            var text = '';
            if (arguments.length == 0) {
                elements.forEach(function(element) {
                    text += Ox.isString(element.textContent)
                        ? element.textContent : element.innerText; 
                });
                return text;
            } else {
                elements.forEach(function(element) {
                    element.empty();
                    element.appendChild(document.createTextNode(string));
                });
                return this;
            }
        },

        /*@
        toggle <f> Toggle visibility of all elements
            () -> This DOM object
        @*/
        toggle: function toggle() {
            return this[
                Ox.$(element).css('display') == 'none' ? 'show' : 'hide'
            ]();
        },

        /*@
        toggleClass <f> Toggles a class name for all elements
            (className) -> <o> This DOM object
            className <s> Class name
        @*/
        toggleClass: function toggleClass(string) {
            elements.forEach(function(element) {
                var $element = Ox.$(element);
                $element[
                    $element.hasClass(string) ? 'removeClass' : 'addClass'
                ](string);
            })
            return this;
        },

        /*@
        trigger <f> Triggers an event
            (event) -> <o> This DOM object
        @*/
        trigger: function trigger(event) {
            elements.forEach(function(element) {
                var e = document.createEvent('MouseEvents');
                e.initEvent(event, true, true);
                element.dispatchEvent(e);
            });
            return this;
        },

        /*@
        val <f> Gets the value of the first or sets the value of all elements
            () -> <s> Value
            (value) -> <o> This DOM object
            value <s> Value
        @*/
        val: function val(value) {
            var ret;
            if (arguments.length == 0) {
                return elements[0].value;
            } else {
                elements.forEach(function(element) {
                    element.value = value;
                });
                return this;
            }
        },

        /*@
        width <f> Returns the width of the first element
            () -> <n> Width in px
        @*/
        width: function width() {
            return elements[0][
                elements[0] == document ? 'width'
                : elements[0] == window ? 'innerWidth'
                : 'offsetWidth'
            ];
        }

    }) : null;

};

/*@
Ox.canvas <function> Generic canvas object
    Returns an object with the properties: `canvas`, `context`, `data` and
    `imageData`.
    (width, height) -> <o> canvas
    (image) -> <o> canvas
    width <n> Width in px
    height <n> Height in px
    image <e> Image object
@*/
Ox.canvas = function() {
    var c = {}, isImage = arguments.length == 1,
        image = isImage ? arguments[0] : {
            width: arguments[0], height: arguments[1]
        };
    c.context = (c.canvas = Ox.$('<canvas>').attr({
        width: image.width, height: image.height
    })[0]).getContext('2d');
    isImage && c.context.drawImage(image, 0, 0);
    c.data = (c.imageData = c.context.getImageData(
        0, 0, image.width, image.height
    )).data;
    return c;
};

/*@
Ox.documentReady <function> Calls a callback function once the DOM is ready
    (callback) -> <b> If true, the document was ready
    callback <f> Callback function
@*/
Ox.documentReady = (function() {
    var callbacks = [];
    document.onreadystatechange = window.onload = function() {
        if (document.readyState == 'complete') {
            callbacks.forEach(function(callback) {
                callback();
            });
            document.onreadystatechange = window.onload = null;
        }
    };
    return function(callback) {
        if (document.readyState == 'complete') {
            callback();
            return true;
        } else {
            callbacks.push(callback);
            return false;
        }
    };
}());