/*!
 * Casper is a navigation utility for PhantomJS.
 *
 * Documentation: http://casperjs.org/
 * Repository:    http://github.com/casperjs/casperjs
 *
 * Copyright (c) 2011-2012 Nicolas Perriault
 *
 * Part of source code is Copyright Joyent, Inc. and other Node contributors.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*global CasperError*/

var isArray = Array.isArray;

var EventEmitter = function EventEmitter() {
    "use strict";
    this._filters = {};
};

exports.EventEmitter = EventEmitter;
var defaultMaxListeners = 10;

/**
 * By default EventEmitters will print a warning if more than 10 listeners
 * are added for a particular event. This is a useful default that helps finding memory leaks.
 * Obviously, not all events should be limited to just 10 listeners.
 * The emitter.setMaxListeners() method allows the limit to be modified for
 * this specific EventEmitter instance. The value can be set to Infinity (or 0)
 * to indicate an unlimited number of listeners.
 *
 * @param  Integer  nb  Max Number of Listeners
 */
EventEmitter.prototype.setMaxListeners = function(nb) {
    "use strict";
    if (!this._events) {
        this._events = {};
    }
    this._maxListeners = nb;
};

/**
 * Synchronously calls each of the listeners registered for event,
 * in the order they were registered, passing the supplied arguments to each.
 * Returns true if event had listeners, false otherwise
 *
 * @param  Array     args...  The rest of arguments passed to fn
 * @return Boolean
 * @throws {Error}            If invokation failed.
 */
EventEmitter.prototype.emit = function emit() {
    "use strict";
    var type = arguments[0];
    // If there is no 'error' event listener then throw.
    if (type === 'error') {
        if (!this._events || !this._events.error ||
        isArray(this._events.error) && !this._events.error.length) {
            if (arguments[1] instanceof Error) {
                throw arguments[1]; // Unhandled 'error' event
            } else {
                throw new CasperError("Uncaught, unspecified 'error' event.");
            }
        }
    }

    if (!this._events) {
        return false;
    }
    var handler = this._events[type];
    if (!handler) {
        return false;
    }

    if (typeof handler === 'function') {
        try {
            switch (arguments.length) {
            // fast cases
            case 1:
                handler.call(this);
                break;
            case 2:
                handler.call(this, arguments[1]);
                break;
            case 3:
                handler.call(this, arguments[1], arguments[2]);
                break;
            // slower
            default:
                var l = arguments.length;
                var args = new Array(l - 1);
                for (var i = 1; i < l; i++) {
                    args[i - 1] = arguments[i];
                }
                handler.apply(this, args);
            }
        } catch (err) {
            this.emit('event.error', err);
        }
        return true;

    } else if (isArray(handler)) {
        var l = arguments.length;
        var args = new Array(l - 1);
        for (var i = 1; i < l; i++) {
            args[i - 1] = arguments[i];
        }

        var listeners = handler.slice();
        for (var i = 0, l = listeners.length; i < l; i++) {
            listeners[i].apply(this, args);
        }
        return true;

    } else {
        return false;
    }
};

/**
 * Adds the listener function to the end of the listeners array for the specified event.
 * No checks are made to see if the listener has already been added.
 * Multiple calls passing the same combination of event and listener
 * will result in the listener being added, and called, multiple times.
 * EventEmitter is defined in src/node_events.cc
 * EventEmitter.prototype.emit() is also defined there.
 *
 * @param  String    type      An event name.
 * @param  function  filterFn  An options callback to apply on event.
 * @return EventEmitter        A reference to the EventEmitter so calls can be chained
 * @throws {CasperError}       If invokation failed.
 */
