fluro.utils.js

import _ from 'lodash';
import moment from 'moment';
import axios from 'axios';
import { isBrowser, isNode } from 'browser-or-node';


///////////////////////////////////////////////////////////////////////////////

/**
 * @classdesc A static service that provides useful helper functions and tools for other Fluro services
 * @alias utils
 * @class
 * @hideconstructor
 */
var FluroUtils = {};

///////////////////////////////////////////////////////////////////////////////

/**
 * A helpful function that can take a keyed object literal and map it to url query string parameters
 * @alias utils.mapParameters
 * @param  {Object} parameters The object you want to transalte
 * @return {String}            The query string
 * @example 
 * //Returns &this=that&hello=world
 * fluro.utils.mapParameters({"this":"that", "hello":"world"})
 */
FluroUtils.mapParameters = function(parameters) {
    return _.chain(parameters)
        .reduce(function(set, v, k) {
            if (v === undefined || v === null || v === false) {
                return set;
            }

            if (_.isArray(v)) {
                _.each(v, function(value) {
                    set.push(`${k}=${encodeURIComponent(value)}`);
                })

            } else {
                set.push(encodeURIComponent(k) + '=' + encodeURIComponent(v));
            }

            return set;
        }, [])
        .compact()
        .value()
        .join('&');
}




///////////////////////////////////////////////////////////////////////////////

/**
 * A function that will take an integer and a currency string and return a formatted numeric amount rounded to 2 decimal places
 * @alias utils.formatCurrency
 * @param  {Integer} value The amount in cents
 * @param  {String} currency The currency to format
 * @return {String}            The formatted value
 * @example 
 * 
 * //Returns £10.00
 * fluro.utils.formatCurrency(1000, 'gbp');
 * 
 * //Returns $10.00
 * fluro.utils.formatCurrency(1000, 'usd');
 * 
 */
FluroUtils.formatCurrency = function(value, currency) {

    if (!value || isNaN(value)) {
        value = 0;
    }

    var currencyPrefix = FluroUtils.currencySymbol(currency);
    return `${currencyPrefix}${parseFloat(parseInt(value) / 100).toFixed(2)}`;

}


/**
 * A function that will take a currency string and return the symbol
 * @alias utils.currencySymbol
 * @param  {String} currency The currency
 * @return {String}            The symbol
 * @example 
 * 
 * //Returns £
 * fluro.utils.currencySymbol('gbp');
 * 
 * //Returns $
 * fluro.utils.currencySymbol('usd');
 * 
 */
FluroUtils.currencySymbol = function(currency) {
    //Ensure lowercase currency
    currency = String(currency).toLowerCase();

    switch (String(currency).toLowerCase()) {
        case 'gbp':
            return '£';
            break;
        case 'eur':
            return '€';
            break;
        default:
            return '$';
            break;
    }
}



FluroUtils.getAvailableCurrencies = function(defaultCountryCode) {


    var array = [];

    array.push({
        name: `USD (${FluroUtils.currencySymbol("usd")})`,
        value: "usd",
        countryCode: { 'US': true },
    });

    array.push({
        name: `GBP (${FluroUtils.currencySymbol("gbp")})`,
        value: "gbp",
        countryCode: { 'GB': true, 'UK': true },
    });

    array.push({
        name: `CAD (${FluroUtils.currencySymbol("cad")})`,
        value: "cad",
        countryCode: { 'CA': true },
    });

    array.push({
        name: `AUD (${FluroUtils.currencySymbol("aud")})`,
        value: "aud",
        countryCode: { 'AU': true },
    });


    array.push({
        name: `NZD (${FluroUtils.currencySymbol("nzd")})`,
        value: "nzd",
        countryCode: { 'NZ': true },
    });

    array.push({
        name: `SGD (${FluroUtils.currencySymbol("sgd")})`,
        value: "sgd",
        countryCode: { 'SG': true },
    });


    if (defaultCountryCode) {

        var findMatch = array.findIndex(function(currency) {
            return currency.countryCode[defaultCountryCode];
        })

        const moveArrayItem = (array, fromIndex, toIndex) => {
            const arr = [...array];
            arr.splice(toIndex, 0, ...arr.splice(fromIndex, 1));
            return arr;
        }

        if (findMatch != -1) {
            array = moveArrayItem(array, findMatch, 0)
            console.log('Default currency is', array[0]);
        }
    }

    return array;
}

///////////////////////////////////////////////////////////////////////////////

