'use strict';

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _toConsumableArray2 = require('babel-runtime/helpers/toConsumableArray');

var _toConsumableArray3 = _interopRequireDefault(_toConsumableArray2);

var _set = require('babel-runtime/core-js/set');

var _set2 = _interopRequireDefault(_set);

var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of');

var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf);

var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');

var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);

var _createClass2 = require('babel-runtime/helpers/createClass');

var _createClass3 = _interopRequireDefault(_createClass2);

var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn');

var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2);

var _inherits2 = require('babel-runtime/helpers/inherits');

var _inherits3 = _interopRequireDefault(_inherits2);

var _events = require('events');

var _events2 = _interopRequireDefault(_events);

var _event = require('../../lib/models/event');

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

/**
 * A container for relation events that supports easy access to common ways of
 * aggregating such events. Each instance holds events that of a single relation
 * type and event type. All of the events also relate to the same original event.
 *
 * The typical way to get one of these containers is via
 * EventTimelineSet#getRelationsForEvent.
 */
/*
Copyright 2019 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

var Relations = function (_EventEmitter) {
    (0, _inherits3.default)(Relations, _EventEmitter);

    /**
     * @param {String} relationType
     * The type of relation involved, such as "m.annotation", "m.reference",
     * "m.replace", etc.
     * @param {String} eventType
     * The relation event's type, such as "m.reaction", etc.
     * @param {?Room} room
     * Room for this container. May be null for non-room cases, such as the
     * notification timeline.
     */
    function Relations(relationType, eventType, room) {
        (0, _classCallCheck3.default)(this, Relations);

        var _this = (0, _possibleConstructorReturn3.default)(this, (Relations.__proto__ || (0, _getPrototypeOf2.default)(Relations)).call(this));

        _this._onEventStatus = function (event, status) {
            if (!event.isSending()) {
                // Sending is done, so we don't need to listen anymore
                event.removeListener("Event.status", _this._onEventStatus);
                return;
            }
            if (status !== _event.EventStatus.CANCELLED) {
                return;
            }
            // Event was cancelled, remove from the collection
            event.removeListener("Event.status", _this._onEventStatus);
            _this._removeEvent(event);
        };

        _this._onBeforeRedaction = function (redactedEvent) {
            if (!_this._relations.has(redactedEvent)) {
                return;
            }

            _this._relations.delete(redactedEvent);

            if (_this.relationType === "m.annotation") {
                // Remove the redacted annotation from aggregation by key
                _this._removeAnnotationFromAggregation(redactedEvent);
            } else if (_this.relationType === "m.replace" && _this._targetEvent) {
                _this._targetEvent.makeReplaced(_this.getLastReplacement());
            }

            redactedEvent.removeListener("Event.beforeRedaction", _this._onBeforeRedaction);

            _this.emit("Relations.redaction");
        };

        _this.relationType = relationType;
        _this.eventType = eventType;
        _this._relations = new _set2.default();
        _this._annotationsByKey = {};
        _this._annotationsBySender = {};
        _this._sortedAnnotationsByKey = [];
        _this._targetEvent = null;
        return _this;
    }

    /**
     * Add relation events to this collection.
     *
     * @param {MatrixEvent} event
     * The new relation event to be added.
     */


    (0, _createClass3.default)(Relations, [{
        key: 'addEvent',
        value: function addEvent(event) {
            if (this._relations.has(event)) {
                return;
            }

            var relation = event.getRelation();
            if (!relation) {
                console.error("Event must have relation info");
                return;
            }

            var relationType = relation.rel_type;
            var eventType = event.getType();

            if (this.relationType !== relationType || this.eventType !== eventType) {
                console.error("Event relation info doesn't match this container");
                return;
            }

            // If the event is in the process of being sent, listen for cancellation
            // so we can remove the event from the collection.
            if (event.isSending()) {
                event.on("Event.status", this._onEventStatus);
            }

            this._relations.add(event);

            if (this.relationType === "m.annotation") {
                this._addAnnotationToAggregation(event);
            } else if (this.relationType === "m.replace" && this._targetEvent) {
                this._targetEvent.makeReplaced(this.getLastReplacement());
            }

            event.on("Event.beforeRedaction", this._onBeforeRedaction);

            this.emit("Relations.add", event);
        }

        /**
         * Remove relation event from this collection.
         *
         * @param {MatrixEvent} event
         * The relation event to remove.
         */

    }, {
        key: '_removeEvent',
        value: function _removeEvent(event) {
            if (!this._relations.has(event)) {
                return;
            }

            var relation = event.getRelation();
            if (!relation) {
                console.error("Event must have relation info");
                return;
            }

            var relationType = relation.rel_type;
            var eventType = event.getType();

            if (this.relationType !== relationType || this.eventType !== eventType) {
                console.error("Event relation info doesn't match this container");
                return;
            }

            this._relations.delete(event);

            if (this.relationType === "m.annotation") {
                this._removeAnnotationFromAggregation(event);
            } else if (this.relationType === "m.replace" && this._targetEvent) {
                this._targetEvent.makeReplaced(this.getLastReplacement());
            }

            this.emit("Relations.remove", event);
        }

        /**
         * Listens for event status changes to remove cancelled events.
         *
         * @param {MatrixEvent} event The event whose status has changed
         * @param {EventStatus} status The new status
         */

    }, {
        key: 'getRelations',


        /**
         * Get all relation events in this collection.
         *
         * These are currently in the order of insertion to this collection, which
         * won't match timeline order in the case of scrollback.
         * TODO: Tweak `addEvent` to insert correctly for scrollback.
         *
         * @return {Array}
         * Relation events in insertion order.
         */
        value: function getRelations() {
            return [].concat((0, _toConsumableArray3.default)(this._relations));
        }
    }, {
        key: '_addAnnotationToAggregation',
        value: function _addAnnotationToAggregation(event) {
            var _event$getRelation = event.getRelation(),
                key = _event$getRelation.key;

            if (!key) {
                return;
            }

            var eventsForKey = this._annotationsByKey[key];
            if (!eventsForKey) {
                eventsForKey = this._annotationsByKey[key] = new _set2.default();
                this._sortedAnnotationsByKey.push([key, eventsForKey]);
            }
            // Add the new event to the set for this key
            eventsForKey.add(event);
            // Re-sort the [key, events] pairs in descending order of event count
            this._sortedAnnotationsByKey.sort(function (a, b) {
                var aEvents = a[1];
                var bEvents = b[1];
                return bEvents.size - aEvents.size;
            });

            var sender = event.getSender();
            var eventsFromSender = this._annotationsBySender[sender];
            if (!eventsFromSender) {
                eventsFromSender = this._annotationsBySender[sender] = new _set2.default();
            }
            // Add the new event to the set for this sender
            eventsFromSender.add(event);
        }
    }, {
        key: '_removeAnnotationFromAggregation',
        value: function _removeAnnotationFromAggregation(event) {
            var _event$getRelation2 = event.getRelation(),
                key = _event$getRelation2.key;

            if (!key) {
                return;
            }

            var eventsForKey = this._annotationsByKey[key];
            if (eventsForKey) {
                eventsForKey.delete(event);

                // Re-sort the [key, events] pairs in descending order of event count
                this._sortedAnnotationsByKey.sort(function (a, b) {
                    var aEvents = a[1];
                    var bEvents = b[1];
                    return bEvents.size - aEvents.size;
                });
            }

            var sender = event.getSender();
            var eventsFromSender = this._annotationsBySender[sender];
            if (eventsFromSender) {
                eventsFromSender.delete(event);
            }
        }

        /**
         * For relations that have been redacted, we want to remove them from
         * aggregation data sets and emit an update event.
         *
         * To do so, we listen for `Event.beforeRedaction`, which happens:
         *   - after the server accepted the redaction and remote echoed back to us
         *   - before the original event has been marked redacted in the client
         *
         * @param {MatrixEvent} redactedEvent
         * The original relation event that is about to be redacted.
         */

    }, {
        key: 'getSortedAnnotationsByKey',


        /**
         * Get all events in this collection grouped by key and sorted by descending
         * event count in each group.
         *
         * This is currently only supported for the annotation relation type.
         *
         * @return {Array}
         * An array of [key, events] pairs sorted by descending event count.
         * The events are stored in a Set (which preserves insertion order).
         */
        value: function getSortedAnnotationsByKey() {
            if (this.relationType !== "m.annotation") {
                // Other relation types are not grouped currently.
                return null;
            }

            return this._sortedAnnotationsByKey;
        }

        /**
         * Get all events in this collection grouped by sender.
         *
         * This is currently only supported for the annotation relation type.
         *
         * @return {Object}
         * An object with each relation sender as a key and the matching Set of
         * events for that sender as a value.
         */

    }, {
        key: 'getAnnotationsBySender',
        value: function getAnnotationsBySender() {
            if (this.relationType !== "m.annotation") {
                // Other relation types are not grouped currently.
                return null;
            }

            return this._annotationsBySender;
        }

        /**
         * Returns the most recent (and allowed) m.replace relation, if any.
         *
         * This is currently only supported for the m.replace relation type,
         * once the target event is known, see `addEvent`.
         *
         * @return {MatrixEvent?}
         */

    }, {
        key: 'getLastReplacement',
        value: function getLastReplacement() {
            var _this2 = this;

            if (this.relationType !== "m.replace") {
                // Aggregating on last only makes sense for this relation type
                return null;
            }
            if (!this._targetEvent) {
                // Don't know which replacements to accept yet.
                // This method shouldn't be called before the original
                // event is known anyway.
                return null;
            }

            // the all-knowning server tells us that the event at some point had
            // this timestamp for its replacement, so any following replacement should definitely not be less
            var replaceRelation = this._targetEvent.getServerAggregatedRelation("m.replace");
            var minTs = replaceRelation && replaceRelation.origin_server_ts;

            return this.getRelations().reduce(function (last, event) {
                if (event.getSender() !== _this2._targetEvent.getSender()) {
                    return last;
                }
                if (minTs && minTs > event.getTs()) {
                    return last;
                }
                if (last && last.getTs() > event.getTs()) {
                    return last;
                }
                return event;
            }, null);
        }

        /*
         * @param {MatrixEvent} targetEvent the event the relations are related to.
         */

    }, {
        key: 'setTargetEvent',
        value: function setTargetEvent(event) {
            if (this._targetEvent) {
                return;
            }
            this._targetEvent = event;
            if (this.relationType === "m.replace") {
                var replacement = this.getLastReplacement();
                // this is the initial update, so only call it if we already have something
                // to not emit Event.replaced needlessly
                if (replacement) {
                    this._targetEvent.makeReplaced(replacement);
                }
            }
        }
    }]);
    return Relations;
}(_events2.default);

exports.default = Relations;
//# sourceMappingURL=relations.js.map