/**
 * @fileoverview Exports an EventEmitter classes that provide methods
 * to emit events and register event listener functions.
 */

defineClass(org.ringojs.wrappers.EventAdapter);

/**
 * This class provides methods to emit events and add or remove event listeners.
 *
 * The `EventEmitter` function can be used as constructor or as mix-in. Use the
 * `new` keyword to construct a new EventEmitter:
 *
 *     const emitter = new EventEmitter();
 *
 * To add event handling methods to an existing object, call or apply the
 * `EventEmitter` function with the object as `this`:
 *
 *     EventEmitter.call(object);
 */
const EventEmitter = exports.EventEmitter = function() {
    // if called on an object other than EventEmitter define properties
    if (!(this instanceof EventEmitter)) {
        Object.defineProperties(this, emitterImpl);
    }
};

/**
 * An adapter for dispatching Java events to Ringo. This class takes a Java
 * class or interface as argument and creates a Java object that extends or
 * implements the  class or interface and forwards method calls to event
 * listener functions registered using the EventEmitter methods.
 *
 * Like [EventEmitter](#EventEmitter), `JavaEventEmitter` can be used as
 * constructor or as mix-in. Use the `new` keyword to construct a new
 * JavaEventEmitter:
 *
 *     const emitter = new JavaEventEmitter(JavaClassOrInterface);
 *
 * To add event handling methods to an existing object, call or apply the
 * `JavaEventEmitter` function with the object as `this`:
 *
 *     JavaEventEmitter.call(object, JavaClassOrInterface);
 *
 * JavaEventEmitter accepts an object as optional second argument that maps
 * Java methods to event names. If the first argument is a Java class
 * this mapping also allows to select which methods should be overridden. If
 * called without event mapping the method name is used as event name, except
 * for methods like `onFoo` which will trigger event `foo`.
 *
 * @param {java.lang.Class|Array} classOrInterface a Java class or interface, or an Array containing
 *     multiple Java interfaces.
 * @param {Object} eventMapping optional object mapping method names to event names.
 *     If this parameter is defined only methods whose name is a property
 *     key in the object will be overridden, and the event type will be
 *     set to the property value instead of the method name.
 */
exports.JavaEventEmitter = function(classOrInterface, eventMapping) {
    const classArg = Array.isArray(classOrInterface) ?
            classOrInterface : [classOrInterface];
    const e = new EventAdapter(classArg, eventMapping);
    Object.defineProperties(this, {
        /**
         * The generated Java object. This implements the Java interface
         * passed to the `JavaEventEmitter` constructor and can be passed
         * to Java code that expects given interface.
         */
        impl: {
            value: e.impl
        },
        /**
         * Emit an event to all listeners registered for this event type
         * @param {String} type type the event type
         * @param {...} [args...] optional arguments to pass to the listeners
         * @return true if the event was handled by at least one listener, false otherwise
         * @throws Error if the event type was "error" and there were no listeners
         * @function
         */
        emit: {
            value: e.emit.bind(e)
        },
        /**
         * Add a listener function for the given event. The function will be
         * called asynchronously on the thread of the local event loop.
         * @param {String} type the event type
         * @param {Function} listener the listener
         * @function
         */
        addListener: {
            value: e.addListener.bind(e)
        },
        /**
         * Add a listener function for the given event. This is a shortcut for
         * [addListener()](#EventEmitter.prototype.addListener)
         * @param {String} type the event type
         * @param {Function} listener the listener
         * @function
         */
        on: {
            get: function() {
                return this.addListener;
            }
        },
        /**
         * Add a synchronous listener function for the given event. A
         * synchronous listener will be called by an outside thread instead
         * of the thread of the local event loop. This means it can be
         * called concurrently while this worker is running other code.
         * @param {String} type the event type
         * @param {Function} listener the listener
         * @function
         */
        addSyncListener: {
            value: e.addSyncListener.bind(e)
        },
        /**
         * Remove a listener function for the given event.
         * @param {String} type the event type
         * @param {Function} listener the listener
         * @function
         */
        removeListener: {
            value: e.removeListener.bind(e)
        },
        /**
         * Removes all listener functions for a given event.
         * @param {String} type the event type
         * @function
         */
        removeAllListeners: {
            value: e.removeAllListeners.bind(e)
        }
    });
    return this;
};