/**
 * A helpful function for creating a fast hash object that can be used for more efficient loops
 * @alias utils.hash
 * @param  {Array} array The array to reduce
 * @param  {String} key The key or path to the property to group by
 * @return {Object}            A hash object literal
 * @example 
 * //Returns {something:[{title:'test', definition:'something'}]}
 * fluro.utils.mapReduce([{title:'test', definition:'something'}], 'definition');
 * 
 */
FluroUtils.hash = function(array, key) {

    return _.reduce(array, function(set, item) {

        var key = _.get(item, key);
        set[key] = item;
        return set;
    }, {});
}


///////////////////////////////////////////////////////////////////////////////

/**
 * A helpful function that can create a globally unique id
 * @alias utils.guid
 * @return {String}            The new guid
 * @example 
 * //Returns 20354d7a-e4fe-47af-8ff6-187bca92f3f9
 * fluro.utils.guid()
 */
FluroUtils.guid = function() {
    var u = (new Date()).getTime().toString(16) +
        Math.random().toString(16).substring(2) + "0".repeat(16);
    var guid = u.substr(0, 8) + '-' + u.substr(8, 4) + '-4000-8' +
        u.substr(12, 3) + '-' + u.substr(15, 12);

    return guid;
}






//////////////////////////////////////////////////

/**
 * A helper function to extract a default value from a fluro field definition
 * @alias utils.getDefaultValueForField
 * @return {String|Number|Object}            The default value
 */
FluroUtils.getDefaultValueForField = function(field) {

    var blankValue;
    var multiple = field.maximum != 1;

    //Check if it's a nested subgroup or embedded form
    var nested = ((field.type == 'group' && field.asObject) || field.directive == 'embedded');

    ///////////////////////////////////////

    if (multiple) {
        blankValue = [];
    }

    ///////////////////////////////////////

    switch (field.type) {
        case 'reference':
            if (field.defaultReferences && field.defaultReferences.length) {
                if (multiple) {
                    blankValue = blankValue.concat(field.defaultReferences);

                } else {
                    blankValue = _.first(field.defaultReferences);
                }
            }
            break;
        default:
            if (field.defaultValues && field.defaultValues.length) {
                if (multiple) {
                    blankValue = blankValue.concat(field.defaultValues);

                } else {
                    blankValue = _.first(field.defaultValues);
                }
            }
            break;
    }

    ///////////////////////////////////////


    if (multiple) {

        var askCount = Math.max(field.askCount, field.minimum);
        var additionalRequired = Math.max((askCount - blankValue.length), 0);

        //If we need some entries by default
        if (additionalRequired) {

            switch (field.type) {
                // case 'string':
                //     _.times(additionalRequired, function() {
                //         blankValue.push('');
                //     })
                //     break;
                default:
                    switch (field.directive) {
                        case 'wysiwyg':
                        case 'textarea':
                        case 'code':
                            _.times(additionalRequired, function() {
                                blankValue.push('');
                            })
                            break;
                        default:
                            //We need to add objects
                            if (nested) {
                                _.times(additionalRequired, function() {
                                    blankValue.push({});
                                })
                            }
                            break;
                    }
                    break;
            }

        }
    } else {

        if (!blankValue) {

            switch (field.type) {
                case 'string':
                    blankValue = '';
                    break;
                default:
                    switch (field.directive) {
                        case 'wysiwyg':
                        case 'textarea':
                        case 'code':
                            // case 'select':
                            blankValue = '';
                            break;
                        default:
                            //We need to add objects
                            if (nested) {
                                blankValue = {};
                            }
                            //  else {
                            //     blankValue =  null;
                            // }
                            break;
                    }
                    break;
            }
        }
    }

    ///////////////////////////////////////

    return blankValue;
}




//////////////////////////////////////////////////////

/**
 * A helpful function that can return a subset of an array compared to specified criteria, This is usually used
 * to evaluate expressions on Fluro forms
 * @alias utils.extractFromArray
 * @param  {Array} array The array you want to filter
 * @param  {String} path The path to the property you want to compare on each item in the array
 * @param  {Object} options Pass through extra options for how to extract the values
 * @return {Array}           An array of all values retrieved from the array, unless options specifies otherwise
 * @example 
 * //Returns [26, 19] as all the values
 * fluro.utils.extractFromArray([{name:'Jerry', age:26}, {name:'Susan', age:19}], 'age');
 * 
 * //Returns 45
 * fluro.utils.extractFromArray([{name:'Jerry', age:26}, {name:'Susan', age:19}], 'age', {sum:true});
 * 
 */
