// bea.wlp.disc.event
/*
 * B E A   S Y S T E M S
 *
 * Copyright (c) 2000-2008  BEA Systems, Inc.
 *
 * All Rights Reserved. Unpublished rights reserved under the copyright laws of the United States. The software
 * contained on this media is proprietary to and embodies the confidential technology of BEA Systems, Inc. The
 * possession or receipt of this information does not convey any right to disclose its contents, reproduce it, or use,
 * or license the use, for manufacture or sale, the information or anything described therein. Any use, disclosure, or
 * reproduction without BEA System's prior written permission is strictly prohibited.
 *
 * Any entity defined in this scope that is named with a leading underscore ('_') is reserved for internal use only.
 */

/**
 * @name bea.wlp.disc.event
 * @overview
 *      This module provides a basic model for interacting with custom Disc events.  For documentation about specific
 *      events, see the documentation for those events in their parent module's documentation.
 */
bea.wlp.disc.Module.create("bea.wlp.disc.event", {
    declare: function($, L) {
        /**
         * This class provides a simple eventing framework.  Instances of this class represent discrete event types,
         * and an occurrence of the event itself is manifested when the event instance is fired.  Listeners may be
         * added or removed from event instances to signal their interest in particular event types derived from this
         * class.
         *
         * @class Event
         */
        $.Event = bea.wlp.disc.Class.create(function() {
            // A well-known static object to signal intent to break from an event handling chain
            var cancel = { };

            /**
             * Initializes a new Event instance.
             *
             * @private
             * @method initialize
             * @param {string} name
             *      The string name of the event in string form; not null or empty; e.g. OnMyCustomEvent
             * @param {boolean} [cancellable]
             *      Whether or not the propagation of this event can be terminated by listeners;
             *      if true, listeners that return the boolean false will halt event propagation;
             *      all other return values, including undefined, will allow the event to progagate normally;
             *      defaults to false
             */
            this.initialize = function(name, cancellable) {
                this._listeners = [];
                this._name = name;
                this._cancellable = !!cancellable;
            }

            /**
             * Returns the name of this event.
             *
             * @method getName
             * @returns {string}
             *      The name of this event
             */
            this.getName = function() {
                return this._name;
            }

            /**
             * Returns whether or not the event instance is cancellable when the event is fired.
             *
             * @method isCancellable
             * @returns {boolean}
             *      True if the event instance is cancellable
             */
            this.isCancellable = function() {
                return this._cancellable;
            }

            /**
             * Cancels the current firing of this event if this is a cancellable event.  If this event is not
             * cancellable, an error is thrown.
             *
             * @method cancel
             */
            this.cancel = function() {
                if (this._cancellable) {
                    throw cancel;
                }
                else {
                    throw L("Event {0} is not cancellable", this._name);
                }
            }

            /**
             * Adds a function as a listener to an event instance.  Arguments passed to the listener and cancelListener
             * callbacks on invocation include an event-specific payload object (if any) and a reference to the event
             * being fired, respectively.
             *
             * @method addListener
             * @param {function} listener
             *      The callback function to execute when this event is fired; not null
             * @param {function} [cancelListener]
             *      An optional listener to be invoked if the firing of this event is cancelled; this will be called
             *      <i>iff</i> the listener with which it is associated has already been called, and is not the listener
             *      responsible for cancelling the event (i.e. neither unfired listeners nor the listener that cancels
             *      the event will have their cancel listeners invoked); note that the cancel listener will also
             *      only ever be called if the event instance to which the cancel listener is added is, in fact,
             *      cancellable
             */
            this.addListener = function(listener, cancelListener) {
                if (typeof listener == "function" && this._find(listener) < 0) {
                    this._listeners.push({
                        listen: listener,
                        cancel: (typeof cancelListener == "function" && cancelListener)
                    });
                }
            }

            /**
             * Removes a listener function (and associated cancel listener function, if any) from an event instance.
             *
             * @method removeListener
             * @param {function} listener
             *      The listener function to be removed
             */
            this.removeListener = function(listener) {
                var i = this._find(listener);
                if (i >= 0) {
                    if (this._isFiring) {
                        this._listeners[i] = null;
                    }
                    else {
                        this._listeners.splice(i, 1);
                    }
                }
            }

            /**
             * Fires an event with the specified payload.
             *
             * @method _fire
             * @param {object} [payload]
             *      An event-instance-appropriate payload data object, if any
             * @returns
             *      True if the event completely fired; false if the event firing was cancelled
             */
            this._fire = function(payload) {
                this._isFiring = true;
                var i;
                try {
                    // Length must be checked on every iteration of this loop
                    for (i = 0; i < this._listeners.length; i++) {
                        if (this._listeners[i]) {
                            this._listeners[i].listen(payload, this);
                        }
                    }
                }
                catch (e) {
                    if (e === cancel) {
                        for (var j = 0; j < i; j++) {
                            if (this._listeners[j] && this._listeners[j].cancel) {
                                this._listeners[j].cancel(payload, this);
                            }
                        }
                        return false;
                    }
                    throw e;
                }
                finally {
                    var len = this._listeners.length;
                    for (i = 0; i < len; i++) {
                        if (!this._listeners[i]) {
                            this._listeners.splice(i, 1);
                        }
                    }
                    delete this._isFiring;
                }
                return true;
            }

            /**
             * Finds a specific listener in the list of this event's listeners.
             *
             * @method _find
             * @param {function} listener
             *      The listener function to find
             * @returns {number}
             *      The index at which the listener was found; -1 otherwise
             */
            this._find = function(listener) {
                var index = -1;
                for (var i = 0, len = this._listeners.length; i < len; i++) {
                    if (this._listeners[i] && this._listeners[i].listen === listener) {
                        index = i;
                        break;
                    }
                }
                return index;
            }
        });
    }
});
