/**
 * An abstract view for rendering a collection.
 *
 * This provides core, reusable functionality for any view that wants to render
 * a collection and respond to add/remove events. Types that extend this should
 * make sure to define the 'itemViewType' attribute, which will be the view
 * instantiated for each model in the collection.
 *
 * Items are added to the view's :js:attr:`RB.CollectionView.$container`
 * element. By default, this is the view's :js:attr:`RB.CollectionView.$el`
 * element. Subclasses that want to add to an inner child should explicitly
 * set ``$container`` to the appropriate element.
 */
RB.CollectionView = Backbone.View.extend({
    /**
     * The view that will be instantiated for rendering items in the collection.
     */
    itemViewType: null,

    /**
     * Initialize the CollectionView.
     *
     * Args:
     *     options (object):
     *         Options for the view.
     *
     * Option Args:
     *     collection (Backbone.Collection):
     *         The collection to display.
     *
     *     itemViewType (function):
     *         The constructor for the child views.
     *
     *     itemViewOptions (object):
     *         Options to pass into the item view constructor.
     */
    initialize(options) {
        if (options.itemViewType) {
            this.itemViewType = options.itemViewType;
        }

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

        const collection = options.collection;
        this.collection = collection;
        this.views = [];
        this._viewsByModelID = {};

        collection.each(this._onAdded, this);
        this.listenTo(collection, 'add', this._onAdded);
        this.listenTo(collection, 'remove', this._onRemoved);
        this.listenTo(collection, 'sort', this._onSorted);
        this.listenTo(collection, 'reset', this._onReset);

        this.$container = this.$el;
    },

    /**
     * Render the view.
     *
     * This will iterate over all the child views and render them as well.
     *
     * Returns:
     *     RB.CollectionView:
     *     This object, for chaining.
     */
    render() {
        this._rendered = true;

        this.$container.empty();
        this._addCollectionViews();

        return this;
    },

    /**
     * Add each view to the collection's container.
     *
     * This will iterate through all views and append them to the container.
     */
    _addCollectionViews() {
        this.views.forEach(view => this.$container.append(view.render().el));
    },

    /**
     * Add a view for an item in the collection.
     *
     * This will instantiate the itemViewType, and if the CollectionView has
     * been rendered, render and append it as well.
     *
     * Args:
     *     item (Backbone.Model):
     *         The item to add.
     */
    _onAdded(item) {
        console.assert(this.itemViewType,
                       'itemViewType must be defined by the subclass');

        const view = new this.itemViewType(_.defaults({
            model: item,
        }, this.itemViewOptions));

        this.views.push(view);
        this._viewsByModelID[item.cid] = view;

        if (this._rendered) {
            this.$container.append(view.render().el);
        }
    },

    /**
     * Remove a view for an item in the collection.
     *
     * Args:
     *     item (Backbone.Model):
     *         The item to remove.
     */
    _onRemoved(item) {
        const toRemove = this._viewsByModelID[item.cid];

        if (toRemove) {
            delete this._viewsByModelID[item.cid];
            this.views = _.without(this.views, toRemove);

            if (this._rendered) {
                toRemove.remove();
            }
        }
    },

    /**
     * Respond to a change in the collection's sort order.
     *
     * This will detach all of the child views and re-add them in the new
     * order.
     */
    _onSorted() {
        let views = this.views;

        /*
         * See if the order of models has changed from our views. This may
         * not be the case. An initial collection.fetch() will add each
         * model and then emit a "sort", and this can end up causing us to
         * rebuild our list of views unnecessarily (which can be problematic
         * for, say, <option selected> items, as the 'selected' attribute
         * will no longer be respected).
         */
        const models = this.collection.models;
        const oldViews = this.views;
        let orderChanged = false;

        if (models.length === oldViews.length) {
            for (let i = 0; i < oldViews.length; i++) {
                if (oldViews[i].model !== models[i]) {
                    orderChanged = true;
                    break;
                }
            }
        } else {
            orderChanged = true;
        }

        if (!orderChanged) {
            return;
        }

        this.views = this.collection.map(
            model => this._viewsByModelID[model.cid]);

        if (this._rendered) {
            this.$container.children().detach();
            this._addCollectionViews();
        }
    },

    /**
     * Handle the collection being reset.
     *
     * This will remove all existing views and create new ones for the new
     * state of the collection.
     */
    _onReset() {
        this.views.forEach(view => view.remove());
        this.views = [];
        this._viewsByModelID = {};
        this.collection.each(this._onAdded, this);
    },
});