FluroUtils.extractFromArray = function(array, key, options) {

    if (!options) {
        options = {}
    }

    /////////////////

    //Filter the array options by a certain value and operator
    var matches = _.reduce(array, function(set, entry) {
        //Get the value from the object
        var retrievedValue = _.get(entry, key);

        if (options.debug) {
            console.log('EXTRACT: entry value?', key, retrievedValue)
        }

        ///////////////////////

        var isNull = (!retrievedValue && (retrievedValue !== false && retrievedValue !== 0));

        if (options.debug) {
            console.log('EXTRACT: is null?', isNull)
        }
        if (options.excludeNullValues && isNull) {
            if (options.debug) {
                console.log('EXTRACT: exclude')
            }
            return set;
        }

        set.push(retrievedValue);
        return set;
    }, [])


    if (options.debug) {
        console.log('EXTRACT: matches', matches)
    }

    /////////////////

    if (options.flatten) {
        matches = _.flatten(matches);

        if (options.debug) {
            console.log('EXTRACT: flattened', matches)
        }
    }

    /////////////////

    if (options.unique) {
        matches = _.uniq(matches);

        if (options.debug) {
            console.log('EXTRACT: unique', matches)
        }
    }

    /////////////////

    if (options.sum) {
        matches = matches.reduce(function(a, b) {
            return a + b;
        }, 0);

        if (options.debug) {
            console.log('EXTRACT: sum', matches)
        }
    }



    /////////////////

    return matches;
}




//////////////////////////////////////////////////////

/**
 * A helpful function that can return a subset of an array compared to specified criteria, This is usually used
 * to evaluate expressions on Fluro forms
 * @alias utils.matchInArray
 * @param  {Array} array The array you want to filter
 * @param  {String} path The path to the property you want to compare on each item in the array
 * @param  {String} value The value to compare with
 * @param  {String} operator Can be Possible options are ('>', '<', '>=', '<=', 'in', '==') Defaults to '==' (Is equal to)
 * @return {Array}           An array that contains all items that matched
 * @example 
 * //Returns [{name:'Jerry', age:26}] as that is only item in the array that matches the criteria
 * fluro.utils.matchInArray([{name:'Jerry', age:26}, {name:'Susan', age:19}], 'age', 26, '>=');
 * 
 */
FluroUtils.matchInArray = function(array, key, value, operator) {

    //Filter the array options by a certain value and operator
    var matches = _.filter(array, function(entry) {
        //Get the value from the object
        var retrievedValue = _.get(entry, key);
        var isMatch;

        ///////////////////////

        //Check how to operate
        switch (operator) {
            case '>':
                isMatch = (retrievedValue > value);
                break;
            case '<':
                isMatch = (retrievedValue < value);
                break;
            case '>=':
                isMatch = (retrievedValue >= value);
                break;
            case '<=':
                isMatch = (retrievedValue <= value);
                break;
            case 'in':
                isMatch = _.includes(retrievedValue, value);
                break;
            default:
                //operator is strict equals
                if (value === undefined) {
                    isMatch = retrievedValue;
                } else {
                    isMatch = (retrievedValue == value);
                }
                break;
        }

        ///////////////////////

        // console.log('MATCH IN ARRAY', isMatch, key, value, retrievedValue,operator)
        return isMatch;
    })

    return matches;
}

///////////////////////////////////////////////////////////////////////////////

/**
 * A helpful class that can take an array of values and return them as a comma seperated
 * string, If the values are objects, then a property to use as the string representation can be specified
 * @alias utils.comma
 * @param  {Array} array The array of values to translate
 * @param  {String} path  An optional property key to use for each value
 * @return {String}       The resulting comma seperated string
 * @example
 * //Returns 'cat, dog, bird'
 * fluro.utils.comma(['cat', 'dog', 'bird']);
 * 
 * //Returns 'cat, dog, bird'
 * fluro.utils.comma([{title:'cat'}, {title:'dog'}, {title:'bird'}], 'title');
 */
FluroUtils.comma = function(array, path, limit) {

    if (limit) {
        array = array.slice(0, limit);
    }

    return _.chain(array)
        .compact()
        .map(function(item) {
            if (path && path.length) {
                return _.get(item, path);
            }

            return item;
        })
        .value()
        .join(', ');

}