const emitterImpl = {
    /**
     * Emit an event to all listeners registered for this event type
     * @param {String} type type the event type
     * @param {...} [args...] optional arguments to pass to the listeners
     * @name EventEmitter.prototype.emit
     * @return true if the event was handled by at least one listener, false otherwise
     * @throws Error if the event type was "error" and there were no listeners
     * @function
     */
    emit: {
        value: function(type) {
            let listeners = this._events ? this._events[type] : null;
            if (Array.isArray(listeners) && !listeners.length) {
                listeners = null; // normalize empty listener array
            }
            let args;
            if (typeof listeners === "function") {
                args = Array.prototype.slice.call(arguments, 1);
                listeners.apply(this, args);
                return true;
            } else if (Array.isArray(listeners)) {
                args = Array.prototype.slice.call(arguments, 1);
                listeners.forEach(func => func.apply(this, args));
                return true;
            } else {
                return false;
            }
        }
    },
    /**
     * Add a listener function for the given event. This is a shortcut for
     * [addListener()](#EventEmitter.prototype.addListener)
     * @param {String} type the event type
     * @param {Function} listener the listener
     * @returns {EventEmitter} this object for chaining
     * @name EventEmitter.prototype.on
     * @function
     */
    on: {
        get: function() {
            return this.addListener;
        }
    },
    /**
     * Add a listener function for the given event. This is a shortcut for
     * [addListener()](#EventEmitter.prototype.addListener)
     * @param {String} type the event type
     * @param {Function} listener the listener
     * @returns {EventEmitter} this object for chaining
     * @name EventEmitter.prototype.on
     * @function
     */
    addListener: {
        value: function(type, listener) {
            if (typeof listener !== "function") {
                throw new Error ("Event listener must be a function");
            }

            this._events || (this._events = {});
            this.emit("newListener", type, listener);

            if (!this._events[type]) {
                // store as single function
                this._events[type] = listener;
            } else if (Array.isArray(this._events[type])) {
                this._events[type].push(listener);
            } else {
                // convert to array
                this._events[type] = [this._events[type], listener];
            }

            return this;
        }
    },
    /**
     * Remove a listener function for the given event.
     * @param {String} type the event type
     * @param {Function} listener the listener
     * @returns {EventEmitter} this object for chaining
     * @name EventEmitter.prototype.removeListener
     * @function
     */
    removeListener: {
        value: function(type, listener) {
            if (typeof listener !== "function") {
                throw new Error ("Event listener must be a function");
            }

            if (this._events && this._events.hasOwnProperty(type)) {
                const listeners = this._events[type];
                if (listeners === listener) {
                    delete this._events[type];
                } else if (Array.isArray(listeners)) {
                    const idx = listeners.indexOf(listener);
                    if (idx > -1) {
                        if (listeners.length === 1) {
                            this._events = null;
                        } else {
                            listeners.splice(idx, 1);
                        }
                    }
                }
            }
            return this;
        }
    },
    /**
     * Remove all listener function for the given event.
     * @param {String} type the event type
     * @returns {EventEmitter} this object for chaining
     * @name EventEmitter.prototype.removeAllListeners
     * @function
     */
    removeAllListeners: {
        value: function(type) {
            if (type && this._events.hasOwnProperty(type)) {
                delete this._events[type];
            }
            return this;
        }
    },
    /**
     * Get an array containing the listener functions for the given event.
     * If no listeners exist for the given event a new array is created.
     * Changes on the return value will be reflected in the EventEmitter instance.
     * @param {String} type the event type
     * @returns {Array} the lister array
     * @name EventEmitter.prototype.listeners
     * @function
     */
    listeners: {
        value: function(type) {
            this._events || (this._events = {});
            if (!this._events[type]) {
                this._events[type] = [];
            } else if (!Array.isArray(this._events[type])) {
                this._events[type] = [this._events[type]];
            }
            return this._events[type];
        }
    }
};

Object.defineProperties(EventEmitter.prototype, emitterImpl);
