/*
 * synergy -- mouse and keyboard sharing utility
 * Copyright (C) 2002 Chris Schoeneman
 * Copyright (C) 2006 Knut St. Osmundsen
 *
 * 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 COPYING 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.
 */

#include "CPMSynergyHook.h"
#include "ProtocolTypes.h"
#include <sys/builtin.h>
#if 0
#include <stdlib.h>
#include <unistd.h>
#include <InnoTekLIBC/FastInfoBlocks.h>
#else
unsigned long fibGetMsCount(void)
{
    ULONG ul = 0;
    DosQuerySysInfo(QSV_MS_COUNT, QSV_MS_COUNT, &ul, sizeof(ul));
    return ul;
}

int getpid(void)
{
    PTIB pTib;
    PPIB pPib;
    DosGetInfoBlocks(&pTib, &pPib);
    return pPib->pib_ulpid;
}

int _gettid(void)
{
    PTIB pTib;
    PPIB pPib;
    DosGetInfoBlocks(&pTib, &pPib);
    return pTib->tib_ptib2->tib2_ultid;
}

void _assert (__const__ char * pszExpr, __const__ char *pszFile, unsigned iLine)
{
    __asm__ ("int3\n");
}

#endif

//
// globals - system wide
//
/** @name The synergy server/client thread
 * @{ */
HMQ                  g_hmqSynergy;
int                  g_pidSynergy;
int                  g_tidSynergy;
/** @} */

/** The current mode.*/
EHookMode volatile   g_enmMode;

/** Which sides do we have warp zones on. */
UInt32               g_zoneSides;
/** The warp zone size in pixels. */
SInt32               g_zoneSize;
/** @name The screen size.
 * @{ */
SInt32               g_xScreen;
SInt32               g_yScreen;
SInt32               g_cxScreen;
SInt32               g_cyScreen;
/** @} */

/** Set if the hook is installed. */
bool                 g_fInputHookInstalled;
/** Set if we're faking input. */
bool                 g_fFakingInput;

/** @name Input Message Faking.
 * @{ */
/** The event semaphore we're waiting on while faking messages. */
HEV                  g_hev;
/** The pending message. */
QMSG                 g_qmsg;
/** Indicator set by the MsgInputHook function when the message 'skipped'. */
volatile bool        g_fMsgInjected;
/** Error description. */
const char          *g_pszErrorMsg;
/** @} */

/** The handle of the hook dll. */
HMODULE             g_hmod = NULLHANDLE;
/** Indicator whether the init() function has been called successfully. */
bool                g_fSynergyInitialized = false;



//
// Internal Functions
//


/**
 * Deal with keyboard messages.
 *
 * @param pQmsg     The WM_CHAR message.
 */
static bool InputHookKeyboardMsg(PQMSG pQmsg)
{
    /*
     * Check for special events indicating if we should start or stop
     * passing events through and not report them to the server.
     *
     * This is used to allow the server to synthesize events locally but
     * not pick them up as user events.
     */
    if (pQmsg->mp1 == SYNERGY_PM_HOOK_FAKE_INPUT_MP1_FIXED) {
        if (pQmsg->mp2 == SYNERGY_PM_HOOK_FAKE_INPUT_MP2_SET) {
            g_fFakingInput = true;
            return true;
        }
        if (pQmsg->mp2 == SYNERGY_PM_HOOK_FAKE_INPUT_MP2_CLEAR) {
            g_fFakingInput = false;
            return true;
        }
    }

    /*
     * If we're expecting fake input then just pass the event through
     * and do not forward to the server
     */
    if (g_fFakingInput)
        return false;

    /*
     * Send the key to the server without any more work.
     * This is probably to simplified, but it'll have to do for now because I'm lazy.
     */
    WinPostQueueMsg(g_hmqSynergy, SYNERGY_PM_MSG_KEY, pQmsg->mp1, pQmsg->mp2);
    return true;
}


/**
 * Checks if the specified mouse position is in a jump zone.
 *
 * @returns true if we're in the jump zone, false if we aren't
 * @param   x   X coordinate.
 * @param   y   Y coordinate. (1st quadrant)
 */
