// backbone.geppetto 0.7.0-rc5
//
// Copyright (C) 2013 Dave Cadwallader, Model N, Inc.
// Distributed under the MIT License
//
// Documentation and full license available at:
// http://modeln.github.com/backbone.geppetto/

(function(factory) {
    if (typeof define === "function" && define.amd) {
        // Register as an AMD module if available...
        define(["underscore", "backbone"], factory);
    } else {
        // Browser globals for the unenlightened...
        factory(_, Backbone);
    }
}(function(_, Backbone) {

    "use strict";

    if (!Backbone) {
        throw "Please include Backbone before Geppetto";
    }

    var NO_MAPPING_FOUND = 'no mapping found for key: ';
    var TYPES = {
        SINGLETON: 'singleton',
        VIEW: 'view',
        OTHER: 'other'
    };

    var Resolver = function(context) {
        this._mappings = {};
        this._context = context;
        this.parent = undefined;
    };
    Resolver.prototype = {
        _createAndSetupInstance: function(Clazz, wiring) {
            var instance = new Clazz();
            this.resolve(instance, wiring);
            return instance;
        },

        _retrieveFromCacheOrCreate: function(key, overrideRules) {
            var output;
            if (this._mappings.hasOwnProperty(key)) {
                var config = this._mappings[key];
                if (!overrideRules && config.type === TYPES.SINGLETON) {
                    if (!config.object) {
                        config.object = this._createAndSetupInstance(config.clazz, config.wiring);
                    }
                    output = config.object;
                } else {
                    if (config.type === TYPES.VIEW) {
                        output = config.clazz;
                    } else if (config.clazz) {
                        output = this._createAndSetupInstance(config.clazz, config.wiring);
                    }
                }
            } else if (this.parent && this.parent.hasWiring(key)) {
                output = this.parent._retrieveFromCacheOrCreate(key, overrideRules);
            } else {
                throw new Error(NO_MAPPING_FOUND + key);
            }
            return output;
        },

        _wrapConstructor: function(OriginalConstructor, wiring) {

            var context = this._context;

            var WrappedConstructor = OriginalConstructor.extend({
                initialize: function() {
                    context.resolver.resolve(this, wiring);
                    OriginalConstructor.prototype.initialize.call(this, arguments);
                }
            });

            return WrappedConstructor;
        },

        createChildResolver: function() {
            var child = new Resolver(this._context);
            child.parent = this;
            return child;
        },

        getObject: function(key) {
            return this._retrieveFromCacheOrCreate(key, false);
        },

        wireValue: function(key, useValue) {
            this._mappings[key] = {
                clazz: null,
                object: useValue,
                type: TYPES.SINGLETON
            };
            return this;
        },

        hasWiring: function(key) {
            return this._mappings.hasOwnProperty(key) || ( !! this.parent && this.parent.hasWiring(key));
        },

        wireClass: function(key, clazz, wiring) {
            this._mappings[key] = {
                clazz: clazz,
                object: null,
                type: TYPES.OTHER,
                wiring: wiring
            };
            return this;
        },

        wireView: function(key, clazz, wiring) {
            this._mappings[key] = {
                clazz: this._wrapConstructor(clazz, wiring),
                object: null,
                type: TYPES.VIEW
            };
            return this;
        },

        wireSingleton: function(key, clazz, wiring) {

            var constructor = (clazz.prototype.initialize ? this._wrapConstructor(clazz, wiring) : clazz);

            this._mappings[key] = {
                clazz: constructor,
                object: null,
                type: TYPES.SINGLETON,
                wiring: wiring
            };
            return this;
        },

        instantiate: function(key) {
            return this._retrieveFromCacheOrCreate(key, true);
        },

        resolve: function(instance, wiring) {
            wiring = wiring || instance.wiring;
            if (wiring) {
                var propNameArgIndex = Number(!_.isArray(wiring));
                _.each(wiring, function(dependencyKey) {
                    instance[arguments[propNameArgIndex]] = this.getObject(dependencyKey);
                }, this);
            }
            this.addPubSub(instance);
            return this;
        },
        addPubSub: function(instance) {
            instance.listen = _.bind(this._context.listen, this._context);
            instance.dispatch = _.bind(this._context.dispatch, this._context);
        },
        release: function(key) {
            delete this._mappings[key];

            return this;
        },
        releaseAll: function() {
            this._mappings = {};
            return this;
        }
    };

    var Geppetto = {};

    Geppetto.version = '0.7.0-rc5';

    Geppetto.EVENT_CONTEXT_SHUTDOWN = "Geppetto:contextShutdown";

    Geppetto.Resolver = Resolver;

    var contexts = {};

    Geppetto.Context = function Context(options) {

        this.options = options || {};
        this.parentContext = this.options.parentContext;

        if (this.options.resolver) {
            this.resolver = this.options.resolver;
        } else if (this.parentContext) {
            this.resolver = this.parentContext.resolver.createChildResolver();
        } else if (!this.resolver) {
            this.resolver = new Resolver(this);
        }

        this.vent = {};
        _.extend(this.vent, Backbone.Events);
        if (_.isFunction(this.initialize)) {
            this.initialize.apply(this, arguments);
        }
        this._contextId = _.uniqueId("Context");
        contexts[this._contextId] = this;

        var wiring = this.wiring || this.options.wiring;
        if (wiring) {
            this._configureWirings(wiring);
        }
    };

    Geppetto.bindContext = function bindContext(options) {

        this.options = options || {};

        var view = this.options.view;

        var context = null;
        if (typeof this.options.context === 'function') {
            // create new context if we get constructor
            context = new this.options.context(this.options);

            // only close context if we are the owner
            if (!view.close) {
                view.close = function() {
                    view.trigger("close");
                    view.remove();
                };
            }

            view.on("close", function() {
                view.off("close");
                context.destroy();
            });
        } else if (typeof this.options.context === 'object') {
            // or use existing context if we get one
            context = this.options.context;
        }

        context.resolver.resolve(view);

        // map context events
        _.each(view.contextEvents, function(callback, eventName) {
            if (_.isFunction(callback)) {
                context.listen(view, eventName, callback);
            } else if (_.isString(callback)) {
                context.listen(view, eventName, view[callback]);
            }
        });

        var returnValue;

        // only set a reference to the context on the view if the view
        // is a pre-0.7.0 component that does not use dependency injection. 
        // this will be removed in a future release...
        if (!view.wiring) {
            view.context = context;
            returnValue = context;
        }

        return returnValue;
    };

    var extractConfig = function(def, key) {
        var thisCtor, thisWiring;
        if (def.hasOwnProperty("ctor")) {
            thisCtor = def.ctor;
            thisWiring = def.wiring;
        } else {
            thisCtor = def;
        }
        return [key, thisCtor, thisWiring];
    };

    Geppetto.Context.prototype._configureWirings = function _configureWirings(wiring) {
        _.each(wiring.singletons, function(def, key) {
            this.wireSingleton.apply(this, extractConfig(def, key));
        }, this);
        _.each(wiring.classes, function(def, key) {
            this.wireClass.apply(this, extractConfig(def, key));
        }, this);
        _.each(wiring.values, function(value, key) {
            this.wireValue(key, value);
        }, this);
        _.each(wiring.views, function(def, key) {
            this.wireView.apply(this, extractConfig(def, key));
        }, this);
        this.wireCommands(wiring.commands);
    };

    var validateListen = function(target, eventName, callback) {

        if (!_.isObject(target) || !_.isFunction(target.listenTo) || !_.isFunction(target.stopListening)) {
            throw "Target for listen() must define a 'listenTo' and 'stopListening' function";
        }

        if (!_.isString(eventName)) {
            throw "eventName must be a String";
        }

        if (!_.isFunction(callback)) {
            throw "callback must be a function";
        }
    };

    Geppetto.Context.prototype.listen = function listen(target, eventName, callback) {
        validateListen(target, eventName, callback);
        return target.listenTo(this.vent, eventName, callback, target);
    };

    Geppetto.Context.prototype.listenToOnce = function listenToOnce(target, eventName, callback) {
        validateListen(target, eventName, callback);
        return target.listenToOnce(this.vent, eventName, callback, target);
    };

    Geppetto.Context.prototype.dispatch = function dispatch(eventName, eventData) {
        if (!_.isUndefined(eventData) && !_.isObject(eventData)) {
            throw "Event payload must be an object";
        }
        eventData = eventData || {};
        eventData.eventName = eventName;
        this.vent.trigger(eventName, eventData);
    };

    Geppetto.Context.prototype.dispatchToParent = function dispatchToParent(eventName, eventData) {
        if (this.parentContext) {
            this.parentContext.vent.trigger(eventName, eventData);
        }
    };

    Geppetto.Context.prototype.dispatchGlobally = function dispatchGlobally(eventName, eventData) {

        _.each(contexts, function(context, contextId) {
            context.vent.trigger(eventName, eventData);
        });
    };

    Geppetto.Context.prototype.wireCommand = function wireCommand(eventName, CommandConstructor, wiring) {

        var _this = this;

        if (!_.isFunction(CommandConstructor)) {
            throw "Command must be constructable";
        }

        this.vent.listenTo(this.vent, eventName, function(eventData) {

            var commandInstance = new CommandConstructor();

            commandInstance.context = _this;
            commandInstance.eventName = eventName;
            commandInstance.eventData = eventData;
            _this.resolver.resolve(commandInstance, wiring);
            if (_.isFunction(commandInstance.execute)) {
                commandInstance.execute();
            }

        }, this);
    };

    Geppetto.Context.prototype.wireCommands = function wireCommands(commandsMap) {
        var _this = this;
        _.each(commandsMap, function(mixedType, eventName) {
            if (_.isArray(mixedType)) {
                _.each(mixedType, function(commandClass) {
                    _this.wireCommand(eventName, commandClass);
                });
            } else {
                _this.wireCommand(eventName, mixedType);
            }
        });
    };

    Geppetto.Context.prototype.wireView = function(key, clazz, wiring) {
        this.resolver.wireView(key, clazz, wiring);
        return this;
    };

    Geppetto.Context.prototype.wireSingleton = function(key, clazz, wiring) {
        this.resolver.wireSingleton(key, clazz, wiring);
        return this;
    };

    Geppetto.Context.prototype.wireValue = function(key, useValue) {
        this.resolver.wireValue(key, useValue);
        return this;
    };

    Geppetto.Context.prototype.wireClass = function(key, clazz, wiring) {
        this.resolver.wireClass(key, clazz, wiring);
        return this;
    };

    Geppetto.Context.prototype.hasWiring = function(key) {
        return this.resolver.hasWiring(key);
    };

    Geppetto.Context.prototype.getObject = function(key) {
        return this.resolver.getObject(key);
    };

    Geppetto.Context.prototype.instantiate = function(key) {
        return this.resolver.instantiate(key);
    };

    Geppetto.Context.prototype.resolve = function(instance, wiring) {
        this.resolver.resolve(instance, wiring);
        return this;
    };

    Geppetto.Context.prototype.release = function(key) {
        this.resolver.release(key);
        return this;
    };

    Geppetto.Context.prototype.releaseAll = function() {
        this.resolver.releaseAll();
        return this;
    };

    Geppetto.Context.prototype.destroy = function destroy() {
        this.vent.stopListening();
        this.resolver.releaseAll();

        delete contexts[this._contextId];

        this.dispatchToParent(Geppetto.EVENT_CONTEXT_SHUTDOWN);
    };

    Geppetto.Context.extend = Backbone.View.extend;

    var debug = {

        contexts: contexts,

        countEvents: function countEvents() {

            var numEvents = 0;

            _.each(contexts, function(context, id) {
                if (contexts.hasOwnProperty(id)) {
                    numEvents += _.size(context.vent._events);
                }
            });

            return numEvents;
        },

        countContexts: function countContexts() {

            var numContexts = 0;

            _.each(contexts, function(context, id) {
                if (contexts.hasOwnProperty(id)) {
                    numContexts++;
                }
            });
            return numContexts;
        }

    };

    Geppetto.setDebug = function setDebug(enableDebug) {
        if (enableDebug) {
            this.debug = debug;
        } else {
            this.debug = undefined;
        }
        return this.debug;
    };

    Backbone.Geppetto = Geppetto;

    return Geppetto;
}));