/**
 * Abstract class Message
 * @constructor
 * @param {*} e
 * @param {number} ts
**/
function Message(e, ts) {
    /** @const @type {string} **/
    this.userId = e["user"];

    /** @type {string} */
    this.username = e["username"];

    /** @const @type {string} **/
    this.id = e["id"] || e["ts"];

    /** @const @type {number} **/
    this.ts = parseFloat(e["ts"]);

    /** @type {string} */
    this.text = "";

    /** @type {string|undefined} */
    this.pendingId;

    /** @type {Array.<*>} */
    this.attachments = [];

    /** @type {boolean} */
    this.starred = e["is_starred"] || false;

    /** @type {number|boolean} */
    this.edited = false;

    /** @type {boolean} */
    this.removed = false;

    /**
     * for each emoji code, an array of user id
     * @type {Object.<string, Array.<string>>}
    **/
    this.reactions = {};

    /** @type {number} */
    this.version = ts;

    this.update(e, ts);
};

/**
 * @constructor
 * @extends {Message}
**/
function MeMessage(e, ts) {
    Message.call(this, e, ts);
};

/**
 * @constructor
 * @extends {Message}
**/
function NoticeMessage(e, ts) {
    Message.call(this, e, ts);
};

/**
 * @param {*} e
 * @param {number} ts
**/
Message.prototype.update = function(e, ts) {
    if (e) {
        this.text = e["text"] || "";
        if (e["attachments"]) this.attachments = e["attachments"];
        this.starred = !!e["is_starred"];
        this.edited = e["edited"] === undefined ? false : e["edited"];
        this.removed = !!e["removed"];

        if (e["reactions"]) {
            var reactions = {};
            e["reactions"].forEach(function(r) {
                reactions[r["name"]] = [];
                r["users"].forEach(function(userId) {
                    reactions[r["name"]].push(userId);
                });
            });
            this.reactions = reactions;
        }

        if (e["pendingId"])
            this.pendingId = e["pendingId"];
    } else {
        this.removed = true;
    }
    this.version = ts;
};

/** @return {*} */
Message.prototype.toStatic = function() {
    var reactions = [];
    for (var reaction in this.reactions) {
        reactions.push({
            "name": reaction
            ,"users": this.reactions[reaction]
        });
    }
    return {
        "id": this.id
        ,"user": this.userId
        ,"username": this.username
        ,"ts": this.ts
        ,"text": this.text
        ,"pendingId": this.pendingId
        ,"attachments": this.attachments.length ? this.attachments : undefined
        ,"is_starred": this.starred || undefined
        ,"edited": this.edited === false ? undefined : this.edited
        ,"removed": this.removed || undefined
        ,"reactions": reactions
        ,"isMeMessage": this instanceof MeMessage || undefined
        ,"isNotice": this instanceof NoticeMessage || undefined
    };
};

/**
 * @param {string} reaction
 * @param {string} userId
 * @param {number} ts
**/
Message.prototype.addReaction = function(reaction, userId, ts) {
    if (!this.reactions[reaction])
        this.reactions[reaction] = [];
    if (this.reactions[reaction].indexOf(userId) === -1) {
        this.reactions[reaction].push(userId);
        this.version = ts;
        return true;
    }
    return false;
};

/**
 * @param {string} reaction
 * @param {string} userId
 * @param {number} ts
**/
Message.prototype.removeReaction = function(reaction, userId, ts) {
    var updated = false;
    if (this.reactions[reaction]) {
        if (this.reactions[reaction].length === 1 && this.reactions[reaction][0] === userId) {
            delete this.reactions[reaction];
            updated = true;
        } else {
            this.reactions[reaction] = this.reactions[reaction].filter(function(i) {
                if (i !== userId)
                    return true;
                updated = true;
                return false;
            });
        }
    }
    if (updated)
        this.version = ts;
    return updated;
};

Message.prototype.hasReactionForUser = function(reaction, userId) {
    if (this.reactions[reaction])
        return this.reactions[reaction].indexOf(userId) !== -1;
    return false;
};

/**
 * @constructor
 * @param {Room|string} room or roomId
 * @param {number} keepMessages number of messages to keep in memory
 * @param {number} maxAge number of messages to keep in memory
 * @param {Array|undefined} evts
 * @param {number|undefined} now
**/
function RoomHistory(room, keepMessages, maxAge, evts, now) {
    /** @type {string} */
    this.id = typeof room === "string" ? room : room.id;
    /** @type {Array.<Message>} */
    this.messages = [];
    /** @type {number} */
    this.maxAge = maxAge;
    /** @type number */
    this.v = 0;

    /** @const @type {number} */
    this.keepMessages = keepMessages;

    if (evts) this.pushAll(evts, now);
};

