Skull.js

(function(root, factory) {
    'use strict';
    /* global define, module, exports, require */
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['underscore', 'jquery', 'backbone', 'exports'], function(_, $, Backbone, exports) {
            // AMD. Register as an anonymous module.
            return factory(
                root,
                exports,
                _,
                $,
                Backbone
            );
        });
    } else if (typeof exports === 'object') {
        // Node-like CommonJS require system
        module.exports = factory(
            root,
            exports,
            require('underscore'),
            require('jquery'),
            require('backbone')
        );
    } else {
        // Browser globals
        root.Skull = factory(
            root,
            {},
            root._,
            (root.jQuery || root.Zepto || root.ender || root.$),
            root.Backbone
        );
  }
}(this, function (root, Skull, _, $, Backbone) {
    'use strict';

    /**
     * @author Konstantin Kitmanov [doctor.hogart@gmail.com]
     * @license MIT
     * @namespace Skull
     */

    // conflict management
    var previousSkull = root.Skull;

    Skull.noConflict = function () {
        root.Skull = previousSkull;
        return this;
    };


    // Utility classes

    /**
     * Abstract class that can be extended in Backbone way.
     * Also works with {@link Skull.ResourceRegistry} if it was passed as `registry` in first argument, utilizing {@link Skull.ResourceRegistry.processRegistry}
     * @class Skull.Abstract
     */
    Skull.Abstract = function () {
        this.initialize.apply(this, arguments);
    };

    Skull.Abstract.prototype = {
        /**
         * @param {Object} options
         */
        initialize: function (options) {
            if (options && options.registry) {
                this.registry = options.registry;

                if (_.result(this, '__registry__')) {
                    Skull.ResourceRegistry.processRegistry(this);
                }
            }
        },

        /**
         * Much like a `_.result`, but ascending to parent
         * @param {Function} cls class derived in usual Skull paradigm (i.e. with `__super__` property pointing to parent's prototype)
         * @param {String} propertyName
         * @returns {*} Most of the times it is Object
         * @protected
         */
        _parentResult: function (cls, propertyName) {
            var parentProp = cls.__super__[propertyName];

            if (_.isFunction(parentProp)) {
                return parentProp.call(this);
            } else {
                return parentProp;
            }
        }
    };

    Skull.Abstract.extend = Backbone.Model.extend;

    /**
     * Class for creating object which can listen and trigger events.
     * Useful when creating buses and so on.
     * @class Skull.Observable
     * @extends Skull.Abstract
     */
    Skull.Observable = Skull.Abstract.extend(Backbone.Events);

    /**
     * Simple implementation of Registry pattern. Can store plain values or factories.
     * Stored factory gets memoized — that is, returns same object given the same parameters. Memoizing assumes factory is pure function.
     * @class Skull.ResourceRegistry
     * @extends Skull.Abstract
     * @constructor
     */
    Skull.ResourceRegistry = Skull.Abstract.extend(/** @lends Skull.ResourceRegistry.prototype */{
        initialize: function () {
            /** @private */
            this._storage = {};

            /** @private */
            this._fabric = {};

            /** @private */
            this._fabricCache = {};
        },

        /**
         * Registers object or factory by given `key`. Pass third argument to store factory.
         * @throws TypeError when not a function passed as factory
         * @param {String} key
         * @param {Object} value
         * @param {Object} [options={}]
         * @return {Object} what was stored
         */
        register: function (key, value, options) {
            if (arguments.length === 3) {
                if (!_.isFunction(value)) {
                    throw new TypeError('Not a function passed as factory with "' + key + '" key');
                }
                this._fabric[key] = [value, options];
                this._fabricCache[key] = {};
                return this._fabric[key];
            } else {
                this._storage[key] = value;
                return this._storage[key];
            }
        },

        /**
         * Deletes value from registry
         * @param {String} key
         * @param {Boolean} isFactory if true, deletes factory
         */
        unregister: function (key, isFactory) {
            if (isFactory) {
                delete this._fabric[key];
                delete this._fabricCache[key];
            } else {
                delete this._storage[key];
            }
        },

        /**
         * Returns requested object from registry
         * @param {String} key
         * @param {Object} [options] if present, factory would be called instead of fetching plain value
         * @returns {*}
         */
        acquire: function (key, options) {
            if (arguments.length === 2) {
                if (this._fabric[key]) {
                    var fabricConfig = this._fabric[key],
                        fabricFn = fabricConfig[0],
                        fabricPreOptions = fabricConfig[1],
                        params = _.extend({}, fabricPreOptions, options),
                        cacheKey = JSON.stringify(params, function (jsonKey, value) {
                            if (value instanceof Skull.ResourceRegistry) { // do not stringify ResourceRegistry instances, they tend to have circular references
                                return undefined;
                            } else {
                                return value;
                            }
                        });
                    if (this._fabricCache[key][cacheKey]) {
                        return this._fabricCache[key][cacheKey];
                    } else {
                        return (this._fabricCache[key][cacheKey] = fabricFn(params));
                    }
                } else {
                    return this._storage[key];
                }
            } else {
                return this._storage[key];
            }
        }
    }, /** @lends Skull.ResourceRegistry */{
        /**
         * Iterates over `context.__registry__`, acquiring dependencies from it via `context.registry.acquire`.
         * `__registry__` can be hash or array (or a function returning such hash or array).
         *
         * Hash keys are keys to inject acquired resources, and values are keys to acquire. If value is array, than first element is key, second is params fo factory.
         * Array is simplified form of hash. Following hash and array are equivalent:
         * ```
         * {
         *  'res1': 'res1',
         *  factory1: ['factory1', {param: 42}]
         * }, // and
         * ['res1', ['factory1', {param: 42}]]
         * ```
         * @type {Function}
         */
        processRegistry: function (context) {
            var items = _.result(context, '__registry__');

            if (items) {
                var registry = context.registry,
                    inject = function (injectAs, resourceRequest) {
                        context[injectAs] = registry.acquire.apply(registry, resourceRequest);
                    };

                if (_.isArray(items)) {
                    _.each(items, function (resourceRequest) {
                        if (!_.isArray(resourceRequest)) {
                            resourceRequest = [resourceRequest];
                        }

                        inject(resourceRequest[0], resourceRequest);
                    });
                } else {
                    _.each(items, function (resourceRequest, injectAs) {
                        if (!_.isArray(resourceRequest)) {
                            resourceRequest = [resourceRequest];
                        }

                        inject(injectAs, resourceRequest);
                    });
                }
            }
        }
    });

    /**
     * Detects host and protocol for your API from `script[data-api-domain="http://my.api.example.com"]`
     * @param {String} [attributeName='data-api-domain'] Definitive attribute name
     * @type {Function}
     */
    Skull.detectDomain = function (attributeName) {
        attributeName = attributeName || 'data-api-domain';

        var script = $('script[' + attributeName + ']');

        if (!script.length) {
            return {};
        }

        var path = script.attr(attributeName) || '',
            pathParts = path.split('//');

        if (pathParts.length === 2) {
            return {
                host: pathParts[1],
                protocol: pathParts[0].substring(0, pathParts[0].length - 1)
            };
        } else {
            return {
                host: path
            };
        }
    };

    /**
     * A tool to combine domains, ports, protocols, API endpoints with versions ans subtypes into URL.
     *
     * Any URL consists of following parts: `protocol://domain/prefix/`
     * None of this parts are required, but you should understand that setting protocol without domain
     * will result in relative URL from current domain root: `/restEndpoint/`.
     *
     * Note that skipping protocol and adding domain will lead to inheriting protocol from current document:
     * `//my.api.example.com/restEndpoint/`, and this is completely valid URL.
     * @class Skull.UrlProvider
     */
    Skull.UrlProvider = Skull.Abstract.extend(/** @lends Skull.UrlProvider.prototype */{
        defaults: {
            host: '',
            protocol: '',
            port: false,
            prefix: ''
        },

        initialize: function (options) {
            this.params = {};
            this.set(options);
        },

        /**
         * Updates inner state of URL pieces
         * @param {Object} options
         */
        set: function (options) {
            this.cachedPath = this.cachedUrl = false; // drop cache
            this.params = _.extend({}, this.defaults, options);
        },

        /**
         * Get absolute URL, with domain and protocol if provided.
         * @returns {String}
         */
        getApiUrl: function () {
            if (!this.cachedPath) {
                var parts = [];

                if (this.params.host) {
                    parts.push('//');
                    if (this.params.protocol) {
                        parts.unshift(this.params.protocol + ':');
                    }
                    parts.push(this.params.host);

                    if (this.params.port) {
                        parts.push(':' + this.params.port);
                    }
                }

                this.cachedPath = parts.join('') + this.getApiPath();
            }

            return this.cachedPath;
        },

        /**
         * Returns relative URL from root of domain.
         * @returns {String}
         */
        getApiPath: function () {
            if (!this.cachedUrl) {
                var parts = _.compact([this.params.prefix]);
                this.cachedUrl = parts.length ? ('/' + parts.join('/') + '/') : '/';
            }

            return this.cachedUrl;
        }
    });

    /**
     * Backbone.sync OOP-style
     * Can emit authorized requests, when provided with `getToken` function via registry
     * @class Skull.Syncer
     */
    Skull.Syncer = Skull.Abstract.extend(/** @lends Skull.Syncer.prototype */{
        __registry__: {
            getToken: 'getToken'
        },

        /**
         * Map from CRUD operations to HTTP verbs for default syncer implementation.
         * @protected
         */
        _methodMap: {
            'create': 'POST',
            'update': 'PUT',
            'delete': 'DELETE',
            'read': 'GET',
            'patch': 'PATCH'
        },

        defaults: {
            authHeaderName: 'Authorization',
            emulateHTTP: false,
            emulateJSON: false
        },

        /**
         * @protected
         */
        _urlError: function () {
            throw new Error('A "url" property or function must be specified');
        },

        /**
         * @constructs
         * @param {Object} options
         * @param {Boolean} [options.emulateHTTP=false] emulate HTTP 1.1 methods for old servers
         * @param {Boolean} [options.emulateJSON=false] emulate JSON by encoding the request into an HTML-form
         * @param {String} [options.authHeaderName='Authorization'] Use this header to pass authorization token
         * @param {Skull.ResourceRegistry} options.registry registry instance
         */
        initialize: function (options) {
            this.registry = options.registry;
            Skull.ResourceRegistry.processRegistry(this);

            this.params = _.extend({}, this.defaults, options);
        },

        /**
         * Pretty much the same as `Backbone.sync`, only allows to extend requests with authorization headers
         * @param {String} method
         * @param {Backbone.Model|Backbone.Collection} model
         * @param {Object} [options={}] Allows to override any request param
         * @returns {jQuery.Deferred}
         */
        sync: function(method, model, options) {
            var type = this._methodMap[method];

            // Default options, unless specified.
            _.defaults(options || (options = {}), _.pick(this.params, 'emulateHTTP', 'emulateJSON'));

            // Default JSON-request options.
            var params = {type: type, dataType: 'json'};

            // Ensure that we have a URL.
            if (!options.url) {
                params.url = _.result(model, 'url') || this._urlError();
            }

            // Ensure that we have the appropriate request data.
            if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
                params.contentType = 'application/json';
                params.data = JSON.stringify(options.attrs || model.toJSON(options));
            }

            // For older servers, emulate JSON by encoding the request into an HTML-form.
            if (options.emulateJSON) {
                params.contentType = 'application/x-www-form-urlencoded';
                params.data = params.data ? {model: params.data} : {};
            }

            // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
            // And an `X-HTTP-Method-Override` header.
            if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
                params.type = 'POST';
                if (options.emulateJSON) {
                    params.data._method = type;
                }
                var beforeSend = options.beforeSend;
                options.beforeSend = function (xhr) { // eslint-disable-line no-shadow
                    xhr.setRequestHeader('X-HTTP-Method-Override', type);
                    if (beforeSend) {
                        return beforeSend.apply(this, arguments);
                    }
                };
            }

            // Don't process data on a non-GET request.
            if (params.type !== 'GET' && !options.emulateJSON) {
                params.processData = false;
            }

            params = this._authorize(params);

            var success = options.success;
            options.success = function (resp, status, xhr) { // eslint-disable-line no-shadow
                if (success) {
                    success(resp, status, xhr);
                }
                model.trigger('sync', model, resp, options);
            };

            var error = options.error;
            options.error = function (xhr/*, status, thrown*/) { // eslint-disable-line no-shadow
                if (error) {
                    error(model, xhr, options);
                }
            };

            // Make the request, allowing the user to override any Ajax options.
            var xhr = this.ajax(_.extend(params, options));
            model.trigger('request', model, xhr, options);
            return xhr;
        },

        /**
         * Augments request params with authorization header. Feel free to override with your logic.
         * @param {Object} params
         * @returns {Object} augmented request params
         * @protected
         */
        _authorize: function (params) {
            var token = this.getToken ? this.getToken() : false,
                headerName = this.params.authHeaderName;

            if (token && headerName) {
                if (!params.headers) {
                    params.headers = {};
                }
                params.headers[headerName] = token;
            }

            return params;
        },

        /**
         * Performs ajax request
         * @returns {jQuery.Deferred}
         */
        ajax: function () {
            return $.ajax.apply($, arguments);
        }
    });

    /**
     * Skull.Template provides wrapper for template engine (_.template by default).
     * This wrapper performs caching, error handling and adding a bit of debugging info.
     * By default Skull.Template fetches templates stored in `script` tags with `js-tpl-<templateName>` class.
     * @class Skull.Template
     */
    Skull.Template = Skull.Abstract.extend(/** @lends Skull.Template.prototype */{
        __registry__: {
            debug: 'debug'
        },

        defaults: {
            selectorPrefix: 'script.js-tpl-',
            trim: true,
            debug: false,
            dontCache: false
        },

        tplFunction: _.template,

        /**
         * @param {Object} options
         * @param {String} [options.selectorPrefix='script.js-tpl-'] default selector for finding template nodes
         * @param {Boolean} [options.trim=true] trim whitespaces from template before compiling
         * @param {Boolean} [options.dontCache=false] Useful when developing, you can change template right on page without reloading it
         * @param {Function} [options.tplFunction=_.template] Template function must accept template string and return compiled to function template. Another (and preferable) way to change this is to inherit and override.
         */
        initialize: function (options) {
            Skull.Template.__super__.initialize.call(this, options);

            /**
             * Holds all compiled templates.
             * Instance property.
             * @private
             */
            this._templates = {};

            this.params = _.extend({}, this.defaults, options);

            if (options && options.tplFunction) {
                this.tplFunction = options.tplFunction;
            }
        },

        /**
         * Fetches template by name.
         * @param {String} name
         * @returns {jQuery}
         * @protected
         * @throws {Error} 'No such template'
         */
        _getTemplateNode: function (name) {
            var fullSelector = this.params.selectorPrefix + name,
                node = $(fullSelector);

            if (node.length) {
                if (node.length > 1) {
                    node = node.eq(0);

                    if (this.params.debug) {
                        /* eslint-disable no-console */
                        console.warn('Too many template nodes: ' + fullSelector);
                        /* eslint-enable no-console */
                    }
                }
            } else {
                throw new Error('No such template: "' + name + '". Make sure you have "' + fullSelector + '" node on your page');
            }

            return node;
        },

        /**
         * Primary templates processing – e.g. whitespace trimming
         * @param {jQuery} node
         * @returns {String}
         * @protected
         */
        _preprocessTemplate: function (node) {
            var rawTemplate = node.text();

            if (this.params.trim) {
                rawTemplate = $.trim(rawTemplate);
            }

            return rawTemplate;
        },

        /**
         * Compiles template to function
         * @param {String} rawTemplate
         * @returns {Function}
         * @protected
         */
        _compileTemplate: function (rawTemplate) {
            return this.tplFunction(rawTemplate);
        },

        /**
         * Gets compiled template by its name
         * @param {String} name
         * @returns {Function}
         * @protected
         */
        _getCompiledTemplate: function (name) {
            var node = this._getTemplateNode(name),
                processed = this._preprocessTemplate(node),
                compiled = this._compileTemplate(processed);

            return compiled;
        },

        /**
         * Returns either cached compiled template or compiles it, caches and returns it
         * @param {String} name template name
         * @returns {Function} compiled template
         * @protected
         */
        _getTemplate: function (name) {
            if (this._templates[name] && !this.params.dontCache) {
                return this._templates[name];
            } else {
                var tpl = this._getCompiledTemplate(name);
                this._templates[name] = tpl;
                return tpl;
            }
        },

        /**
         * This normally should be only one Template method you call from other places.
         * When provided with truthie second argument, returns rendered templates, otherwise, compiled.
         * When provided with third argument, calls it with passing, again, rendered or compiled template.
         * @param {String} name
         * @param {Object} [tplData=null] if passed, function returns rendered template. If not, compiled template.
         * @param {Function} [callback=null] if defined, will be called instead of returning result.
         * @return {Function|String|undefined}
         */
        tmpl: function (name, tplData, callback) {
            var tpl = this._getTemplate(name);

            if (arguments.length > 1 && tplData) { // should return already rendered template

                // specify context information, e.g. l10n string, common application data…
                if (this.params.context) {
                    (tplData.__context__ = this.params.context);
                }

                tpl = tpl(tplData);

                if (this.params.debug) {
                    // surround with debugging comment so we can see where template starts and ends
                    tpl = '<!-- tpl:' + name + ' -->\n' + tpl + '\n<!-- /tpl:' + name + ' -->';
                }
            }

            // can be used with async rendering
            if (callback) {
                callback(tpl);
            } else {
                return tpl;
            }
        }
    });

    /**
     * Extends given model class with all {@link Skull.Model} qualities. Useful when you need to use other models,
     * e.g. http://afeld.github.io/backbone-nested/, but still want DI and registry.
     * @param {Function} baseModelClass
     * @returns {constructor}
     */
    Skull.extendModel = function (baseModelClass) {
        var Model = baseModelClass.extend(/** @lends Skull.Model.prototype */{
            __registry__: function () {
                var registry = {
                    syncer: 'syncer'
                };

                if (this.resource) {
                    registry.getApiUrl = 'getApiUrl';
                }

                return registry;
            },

            url: function () {
                if (this.resource) { // REST model
                    var url = this.getApiUrl() + this.resource + '/';

                    if (this.id) {
                        url += encodeURIComponent(this.id) + '/';
                    }

                    return url;
                } else { // conventional model
                    return this._parentResult(Model, 'url');
                }
            },

            /** @constructs */
            constructor: function (attributes, options) {
                this.registry = options.registry;
                Skull.ResourceRegistry.processRegistry(this);

                Model.__super__.constructor.call(this, attributes, options);

                // more readable cid
                if (this.resource) { // REST model
                    this.cid = _.uniqueId('model.' + this.resource);
                } else {
                    this.cid = _.uniqueId('model');
                }
            },

            /**
             * Almost the same as .set method, but always do it's work silently (i.e. not firing any event).
             * Useful when setting values from UI to prevent «event loop».
             * @param {Object|String} key Either key or properties hash
             * @param {Object} [val] Either value or options
             * @param {Object} [options={}] Additional options
             */
            silentSet: function (key, val, options) {
                var attrs;

                // Handle both `"key", value` and `{key: value}` -style arguments.
                if (_.isObject(key)) {
                    attrs = key;
                    options = val;
                } else {
                    (attrs = {})[key] = val;
                }

                (options || (options = {})).silent = true;
                return this.set(attrs, options);
            },

            /**
             * Overridden for registry handling
             * @returns {Skull.Model}
             */
            clone: function () {
                return new this.constructor(this.attributes, {registry: this.registry});
            },

            /**
             * Delegates sync operations to this.syncer
             * @return {jQuery.Deferred}
             */
            sync: function (method, model, options) {
                return this.syncer.sync(method, model, options);
            },

            /**
             * toTemplate is reserved for generating data for rendering,
             * e.g. for computed values and so on. Feel free to override.
             * @returns {Object}
             */
            toTemplate: function () {
                var tplData = _.clone(this.attributes);

                return tplData;
            },

            /**
             * {@link Skull.Abstract#_parentResult}
             * @type Function
             */
            _parentResult: Skull.Abstract.prototype._parentResult
        });

        return Model;
    };

    /**
     * Skull.Model is basic model with few enhancements:
     * * registry handling
     * * meaningful cid
     * * easier REST urls generation (when `resource` field provided)
     * @class Skull.Model
     * @extends Backbone.Model
     */
    Skull.Model = Skull.extendModel(Backbone.Model);


    /**
     * Extends given collection class with all {@link Skull.Collection} qualities. Useful when you need to use other collections,
     * but still need DI, registry and other stuff.
     * @param {Function} baseCollectionClass
     * @param {Function} [modelClass=SkullModel]
     * @returns {constructor}
     */
    Skull.extendCollection = function (baseCollectionClass, modelClass) {
        modelClass = modelClass || Skull.Model;

        var Collection = baseCollectionClass.extend(/** @lends Skull.Collection.prototype */{
            __registry__: function () {
                var registry = {
                    syncer: 'syncer'
                };

                if (this.resource) {
                    registry.getApiUrl = 'getApiUrl';
                }

                return registry;
            },

            url: function () {
                if (this.resource) {
                    var url = this.getApiUrl();
                    url += this.resource + '/';

                    return url;
                } else {
                    return this._parentResult(Collection, 'url');
                }
            },

            model: modelClass,

            /** @constructs */
            constructor: function (models, options) {
                this.resource = this.model.prototype.resource;

                this.registry = options.registry;
                Skull.ResourceRegistry.processRegistry(this);

                Collection.__super__.constructor.call(this, models, options);

                // more readable cid
                if (this.resource) { // REST collection
                    this.cid = _.uniqueId('collection.' + this.resource);
                } else {
                    this.cid = _.uniqueId('collection');
                }
            },

            /**
             * Delegates sync operations to this.syncer
             * @return {jQuery.Deferred}
             */
            sync: function (method, model, options) {
                return this.syncer.sync(method, model, options);
            },

            /**
             * Provides data for templates.
             * {@link Skull.Model#toTemplate}
             * @returns {Object[]}
             */
            toTemplate: function () {
                return _.invoke(this.models, 'toTemplate');
            },

            /**
             * Prepare a hash of attributes (or other model) to be added to this collection.
             * Takes care of registry passing.
             * @param {Object} attrs future model attrs
             * @param {Object} [options={}]
             * @private
             */
            _prepareModel: function (attrs, options) {
                (options || (options = {})).registry = this.registry;

                return Collection.__super__._prepareModel.call(this, attrs, options);
            },

            /**
             * Returns a new instance of the collection with an identical list of models.
             * Takes care of registry passing.
             * @returns {Skull.Collection}
             */
            clone: function () {
                return new this.constructor(this.models, {registry: this.registry});
            },

            /**
             * {@link Skull.Abstract#_parentResult}
             * @type Function
             */
            _parentResult: Skull.Abstract.prototype._parentResult
        });

        return Collection;
    };

    /**
     * Skull.Collection is a collection with few enhancements:
     * * registry handling
     * * meaningful cid
     * * easier REST urls generation
     * @class Skull.Collection
     * @extends Backbone.Collection
     */
    Skull.Collection = Skull.extendCollection(Backbone.Collection);


    /**
     * Recursively replaces tokens with values from `ctx`.
     * @example unfold('$test $test2', false, {test: 'simple string', test2: 'string with $catch22', catch22: 'catch'}) === 'simple string string with catch'
     * @param {String} src
     * @param {RegExp|Boolean} [tokenRe=unfold.tokenRe]
     * @param {Object} [ctx=this]
     * @returns {String}
     */
    function unfold (src, tokenRe, ctx) {
        tokenRe = (tokenRe || unfold.tokenRe);
        ctx = (ctx || this); // jshint ignore:line

        var replace = function (match, key) {
            return ctx[key];
        };

        while (src.match(tokenRe)) {
            src = src.replace(tokenRe, replace);
        }

        return src;
    }

    unfold.tokenRe = /\$([^\., -]+)/mg;

    /**
     * Extends given view class with all {@link Skull.View} goodness.
     * @param {Function} baseViewClass
     * @returns {constructor}
     */
    Skull.extendView = function (baseViewClass) {
        var View = baseViewClass.extend(/** @lends Skull.View.prototype */{
            __registry__: {
                template: 'template'
            },

            /**
             * Whether `this.$el` will be completely replaced on rendering
             */
            replaceEl: false,

            /**
             * Automatically (and not, if you wish) creates and renders nested views.
             * Actually is a hash. Each field can take 4 forms:
             * * `'.js-someSelector': MyViewClass`
             * * `'.js-anotherSelector': [MyViewClass, {answer: 42}]` // second element will be passed to MyViewClass constructor
             * * `'.js-yetAnotherSelector': [MyViewClass, 'someMethodName']` // this['someMethodName'] will be called in proper context (`this`),
             *   and result will be passed to MyViewClass constructor
             * * `'.js-selectorToo': [MyViewClass, function () { return {answer: 42} }]` // second element will be called in proper context,
             *   and result will be passed to MyViewClass constructor
             *
             * All mentioned views will be placed to `this.children` hash for further managing during {@link Skull.View#onRender}.
             *
             * @type {Object}
             */
            __children__: null,

            /**
             * Automatically (and not, if you wish) creates links to nodes inside your view. This is useful (and handy),
             * when you change some node's attributes several times during view's lifecycle.
             * Actually is a config in following form:
             * `somePrettyName: '.some .selector'`
             *
             * All defined bits will be placed to `this.ui` hash for further managing during {@link Skull.View#onRender}.
             */
            __ui__: null,

            /**
             * @constructs
             * @param {Object} options
             */
            constructor: function (options) {
                this.registry = options.registry;
                Skull.ResourceRegistry.processRegistry(this);

                View.__super__.constructor.call(this, options);

                // more readable cid
                this.cid = _.uniqueId('view');
            },

            initialize: function (options) {
                View.__super__.initialize.call(this.options);

                // save reference to parent view
                this.parent = options.parent;

                // semi-automated child views management. Should be instance property.
                this.children = {};
            },

            /**
             * Shortcut for rendering this.tpl to `this.$el` (or instead of this element)
             * @param {Skull.Model|Skull.Collection|Object} [tplData={}] if this parameter have `.toTemplate` method,
             * it would be called and result will be passed instead
             * @param {Boolean} [replace=false] whether replace whole `$el` or only replace it's `.html()`
             */
            rr: function (tplData, replace) {
                // work out parameters
                var data = {},
                    replaceEl = false;
                if (arguments.length === 2) {
                    data = tplData;
                    replaceEl = replace;
                } else if (arguments.length === 1) {
                    if (_.isBoolean(arguments[0])) {
                        replaceEl = arguments[0];
                    } else {
                        data = arguments[0];
                    }
                }


                // get data
                if (data && 'toTemplate' in data && _.isFunction(data.toTemplate)) {
                    data = data.toTemplate();
                }

                // get template
                var tpl = _.result(this, 'tpl');

                if (!tpl) {
                    throw new Error('"tpl" property not found while attaching view "' + this.cid + '" to "' + this.$el.selector + '"');
                }

                // rendering at last
                var renderedStr = this.template.tmpl(tpl, data);

                if (replaceEl) {
                    var renderedDom = $(renderedStr);
                    this.$el.replaceWith(renderedDom);
                    this.setElement(renderedDom);
                } else {
                    this.$el.html(renderedStr);
                }
            },

            /**
             * Default rendering procedure: renders `this.collection` or `this.model` or `{}`.
             * Feel free to override if needed.
             * @return {*} data passed to template
             */
            render: function () {
                var uiState = this.collection || this.model || {};

                // if we haven't yet $el, then we should replace element either way
                this.rr(uiState, this.replaceEl ? this.$el : undefined);
                this.onRender();

                return uiState;
            },

            /**
             * Performs declarative bindings: `__children__`, `__ui__`, events.
             * Call this method when html is ready.
             */
            onRender: function () {
                this._ensureUI();

                this._ensureSubviews();

                this.delegateEvents();
            },

            _unfoldSelector: function (selector) {
                var ui = _.result(this, '__ui__');

                return unfold(selector, false, ui);
            },

            _ensureUI: function (ui) {
                if (!ui) {
                    ui = _.result(this, '__ui__');
                }

                if (!ui) {
                    return; // nothing to do here anymore
                }

                this.ui = {};

                _.each(ui, function (selector, name) {
                    this.ui[name] = this.$(this._unfoldSelector(selector));
                }, this);
            },

            delegateEvents: function (events) {
                if (!events) {
                    events = _.result(this, 'events');
                }

                var ui = _.result(this, '__ui__');

                if (!ui || _.isEmpty(ui)) {
                    return View.__super__.delegateEvents.call(this, events);
                }

                var refinedEvents = {};

                _.each(events, function (handler, eventSignature) {
                    refinedEvents[this._unfoldSelector(eventSignature)] = handler;
                }, this);

                return View.__super__.delegateEvents.call(this, refinedEvents);
            },

            _ensureSubviews: function (children, options) {
                if (!children) {
                    children = _.result(this, '__children__');
                }

                if (children) {
                    var renderView = function (viewClass, selector) {
                        this._renderView(viewClass, selector, options);
                    };

                    _.each(children, renderView, this);
                }
            },

            _renderView: function (viewClass, selector, options) {
                var params = {
                    el: selector
                };

                if (this.model) {
                    params.model = this.model;
                }

                if (this.collection) {
                    params.collection = this.collection;
                }

                params.viewName = selector;
                if (options) {
                    params = _.extend({}, params, options);
                }

                if (_.isArray(viewClass) && viewClass.length > 1) {
                    params.viewName = viewClass[1].viewName ? viewClass[1].viewName : params.viewName;
                }

                this.registerChild(params.viewName, viewClass, params);
            },

            /**
             * Registers nested view
             * @param {String} [viewName] defaults to `cid`, if falsy
             * @param {View} viewClass
             * @param {Object} options
             * @return {View}
             */
            registerChild: function (viewName, viewClass, options) {
                var params = _.extend(
                    {
                        parent: this,
                        registry: this.registry
                    },
                    options
                );

                if (_.isArray(viewClass)) {
                    if (viewClass.length > 1) {
                        if (_.isFunction(viewClass[1])) {
                            params = _.extend(viewClass[1].call(this), params);
                        } else if (_.isString(viewClass[1])) {
                            if (!(viewClass[1] in this)) {
                                throw new TypeError('Method "' + viewClass[1] + '" does not exist');
                            }
                            params = _.extend(this[viewClass[1]].call(this), params);
                        } else {
                            params = _.extend(viewClass[1], params);
                        }

                    }
                    viewClass = viewClass[0];
                }

                if (_.isString(params.el)) {
                    params.el = this.$(this._unfoldSelector(params.el));
                }

                if (!viewClass) {
                    throw 'Invalid class when registering child: ' + viewName;
                }

                var child = new viewClass(params); // eslint-disable-line new-cap
                if (viewName) {
                    child.cid = viewName;
                } else {
                    viewName = child.cid;
                    child.viewName = viewName;
                }

                this.children[viewName] = child;

                return child;
            },

            /**
             * Carefully removes nested view
             * @param {String} viewName
             */
            unregisterChild: function (viewName) {
                if (!this.children[viewName]) {
                    return;
                }

                this.children[viewName].remove();
                delete this.children[viewName];
            },

            /**
             * Carefully removes *all* nested views
             * @private
             */
            _unregisterChildren: function () {
                _.each(this.children, function (child, childName) {
                    this.unregisterChild(childName);
                }, this);
            },

            /**
             * Cleans up: removes nested views, shuts down events both DOM and Backbone's
             */
            onBeforeRemove: function () {
                this._unregisterChildren();

                this.undelegateEvents();
                this.$el.off();

                this.off();
            },

            /**
             * Acts as `destructor`
             */
            remove: function () {
                this.onBeforeRemove();
                View.__super__.remove.call(this);
            },

            /**
             * {@link Skull.Abstract#_parentResult}
             * @type Function
             */
            _parentResult: Skull.Abstract.prototype._parentResult
        });

        return View;
    };

    /**
     * Fused with automagic, Skull.View is highly configurable tool for creating and manipulating your app's views.
     * Core differences with vanilla Backbone.View is following:
     * * Full-cycle nested views automated managing, {@link `Skull.View#__children__`}
     * * Handy access to often used nodes inside view, {@link `Skull.View#__ui__`}
     * * Preventing memory leaks and "zombie" callbacks with more thorough {@link Skull.View#remove} method
     * @class Skull.View
     */
    Skull.View = Skull.extendView(Backbone.View);

    /**
     * Skull.Application is very basic sample of application.
     * It does several things:
     * 1. creates registry and registers itself as `'app'`
     * 2. detects domain and other passes URL to {@link UrlProvider}
     * 3. instantiates syncer
     * 4. instantiates router
     * 5. Detects if debug mode is on
     * 5. renders root view and starts Backbone.history, if `autostart` option passed
     *
     * App dispatches route changes. Bind to `path` event to handle them.
     * @class Skull.Application
     * @extends Skull.Observable
     */
    Skull.Application = Skull.Observable.extend(/** @lends Skull.Application.prototype */{
        defaults: {
            node: 'html',
            router: Backbone.Router,
            syncer: Skull.Syncer,
            template: Skull.Template,

            history: {
                root: '/'
            }
        },

        /**
         * @param options app options
         * @param {Skull.View} options.rootView Skull.View class, intended to be root view
         * @param {Backbone.Router} options.router Router class to be used
         * @param {Skull.Syncer} [options.syncer=Skull.Syncer] {@link Skull.Syncer} class to be used
         * @param {$|String|HTMLElement} [options.node='html'] root node for application; gets passed to `options.rootView`
         * @param {String} [options.dataDomainSelector] selector to be passed to {@link Skull.detectDomain}
         * @param {Object} [options.urlOptions] options for {@link Skull.UrlProvider}.
         * @param {Boolean} [options.debug=false] Whether we are in debug mode, you can provide other ways for checking it
         *
         * @param {Boolean} [options.autostart=false] Whether application should start right when instantiated
         */
        initialize: function (options) {
            this.params = _.extend({}, this.defaults, options);

            var registry = this.registry = this.params.registry || new Skull.ResourceRegistry(),
                register = _.bind(registry.register, registry),
                regComp = this._registerComponent.bind(this);

            register('app', this);

            // determine if we're in debug mode
            register('debug', this._isDebug(this.params));

            // URLs detecting
            var domain = register('domain', Skull.detectDomain(this.params.dataDomainSelector)),
                urlProvider = register(
                    'urlProvider',
                    new Skull.UrlProvider(_.extend({}, domain, this.params.urlOptions))
                );
            register('getApiUrl', _.bind(urlProvider.getApiUrl, urlProvider));

            // create router
            regComp('syncer');

            // create syncer
            regComp('router');

            // create template handler
            regComp('template');

            // start app, if we should
            if (this.params.autostart) {
                this.start();
            }
        },

        /**
         * Creates application component and registers it, following config
         * @param {String} componentName should be present in params passed to application
         * @returns {Object} instance of component
         * @protected
         */
        _registerComponent: function (componentName) {
            var component = this.params[componentName];

            if (_.isFunction(component)) {
                return this.registry.register(
                    componentName,
                    new component({ // eslint-disable-line new-cap
                        registry: this.registry
                    })
                );
            } else {
                var constructor = component[0],
                    params = component[1];

                if (_.isFunction(params)) {
                    params = params.call(this);
                }

                params.registry = this.registry;

                return this.registry.register(
                    componentName,
                    new constructor( // jshint ignore:line
                        params
                    )
                );
            }
        },

        /**
         * Renders root view and starts up Backbone.history.
         * Call this when your app is ready (or pass `options.autostart` to {@link Skull.Application#initialize}).
         * Feel free to override.
         */
        start: function () {
            this.listenTo(
                Backbone.history,
                'route',
                this.onRoute
            );

            this.registry.register(
                'rootView',
                new this.params.rootView({ // eslint-disable-line new-cap
                    el: this.params.node,
                    registry: this.registry
                })
            );

            Backbone.history.start(this.params.history);
        },

        /**
         * Primarily dispatches route change. Feel free to override.
         * @param {Backbone.Router} router router which fired event
         * @param {String} routeName name of matched route
         * @param {Object} params parameters parsed from route, if any
         */
        onRoute: function (router, routeName, params) {
            this.currentRoute = [routeName, params];
            this.trigger('path', routeName, params);
        },

        /**
         * How application determines if debug is on. Feel free to override,
         * this naïve implementation considers only if there's truthy field `debug`
         * @param {Object} params
         * @private
         */
        _isDebug: function (params) {
            this.debug = !!params.debug;
        }
    });

    return Skull;
}));