///////////////////////////////////////////////////////////////////////////////

//Helper function to get an id of an object

/**
 * Returns a specified _id for an object
 * @alias utils.getStringID
 * @param  {Object} input      An object that is or has an _id property
 * @param  {Boolean} asObjectID Whether to convert to a Mongo ObjectId
 * @return {String}            Will return either a string or a Mongo ObjectId
 *
 * @example
 *
 * //Returns '5cb3d8b3a2219970e6f86927'
 * fluro.utils.getStringID('5cb3d8b3a2219970e6f86927')
 *
 * //Returns true
 * typeof FluroUtils.getStringID({_id:'5cb3d8b3a2219970e6f86927', title, ...}) == 'string';

 * //Returns true
 * typeof FluroUtils.getStringID({_id:'5cb3d8b3a2219970e6f86927'}, true) == 'object';
 */
FluroUtils.getStringID = function(input, asObjectID) {

    if (!input) {
        return input;
    }

    /////////////////////////////////

    var output;

    if (input._id) {
        output = String(input._id);
    } else {
        output = String(input);
    }

    if (!asObjectID || isBrowser) {


        // console.log('NORMAL', asObjectID, isBrowser)
        return output;
    }

    return output;

    // //Load mongoose if we can
    // try {
    //     var mongoose = require('mongoose');
    // } catch(e) {
    //     console.log('ERROR', e);
    //     return output;
    // }

    // // console.log('Type as object id')
    // var ObjectId = mongoose.Types.ObjectId;
    // var isValid = ObjectId.isValid(String(output));
    // if (!isValid) {
    //     return;
    // }

    // return new ObjectId(output);



}




// distance(point1, point2, unit) {

//                 var lat1 = point1.lat;
//                 var lon1 = point1.lon;

//                 var lat2 = point2.lat;
//                 var lon2 = point2.lon;

//                 if ((lat1 == lat2) && (lon1 == lon2)) {
//                     return 0;
//                 } else {
//                     var radlat1 = Math.PI * lat1 / 180;
//                     var radlat2 = Math.PI * lat2 / 180;
//                     var theta = lon1 - lon2;
//                     var radtheta = Math.PI * theta / 180;
//                     var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
//                     if (dist > 1) {
//                         dist = 1;
//                     }
//                     dist = Math.acos(dist);
//                     dist = dist * 180 / Math.PI;
//                     dist = dist * 60 * 1.1515;
//                     if (unit == "K") { dist = dist * 1.609344 }
//                     if (unit == "N") { dist = dist * 0.8684 }
//                     return dist;
//                 }
//             }

///////////////////////////////////////////////////////////////////////////////

/**
 * Cleans and maps an array of objects to an array of IDs  
 * @alias utils.arrayIDs      
 * @param  {Array} array      An array of objects or object ids
 * @param  {Boolean} asObjectID Whether or not to map the ids as Mongo ObjectIds
 * @return {Array}            An array of Ids
 *
 * @example
 * //Returns ['5cb3d8b3a2219970e6f86927', '5cb3d8b3a2219970e6f86927', '5cb3d8b3a2219970e6f86927']
 * fluro.utils.arrayIDs([{_id:'5cb3d8b3a2219970e6f86927'}, {_id:'5cb3d8b3a2219970e6f86927'}, null, '5cb3d8b3a2219970e6f86927'])
 */
FluroUtils.arrayIDs = function(array, asObjectID) {

    if (!array) {
        return [];
    }

    return _.chain(array)
        .compact()
        .map(function(input) {
            return FluroUtils.getStringID(input, asObjectID);
        })
        .compact()
        .uniq()
        .value();

}

///////////////////////////////////////////////////////////////////////////////

/**
 * Helper function for retrieving a human readable error message from server error response objects
 * @alias utils.errorMessage
 * @param  {Object} error The error object to translate    
 * @return {String}     The resulting human readable error message
 */
FluroUtils.errorMessage = function(err) {


    if (_.isArray(err)) {
        err = _.first(err);
    }

    ////////////////////////////////////

    var candidates = [
        'response.data.message',
        'response.data',
        'message',
    ]

    ////////////////////////////////////

    var message = _.chain(candidates)
        .map(function(path) {
            return _.get(err, path);
        })
        .compact()
        .first()
        .value();

    ////////////////////////////////////

    if (Array.isArray(message)) {
        message = message[0];
    }

    ////////////////////////////////////

    if (!message || !message.length) {
        return String(err);
    }

    ////////////////////////////////////

    return message;
}