static bool isInJumpZone(SInt32 x, SInt32 y)
{
    /* any jump zones at all? */
    if (g_zoneSides == 0)
        return false;

    /*
     * Normalize the coordinates.
     */
    bool fBogus = false;
    if (x < g_xScreen) {
        x     = g_xScreen;
        fBogus = true;
    } else if (x >= g_xScreen + g_cxScreen) {
        x      = g_xScreen + g_cxScreen - 1;
        fBogus = true;
    }

    if (y < g_yScreen) {
        y      = g_yScreen;
        fBogus = true;
    } else if (y >= g_yScreen + g_cyScreen) {
        y      = g_yScreen + g_cyScreen - 1;
        fBogus = true;
    }

    /*
     * check if the mouse is in any of the jump zones.
     */
    bool fJump = false;
    if (g_zoneSides & kLeftMask)
        fJump |= (unsigned)x - (unsigned)g_xScreen < (unsigned)g_zoneSize;
    if (g_zoneSides & kRightMask)
        fJump |= (unsigned)(g_xScreen + g_cxScreen) - (unsigned)x < (unsigned)g_zoneSize;
    if (g_zoneSides & kTopMask)
        fJump |= (unsigned)(g_yScreen + g_cyScreen) - (unsigned)y < (unsigned)g_zoneSize;
    if (g_zoneSides & kBottomMask)
        fJump |= (unsigned)y - (unsigned)(g_yScreen) < (unsigned)g_zoneSize;

    return fJump && !fBogus;
}


/**
 * The input hook procedure (any context).
 *
 * @returns Processed indicator; TRUE if processed and should be discarded, FALSE if should be passed on.
 *
 * @param   hab     The anchor block of the current thread.
 * @param   pQmsg   The message structure containing the message we're called for.
 * @param   fs      The removal options (PM_REMOVE / PM_NOREMOVE).
 */
BOOL EXPENTRY InputHook(HAB hab, PQMSG pQmsg, ULONG fs)
{
    /*
     * Get hook mode and check if it's enabled.
     */
    const EHookMode enmMode = g_enmMode;
    if (enmMode == kHOOK_DISABLE)
        return FALSE;


    switch (pQmsg->msg) {
        /*
         * Keyboard message.
         */
        case WM_CHAR:
            if (enmMode == kHOOK_RELAY_EVENTS)
                return InputHookKeyboardMsg(pQmsg);
            break;

        /*
         * Mouse message.
         */
        case WM_MOUSEMOVE:
            if (    enmMode == kHOOK_RELAY_EVENTS
                ||  enmMode == kHOOK_WATCH_JUMP_ZONE) {
                WinPostQueueMsg(g_hmqSynergy, SYNERGY_PM_MSG_MOUSE_MOVE, MPFROM2SHORT(pQmsg->msg, 0), MPFROM2SHORT(pQmsg->ptl.x, pQmsg->ptl.y));
                return enmMode == kHOOK_RELAY_EVENTS
                    || isInJumpZone(pQmsg->ptl.x, pQmsg->ptl.y);
            }
            break;

        /*
         * Mouse click message.
         */
        case WM_BUTTON1DOWN:
        case WM_BUTTON1UP:
        case WM_BUTTON1DBLCLK:
        case WM_BUTTON2DOWN:
        case WM_BUTTON2UP:
        case WM_BUTTON2DBLCLK:
        case WM_BUTTON3DOWN:
        case WM_BUTTON3UP:
        case WM_BUTTON3DBLCLK:
            if (    enmMode == kHOOK_RELAY_EVENTS
                ||  enmMode == kHOOK_WATCH_JUMP_ZONE) {
                WinPostQueueMsg(g_hmqSynergy, SYNERGY_PM_MSG_MOUSE_BUTTON, MPFROM2SHORT(pQmsg->msg, 0), MPFROM2SHORT(pQmsg->ptl.x, pQmsg->ptl.y));
                return enmMode == kHOOK_RELAY_EVENTS;
            }
            break;

        /*
         * If I understand it correctly these messages are created
         * from the ones above and should be discarded. (If I'm wrong,
         * we'll not be able to drag stuff or open the window list.)
         */
        case WM_CHORD:
        case WM_BUTTON1MOTIONSTART:
        case WM_BUTTON1MOTIONEND:
        case WM_BUTTON1CLICK:
        case WM_BUTTON2MOTIONSTART:
        case WM_BUTTON2MOTIONEND:
        case WM_BUTTON2CLICK:
        case WM_BUTTON3MOTIONSTART:
        case WM_BUTTON3MOTIONEND:
        case WM_BUTTON3CLICK:
            if (enmMode == kHOOK_RELAY_EVENTS) {
                return true;
            }
            break;

        default:
            // log these.
            break;
    }

    /* don't eat it. */
    return false;
}


