/*
 * vim: ts=4:sw=4:expandtab
 */

function MessageReceiver(url, username, password, signalingKey, attachment_server_url) {
    this.url = url;
    this.signalingKey = signalingKey;
    this.username = username;
    this.password = password;
    this.server = new TextSecureServer(url, username, password, attachment_server_url);

    var unencoded = textsecure.utils.unencodeNumber(username);
    this.number = unencoded[0];
    this.deviceId = unencoded[1];
}
MessageReceiver.prototype = {
    constructor: MessageReceiver,
    connect: function() {
        if (this.socket && this.socket.readyState !== WebSocket.CLOSED) {
            this.socket.close();
        }
        console.log('opening websocket');
        // initialize the socket and start listening for messages
        this.socket = this.server.getMessageSocket();
        this.socket.onclose = this.onclose.bind(this);
        this.socket.onerror = this.onerror.bind(this);
        this.socket.onopen = this.onopen.bind(this);
        this.wsr = new WebSocketResource(this.socket, {
            handleRequest: this.queueRequest.bind(this),
            keepalive: { path: '/v1/keepalive', disconnect: true }
        });
        this.pending = Promise.resolve();
    },
    close: function() {
        this.socket.close(3000, 'called close');
        delete this.listeners;
    },
    onopen: function() {
        console.log('websocket open');
    },
    onerror: function(error) {
        console.log('websocket error');
    },
    onclose: function(ev) {
        console.log('websocket closed', ev.code, ev.reason || '');
        if (ev.code === 3000) {
            return;
        }
        var eventTarget = this;
        // possible 403 or network issue. Make an request to confirm
        this.server.getDevices(this.number).
            then(this.connect.bind(this)). // No HTTP error? Reconnect
            catch(function(e) {
                var ev = new Event('error');
                ev.error = e;
                eventTarget.dispatchEvent(ev);
            });
    },
    queueRequest: function(request) {
        var handleRequest = this.handleRequest.bind(this, request);
        this.pending = this.pending.then(handleRequest, handleRequest);
    },
    handleRequest: function(request) {
        // TODO: handle different types of requests. for now we only expect
        // PUT /messages <encrypted IncomingPushMessageSignal>
        return textsecure.crypto.decryptWebsocketMessage(request.body, this.signalingKey).then(function(plaintext) {
            var envelope = textsecure.protobuf.Envelope.decode(plaintext);
            // After this point, decoding errors are not the server's
            // fault, and we should handle them gracefully and tell the
            // user they received an invalid message
            request.respond(200, 'OK');

            if (envelope.type === textsecure.protobuf.Envelope.Type.RECEIPT) {
                return this.onDeliveryReceipt(envelope);
            } else if (envelope.content) {
                return this.handleContentMessage(envelope);
            } else if (envelope.legacyMessage) {
                return this.handleLegacyMessage(envelope);
            } else {
                throw new Error('Received message with no content and no legacyMessage');
            }

        }.bind(this)).catch(function(e) {
            request.respond(500, 'Bad encrypted websocket message');
            console.log("Error handling incoming message:", e);
            var ev = new Event('error');
            ev.error = e;
            this.dispatchEvent(ev);
        }.bind(this));
    },
    getStatus: function() {
        if (this.socket) {
            return this.socket.readyState;
        } else {
            return -1;
        }
    },
    onDeliveryReceipt: function (envelope) {
        var ev = new Event('receipt');
        ev.proto = envelope;
        this.dispatchEvent(ev);
    },
    decrypt: function(envelope, ciphertext) {
        var fromAddress = [envelope.source , (envelope.sourceDevice || 0)].join('.');
        var promise;
        switch(envelope.type) {
            case textsecure.protobuf.Envelope.Type.CIPHERTEXT:
                promise = textsecure.protocol_wrapper.decryptWhisperMessage(fromAddress, ciphertext);
                break;
            case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE:
                promise = textsecure.protocol_wrapper.handlePreKeyWhisperMessage(fromAddress, ciphertext);
                break;
            default:
                promise = Promise.reject(new Error("Unknown message type"));
        }
        return promise.catch(function(error) {
            var ev = new Event('error');
            ev.error = error;
            ev.proto = envelope;
            this.dispatchEvent(ev);
            return Promise.reject(error);
        }.bind(this));
    },
    handleSentMessage: function(destination, timestamp, message) {
        return this.processDecrypted(message, this.number).then(function(message) {
            var ev = new Event('sent');
            ev.data = {
                destination : destination,
                timestamp   : timestamp.toNumber(),
                message     : message
            };
            this.dispatchEvent(ev);
        }.bind(this));
    },
    handleDataMessage: function(envelope, message, close_session) {
        if ((message.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) ==
                textsecure.protobuf.DataMessage.Flags.END_SESSION ) {
            close_session();
        }
        return this.processDecrypted(message, envelope.source).then(function(message) {
            var ev = new Event('message');
            ev.data = {
                source    : envelope.source,
                timestamp : envelope.timestamp.toNumber(),
                message   : message
            };
            this.dispatchEvent(ev);
        }.bind(this));
    },
    handleLegacyMessage: function (envelope) {
        return this.decrypt(envelope, envelope.legacyMessage).then(function(result) {
            var plaintext = result[0]; // array buffer
            var close_session = result[1]; // function
            var message = textsecure.protobuf.DataMessage.decode(plaintext);
            return this.handleDataMessage(envelope, message, close_session);
        }.bind(this));
    },
    handleContentMessage: function (envelope) {
        return this.decrypt(envelope, envelope.content).then(function(result) {
            var plaintext = result[0]; // array buffer
            var close_session = result[1]; // function
            var content = textsecure.protobuf.Content.decode(plaintext);
            if (content.syncMessage) {
                return this.handleSyncMessage(envelope, content.syncMessage);
            } else if (content.dataMessage) {
                return this.handleDataMessage(envelope, content.dataMessage, close_session);
            } else {
                throw new Error('Got Content message with no dataMessage and no syncMessage');
            }
        }.bind(this));
    },
    handleSyncMessage: function(envelope, syncMessage) {
        if (envelope.source !== this.number) {
            throw new Error('Received sync message from another number');
        }
        if (envelope.sourceDevice == this.deviceId) {
            throw new Error('Received sync message from our own device');
        }
        if (syncMessage.sent) {
            var sentMessage = syncMessage.sent;
            return this.handleSentMessage(
                    sentMessage.destination,
                    sentMessage.timestamp,
                    sentMessage.message
            );
        } else if (syncMessage.contacts) {
            this.handleContacts(syncMessage.contacts);
        } else if (syncMessage.groups) {
            this.handleGroups(syncMessage.groups);
        } else {
            throw new Error('Got SyncMessage with no sent, contacts, or groups');
        }
    },
    handleContacts: function(contacts) {
        var eventTarget = this;
        var attachmentPointer = contacts.blob;
        return this.handleAttachment(attachmentPointer).then(function() {
            var contactBuffer = new ContactBuffer(attachmentPointer.data);
            var contactDetails = contactBuffer.next();
            while (contactDetails !== undefined) {
                var ev = new Event('contact');
                ev.contactDetails = contactDetails;
                eventTarget.dispatchEvent(ev);
                contactDetails = contactBuffer.next();
            }
            eventTarget.dispatchEvent(new Event('contactsync'));
        });
    },
    handleGroups: function(groups) {
        var eventTarget = this;
        var attachmentPointer = groups.blob;
        return this.handleAttachment(attachmentPointer).then(function() {
            var groupBuffer = new GroupBuffer(attachmentPointer.data);
            var groupDetails = groupBuffer.next();
            while (groupDetails !== undefined) {
                (function(groupDetails) {
                    groupDetails.id = getString(groupDetails.id);
                    textsecure.storage.groups.getGroup(groupDetails.id).
                    then(function(existingGroup) {
                        if (existingGroup === undefined) {
                            return textsecure.storage.groups.createNewGroup(
                                groupDetails.members, groupDetails.id
                            );
                        } else {
                            return textsecure.storage.groups.updateNumbers(
                                groupDetails.id, groupDetails.members
                            );
                        }
                    }).then(function() {
                        var ev = new Event('group');
                        ev.groupDetails = groupDetails;
                        eventTarget.dispatchEvent(ev);
                    });
                })(groupDetails);
                groupDetails = groupBuffer.next();
            }
        });
    },
    handleAttachment: function(attachment) {
        function decryptAttachment(encrypted) {
            return textsecure.crypto.decryptAttachment(
                encrypted,
                attachment.key.toArrayBuffer()
            );
        }

        function updateAttachment(data) {
            attachment.data = data;
        }

        return this.server.getAttachment(attachment.id.toString()).
        then(decryptAttachment).
        then(updateAttachment);
    },
    tryMessageAgain: function(from, encodedMessage) {
        var bytes = dcodeIO.ByteBuffer.wrap(encodedMessage);
        return textsecure.protocol_wrapper.handlePreKeyWhisperMessage(from, bytes).then(function(res) {
            var finalMessage = textsecure.protobuf.DataMessage.decode(res[0]);

            if ((finalMessage.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION)
                    == textsecure.protobuf.DataMessage.Flags.END_SESSION &&
                    finalMessage.sync !== null)
                res[1]();

            return this.processDecrypted(finalMessage);
        }.bind(this));
    },
    processDecrypted: function(decrypted, source) {
        // Now that its decrypted, validate the message and clean it up for consumer processing
        // Note that messages may (generally) only perform one action and we ignore remaining fields
        // after the first action.

        if (decrypted.flags == null)
            decrypted.flags = 0;

        if ((decrypted.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION)
                    == textsecure.protobuf.DataMessage.Flags.END_SESSION) {
            decrypted.body = null;
            decrypted.attachments = [];
            decrypted.group = null;
            return Promise.resolve(decrypted);
        }
        if (decrypted.flags != 0) {
            throw new Error("Unknown flags in message");
        }

        var promises = [];

        if (decrypted.group !== null) {
            decrypted.group.id = getString(decrypted.group.id);

            if (decrypted.group.type == textsecure.protobuf.GroupContext.Type.UPDATE) {
                if (decrypted.group.avatar !== null) {
                    promises.push(this.handleAttachment(decrypted.group.avatar));
                }
            }

            promises.push(textsecure.storage.groups.getNumbers(decrypted.group.id).then(function(existingGroup) {
                if (existingGroup === undefined) {
                    if (decrypted.group.type != textsecure.protobuf.GroupContext.Type.UPDATE) {
                        decrypted.group.members = [source];
                        console.log("Got message for unknown group");
                    }
                    return textsecure.storage.groups.createNewGroup(decrypted.group.members, decrypted.group.id);
                } else {
                    var fromIndex = existingGroup.indexOf(source);

                    if (fromIndex < 0) {
                        //TODO: This could be indication of a race...
                        console.log("Sender was not a member of the group they were sending from");
                    }

                    switch(decrypted.group.type) {
                    case textsecure.protobuf.GroupContext.Type.UPDATE:
                        return textsecure.storage.groups.updateNumbers(
                            decrypted.group.id, decrypted.group.members
                        ).then(function(added) {
                            decrypted.group.added = added;

                            if (decrypted.group.avatar === null &&
                                decrypted.group.added.length == 0 &&
                                decrypted.group.name === null) {
                                return;
                            }

                            decrypted.body = null;
                            decrypted.attachments = [];
                        });

                        break;
                    case textsecure.protobuf.GroupContext.Type.QUIT:
                        decrypted.body = null;
                        decrypted.attachments = [];
                        return textsecure.storage.groups.removeNumber(decrypted.group.id, source);
                    case textsecure.protobuf.GroupContext.Type.DELIVER:
                        decrypted.group.name = null;
                        decrypted.group.members = [];
                        decrypted.group.avatar = null;

                        break;
                    default:
                        throw new Error("Unknown group message type");
                    }
                }
            }));
        }

        for (var i in decrypted.attachments) {
            promises.push(this.handleAttachment(decrypted.attachments[i]));
        }
        return Promise.all(promises).then(function() {
            return decrypted;
        });
    },

    /* Implements EventTarget */
    dispatchEvent: function(ev) {
        if (!(ev instanceof Event)) {
            throw new Error('Expects an event');
        }
        if (this.listeners === null || typeof this.listeners !== 'object') {
            this.listeners = {};
        }
        var listeners = this.listeners[ev.type];
        if (typeof listeners === 'object') {
            for (var i=0; i < listeners.length; ++i) {
                if (typeof listeners[i] === 'function') {
                    listeners[i].call(null, ev);
                }
            }
        }
    },
    addEventListener: function(eventName, callback) {
        if (typeof eventName !== 'string') {
            throw new Error('First argument expects a string');
        }
        if (typeof callback !== 'function') {
            throw new Error('Second argument expects a function');
        }
        if (this.listeners === null || typeof this.listeners !== 'object') {
            this.listeners = {};
        }
        var listeners = this.listeners[eventName];
        if (typeof listeners !== 'object') {
            listeners = [];
        }
        listeners.push(callback);
        this.listeners[eventName] = listeners;
    },
    removeEventListener: function(eventName, callback) {
        if (typeof eventName !== 'string') {
            throw new Error('First argument expects a string');
        }
        if (typeof callback !== 'function') {
            throw new Error('Second argument expects a function');
        }
        if (this.listeners === null || typeof this.listeners !== 'object') {
            this.listeners = {};
        }
        var listeners = this.listeners[eventName];
        for (var i=0; i < listeners.length; ++ i) {
            if (listeners[i] === callback) {
                listeners.splice(i, 1);
                return;
            }
        }
        this.listeners[eventName] = listeners;
    }

};

window.textsecure = window.textsecure || {};

textsecure.MessageReceiver = function(url, username, password, signalingKey, attachment_server_url) {
    var messageReceiver = new MessageReceiver(url, username, password, signalingKey, attachment_server_url);
    this.addEventListener    = messageReceiver.addEventListener.bind(messageReceiver);
    this.removeEventListener = messageReceiver.removeEventListener.bind(messageReceiver);
    this.getStatus           = messageReceiver.getStatus.bind(messageReceiver);
    this.close               = messageReceiver.close.bind(messageReceiver);
    messageReceiver.connect();

    textsecure.replay.registerFunction(messageReceiver.tryMessageAgain.bind(messageReceiver), textsecure.replay.Type.INIT_SESSION);
};

textsecure.MessageReceiver.prototype = {
    constructor: textsecure.MessageReceiver
};