/////////////////////////////////////////////
/////////////////////////////////////////////



/**
 * Helper function for sorting process cards by priority
 * @alias utils.processCardPrioritySort
 * @param  {Object} card The process card to sort
 * @return {Integer}     An integer representing it's sorting priority
 */


FluroUtils.processCardPrioritySort = function(card) {

    var num = '2';
    var trailer = 0;
    var val;

    ///////////////////////////////////////////

    //If we are archived then add straight to the bottom of the list
    if (card.status == 'archived') {
        num = '4';
        val = parseFloat(num + '.' + trailer);
        return val + '-' + card.title;
    }

    ///////////////////////////////////////////

    //If we are complete then add us to the bottom of the list
    if (card.processStatus == 'complete') {
        num = '3';
        val = parseFloat(num + '.' + trailer);
        return val + '-' + card.title;
    }

    ///////////////////////////////////////////

    if (card.dueDate) {

        var dueMoment = moment(card.dueDate);
        var dueDate = dueMoment.toDate();

        var nowMoment = moment();
        var now = nowMoment.toDate();

        var duetime = dueDate.getTime();
        trailer = dueDate.getTime()

        if (duetime < now.getTime()) {
            //If it's overdue then we add it to the very very top
            num = '0';
        } else {
            //Otherwise just add it to the top of the 
            //pending cards
            num = '1';
        }
    }

    ///////////////////////////////////////////

    var val = parseFloat(num + '.' + trailer);

    return val + '-' + card.title;

}


////////////////////////////////////
////////////////////////////////////
////////////////////////////////////

/**
 * Helper function for cleaning strings to use as database ids
 * @alias utils.machineName
 * @param  {String} string The string to clean eg. (Awesome Event!)
 * @return {String}     A cleaned and formatted string eg. (awesomeEvent)
 */

FluroUtils.machineName = function(string) {

    if (!string || !string.length) {
        return;
    }

    var regexp = /[^a-zA-Z0-9-_]+/g;
    return string.replace(regexp, '');
}


////////////////////////////////////


FluroUtils.hhmmss = function(secs) {
    function pad(str) {
        return ("0" + str).slice(-2);
    }
    var minutes = Math.floor(secs / 60);
    secs = secs % 60;
    var hours = Math.floor(minutes / 60)
    minutes = minutes % 60;
    return pad(hours) + ":" + pad(minutes) + ":" + pad(secs);
}

////////////////////////////////////


var injectedScripts = {}
/**
 * Helper function for including external javascript resources
 * This ensures that scripts are only included a single time on each page
 * @alias utils.injectScript
 * @param  {String} url The URL of the script to import
 * @return {Promise}     A promise that resolves once the script has been included on the page
 */

FluroUtils.injectScript = function(scriptURL) {

    return new Promise(function(resolve, reject) {

        if (!document) {
            return reject('Script injection can only be used when running in a browser context')
        }

        if (injectedScripts[scriptURL]) {
            return resolve(scriptURL);
        }


        //Keep note so we don't inject twice
        injectedScripts[scriptURL] = true;

        //////////////////////////////////////

        var script = document.createElement('script');
        script.type = 'text/javascript';
        script.async = true;
        script.onload = function() {
            console.log('Included external script', scriptURL);
            return resolve(scriptURL);
        };
        script.src = scriptURL;
        document.getElementsByTagName('head')[0].appendChild(script);

        ////////////////////////////////////




    })
}



////////////////////////////////////

/**
 * Helper function for including external javascript resources
 * This ensures that scripts are only included a single time on each page
 * @alias utils.injectModule
 * @param  {String} url The URL of the script to import
 * @return {Promise}     A promise that resolves once the script has been included on the page
 */

var inflightPromises = {};


FluroUtils.injectModule = function(scriptURL, options) {

    if (!options) {
        options = {};
    }

    ////////////////////////////////////////////////////

    if (!document) {
        return Promise.reject('Script injection can only be used when running in a browser context');
    }

    ////////////////////////////////////////////////////

    //If we aren't requesting a cache clear
    if (!options.clearCache) {

        //If there is an inflight promise
        if (inflightPromises[scriptURL]) {
            return inflightPromises[scriptURL];
        }
    }

    ////////////////////////////////////////////////////

    var promise = new Promise(function(resolve, reject) {
        axios.get(scriptURL).then(function(res) {
            var source = res.data;
            var script = `"use strict"; var object = {}; try {object = ${source}} catch(e) {console.log(e)} finally {return object}`;


            var compiled = Function(script)();
            return resolve(compiled);
        })


    })

    ////////////////////////////////////////////////////

    //Cache for multiple requests
    inflightPromises[scriptURL] = promise;

    return promise;
}