//
// external functions
//

/**
 * The DLL init/term function.
 */
ULONG _System _DLL_InitTerm(HMODULE hmod, ULONG ulReason)
{
    switch (ulReason) {
    /* init */
    case 0: {
        HEV hev = g_hev;
        if (hev != NULLHANDLE) {
            DosOpenEventSem(NULL, &hev);
        }
        g_hmod = hmod;
        break;
    }

    /* term */
    case 1:
        if (g_hev != NULLHANDLE) {
            DosCloseEventSem(g_hev);
        }
        break;
    }
    return TRUE;
}



/**
 * Initialize the hook dll (shared data mainly) in the synergy context.
 *
 * @returns success indicator (true/false)
 * @param   hmqSynergy
 * @param   tidSynergy
 * @param   pidSynergy
 */
CSYNERGYHOOK_API bool init(HMQ hmqSynergy, int tidSynergy, int pidSynergy)
{
    /*
     * Check that there isn't any synergy instance already running.
     * If the verification fails, we will take the place dead synergy instance.
     */
    if (   g_pidSynergy
        && g_tidSynergy
        && DosVerifyPidTid(g_pidSynergy, g_tidSynergy) == NO_ERROR)
        return false;

    /*
     * Create the event semaphore.
     */
    ULONG cPosts;
    if (    g_hev == NULLHANDLE
        ||  DosQueryEventSem(g_hev, &cPosts) != NO_ERROR
        ) {
        g_hev = NULLHANDLE;
        APIRET rc = DosCreateEventSem(NULL, &g_hev, DC_SEM_SHARED, FALSE);
        if (rc != NO_ERROR)
            return false;
    }

    /*
     * Initialize the globals.
     */
    g_hmqSynergy = hmqSynergy;
    g_pidSynergy = pidSynergy;
    g_tidSynergy = tidSynergy;

    /* only do this if the hook is inactive. */
    if (g_enmMode == kHOOK_DISABLE) {
        g_zoneSides = 0;
        g_zoneSize  = 0;
        g_xScreen   = 0;
        g_yScreen   = 0;
        g_cxScreen  = 0;
        g_cyScreen  = 0;
    }

    g_fSynergyInitialized = true;
    return true;
}


/**
 * Cleanup before unloading the dll (synergy context).
 *
 * @returns success indicator (true/false)
 * @param   hab     The anchor block.
 */
CSYNERGYHOOK_API bool cleanup(HAB hab)
{
    assert(g_hmod != NULLHANDLE);
    assert(g_fSynergyInitialized == true);

    /*
     * If we're the synergy process or if it's dead, shut down everything.
     */
    if (    (   g_pidSynergy != getpid()
            ||  g_tidSynergy != _gettid())
        &&  DosVerifyPidTid(g_pidSynergy, g_tidSynergy) == NO_ERROR)
        return false;

    /* parnaoia */
    uninstall(hab);

    g_hmqSynergy = NULLHANDLE;
    g_pidSynergy = 0;
    g_tidSynergy = 0;
    g_fSynergyInitialized = false;
    return true;
}


CSYNERGYHOOK_API EHookResult install(HAB hab)
{
    // must be initialized
    if (    g_tidSynergy == 0
        ||  !g_fSynergyInitialized) {
        return kHOOK_FAILED;
    }
    assert(g_pidSynergy == getpid());
    assert(g_tidSynergy == _gettid());

    //// discard old dead keys
    //g_deadVirtKey = 0;
    //g_deadLParam  = 0;

    //// reset fake input flag
    //g_fakeInput = false;

    /*
     * Install the hook.
     */
    if (    !g_fInputHookInstalled
        &&  WinSetHook(hab, NULLHANDLE, HK_INPUT, (PFN)InputHook, g_hmod))
        __atomic_xchg((unsigned volatile *)&g_fInputHookInstalled, true);

    if (g_fInputHookInstalled)
        return kHOOK_OKAY;

    uninstall(hab);
    return kHOOK_FAILED;
}


/**
 * Uninstall the hooks.
 *
 * @returns
 * @param   hab
 */