EventEmitter.prototype.addListener = function addListener(type, listener) {
    "use strict";
    if (typeof listener !== 'function') {
        throw new CasperError('addListener only takes instances of Function');
    }

    if (!this._events) {
        this._events = {};
    }

    // To avoid recursion in the case that type == "newListeners"! Before
    // adding it to the listeners, first emit "newListeners".
    this.emit('newListener', type, listener);

    if (!this._events[type]) {
        // Optimize the case of one listener. Don't need the extra array object.
        this._events[type] = listener;
    } else if (isArray(this._events[type])) {
        // If we've already got an array, just append.
        this._events[type][ type === 'fail' ? 'unshift' : 'push'](listener);

        // Check for listener leak
        if (!this._events[type].warned) {
            var m;
            if (typeof this._maxListeners !== 'undefined') {
                m = this._maxListeners;
            } else {
                m = defaultMaxListeners;
            }

            if (m && m > 0 && this._events[type].length > m) {
                this._events[type].warned = true;
                console.error('Warning: Possible EventEmitter memory leak detected. ' +
                    this._events[type].length +
                    ' listeners of type ' + type + ' added. Use casper.setMaxListeners() to increase' +
                    ' the limit if you are expecting the current limit of ' + m + ' to be exceeded.' +
                    ' Please refer to online documentation for more information.');
                console.trace();
            }
        }
    } else {
        // Adding the second element, need to change to array.
        this._events[type] = type === 'fail' ? [listener, this._events[type]] : [this._events[type], listener];
    }
    return this;
};

/**
 * Alias for emitter.addListener(type, listener).
 *
 * @param  String    type      An event name.
 * @param  function  filterFn  An options callback to apply on event.
 * @return EventEmitter        A reference to the EventEmitter so calls can be chained
 * @throws {CasperError}       If invokation failed.
 */
EventEmitter.prototype.prependListener = function prependListener(type, listener) {
    "use strict";
    if (typeof listener !== 'function') {
        throw new CasperError('addListener only takes instances of Function');
    }

    if (!this._events) {
        this._events = {};
    }

    // To avoid recursion in the case that type == "newListeners"! Before
    // adding it to the listeners, first emit "newListeners".
    this.emit('newListener', type, listener);

    if (!this._events[type]) {
        // Optimize the case of one listener. Don't need the extra array object.
        this._events[type] = listener;
    } else if (isArray(this._events[type])) {

        // If we've already got an array, just append.
        this._events[type].unshift(listener);

        // Check for listener leak
        if (!this._events[type].warned) {
            var m;
            if (typeof this._maxListeners !== 'undefined') {
                m = this._maxListeners;
            } else {
                m = defaultMaxListeners;
            }

            if (m && m > 0 && this._events[type].length > m) {
                this._events[type].warned = true;
                console.error('Warning: Possible EventEmitter memory leak detected. ' +
                    this._events[type].length +
                    ' listeners of type ' + type + ' added. Use casper.setMaxListeners() to increase' +
                    ' the limit if you are expecting the current limit of ' + m + ' to be exceeded.' +
                    ' Please refer to online documentation for more information.');
                console.trace();
            }
        }
    } else {
        // Adding the second element, need to change to array.
        this._events[type] = [listener, this._events[type]];
    }
    return this;
};

/**
 * Alias for emitter.addListener(type, listener).
 *
 * @param  String    type      An event name.
 * @param  function  filterFn  An options callback to apply on event.
 * @return EventEmitter        A reference to the EventEmitter so calls can be chained
 */
EventEmitter.prototype.on = EventEmitter.prototype.addListener;

/**
 * Adds a one time listener function for the event.
 * This listener is invoked only the next time event is triggered, after which it is removed.
 *
 * @param  String    type      An event name.
 * @param  function  filterFn  An options callback to apply on event.
 * @return EventEmitter        A reference to the EventEmitter so calls can be chained
 * @throws {CasperError}       If invokation failed.
 */
EventEmitter.prototype.once = function once(type, listener) {
    "use strict";
    if (typeof listener !== 'function') {
        throw new CasperError('.once only takes instances of Function');
    }

    var self = this;
    var g = function g() {
        self.removeListener(type, g);
        listener.apply(this, arguments);
    };

    g.listener = listener;
    self.on(type, g);

    return this;
};

