'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.$('<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.$('<input>').val('red').val()
    'red'
    > !!Ox.$('<div>').on({click: function(e) { Ox.test(e.type, 'click'); }}).trigger('click')
    true
@*/
Ox.$ = Ox.element = function(value) {

    var data = {},
        element = !Ox.isString(value) ? value // window, document or element
            : value[0] == '<' ? document.createElement(value.slice(1, -1))
            : value[0] == '#' ? document.getElementById(value.slice(1))
            : value[0] == '.' ? document.getElementsByClassName(value.slice(1))[0]
            : document.getElementsByTagName(value)[0],
        mousewheelEvents = ['wheel', 'mousewheel'],
        originalMousewheelEvents = 'onwheel' in document ? ['wheel']
            : ['mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll'];

    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 element ? {
        //@ 0 <h> The DOM element itself
        0: element,
        /*@
        addClass <f> Adds a class name
            (className) -> <o> This element
            className <s> Class name
        @*/
        addClass: function(string) {
            this[0].className = Ox.unique(((
                this[0].className ? this[0].className + ' ' : ''
            ) + Ox.clean(string)).split(' ')).join(' ');
            return this;
        },
        /*@
        append <f> Appends one or more elements to this element
            (element[, element[, ...]]) -> <o> This element
            element <o> Another element
        @*/
        append: function() {
            var that = this;
            Ox.slice(arguments).forEach(function($element) {
                that[0].appendChild($element[0]);                
            });
            return this;
        },
        /*@
        appendTo <f> Appends this element object to another element object
            (element) -> <o> This element
            element <o> Another element
        @*/
        appendTo: function($element) {
            $element[0].appendChild(this[0]);
            return this;
        },
        /*@
        attr <f> Gets or sets an attribute
            (key) -> <s> Value
            (key, value) -> <o> This element
            ({key0: value0, key1: value1, ...}) -> <o> This element
            key <s> Attribute name
            value <s> Attribute value
        @*/
        attr: function() {
            var ret, that = this;
            if (arguments.length == 1 && Ox.isString(arguments[0])) {
                ret = this[0].getAttribute
                    ? this[0].getAttribute(arguments[0])
                    : null;
                // fixme: why exactly is this needed?
                if (ret === null) {
                    ret = void 0;
                }
            } else {
                Ox.forEach(Ox.makeObject(arguments), function(value, key) {
                    that[0].setAttribute && that[0].setAttribute(key, value);
                });
                ret = this;
            }
            return ret;
        },
        /*@
        children <f> Returns the children of this element
            () -> <[h]> Children
        @*/
        children: function() {
            return Ox.slice(this[0].childNodes);
        },
        /*@
        css <f> Gets or sets a CSS attribute
            (key) -> <s> Value
            (key, value) -> <o> This element
            ({key0: value0, key1: value1, ...}) -> <o> This element
            key <s> Attribute name
            value <s> Attribute value
        @*/
        css: function() {
            var ret, that = this;
            if (arguments.length == 1 && Ox.isString(arguments[0])) {
                ret = this[0].style[arguments[0]];
            } else {
                Ox.forEach(Ox.makeObject(arguments), function(value, key) {
                    that[0].style[key] = value;
                });
                ret = this;
            }
            return ret;
        },
        /*@
        data <f> Gets or sets data
            () -> <o> All data
            (key) -> <s> Value
            (key, value) -> <o> This element
            ({key0: value0, key1: value1, ...}) -> <o> This element
            key <s> Property
            value <*> Value
        @*/
        data: function() {
            var ret;
            if (arguments.length == 0) {
                ret = data;
            } else if (arguments.length == 1 && Ox.isString(arguments[0])) {
                ret = data[arguments[0]]
            } else {
                Ox.forEach(Ox.makeObject(arguments), function(value, key) {
                    data[key] = value;
                });
                ret = this;
            }
            return ret;
        },
        /*@
        empty <f> Empties the inner HTML
            () -> <o> This element
        @*/
        empty: function() {
            return this.html('');
        },
        /*@
        find <f> Find descendant elements
            ([selector]) -> <[h]> Elements
            selector <s|'*'> CSS selector
        @*/
        find: function(string) {
            return Ox.slice(this[0].querySelectorAll(string || '*'));
        },
        /*@
        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(string) {
            return this[0].className.split(' ').indexOf(string) > -1;
        },
        /*@
        height <f> Returns the height of this element
            () -> <n> Height in px
        @*/
        height: function() {
            return this[0].offsetHeight;
        },
        /*@
        hide <f> Hides this element
            () -> <o> This element
        @*/
        hide: function() {
            return this.css({display: 'none'});
        },
        /*@
        html <f> Gets or sets the inner HTML
            () -> <s> The inner HTML
            (html) -> <o> This element
            html <s> The inner HTML
        @*/
        html: function(string) {
            var ret;
            if (arguments.length == 0) {
                ret = this[0].innerHTML;
            } else {
                this[0].innerHTML = string;
                ret = this;
            }
            return ret;
        },
        /*@
        insertAfter <f> Inserts this element after another element
            (element) -> <o> This element
            element <o> Another element
        @*/
        insertAfter: function($element) {
            $element[0].parentNode.insertBefore(this[0], $element[0].nextSibling);
            return this;
        },
        /*@
        insertBefore <f> Inserts this element before another element
            (element) -> <o> This element
            element <o> Another element
        @*/
        insertBefore: function($element) {
            $element[0].parentNode.insertBefore(this[0], $element[0]);
            return this;
        },
        /*@
        next <f> Returns the sibling after this element
            () -> <h> Next element
        @*/
        next: function() {
            return this[0].nextSibling;
        },
        /*@
        nextAll <f> Returns all siblings after this element
            () -> <[h]> Next elements
        @*/
        nextAll: function() {
            var sibling = this[0], siblings = [];
            while (true) {
                var sibling = sibling.nextSibling;
                if (!sibling) {
                    break;
                }
                siblings.push(sibling);
            }
            return siblings;
        },
        /*@
        off <f> Unbinds a callback from an event
            (event) -> <o> This element (unbinds all callbacks)
            (event, callback) -> <o> This element
            ({event0: callback0, event1: callback1, ...}) -> <o> This element
            event <s> Event name
            callback <f> Callback function
        @*/
        off: function(event, callback) {
            var that = this;
            Ox.forEach(normalizeEvents(arguments), function(callback, event) {
                if (callback) {
                    that[0].removeEventListener(event, callback, false);
                } else {
                    that[0]['on' + event] = null;
                }
            });
            return this;
        },
        /*@
        on <f> Binds a callback to an event
            (event, callback) -> <o> This element
            ({event0: callback0, event1: callback1, ...}) -> <o> This element
            event <s> Event name
            callback <f> Callback function
                e <o> Event properties
        @*/
        on: function() {
            var that = this;
            Ox.forEach(normalizeEvents(arguments), function(callback, event) {
                that[0].addEventListener(event, callback, false);
            });
            return this;
        },
        /*@
        one <f> Binds a callback to an event and unbinds it on first invocation
            (event, callback) -> <o> This element
            ({event0: callback0, event1: callback1, ...}) -> <o> This element
            event <s> Event name
            callback <f> Callback function
                e <o> Event properties
        @*/
        one: function(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 parent of this element
            () -> <h> Parent element
        @*/
        parent: function() {
            return this[0].parentNode;
        },
        /*@
        parents <f> Returns all ancestors of this element
            () -> <[h]> Ancestor elements
        @*/
        parents: function() {
            var parent = this[0], parents = [];
            while (true) {
                var parent = parent.parentNode;
                if (!parent) {
                    break;
                }
                parents.unshift(parent);
            }
            return parents;
        },
        /*@
        prepend <f> Prepends one or more elements to this element
            (element[, element[, ...]]) -> <o> This element
            element <o> Another element
        @*/
        prepend: function() {
            var parent = this[0].parentNode;
            Ox.slice(arguments).reverse().forEach(function($element) {
                parent.insertBefore($element[0], parent.firstChild);
            });
            return this;
        },
        /*@
        prependTo <f> Prepends this element object to another element object
            (element) -> <o> This element
            element <o> Another element
        @*/
        prependTo: function($element) {
            var element = $element[0];
            element.insertBefore(this[0], element.firstChild);
            return this;
        },
        /*@
        prev <f> Returns the sibling before this element
            () -> <h> Next element
        @*/
        prev: function() {
            return this[0].previousSibling;
        },
        /*@
        prevAll <f> Returns all siblings before this element
            () -> <[h]> Next elements
        @*/
        prevAll: function() {
            var sibling = this[0], siblings = [];
            while (true) {
                var sibling = sibling.previousSibling;
                if (!sibling) {
                    break;
                }
                siblings.unshift(sibling);
            }
            return siblings;
        },
        /*@
        remove <f> Removes this element from the DOM
            () -> <o> This element
        @*/
        remove: function() {
            this[0].parentNode.removeChild(this[0]);
            return this;
        },
        /*@
        removeAttr <f> Removes an attribute
            (key) -> <o> This element
            ([key0, key1, ...]) -> <o> This element
            key <s> The attribute
        @*/
        removeAttr: function() {
            var that = this;
            Ox.makeArray(arguments[0]).forEach(function(key) {
                that[0].removeAttribute(key);
            });
            return this;
        },
        /*@
        removeClass <f> Removes a class name
            (className) -> <o> This element
            className <s> Class name
        @*/
        removeClass: function(string) {
            var array = Ox.clean(string).split(' ');
            this[0].className = this[0].className.split(' ').filter(
                function(className) {
                    return array.indexOf(className) == -1;
                }
            ).join(' ');
            return this;
        },
        /*@
        replace <f> Replaces another element with this element
            (element) -> <o> This element
            element <o> Another element
        @*/
        replace: function($element) {
            var next = $element[0].nextSibling, parent = $element[0].parentNode;
            $element.remove();
            parent.insertBefore(this[0], next);
            return this;
        },
        /*@
        replaceWith <f> Replaces this element with another element
            (element) -> <o> This element
            element <o> Another element
        @*/
        replaceWith: function($element) {
            var next = this[0].nextSibling, parent = this[0].parentNode;
            this.remove();
            parent.insertBefore($element[0], next);
            return this;
        },
        /*@
        show <f> Shows this element
            () -> This element
        @*/
        show: function() {
            return this.css({display: 'block'});
        },
        /*@
        siblings <f> Returns all siblings of this element
        () -> <[oh]> Sibling elements
        @*/
        siblings: function() {
            var that = this;
            return Ox.filter(this[0].parentNode.childNodes, function(element) {
                return element !== that[0];
            });
        },
        /*@
        text <f> Gets or sets the text contents
            () -> <s> The text contents
            (text) -> <o> This element
            text <s> The text contents
        @*/
        text: function() {
            var ret;
            if (arguments.length == 0) {
                ret = Ox.isString(this.textContent)
                    ? this.textContent : this.innerText;
            } else {
                ret = this.empty().append(this[0].createTextNode(string));
            }
            return ret;
        },
        /*@
        toggleClass <f> Toggles a class name
            (className) -> <o> This element
            className <s> Class name
        @*/
        toggleClass: function(string) {
            return this[
                this.hasClass(string) ? 'removeClass' : 'addClass'
            ](string);
        },
        /*@
        trigger <f> Triggers an event
            (event) -> <o> This element
        @*/
        trigger: function(event) {
            var e = document.createEvent('MouseEvents');
            e.initEvent(event, true, true);
            this[0].dispatchEvent(e);
            return this;
        },
        /*@
        val <f> Gets or sets the value property of this element
            () -> <s> Value
            (value) -> <o> This element
            value <s> Value
        @*/
        val: function(value) {
            var ret;
            if (arguments.length == 0) {
                ret = this[0].value;
            } else {
                this[0].value = value;
                ret = this;
            }
            return ret;
        },
        /*@
        width <f> Returns the width of this element
            () -> <n> Width in px
        @*/
        width: function() {
            return this[0].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;
        }
    };
}());