CSYNERGYHOOK_API bool uninstall(HAB hab)
{
    assert(g_pidSynergy == getpid());
    assert(g_tidSynergy == _gettid());
    assert(g_fSynergyInitialized);
    if (!g_fSynergyInitialized) {
        return false;
    }

    //// discard old dead keys
    //g_deadVirtKey = 0;
    //g_deadLParam  = 0;

    /*
     * Release the hooks.
     */
    if (    g_fInputHookInstalled
        &&  WinReleaseHook(hab, NULLHANDLE, HK_INPUT, (PFN)InputHook, g_hmod))
        __atomic_xchg((unsigned volatile *)&g_fInputHookInstalled, false);

    return !g_fInputHookInstalled;
}


/**
 * Sets the sides we should be watching out for jumps in.
 *
 * @param   sides   The new sides flags.
 */
CSYNERGYHOOK_API void setSides(UInt32 sides)
{
    assert(g_pidSynergy == getpid());
    assert(g_tidSynergy == _gettid());
    assert(g_fSynergyInitialized);
    if (g_fSynergyInitialized) {
        __atomic_xchg((unsigned volatile *)&g_zoneSides, (unsigned)sides);
    }
}


/**
 * Set the jump zone size and screen size.
 *
 * @param x     The screen x coordinate.
 * @param y     The screen y coordinate.
 * @param cx    The screen width.
 * @param cy    The screen heigth.
 * @param jumpZoneSize  The width of the jump zone (pixels).
 */
CSYNERGYHOOK_API void setZone(SInt32 x, SInt32 y, SInt32 cx, SInt32 cy, SInt32 jumpZoneSize)
{
    assert(g_pidSynergy == getpid());
    assert(g_tidSynergy == _gettid());
    assert(g_fSynergyInitialized);
    if (g_fSynergyInitialized) {
        __atomic_xchg((unsigned volatile *)&g_zoneSize, (unsigned)jumpZoneSize);
        __atomic_xchg((unsigned volatile *)&g_xScreen,  (unsigned)x);
        __atomic_xchg((unsigned volatile *)&g_yScreen,  (unsigned)y);
        __atomic_xchg((unsigned volatile *)&g_cxScreen, (unsigned)cx);
        __atomic_xchg((unsigned volatile *)&g_cyScreen, (unsigned)cy);
    }
}


/**
 * Check the mode of operation.
 *
 * @param   mode    The new mode.
 */
CSYNERGYHOOK_API void setMode(EHookMode mode)
{
    assert(g_pidSynergy == getpid());
    assert(g_tidSynergy == _gettid());
    assert(g_fSynergyInitialized);
    if (g_fSynergyInitialized) {
        if (mode != g_enmMode) {
            switch (mode) {
            case kHOOK_DISABLE:
            case kHOOK_WATCH_JUMP_ZONE:
            case kHOOK_RELAY_EVENTS:
                __atomic_xchg((unsigned volatile *)&g_enmMode, (unsigned)mode);
                break;
            default:
                __atomic_xchg((unsigned volatile *)&g_enmMode, kHOOK_DISABLE);
                break;
            }
        }
    }
}


//
// Message faking.
//

/**
 * Hook callback used to insert message into the system queue.
 *
 * @returns TRUE if we've got a message.
 * @returns FALSE if we haven't got a message handy.
 * @param   hab         The hab of the current thread.
 * @param   pqmsg       Where to put the message.
 * @param   fSkip       Skip message flag.
 *                      If TRUE we should just skip a message (pqmsg is NULL). If no futher
 *                      messages are queued, we must release the hook.
 *                      If FALSE we should return the current pending message.
 * @param   pfNoRecord  Record message flag.
 */