/**
 * Removes the specified listener from the listener array for the specified event.
 *
 * @param  String    type      An event name
 * @param  function  filterFn  An options callback to apply on event
 * @return EventEmitter        A reference to the EventEmitter so calls can be chained
 * @throws {CasperError}      If invokation failed.
 */
EventEmitter.prototype.removeListener = function removeListener(type, listener) {
    "use strict";
    if (typeof listener !== 'function') {
        throw new CasperError('removeListener only takes instances of Function');
    }

    // does not use listeners(), so no side effect of creating _events[type]
    if (!this._events || !this._events[type]) {
        return this;
    }

    var list = this._events[type];

    if (isArray(list)) {
        var position = -1;
        for (var i = 0, length = list.length; i < length; i++) {
            if (list[i] === listener ||
                list[i].listener && list[i].listener === listener) {
                position = i;
                break;
            }
        }
        if (position < 0) {
            return this;
        }
        list.splice(position, 1);
        if (list.length === 0) {
            delete this._events[type];
        }
    } else if (list === listener ||
             list.listener && list.listener === listener) {
        delete this._events[type];
    }

    return this;
};

/**
 * Removes all listeners, or those of the specified event.
 *
 * @param  String    type      An event name
 * @return EventEmitter        A reference to the EventEmitter so calls can be chained
 */
EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) {
    "use strict";
    if (arguments.length === 0) {
        this._events = {};
        return this;
    }

    // does not use listeners(), so no side effect of creating _events[type]
    if (type && this._events && this._events[type]) {
        this._events[type] = null;
    }
    return this;
};

/**
 * get callbacks listener.
 *
 * @param  String    type      An event name
 * @return mixed
 */
EventEmitter.prototype.listeners = function listeners(type) {
    "use strict";
    if (!this._events) {
        this._events = {};
    }
    if (!this._events[type]) {
        this._events[type] = [];
    }
    if (!isArray(this._events[type])) {
        this._events[type] = [this._events[type]];
    }
    return this._events[type];
};

/**
 * Trigger filter attached to an event and return value(s).
 *
 * @param  String    type      An event name
 * @param  Array     args...  The rest of arguments passed to fn
 * @return mixed
 */
EventEmitter.prototype.filter = function filter() {
    "use strict";
    var type = arguments[0], res;
    if (!this._filters) {
        this._filters = {};
    }
    var _filter = this._filters[type];
    if (typeof _filter === 'function') {
        res = _filter.apply(this, Array.prototype.slice.call(arguments, 1));
    }
    this.emit(Array.prototype.slice.call(arguments, 0).concat([res]));
    return res;
};

/**
 * Remove all filters attached to events or only filters attached to a specific event.
 *
 * @param  String    type      An event name
 * @return EventEmitter        A reference to the EventEmitter so calls can be chained
 */
EventEmitter.prototype.removeAllFilters = function removeAllFilters(type) {
    "use strict";
    if (arguments.length === 0) {
        this._filters = {};
        return this;
    }
    if (type && this._filters && this._filters[type]) {
        this._filters[type] = null;
    }
    return this;
};

/**
 * attaches a filter to an event.
 *
 * @param  String    type      An event name
 * @param  function  filterFn  An options callback to apply on event
 * @return Boolean
 * @throws {CasperError}       If invokation failed
 */
EventEmitter.prototype.setFilter = function setFilter(type, filterFn) {
    "use strict";
    if (!this._filters) {
        this._filters = {};
    }
    if (typeof filterFn !== 'function') {
        throw new CasperError('setFilter only takes instances of Function');
    }
    if (!this._filters[type]) {
        this._filters[type] = filterFn;
        return true;
    }
    // TODO: process multiple filters? in which order? disallow?
    return false;
};
