/**
 * Event emitter
 * 
 * Provides machinery for emitting events, managing event receivers and such. It's intended to be used as a
 * base class of classes which need event publishing. On a sublclass addEvents() should be called to create
 * initial event context, after that emitEvent() can be used anywhere to publish events.
 * 
 * Example:
 * 
 * // creating a new cass by subclassing emitter cass.
 * var EmittingClass = $.extendCls(hcom.common.modules.emitter, {
 *     constructor: function () {
 *         this.something = "A";
 *         
 *         // initialize event emitting machinery,
 *         // the last two events won't be used in this example:
 *         
 *         this.addEvents("change", "start", "stop");
 *         EmittingClass.superclass.call(this);
 *     },
 *     changeSomething: function (newSomething) {
 *         var oldSomething = this.something;
 *         this.something = newSomething;
 *         
 *         // emit "change" event to listening receivers if tehere is any,
 *         // parametrs after the event name will be passed to the receivers as arguments
 *         this.emitEvent("change", oldSomething, this.something);
 *     }
 * });
 * 
 * var RecevingClass = function () {
 *     this.emittingObj = new EmittingClass();
 *     
 *     // hook up this class to the emitting class' receivers
 *     this.emittingObj.addReceiver("change", this.onChange, this);
 *     
 *      // call a method on the emitting class which will emit an event
 *     this.emittingObj.changeSomething("B");
 * };
 * 
 * ReceivingClass.prototype = {
 *     // event receiver method, hooked up at the class constructor, it'll receive parameters too
 *     onChange: function (oldSomething, newSomething) {
 *         console.debug("change event received, something changed from %s to %s",
 *                 oldSomething, newSomething);
 *     }
 * };
 * 
 * var obj = new RecevingClass();
 * 
 * You should see "change event received, something changed from A to B" on the firebug console.
 *
 * @author zsolt_meszarovics
 */
/*extern $ */
Object.setToDotRef("pmmi.Emitter", function () {
    var Emitter;

    /*
     * Emitter class
     */

    Emitter = function () {
        this.emitSuspended = false;
    };

    Emitter.prototype = {
        /**
         * Add event definitions.
         *
         * Typically it's called from the host object's constructor.
         *
         * Example:
         *
         * this.addEvents("update", "delete"); // creates "update" and "delete" events.
         *
         * @return undefined
         */
        addEvents: function () {
            var i, arg, events;

            events = (this.events = this.events || {});

            for (i = 0; (arg = arguments[i]); i++) {
                events[arg] = events[arg] || true;
            }
        },

        /**
         * Suspend event emitting, all emitEvent() messages are swallowed, no receivers will be called.
         */
        suspendEmit: function () {
            this.emitSuspended = true;
        },

        /**
         * Resume event emitting -- back to normal operation.
         */
        resumeEmit: function () {
            this.emitSuspended = false;
        },

        /**
         * Emit an event by name
         *
         * Parameters other than name will be passed to the receiver(s) as arguments.
         *
         * @param {String} name event name
         */
        emitEvent: function (name) {
            var event;

            if (!this.emitSuspended) {
                event = this.events[name];
                if (event !== true) {
                    event.emit.apply(event, Array.prototype.slice.call(arguments, 1));
                }
            }
        },

        /**
         * Add event receiver
         *
         * @param {String} name existing event name, which is added previously with addEvent()
         * @param {Function} receiver receiver implementation
         * @param {Object} scope optional scope of receiver, if omitted it will be the emitter object
         */
        addReceiver: function (name, receiver, scope) {
            var event = this.events[name] || true;

            if (event === true) {
                event = (this.events[name] = new Emitter.Event(name, this));
            }

            event.addReceiver(receiver, scope);
            return this;
        },

        /**
         * Mass add more event receivers.
         *
         * Example:
         *
         * obj.addReceivers({
         *     change: {
         *         receiver: this.onChange,
         *         scope: this
         *     },
         *     load: { // scope is optional
         *         receiver: this.onLoad,
         *     },
         *     save: [ // multiple receivers per event can also be defined using an array literal
         *         {receiver: this.onSaveFirst},
         *         {receiver: this.onSaveSecond},
         *     ]
         * });
         *
         * @param {Object} receivers object with members for event names, having values of objects with
         * members name, receiver and optionally scope or arrays containing multiple ones.
         * @see addReceiver arguments for object member description.
         */
        addReceivers: function (receivers) {
            var i, j, recvOfEvent, receiver, event, events = Object.getKeys(receivers);
            for (i = 0; (event = events[i]); i++) {
                recvOfEvent = receivers[event];
                if (recvOfEvent.constructor !== Array) {
                    recvOfEvent = [recvOfEvent];
                }
                for (j = 0; (receiver = recvOfEvent[j]); j++) {
                    this.addReceiver(event, receiver.receiver, receiver.scope);
                }
            }
        },

        /**
         * Remove specified event receiver
         *
         * @param {String} name event name
         * @param {Function} handler handler function
         * @param {Object} scope function scope.
         * @return {Boolean} true when specified receiver is found and removed, false otherwise.
         */
        removeReceiver: function (name, handler, scope) {
            var event = this.events[name];

            if (event !== true) {
                return event.removeReceiver(handler, scope);
            }

            return false;
        },

        /**
         * Purge all event receivers associated with this object.
         */
        purgeReceivers: function () {
            var evtName, event;

            for (evtName in this.events) if (this.events.hasOwnProperty(evtName)) {
                event = this.events[evtName];
                if (event !== true) {
                    event.purgeReceivers();
                }
            }
            return this;
        }
    };

    /*
     * Event class, used internally
     */
    Emitter.Event = function (name, scope) {
        this.scope = scope;
        this.name = name;
        this.receivers = [];
        this.emitting = false;
    };

    Emitter.Event.prototype = {
        addReceiver: function (handlerFn, scope) {
            var listener;
            scope = scope || this.scope;

            if (this.getReceiverIndex(handlerFn, scope) < 0) {
                listener = this.createReceiver(handlerFn, scope);
                if (this.emitting) { // protect emitter's loop by cloning the array being itarated
                    this.receivers = this.receivers.slice(0);
                }
                this.receivers.push(listener);
            }
        },

        createReceiver: function (handlerFn, scope) {
            var receiver = {handler: handlerFn, scope: scope};

            return receiver;
        },

        getReceiverIndex: function (handlerFn, scope) {
            var i, receivers = this.receivers, receiver;

            for (i = 0; (receiver = receivers[i]); i++) {
                if (receiver.handler === handlerFn && receiver.scope === scope) {
                    return i;
                }
            }

            return -1;
        },

        removeReceiver: function (handlerFn, scope) {
            var receivers = this.emitting ? (this.receivers = this.receivers.slice(0)) : this.receivers,
                idx = this.getReceiverIndex(handlerFn, scope),
                found = idx >= 0;

            if (found) {
                receivers.splice(idx, 1);
            }

            return found;
        },

        purgeReceivers: function () {
            this.receivers = [];
        },

        emit: function () {
            var i, receiver, args;

            if (this.receivers.length) {
                args = Array.prototype.slice.call(arguments, 0); // clone arguments to protect them
                this.emitting = true;

                for (i = 0; (receiver = this.receivers[i]); i++) {
                    receiver.handler.apply(receiver.scope || this.scope || window, args);
                }

                this.emitting = false;
            }
        }
    };

    return Emitter;
}());