static BOOL EXPENTRY MsgInputHook(HAB hab, PQMSG pqmsg, BOOL fSkip, PBOOL pfNoRecord)
{
    /*
     * We've only got one message, so everything is dead simple.
     */
    if (fSkip) {
        __atomic_xchg((unsigned volatile *)&g_fMsgInjected, 1);
        if (WinReleaseHook(hab, NULLHANDLE, HK_MSGINPUT, (PFN)MsgInputHook, g_hmod)) {
            HEV     hev = g_hev;
            APIRET  rc = DosPostEventSem(hev);
            if (    rc != ERROR_INVALID_HANDLE
                ||  DosOpenEventSem(NULL, &hev) != NO_ERROR
                ||  DosPostEventSem(hev) != NO_ERROR)
                g_pszErrorMsg = "failed to post event semaphore";
        } else {
            g_pszErrorMsg = "failed to release hook";
        }
    } else {
        *pqmsg = g_qmsg;
        if (pqmsg->msg == WM_VIOCHAR) {
            /* mp1.s1: KC_ flags.
             * mp1.c3: repeat
             * mp1.c4: scancode
             * mp2.c1: translated char
             * mp2.c2: translated scancode
             * mp2.s2: KDD_ flags.
             */
            KbdPacket[0].monFlags = 0; // no monitor flag
            KbdPacket[0].scancode = CHAR4FROMMP(pqmsg->mp1);
            KbdPacket[0].xlatedchar = CHAR1FROMMP(pqmsg->mp2);
            KbdPacket[0].xlatedscan = CHAR2FROMMP(pqmsg->mp2);
            KbdPacket[0].time = pqmsg->time;
            KbdPacket[0].ddFlags = SHORT2FROMMP(pqmsg->mp2);
            /** @todo extended stuff, keypad, workarounds. see pmvnc. */
        }
    }

    /*
     * We're not recording, so no chance of a feedback loop here.
     * This doesn't seem to work, bah!
     */
    if (pfNoRecord) {
        *pfNoRecord = FALSE;
    }
    return TRUE;
}


/**
 * Inserts a fake input message.
 *
 * @returns NULL on success.
 * @returns String describing the error on failure.
 * @param   hab     The anchor block of the calling thread.
 * @param   pQmsg   Pointer to the message.
 * @remark  The caller must serialize calls to this function!
 */
CSYNERGYHOOK_API const char *fakeMsg(HAB hab, const QMSG *pQmsg)
{
    const char *pszRet;
    assert(g_fSynergyInitialized);
    if (g_fSynergyInitialized) {
        /*
         * Reset the event semaphore and queue the message for MsgInputHook.
         */
        ULONG cPosts;
        APIRET rc = DosResetEventSem(g_hev, &cPosts);
        if (rc == NO_ERROR || rc == ERROR_ALREADY_RESET) {
            g_qmsg = *pQmsg;
            g_pszErrorMsg = NULL;
            __atomic_xchg((unsigned volatile *)&g_fMsgInjected, 0);

            /*
             * Install the hook and wait for the message to be delivered.
             */
            WinCheckInput(hab);
            if (WinSetHook(hab, NULLHANDLE, HK_MSGINPUT, (PFN)MsgInputHook, g_hmod)) {
                WinCheckInput(hab);
                rc = DosWaitEventSem(g_hev, 1000 /* 33 */);
                if (rc == NO_ERROR || g_fMsgInjected) {
                    return NULL;
                }

                /*
                 * It took a bit longer than expected. We'll wait for some 2-3
                 * seconds before we give up. This might be a bit paranoid, but
                 * I'd rather drop a message than locking up PM.
                 */
                if (rc == ERROR_TIMEOUT || rc == ERROR_SEM_TIMEOUT || rc == ERROR_INTERRUPT) {
                    ULONG ulStart = fibGetMsCount(); /* monotone time in milliseconds */
                    for (;;) {
                        WinCheckInput(hab);
                        rc = DosWaitEventSem(g_hev, 33);
                        if (rc == NO_ERROR || g_fMsgInjected) {
                            return NULL;
                        }
                        if (rc != ERROR_TIMEOUT && rc != ERROR_SEM_TIMEOUT && rc != ERROR_INTERRUPT) {
                            break;
                        }
                        if (fibGetMsCount() - ulStart > 2500) {
                            break;
                        }
                    }
                }

                /*
                 * Since we probably didn't manage to inject the message, we should
                 * release the hook to make sure we're not overwriting g_qmsg while
                 * some other thread is reading it from MsgInputHook.
                 */
                WinReleaseHook(hab, NULLHANDLE, HK_MSGINPUT, (PFN)MsgInputHook, g_hmod);
                DosSleep(10);
                pszRet = g_pszErrorMsg;
                return pszRet ? pszRet : "timed out";
            }
            pszRet = "WinSetHook failed";
        } else {
            pszRet = "DosResetEventSem failed";
        }
    } else {
        pszRet = "!g_fSynergyInitialized";
    }
    DosSleep(0);
    return pszRet;
}

