/*
 * synergy -- mouse and keyboard sharing utility
 * Copyright (C) 2012-2016 Symless Ltd.
 * Copyright (C) 2002 Chris Schoeneman
 * 
 * This package is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * found in the file LICENSE that should have accompanied this file.
 * 
 * This package is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "client/ServerProxy.h"

#include "client/Client.h"
#include "synergy/FileChunk.h"
#include "synergy/ClipboardChunk.h"
#include "synergy/StreamChunker.h"
#include "synergy/Clipboard.h"
#include "synergy/ProtocolUtil.h"
#include "synergy/option_types.h"
#include "synergy/protocol_types.h"
#include "synergy/AppUtil.h"
#include "io/IStream.h"
#include "base/Log.h"
#include "base/IEventQueue.h"
#include "base/TMethodEventJob.h"
#include "base/XBase.h"

#include <memory>
#include <algorithm>
#include <cstring>

//
// ServerProxy
//

ServerProxy::ServerProxy(Client* client, synergy::IStream* stream, IEventQueue* events) :
    m_client(client),
    m_stream(stream),
    m_seqNum(0),
    m_compressMouse(false),
    m_compressMouseRelative(false),
    m_xMouse(0),
    m_yMouse(0),
    m_dxMouse(0),
    m_dyMouse(0),
    m_ignoreMouse(false),
    m_keepAliveAlarm(0.0),
    m_keepAliveAlarmTimer(NULL),
    m_parser(&ServerProxy::parseHandshakeMessage),
    m_events(events)
{
    assert(m_client != NULL);
    assert(m_stream != NULL);

    // initialize modifier translation table
    for (KeyModifierID id = 0; id < kKeyModifierIDLast; ++id)
        m_modifierTranslationTable[id] = id;

    // handle data on stream
    m_events->adoptHandler(m_events->forIStream().inputReady(),
                            m_stream->getEventTarget(),
                            new TMethodEventJob<ServerProxy>(this,
                                &ServerProxy::handleData));

    m_events->adoptHandler(m_events->forClipboard().clipboardSending(),
                            this,
                            new TMethodEventJob<ServerProxy>(this,
                                &ServerProxy::handleClipboardSendingEvent));

    // send heartbeat
    setKeepAliveRate(kKeepAliveRate);
}

ServerProxy::~ServerProxy()
{
    setKeepAliveRate(-1.0);
    m_events->removeHandler(m_events->forIStream().inputReady(),
                            m_stream->getEventTarget());
}

void
ServerProxy::resetKeepAliveAlarm()
{
    if (m_keepAliveAlarmTimer != NULL) {
        m_events->removeHandler(Event::kTimer, m_keepAliveAlarmTimer);
        m_events->deleteTimer(m_keepAliveAlarmTimer);
        m_keepAliveAlarmTimer = NULL;
    }
    if (m_keepAliveAlarm > 0.0) {
        m_keepAliveAlarmTimer =
            m_events->newOneShotTimer(m_keepAliveAlarm, NULL);
        m_events->adoptHandler(Event::kTimer, m_keepAliveAlarmTimer,
                            new TMethodEventJob<ServerProxy>(this,
                                &ServerProxy::handleKeepAliveAlarm));
    }
}

void
ServerProxy::setKeepAliveRate(double rate)
{
    m_keepAliveAlarm = rate * kKeepAlivesUntilDeath;
    resetKeepAliveAlarm();
}

void
ServerProxy::handleData(const Event&, void*)
{
    // handle messages until there are no more.  first read message code.
    UInt8 code[4];
    UInt32 n = m_stream->read(code, 4);
    while (n != 0) {
        // verify we got an entire code
        if (n != 4) {
            LOG((CLOG_ERR "incomplete message from server: %d bytes", n));
            m_client->disconnect("incomplete message from server");
            return;
        }

        // parse message
        LOG((CLOG_DEBUG2 "msg from server: %c%c%c%c", code[0], code[1], code[2], code[3]));
        switch ((this->*m_parser)(code)) {
        case kOkay:
            break;

        case kUnknown:
            LOG((CLOG_ERR "invalid message from server: %c%c%c%c", code[0], code[1], code[2], code[3]));
            // not possible to determine message boundaries
            // read the whole stream to discard unkonwn data
            while (m_stream->read(nullptr, 4));
            break;

        case kDisconnect:
            return;
        }

        // next message
        n = m_stream->read(code, 4);
    }

    flushCompressedMouse();
}

ServerProxy::EResult
ServerProxy::parseHandshakeMessage(const UInt8* code)
{
    if (memcmp(code, kMsgQInfo, 4) == 0) {
        queryInfo();
    }

    else if (memcmp(code, kMsgCInfoAck, 4) == 0) {
        infoAcknowledgment();
    }

    else if (memcmp(code, kMsgDSetOptions, 4) == 0) {
        setOptions();

        // handshake is complete
        m_parser = &ServerProxy::parseMessage;
        checkMissedLanguages();
        m_client->handshakeComplete();
    }

    else if (memcmp(code, kMsgCResetOptions, 4) == 0) {
        resetOptions();
    }

    else if (memcmp(code, kMsgCKeepAlive, 4) == 0) {
        // echo keep alives and reset alarm
        ProtocolUtil::writef(m_stream, kMsgCKeepAlive);
        resetKeepAliveAlarm();
    }

    else if (memcmp(code, kMsgCNoop, 4) == 0) {
        // accept and discard no-op
    }

    else if (memcmp(code, kMsgCClose, 4) == 0) {
        // server wants us to hangup
        LOG((CLOG_DEBUG1 "recv close"));
        m_client->disconnect(NULL);
        return kDisconnect;
    }

    else if (memcmp(code, kMsgEIncompatible, 4) == 0) {
        SInt32 major, minor;
        ProtocolUtil::readf(m_stream,
                        kMsgEIncompatible + 4, &major, &minor);
        LOG((CLOG_ERR "server has incompatible version %d.%d", major, minor));
        m_client->refuseConnection("server has incompatible version");
        return kDisconnect;
    }

    else if (memcmp(code, kMsgEBusy, 4) == 0) {
        LOG((CLOG_ERR "server already has a connected client with name \"%s\"", m_client->getName().c_str()));
        m_client->refuseConnection("server already has a connected client with our name");
        return kDisconnect;
    }

    else if (memcmp(code, kMsgEUnknown, 4) == 0) {
        LOG((CLOG_ERR "server refused client with name \"%s\"", m_client->getName().c_str()));
        m_client->refuseConnection("server refused client with our name");
        return kDisconnect;
    }

    else if (memcmp(code, kMsgEBad, 4) == 0) {
        LOG((CLOG_ERR "server disconnected due to a protocol error"));
        m_client->refuseConnection("server reported a protocol error");
        return kDisconnect;
    }
    else if (memcmp(code, kMsgDLanguageSynchronisation, 4) == 0) {
        setServerLanguages();
    }
    else {
        return kUnknown;
    }

    return kOkay;
}

ServerProxy::EResult
ServerProxy::parseMessage(const UInt8* code)
{
    if (memcmp(code, kMsgDMouseMove, 4) == 0) {
        mouseMove();
    }

    else if (memcmp(code, kMsgDMouseRelMove, 4) == 0) {
        mouseRelativeMove();
    }

    else if (memcmp(code, kMsgDMouseWheel, 4) == 0) {
        mouseWheel();
    }

    else if (memcmp(code, kMsgDKeyDown, 4) == 0) {
        UInt16 id = 0;
        UInt16 mask = 0;
        UInt16 button = 0;
        ProtocolUtil::readf(m_stream, kMsgDKeyDown + 4, &id, &mask, &button);
        LOG((CLOG_DEBUG1 "recv key down id=0x%08x, mask=0x%04x, button=0x%04x", id, mask, button));

        keyDown(id, mask, button, "");
    }

    else if (memcmp(code, kMsgDKeyDownLang, 4) == 0) {
        String lang;
        UInt16 id = 0;
        UInt16 mask = 0;
        UInt16 button = 0;

        ProtocolUtil::readf(m_stream, kMsgDKeyDownLang + 4, &id, &mask, &button, &lang);
        LOG((CLOG_DEBUG1 "recv key down id=0x%08x, mask=0x%04x, button=0x%04x, lang=\"%s\"", id, mask, button, lang.c_str()));

        keyDown(id, mask, button, lang);
    }

    else if (memcmp(code, kMsgDKeyUp, 4) == 0) {
        keyUp();
    }

    else if (memcmp(code, kMsgDMouseDown, 4) == 0) {
        mouseDown();
    }

    else if (memcmp(code, kMsgDMouseUp, 4) == 0) {
        mouseUp();
    }

    else if (memcmp(code, kMsgDKeyRepeat, 4) == 0) {
        keyRepeat();
    }

    else if (memcmp(code, kMsgCKeepAlive, 4) == 0) {
        // echo keep alives and reset alarm
        ProtocolUtil::writef(m_stream, kMsgCKeepAlive);
        resetKeepAliveAlarm();
    }

    else if (memcmp(code, kMsgCNoop, 4) == 0) {
        // accept and discard no-op
    }

    else if (memcmp(code, kMsgCEnter, 4) == 0) {
        enter();
    }

    else if (memcmp(code, kMsgCLeave, 4) == 0) {
        leave();
    }

    else if (memcmp(code, kMsgCClipboard, 4) == 0) {
        grabClipboard();
    }

    else if (memcmp(code, kMsgCScreenSaver, 4) == 0) {
        screensaver();
    }

    else if (memcmp(code, kMsgQInfo, 4) == 0) {
        queryInfo();
    }

    else if (memcmp(code, kMsgCInfoAck, 4) == 0) {
        infoAcknowledgment();
    }

    else if (memcmp(code, kMsgDClipboard, 4) == 0) {
        setClipboard();
    }

    else if (memcmp(code, kMsgCResetOptions, 4) == 0) {
        resetOptions();
    }

    else if (memcmp(code, kMsgDSetOptions, 4) == 0) {
        setOptions();
    }

    else if (memcmp(code, kMsgDFileTransfer, 4) == 0) {
        fileChunkReceived();
    }
    else if (memcmp(code, kMsgDDragInfo, 4) == 0) {
        dragInfoReceived();
    }
    else if (memcmp(code, kMsgDSecureInputNotification, 4) == 0) {
        secureInputNotification();
    }

    else if (memcmp(code, kMsgCClose, 4) == 0) {
        // server wants us to hangup
        LOG((CLOG_DEBUG1 "recv close"));
        m_client->disconnect(NULL);
        return kDisconnect;
    }
    else if (memcmp(code, kMsgEBad, 4) == 0) {
        LOG((CLOG_ERR "server disconnected due to a protocol error"));
        m_client->disconnect("server reported a protocol error");
        return kDisconnect;
    }
    else {
        return kUnknown;
    }

    // send a reply.  this is intended to work around a delay when
    // running a linux server and an OS X (any BSD?) client.  the
    // client waits to send an ACK (if the system control flag
    // net.inet.tcp.delayed_ack is 1) in hopes of piggybacking it
    // on a data packet.  we provide that packet here.  i don't
    // know why a delayed ACK should cause the server to wait since
    // TCP_NODELAY is enabled.
    ProtocolUtil::writef(m_stream, kMsgCNoop);

    return kOkay;
}

void
ServerProxy::handleKeepAliveAlarm(const Event&, void*)
{
    LOG((CLOG_NOTE "server is dead"));
    m_client->disconnect("server is not responding");
}

void
ServerProxy::onInfoChanged()
{
    // ignore mouse motion until we receive acknowledgment of our info
    // change message.
    m_ignoreMouse = true;

    // send info update
    queryInfo();
}

bool
ServerProxy::onGrabClipboard(ClipboardID id)
{
    LOG((CLOG_DEBUG1 "sending clipboard %d changed", id));
    ProtocolUtil::writef(m_stream, kMsgCClipboard, id, m_seqNum);
    return true;
}

void
ServerProxy::onClipboardChanged(ClipboardID id, const IClipboard* clipboard)
{
    String data = IClipboard::marshall(clipboard);
    LOG((CLOG_DEBUG "sending clipboard %d seqnum=%d", id, m_seqNum));

    StreamChunker::sendClipboard(data, data.size(), id, m_seqNum, m_events, this);
}

void
ServerProxy::flushCompressedMouse()
{
    if (m_compressMouse) {
        m_compressMouse = false;
        m_client->mouseMove(m_xMouse, m_yMouse);
    }
    if (m_compressMouseRelative) {
        m_compressMouseRelative = false;
        m_client->mouseRelativeMove(m_dxMouse, m_dyMouse);
        m_dxMouse = 0;
        m_dyMouse = 0;
    }
}

void
ServerProxy::sendInfo(const ClientInfo& info)
{
    LOG((CLOG_DEBUG1 "sending info shape=%d,%d %dx%d", info.m_x, info.m_y, info.m_w, info.m_h));
    ProtocolUtil::writef(m_stream, kMsgDInfo,
                                info.m_x, info.m_y,
                                info.m_w, info.m_h, 0,
                                info.m_mx, info.m_my);
}

KeyID
ServerProxy::translateKey(KeyID id) const
{
    static const KeyID s_translationTable[kKeyModifierIDLast][2] = {
        { kKeyNone,      kKeyNone },
        { kKeyShift_L,   kKeyShift_R },
        { kKeyControl_L, kKeyControl_R },
        { kKeyAlt_L,     kKeyAlt_R },
        { kKeyMeta_L,    kKeyMeta_R },
        { kKeySuper_L,   kKeySuper_R },
        { kKeyAltGr,     kKeyAltGr}
    };

    KeyModifierID id2 = kKeyModifierIDNull;
    UInt32 side      = 0;
    switch (id) {
    case kKeyShift_L:
        id2  = kKeyModifierIDShift;
        side = 0;
        break;

    case kKeyShift_R:
        id2  = kKeyModifierIDShift;
        side = 1;
        break;

    case kKeyControl_L:
        id2  = kKeyModifierIDControl;
        side = 0;
        break;

    case kKeyControl_R:
        id2  = kKeyModifierIDControl;
        side = 1;
        break;

    case kKeyAlt_L:
        id2  = kKeyModifierIDAlt;
        side = 0;
        break;

    case kKeyAlt_R:
        id2  = kKeyModifierIDAlt;
        side = 1;
        break;

    case kKeyAltGr:
        id2 = kKeyModifierIDAltGr;
        side = 1; // there is only one alt gr key on the right side
        break;

    case kKeyMeta_L:
        id2  = kKeyModifierIDMeta;
        side = 0;
        break;

    case kKeyMeta_R:
        id2  = kKeyModifierIDMeta;
        side = 1;
        break;

    case kKeySuper_L:
        id2  = kKeyModifierIDSuper;
        side = 0;
        break;

    case kKeySuper_R:
        id2  = kKeyModifierIDSuper;
        side = 1;
        break;
    }

    if (id2 != kKeyModifierIDNull) {
        return s_translationTable[m_modifierTranslationTable[id2]][side];
    }
    else {
        return id;
    }
}

KeyModifierMask
ServerProxy::translateModifierMask(KeyModifierMask mask) const
{
    static const KeyModifierMask s_masks[kKeyModifierIDLast] = {
        0x0000,
        KeyModifierShift,
        KeyModifierControl,
        KeyModifierAlt,
        KeyModifierMeta,
        KeyModifierSuper,
        KeyModifierAltGr
    };

    KeyModifierMask newMask = mask & ~(KeyModifierShift |
                                        KeyModifierControl |
                                        KeyModifierAlt |
                                        KeyModifierMeta |
                                        KeyModifierSuper |
                                        KeyModifierAltGr );
    if ((mask & KeyModifierShift) != 0) {
        newMask |= s_masks[m_modifierTranslationTable[kKeyModifierIDShift]];
    }
    if ((mask & KeyModifierControl) != 0) {
        newMask |= s_masks[m_modifierTranslationTable[kKeyModifierIDControl]];
    }
    if ((mask & KeyModifierAlt) != 0) {
        newMask |= s_masks[m_modifierTranslationTable[kKeyModifierIDAlt]];
    }
    if ((mask & KeyModifierAltGr) != 0) {
        newMask |= s_masks[m_modifierTranslationTable[kKeyModifierIDAltGr]];
    }
    if ((mask & KeyModifierMeta) != 0) {
        newMask |= s_masks[m_modifierTranslationTable[kKeyModifierIDMeta]];
    }
    if ((mask & KeyModifierSuper) != 0) {
        newMask |= s_masks[m_modifierTranslationTable[kKeyModifierIDSuper]];
    }
    return newMask;
}

void
ServerProxy::enter()
{
    // parse
    SInt16 x, y;
    UInt16 mask;
    UInt32 seqNum;
    ProtocolUtil::readf(m_stream, kMsgCEnter + 4, &x, &y, &seqNum, &mask);
    LOG((CLOG_DEBUG1 "recv enter, %d,%d %d %04x", x, y, seqNum, mask));

    // discard old compressed mouse motion, if any
    m_compressMouse                        = false;
    m_compressMouseRelative                = false;
    m_dxMouse                              = 0;
    m_dyMouse                              = 0;
    m_seqNum                               = seqNum;
    m_serverLanguage                       = "";
    m_isUserNotifiedAboutLanguageSyncError = false;

    // forward
    m_client->enter(x, y, seqNum, static_cast<KeyModifierMask>(mask), false);
}

void
ServerProxy::leave()
{
    // parse
    LOG((CLOG_DEBUG1 "recv leave"));

    // send last mouse motion
    flushCompressedMouse();

    // forward
    m_client->leave();
}

void
ServerProxy::setClipboard()
{
    // parse
    static String dataCached;
    ClipboardID id;
    UInt32 seq;
    
    int r = ClipboardChunk::assemble(m_stream, dataCached, id, seq);

    if (r == kStart) {
        size_t size = ClipboardChunk::getExpectedSize();
        LOG((CLOG_DEBUG "receiving clipboard %d size=%d", id, size));
    }
    else if (r == kFinish) {
        LOG((CLOG_DEBUG "received clipboard %d size=%d", id, dataCached.size()));
        
        // forward
        Clipboard clipboard;
        clipboard.unmarshall(dataCached, 0);
        m_client->setClipboard(id, &clipboard);

        LOG((CLOG_INFO "clipboard was updated"));
    }
}

void
ServerProxy::grabClipboard()
{
    // parse
    ClipboardID id;
    UInt32 seqNum;
    ProtocolUtil::readf(m_stream, kMsgCClipboard + 4, &id, &seqNum);
    LOG((CLOG_DEBUG "recv grab clipboard %d", id));

    // validate
    if (id >= kClipboardEnd) {
        return;
    }

    // forward
    m_client->grabClipboard(id);
}

void
ServerProxy::keyDown(UInt16 id, UInt16 mask, UInt16 button, const String& lang)
{
    // get mouse up to date
    flushCompressedMouse();
    setActiveServerLanguage(lang);

    // translate
    KeyID id2             = translateKey(static_cast<KeyID>(id));
    KeyModifierMask mask2 = translateModifierMask(
                                static_cast<KeyModifierMask>(mask));
    if (id2   != static_cast<KeyID>(id) ||
        mask2 != static_cast<KeyModifierMask>(mask))
        LOG((CLOG_DEBUG1 "key down translated to id=0x%08x, mask=0x%04x", id2, mask2));

    // forward
    m_client->keyDown(id2, mask2, button, lang);
}

void
ServerProxy::keyRepeat()
{
    // get mouse up to date
    flushCompressedMouse();

    // parse
    UInt16 id, mask, count, button;
    String lang;
    ProtocolUtil::readf(m_stream, kMsgDKeyRepeat + 4,
                                &id, &mask, &count, &button, &lang);
    LOG((CLOG_DEBUG1 "recv key repeat id=0x%08x, mask=0x%04x, count=%d, button=0x%04x, lang=\"%s\"", id, mask, count, button, lang.c_str()));

    // translate
    KeyID id2             = translateKey(static_cast<KeyID>(id));
    KeyModifierMask mask2 = translateModifierMask(
                                static_cast<KeyModifierMask>(mask));
    if (id2   != static_cast<KeyID>(id) ||
        mask2 != static_cast<KeyModifierMask>(mask))
        LOG((CLOG_DEBUG1 "key repeat translated to id=0x%08x, mask=0x%04x", id2, mask2));

    // forward
    m_client->keyRepeat(id2, mask2, count, button, lang);
}

void
ServerProxy::keyUp()
{
    // get mouse up to date
    flushCompressedMouse();

    // parse
    UInt16 id, mask, button;
    ProtocolUtil::readf(m_stream, kMsgDKeyUp + 4, &id, &mask, &button);
    LOG((CLOG_DEBUG1 "recv key up id=0x%08x, mask=0x%04x, button=0x%04x", id, mask, button));

    // translate
    KeyID id2             = translateKey(static_cast<KeyID>(id));
    KeyModifierMask mask2 = translateModifierMask(
                                static_cast<KeyModifierMask>(mask));
    if (id2   != static_cast<KeyID>(id) ||
        mask2 != static_cast<KeyModifierMask>(mask))
        LOG((CLOG_DEBUG1 "key up translated to id=0x%08x, mask=0x%04x", id2, mask2));

    // forward
    m_client->keyUp(id2, mask2, button);
}

void
ServerProxy::mouseDown()
{
    // get mouse up to date
    flushCompressedMouse();

    // parse
    SInt8 id;
    ProtocolUtil::readf(m_stream, kMsgDMouseDown + 4, &id);
    LOG((CLOG_DEBUG1 "recv mouse down id=%d", id));

    // forward
    m_client->mouseDown(static_cast<ButtonID>(id));
}

void
ServerProxy::mouseUp()
{
    // get mouse up to date
    flushCompressedMouse();

    // parse
    SInt8 id;
    ProtocolUtil::readf(m_stream, kMsgDMouseUp + 4, &id);
    LOG((CLOG_DEBUG1 "recv mouse up id=%d", id));

    // forward
    m_client->mouseUp(static_cast<ButtonID>(id));
}

void
ServerProxy::mouseMove()
{
    // parse
    bool ignore;
    SInt16 x, y;
    ProtocolUtil::readf(m_stream, kMsgDMouseMove + 4, &x, &y);

    // note if we should ignore the move
    ignore = m_ignoreMouse;

    // compress mouse motion events if more input follows
    if (!ignore && !m_compressMouse && m_stream->isReady()) {
        m_compressMouse = true;
    }

    // if compressing then ignore the motion but record it
    if (m_compressMouse) {
        m_compressMouseRelative = false;
        ignore    = true;
        m_xMouse  = x;
        m_yMouse  = y;
        m_dxMouse = 0;
        m_dyMouse = 0;
    }
    LOG((CLOG_DEBUG2 "recv mouse move %d,%d", x, y));

    // forward
    if (!ignore) {
        m_client->mouseMove(x, y);
    }
}

void
ServerProxy::mouseRelativeMove()
{
    // parse
    bool ignore;
    SInt16 dx, dy;
    ProtocolUtil::readf(m_stream, kMsgDMouseRelMove + 4, &dx, &dy);

    // note if we should ignore the move
    ignore = m_ignoreMouse;

    // compress mouse motion events if more input follows
    if (!ignore && !m_compressMouseRelative && m_stream->isReady()) {
        m_compressMouseRelative = true;
    }

    // if compressing then ignore the motion but record it
    if (m_compressMouseRelative) {
        ignore     = true;
        m_dxMouse += dx;
        m_dyMouse += dy;
    }
    LOG((CLOG_DEBUG2 "recv mouse relative move %d,%d", dx, dy));

    // forward
    if (!ignore) {
        m_client->mouseRelativeMove(dx, dy);
    }
}

void
ServerProxy::mouseWheel()
{
    // get mouse up to date
    flushCompressedMouse();

    // parse
    SInt16 xDelta, yDelta;
    ProtocolUtil::readf(m_stream, kMsgDMouseWheel + 4, &xDelta, &yDelta);
    LOG((CLOG_DEBUG2 "recv mouse wheel %+d,%+d", xDelta, yDelta));

    // forward
    m_client->mouseWheel(xDelta, yDelta);
}

void
ServerProxy::screensaver()
{
    // parse
    SInt8 on;
    ProtocolUtil::readf(m_stream, kMsgCScreenSaver + 4, &on);
    LOG((CLOG_DEBUG1 "recv screen saver on=%d", on));

    // forward
    m_client->screensaver(on != 0);
}

void
ServerProxy::resetOptions()
{
    // parse
    LOG((CLOG_DEBUG1 "recv reset options"));

    // forward
    m_client->resetOptions();

    // reset keep alive
    setKeepAliveRate(kKeepAliveRate);

    // reset modifier translation table
    for (KeyModifierID id = 0; id < kKeyModifierIDLast; ++id) {
        m_modifierTranslationTable[id] = id;
    }
}

void
ServerProxy::setOptions()
{
    // parse
    OptionsList options;
    ProtocolUtil::readf(m_stream, kMsgDSetOptions + 4, &options);
    LOG((CLOG_DEBUG1 "recv set options size=%d", options.size()));

    // forward
    m_client->setOptions(options);

    // update modifier table
    for (UInt32 i = 0, n = (UInt32)options.size(); i < n; i += 2) {
        KeyModifierID id = kKeyModifierIDNull;
        if (options[i] == kOptionModifierMapForShift) {
            id = kKeyModifierIDShift;
        }
        else if (options[i] == kOptionModifierMapForControl) {
            id = kKeyModifierIDControl;
        }
        else if (options[i] == kOptionModifierMapForAlt) {
            id = kKeyModifierIDAlt;
        }
        else if (options[i] == kOptionModifierMapForAltGr) {
            id = kKeyModifierIDAltGr;
        }
        else if (options[i] == kOptionModifierMapForMeta) {
            id = kKeyModifierIDMeta;
        }
        else if (options[i] == kOptionModifierMapForSuper) {
            id = kKeyModifierIDSuper;
        }
        else if (options[i] == kOptionHeartbeat) {
            // update keep alive
            setKeepAliveRate(1.0e-3 * static_cast<double>(options[i + 1]));
        }

        if (id != kKeyModifierIDNull) {
            m_modifierTranslationTable[id] =
                static_cast<KeyModifierID>(options[i + 1]);
            LOG((CLOG_DEBUG1 "modifier %d mapped to %d", id, m_modifierTranslationTable[id]));
        }
    }
}

void
ServerProxy::queryInfo()
{
    ClientInfo info;
    m_client->getShape(info.m_x, info.m_y, info.m_w, info.m_h);
    m_client->getCursorPos(info.m_mx, info.m_my);
    sendInfo(info);
}

void
ServerProxy::infoAcknowledgment()
{
    LOG((CLOG_DEBUG1 "recv info acknowledgment"));
    m_ignoreMouse = false;
}

void
ServerProxy::fileChunkReceived()
{
    int result = FileChunk::assemble(
                    m_stream,
                    m_client->getReceivedFileData(),
                    m_client->getExpectedFileSize());

    if (result == kFinish) {
        m_events->addEvent(Event(m_events->forFile().fileRecieveCompleted(), m_client));
    }
    else if (result == kStart) {
        if (m_client->getDragFileList().size() > 0) {
            String filename = m_client->getDragFileList().at(0).getFilename();
            LOG((CLOG_DEBUG "start receiving %s", filename.c_str()));
        }
    }
}

void
ServerProxy::dragInfoReceived()
{
    // parse
    UInt32 fileNum = 0;
    String content;
    ProtocolUtil::readf(m_stream, kMsgDDragInfo + 4, &fileNum, &content);

    m_client->dragInfoReceived(fileNum, content);
}

void
ServerProxy::handleClipboardSendingEvent(const Event& event, void*)
{
    ClipboardChunk::send(m_stream, event.getDataObject());
}

void
ServerProxy::fileChunkSending(UInt8 mark, char* data, size_t dataSize)
{
    FileChunk::send(m_stream, mark, data, dataSize);
}

void
ServerProxy::sendDragInfo(UInt32 fileCount, const char* info, size_t size)
{
    String data(info, size);
    ProtocolUtil::writef(m_stream, kMsgDDragInfo, fileCount, &data);
}

void
ServerProxy::secureInputNotification()
{
    String app;
    ProtocolUtil::readf(m_stream, kMsgDSecureInputNotification + 4, &app);

    // display this notification on the client
    if (app != "unknown") {
        AppUtil::instance().showNotification(
                    "The keyboard may stop working.",
                    "'Secure input' enabled by " + app + " on the server. " \
                    "To fix the keyboard, " + app + " must be closed.");
    }
    else {
        AppUtil::instance().showNotification(
                    "The keyboard may stop working.",
                    "'Secure input' enabled by an application on the server. " \
                    "To fix the keyboard, the application must be closed.");
    }
}

void
ServerProxy::setServerLanguages()
{
    String serverLanguages;
    ProtocolUtil::readf(m_stream, kMsgDLanguageSynchronisation + 4, &serverLanguages);
    m_languageManager.setRemoteLanguages(serverLanguages);
}

void
ServerProxy::setActiveServerLanguage(const String& language)
{
    if (!language.empty() && std::strlen(language.c_str()) > 0) {
        if(m_serverLanguage != language) {
            m_isUserNotifiedAboutLanguageSyncError = false;
            m_serverLanguage = language;
        }

        if (!m_languageManager.isLanguageInstalled(m_serverLanguage)) {
            if(!m_isUserNotifiedAboutLanguageSyncError) {
                LOG((CLOG_WARN "Current server language is not installed on client."));
                m_isUserNotifiedAboutLanguageSyncError = true;
            }
        }
        else {
            m_isUserNotifiedAboutLanguageSyncError = false;
        }
    }
    else {
        LOG((CLOG_DEBUG1 "Active server langauge is empty!"));
    }
}

void
ServerProxy::checkMissedLanguages() const
{
    auto missedLanguages = m_languageManager.getMissedLanguages();
    if (!missedLanguages.empty()) {
        LOG((CLOG_WARN
             "You need to install these languages on this computer and restart Synergy to enable support for multiple languages: %s",
             missedLanguages.c_str()));
    }
}
