// vim: et:ts=4:sw=4:sts=4:ft=javascript

/*@
Ox.Element <function:Ox.JQueryElement> Basic UI element object
    # Usage --------------------------------------------------------------------
    (element)       -> <object> UI element
    (options)       -> <object> UI element
    (options, self) -> <object> UI element
    # Arguments ----------------------------------------------------------------
    element <string> Tagname or CSS selector
    options <object> Options of the element
        # Properties
        element <string> Tagname or CSS selector
        tooltip <string|function> Tooltip title, or a function that returns one
            (e) -> <string> tooltip title
            e <object> mouse event
    self    <object> Shared private variable
    # Events -------------------------------------------------------------------
    anyclick    <event> anyclick
        Fires on mouseup, but not on any subsequent mouseup within 250 ms (this
        is useful if one wants to listen for singleclicks, but not doubleclicks,
        since it will fire immediately, and won't fire again in case of a
        doubleclick)
        *           <*> original event properties
    doubleclick <event> doubleclick
        Fires on the second mousedown within 250 ms (this is useful if one wants
        to listen for both singleclicks and doubleclicks, since it will not
        trigger a singleclick event)
        *           <*> original event properties
    drag        <event> drag
        Fires on mousemove after dragstart, stops firing on mouseup
        clientDX    <number> horizontal drag delta in px
        clientDY    <number> vertical drag delta in px
        *           <*> original event properties
    dragend     <event> dragpause
        Fires on mouseup after dragstart
        clientDX    <number> horizontal drag delta in px
        clientDY    <number> vertical drag delta in px
        *           <*> original event properties
    dragenter   <event> dragenter
        Fires when entering an element during drag (this fires on the element
        being dragged -- the target element is the event's target property)
        clientDX    <number> horizontal drag delta in px
        clientDY    <number> vertical drag delta in px
        *           <*> original event properties
    dragleave   <event> dragleave
        Fires when leaving an element during drag (this fires on the element
        being dragged -- the target element is the event's target property)
        clientDX    <number> horizontal drag delta in px
        clientDY    <number> vertical drag delta in px
        *           <*> original event properties
    dragpause   <event> dragpause
        Fires once when the mouse doesn't move for 250 ms during drag (this is
        useful in order to execute operations that are too expensive to be
        attached to the drag event)
        clientDX    <number> horizontal drag delta in px
        clientDY    <number> vertical drag delta in px
        *           <*> original event properties
    mousedown   <event> mousedown
        Fires on mousedown (this is useful if one wants to listen for
        singleclicks, but not doubleclicks or drag events, and wants the event
        to fire as early as possible)
        *           <*> original event properties
    dragstart   <event> dragstart
        Fires when the mouse is down for 250 ms
        *           <*> original event properties
    mouserepeat <event> mouserepeat
        Fires every 50 ms after the mouse was down for 250 ms, stops firing on
        mouseleave or mouseup (this fires like a key that is being pressed and
        held, and is useful for buttons like scrollbars arrows that need to
        react to both clicking and holding)
    singleclick <event> singleclick
        Fires 250 ms after mouseup, if there was no subsequent mousedown (this
        is useful if one wants to listen for both singleclicks and doubleclicks,
        since it will not fire for doubleclicks)
        *           <*> original event properties
@*/