/**
 * @param {Array} evts
 * @return {number} biggest ts
**/
RoomHistory.prototype.pushAll = function(evts, t) {
    var result = 0;

    evts.forEach(function(e) {
        result = Math.max(this.push(e, t), result);
    }.bind(this));
    this.resort();
    return result;
};

/**
 * @param {*} ev
 * @param {number} ts
 * @return {Message}
**/
RoomHistory.prototype.messageFactory = function(ev, ts) {
    if (ev["isMeMessage"] === true)
        return new MeMessage(ev, ts);
    if (ev["isNotice"] === true)
        return new NoticeMessage(ev, ts);
    return new Message(ev, ts);
};

/**
 * @param {*} e
 * @return {number} t
**/
RoomHistory.prototype.push = function(e, t) {
    var exists = false
        ,ts;

    for (var i =0, nbMsg = this.messages.length; i < nbMsg; i++) {
        var msgObj = this.messages[i];
        if (msgObj.id === e["id"]) {
            ts = msgObj.update(e, t);
            exists = true;
            break;
        }
    }
    if (!exists) {
        var msgObj = this.messageFactory(e, t);
        this.messages.push(msgObj);
        ts = msgObj.ts;
    }
    this.cleanOld(t);
    return ts || 0;
};

RoomHistory.prototype.cleanOld = function(t) {
    while (this.messages.length > this.keepMessages)
        this.messages.shift();
    if (this.maxAge) {
        for (var i =0; i < this.messages.length; i++) {
            if (this.messages[i].version < t -this.maxAge) {
                this.messages.splice(i--, 1);
            }
        }
    }
};

/**
 * @return {Message|undefined}
**/
RoomHistory.prototype.lastMessage = function() {
    return this.messages[this.messages.length -1];
};

/**
 * @param {string} reaction
 * @param {string} userId
 * @param {string} msgId
 * @param {number} ts
 * @return {Message|null} WARNING if null, you *should* fetch new message and append it
**/
RoomHistory.prototype.addReaction = function(reaction, userId, msgId, ts) {
    var msg = this.getMessageById(msgId);
    if (msg)
        msg.addReaction(reaction, userId, ts);
    return msg;
};

/**
 * @param {string} reaction
 * @param {string} userId
 * @param {string} msgId
 * @param {number} ts
 * @return {Message|null} WARNING if null, you *should* fetch new message and append it
**/
RoomHistory.prototype.removeReaction = function(reaction, userId, msgId, ts) {
    var msg = this.getMessageById(msgId);
    if (msg)
        msg.removeReaction(reaction, userId, ts);
    return msg;
};

/**
 * @param {number} ts
 * @return {Message|null}
**/
RoomHistory.prototype.getMessage = function(ts) {
    for (var i =0, nbMessages = this.messages.length; i < nbMessages && ts >= this.messages[i].ts; i++) {
        if (this.messages[i].ts === ts)
            return this.messages[i];
    }
    return null;
};

/**
 * @param {string} id
 * @return {Message|null}
**/
RoomHistory.prototype.getMessageById = function(id) {
    for (var i =0, nbMessages = this.messages.length; i < nbMessages; i++) {
        if (this.messages[i].id == id)
            return this.messages[i];
    }
    return null;
};

/**
 * @return {Array.<*>}
**/
RoomHistory.prototype.toStatic = function(knownVersion) {
    var result = [];

    for (var i = this.messages.length -1; i >= 0; i--)
        if (this.messages[i].version > knownVersion)
            result.push(this.messages[i].toStatic());
    return result;
};

RoomHistory.prototype.resort = function() {
    this.messages.sort(function(a, b) {
        return a.ts -b.ts;
    });
};

MeMessage.prototype = Object.create(Message.prototype);
MeMessage.prototype.constructor = MeMessage;
NoticeMessage.prototype = Object.create(Message.prototype);
NoticeMessage.prototype.constructor = NoticeMessage;

/** @suppress {undefinedVars,checkTypes} */
(function() {
    if (typeof module !== "undefined") module.exports = {
        Message: Message
        ,MeMessage: MeMessage
        ,NoticeMessage: NoticeMessage
        ,RoomHistory: RoomHistory
    }
})();