/////////////////////////////////////////////
/////////////////////////////////////////////

////////////////////////////////////


/**
 * Helper function for getting a flattened list of all nested fields
 * defined for a definition in Fluro
 * @alias utils.getFlattenedFields
 * @param  {Array} fields The array of fields
 * @param  {Array} trail An array to append trails to (required)
 * @param  {Array} trail An array to append titles to (required)
 * @return {Array}     A flattened list of all fields with their nested trails and titles
 */

FluroUtils.getFlattenedFields = function(array, trail, titles) {

    if (!trail) {
        trail = [];
    }

    if (!titles) {
        titles = [];
    }

    return _.chain(array)
        .map(function(field, key) {

            //Create a new object so we don't mutate
            var field = Object.assign({}, field);

            var returnValue = [];



            /////////////////////////////////////////

            //If there are sub fields
            if (field.fields && field.fields.length) {


                if (field.asObject || field.directive == 'embedded') {
                    //Push the field itself
                    trail.push(field.key);
                    titles.push(field.title)

                    field.trail = trail.slice();
                    field.titles = titles.slice();

                    trail.pop();
                    titles.pop();
                    returnValue.push(field);


                    ///////////////////////////////

                    //Prepend the key to all lowed fields




                    if (field.maximum != 1) {
                        // trail.push(field.key + '[' + indexIterator + ']');
                        trail.push(field.key + '[]');
                        titles.push(field.title);
                    } else {
                        trail.push(field.key);
                        titles.push(field.title);
                    }
                }

                var fields = FluroUtils.getFlattenedFields(field.fields, trail, titles);

                if (field.asObject || field.directive == 'embedded') {
                    trail.pop()
                    titles.pop();
                }
                returnValue.push(fields);


            } else {
                /////////////////////////////////////////

                //Push the field key
                trail.push(field.key);
                titles.push(field.title);

                field.trail = trail.slice();
                field.titles = titles.slice();
                trail.pop();
                titles.pop();
                returnValue.push(field);
            }

            /////////////////////////////////////////

            return returnValue;

        })
        .flattenDeep()
        .value();
}



/////////////////////////////////////////////
/////////////////////////////////////////////
/////////////////////////////////////////////


export default FluroUtils;


/////////////////////////////////////////////

//Export the event dispatcher
export function EventDispatcher() {

    var listeners = {};

    /////////////////////////////////////////////

    var dispatcher = {}

    /////////////////////////////////////////////

    //Remove all listeners
    dispatcher.removeAllListeners = function() {
        listeners = {};
    }

    /////////////////////////////////////////////

    dispatcher.dispatch = function(event, details) {

        if (listeners[event]) {

            // console.log('Listeners', event, listeners[event]);
            //For each listener
            listeners[event].forEach(function(callback) {
                //Fire the callback
                // console.log('Fire listener', event, details);
                return callback(details);
            });
        }
    }

    /////////////////////////////////////////////

    dispatcher.addEventListener = function(event, callback) {

        if (!listeners[event]) {
            listeners[event] = [];
        }

        if (listeners[event].indexOf(callback) == -1) {
            //Add to the listeners
            listeners[event].push(callback)
        } else {
            //Already listening
        }
    }

    /////////////////////////////////////////////

    dispatcher.removeEventListener = function(event, callback) {

        if (!listeners[event]) {
            listeners[event] = [];
        }

        //Get the index of the listener
        var index = listeners[event].indexOf(callback);

        if (index != -1) {
            //Remove from the listeners
            listeners[event].splice(index, 1);
        }
    }


    /////////////////////////////////////////////

    //Wrap the event listener functionality
    dispatcher.bootstrap = function(service) {
        if (!service) {
            // console.log('No service to bootstrap to')
            return;
        }

        service.dispatch = dispatcher.dispatch;
        service.addEventListener = dispatcher.addEventListener;
        service.removeEventListener = dispatcher.removeEventListener;
        service.removeAllListeners = dispatcher.removeAllListeners;
    }

    /////////////////////////////////////////////

    return dispatcher;
}