Ox.Element = function(options, self) {

    // create private object
    self = self || {};
    // create defaults and options objects
    self.defaults = {};
    self.options = options || {};
    // allow for Ox.TestElement('<tagname>')
    // or Ox.TestElement('cssSelector')
    if (Ox.isString(self.options)) {
        self.options = {
            element: self.options
        };
    }
    // create event handler
    // (this can be passed as part of self)
    if (!self.$eventHandler) {
        self.$eventHandler = $('<div>');
    }

    // create public object
    var that = new Ox.JQueryElement(
            $(self.options.element || '<div>')
        )
        .addClass('OxElement')
        .mousedown(mousedown);

    if (self.options.tooltip) {
        if (Ox.isString(self.options.tooltip)) {
            that.$tooltip = Ox.Tooltip({
                title: self.options.tooltip,
            });
            that.bind({
                mouseenter: mouseenter
            });
        } else {
            that.$tooltip = Ox.Tooltip({
                animate: false
            });
            that.bind({
                mousemove: mousemove
            });
        }
        that.bind({
            mouseleave: mouseleave
        });
    }

    function bind(action, event, callback) {
        self.$eventHandler[action]('ox_' + event, function(event, data) {
            callback(Ox.extend({
                _element: that.$element,
                _event: event
            }, data));
        });
    }

    function mousedown(e) {
        /*
        better mouse events
        mousedown:
            trigger mousedown
        within 250 msec:
            mouseup: trigger anyclick
            mouseup + mousedown: trigger doubleclick
        after 250 msec:
            mouseup + no mousedown within 250 msec: trigger singleclick
            no mouseup within 250 msec:
                trigger mouserepeat every 50 msec
                trigger dragstart
                    mousemove: trigger drag
                    no mousemove for 250 msec:
                        trigger dragpause
                    mouseup: trigger dragend
        "anyclick" is not called "click" since this would collide with the click
        events of some widgets
        */
        var clientX, clientY,
            dragTimeout = 0,
            mouseInterval = 0;
        if (!self._mouseTimeout) {
            // first mousedown
            that.triggerEvent('mousedown', e);
            self._drag = false;
            self._mouseup = false;
            self._mouseTimeout = setTimeout(function() {
                // 250 ms later, no subsequent click
                self._mouseTimeout = 0;
                if (self._mouseup) {
                    // mouse went up, trigger singleclick
                    that.triggerEvent('singleclick', e);
                } else {
                    // mouse is still down, trigger mouserepeat
                    // every 50 ms until mouseleave or mouseup
                    mouserepeat();
                    mouseInterval = setInterval(mouserepeat, 50);
                    that.one('mouseleave', function() {
                        clearInterval(mouseInterval);
                    });
                    // trigger dragstart, set up drag events
                    that.triggerEvent('dragstart', e);
                    $('*').bind({
                        mouseenter: dragenter,
                        mouseleave: dragleave
                    });
                    clientX = e.clientX;
                    clientY = e.clientY;
                    Ox.UI.$window
                        .unbind('mouseup', mouseup)
                        .mousemove(mousemove)
                        .one('mouseup', function(e) {
                            // stop checking for mouserepeat
                            clearInterval(mouseInterval);
                            // stop checking for dragpause
                            clearTimeout(dragTimeout);
                            // stop checking for drag
                            Ox.UI.$window.unbind('mousemove', mousemove);
                            // stop checking for dragenter and dragleave
                            $('*').unbind({
                                mouseenter: dragenter,
                                mouseleave: dragleave
                            });
                            // trigger dragend
                            that.triggerEvent('dragend', extend(e));
                        });
                    self._drag = true;
                }
            }, 250);
        } else {
            // second mousedown within 250 ms, trigger doubleclick
            clearTimeout(self._mouseTimeout);
            self._mouseTimeout = 0;
            that.triggerEvent('doubleclick', e);
        }
        Ox.UI.$window.one('mouseup', mouseup);
        function dragenter(e) {
            that.triggerEvent('dragenter', extend(e));
        }
        function dragleave(e) {
            that.triggerEvent('dragleave', extend(e));
        }
        function extend(e) {
            return Ox.extend({
                clientDX: e.clientX - clientX,
                clientDY: e.clientY - clientY
            }, e);
        }
        function mousemove(e) {
            e = extend(e);
            clearTimeout(dragTimeout);
            dragTimeout = setTimeout(function() {
                // mouse did not move for 250 ms, trigger dragpause
                that.triggerEvent('dragpause', e);
            }, 250);
            that.triggerEvent('drag', e);
        }
        function mouserepeat(e) {
            that.triggerEvent('mouserepeat', e);
        }
        function mouseup(e) {
            if (!self._mouseup && !self._drag) {
                // mouse went up for the first time, trigger anyclick
                that.triggerEvent('anyclick', e);
                self._mouseup = true;
            }
        }
    }

    function mouseenter(e) {
        that.$tooltip.show(e);
    }

    function mouseleave(e) {
        that.$tooltip.hide();
    }

    function mousemove(e) {
        //Ox.Log('Core', 'mousemove!!')
        that.$tooltip.options({
            title: self.options.tooltip(e)
        }).show(e);
    }

    self.setOption = function(key, value) {
        // self.setOption(key, value)
        // is called when an option changes
        // (to be implemented by widget)
        if (key == 'tooltip') {
            that.$tooltip && that.$tooltip.options({title: value});
        }
    };

    that._self = self; // fixme: remove

    /*@
    bindEvent <function> Binds a function to an event
        (event, callback)        -> <o> This element
        ({event: callback, ...}) -> <o> This element
        callback <f> Callback function
            data <o> event data (key/value pairs)
        event <s> Event name
            Event names can be namespaced, like <code>'click.foo'</code>
    @*/
    that.bindEvent = function() {
        Ox.forEach(Ox.makeObject(arguments), function(callback, event) {
            bind('bind', event, callback);
        });
        return that;
    }

    /*@
    bindEventOnce <function> Binds a function to an event, once
        (event, callback)        -> <obj> This element object
        ({event: callback, ...}) -> <obj> This element object
        callback <f> Callback function
            data <o> event data (key/value pairs)
        event <s> Event name
            Event names can be namespaced, like <code>'click.foo'</code>
    @*/
    that.bindEventOnce = function() {
        Ox.forEach(Ox.makeObject(arguments), function(callback, event) {
            bind('one', event, callback);
        });
        return that;
    };

    that.bindKeyboard = function() {
        Ox.Keyboard.bind(that.id);
        return that;
    };

    /*@
    defaults <function> Sets the default options for an element object
    ({key: value, ...}) -> <obj> This element object
    key     <str> The name of the default option
    value   <val> The value of the default option
    @*/
    that.defaults = function(defaults) {
        // sets the default options
        self.defaults = defaults;
        self.options = defaults;
        return that;
    };

    that.empty = function() {
        that.$element.find('.OxElement').each(function() {
            var oxid = $(this).data('oxid'),
                element = Ox.UI.elements[oxid];
            //!element && Ox.Log('Core', 'NO ELEMENT FOR', oxid, this.className)
            //element && Ox.Log('Core', 'OK', oxid, this.className)
            element && element.removeElement(false);
        });
        that.$element.empty();
        return that;
    };

    /*@
    gainFocus <function> Makes an element object gain focus
    () -> <obj> This element object
    @*/
    that.gainFocus = function() {
        Ox.Focus.focus(that.id);
        return that;
    };

    /*@
    hasFocus <function> Returns true if an element object has focus
    () -> <boolean> True if the element has focus
    @*/
    that.hasFocus = function() {
        return Ox.Focus.focused() == that.id;
    };

    /*@
    loseFocus <function> Makes an element object lose focus
    () -> <object> This element object
    @*/
    that.loseFocus = function() {
        Ox.Focus.blur(that.id);
        return that;
    };

    /*@
    options <function> Gets or sets the options of an element object
        # Usage
        ()                  -> <obj> all options
        (key)               -> <any> the value of option[key]
        (key, value)        -> <obj> this element
            Sets options[key] to value and calls self.setOption(key, value)
            if the key/value pair was added or modified
        ({key: value, ...}) -> <obj> this element
            Sets multiple options and calls self.setOption(key, value)
            for every key/value pair that was added or modified
        # Arguments
        key     <str> the name of the option
        value   <val> the value of the option
    @*/

    that.options = function() {
        return Ox.getset(self.options, arguments, self.setOption, that);
    };

    /*@
    removeElement <function> Removes an element object and its event handler
        () -> <obj> This element
    @*/
    that.remove = that.removeElement = function(remove) {
        //remove !== false && (len = Ox.len(Ox.UI.elements));
        ///*
        remove !== false && that.$element.find('.OxElement').each(function() {
            var oxid = $(this).data('oxid'),
                element = Ox.UI.elements[oxid];
            //!element && Ox.Log('Core', 'NO ELEMENT FOR', oxid, this.className)
            //element && Ox.Log('Core', 'OK', oxid, this.className)
            element && element.removeElement(false);
        });
        //*/
        Ox.Focus.remove(that.id);
        delete self.$eventHandler;
        delete Ox.UI.elements[that.id];
        that.$tooltip && that.$tooltip.remove();
        remove !== false && that.$element.remove();
        /*
        if (remove !== false) {
            var orphaned = 0;
            Ox.forEach(Ox.UI.elements, function(element) {
                if (!element.parent().length) {
                    orphaned++;
                }
            });
            Ox.Log('Core', 'LEN', len, '-->', Ox.len(Ox.UI.elements), 'orphaned:', orphaned);
        }
        */
        return that;
    };

    that.replaceWith = function($element) {
        $element.insertAfter(that);
        that.remove();
        return that;
    };

    /*@
    triggerEvent <function> Triggers an event
        (event) -> <object> This element object
        (event, data) -> <object> This element object
        ({event: data, ...}) -> <object> This element object
        event <string> Event name
        data <object> Event data (key/value pairs)
    @*/
    that.triggerEvent = function() {
        Ox.forEach(Ox.makeObject(arguments), function(data, event) {
            if ([
                'mousedown', 'mouserepeat', 'anyclick', 'singleclick', 'doubleclick',
                'dragstart', 'drag', 'dragenter', 'dragleave', 'dragpause', 'dragend',
                'draganddropstart', 'draganddrop', 'draganddropenter', 'draganddropleave', 'draganddropend',
                'playing', 'position', 'progress'
            ].indexOf(event) == -1) {
                if (!/^pandora_/.test(event)) {
                    Ox.Log('Core', that.id, self.options.id, 'trigger', event, data);
                }
            }
            // it is necessary to check if self.$eventHandler exists,
            // since, for example, when removing the element on click,
            // singleclick will fire after the removal of the event handler
            self.$eventHandler && self.$eventHandler.trigger('ox_' + event, data);
        });
        return that;
    };

    /*@
    unbindEvent <function> Unbinds all callbacks from an event
        To unbind a specific handler, use namespaced events, like
        <code>bindEvent('click.foo', callback)</code>, and then
        <code>unbindEvent('click.foo')</code>.
        () -> <object> This element object
            Unbinds all events
        (event) -> <object> This element object
            Unbinds one event
        (event, event, ...) -> <object> This element object
            Unbinds multiple events
        ([event, event, ...]) -> <object> This element object
            Unbinds multiple events
        event <string> Event name
    @*/
    that.unbindEvent = function() {
        if (arguments.length == 0) {
            self.$eventHandler.unbind();
        } else {
            Ox.makeArray(arguments).forEach(function(event) {
                self.$eventHandler.unbind('ox_' + event);
            });
        }
        return that;
    };

    that.unbindKeyboard = function() {
        Ox.Keyboard.unbind(that.id);
        return that;
    };

    return that;

};
