/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Copyright @ 2015 Atlassian Pty 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.
 */
#include "OutOfProcessServer.h"

#include "Log.h"
#include "Messenger.h"
#include "MessengerClassFactory.h"
#include "MessengerContact.h"
#include "net_java_sip_communicator_plugin_msofficecomm_OutOfProcessServer.h"
#include "process.h"

EXTERN_C const GUID DECLSPEC_SELECTANY LIBID_CommunicatorUA
    = { 0x2B317E1D, 0x50E5, 0x4f5e, { 0xA3, 0xA4, 0xFB, 0x85, 0x20, 0x6E, 0xDA, 0x48 } };

JNIEXPORT jint JNICALL
Java_net_java_sip_communicator_plugin_msofficecomm_OutOfProcessServer_start
    (JNIEnv *env, jclass clazz)
{
    LPSTR functionName = ::_strdup(__FUNCTION__);
    LPSTR packageName;

    if (functionName)
    {
        packageName = functionName + 5 /* Java_ */;

        size_t packageNameLength
            = ::strlen(packageName) - 24 /* OutOfProcessServer_start */;

        packageName[packageNameLength] = '\0';

        char ch;
        LPSTR str = packageName;

        while ((ch = *str))
        {
            if ('_' == ch)
                *str = '/';
            str++;
        }
    }
    else
        packageName = NULL;

    jint ret = OutOfProcessServer::start(env, clazz, packageName);

    if (functionName)
        ::free(functionName);

    return ret;
}

JNIEXPORT jint JNICALL
Java_net_java_sip_communicator_plugin_msofficecomm_OutOfProcessServer_stop
    (JNIEnv *env, jclass clazz)
{
    return OutOfProcessServer::stop(env, clazz);
}

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)
{
    return OutOfProcessServer::JNI_OnLoad(vm);
}

JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved)
{
    OutOfProcessServer::JNI_OnUnload(vm);
}

CRITICAL_SECTION OutOfProcessServer::_criticalSection;
LPTYPELIB        OutOfProcessServer::_iTypeLib;
ClassFactory *   OutOfProcessServer::_messengerClassFactory = NULL;
LPSTR            OutOfProcessServer::_packageName;
HANDLE           OutOfProcessServer::_threadHandle;
DWORD            OutOfProcessServer::_threadId;
JavaVM *         OutOfProcessServer::_vm = NULL;

LPSTR OutOfProcessServer::getClassName(LPCSTR className)
{
    size_t packageNameLength = _packageName ? ::strlen(_packageName) : 0;
    size_t classNameLength = ::strlen(className);
    LPSTR ret = (LPSTR) ::malloc(packageNameLength + classNameLength + 1);

    if (ret)
    {
        LPSTR str = ret;

        if (packageNameLength)
        {
            ::memcpy(str, _packageName, packageNameLength);
            str += packageNameLength;
        }
        if (classNameLength)
        {
            ::memcpy(str, className, classNameLength);
            str += classNameLength;
        }
        *str = '\0';
    }
    return ret;
}

BOOL OutOfProcessServer::isMicrosoftOfficeOutlookCallIntegrationIMApplication()
{
    TCHAR path[MAX_PATH + 1];
    DWORD pathCapacity = sizeof(path) / sizeof(TCHAR);
    DWORD pathLength = ::GetModuleFileName(NULL, path, pathCapacity);
    BOOL b;

    if (pathLength && (pathLength < pathCapacity))
    {
        LPTSTR fileName = NULL;

        for (LPTSTR str = path + (pathLength - 1); str != path; str--)
        {
            TCHAR ch = *str;

            if (('\\' == ch) || ('/' == ch))
            {
                fileName = str + 1;
                break;
            }
        }
        if (fileName && *fileName)
        {
            DWORD dataSize = (pathLength + 2) * sizeof(TCHAR);
            LPBYTE data = (LPBYTE) ::malloc(dataSize);

            if (data)
            {
                SYSTEM_INFO systemInfo;
                REGSAM alternatives86[] = { 0 };
                REGSAM alternatives64[] = { KEY_WOW64_32KEY, KEY_WOW64_64KEY };
                REGSAM *alternatives;
                size_t alternativeCount;

                ::GetNativeSystemInfo(&systemInfo);
                if (PROCESSOR_ARCHITECTURE_INTEL
                        == systemInfo.wProcessorArchitecture)
                {
                    alternatives = alternatives86;
                    alternativeCount = sizeof(alternatives86) / sizeof(REGSAM);
                }
                else
                {
                    alternatives = alternatives64;
                    alternativeCount = sizeof(alternatives64) / sizeof(REGSAM);
                }

                LPCTSTR key
                    = _T("SOFTWARE\\Microsoft\\Office\\Outlook\\Call Integration");
                LPCTSTR valueName = _T("IMApplication");
                size_t fileNameLength = ::_tcslen(fileName);

                b = FALSE;
                for (size_t i = 0; i < alternativeCount; i++)
                {
                    HKEY hkey;

                    if (::RegOpenKeyEx(
                                HKEY_LOCAL_MACHINE,
                                key,
                                0,
                                KEY_QUERY_VALUE | alternatives[i],
                                &hkey)
                            == ERROR_SUCCESS)
                    {
                        DWORD type;

                        dataSize = (pathLength + 1) * sizeof(TCHAR);
                        ::ZeroMemory(data, dataSize + sizeof(TCHAR));
                        if ((::RegQueryValueEx(
                                        hkey,
                                        valueName,
                                        NULL,
                                        &type,
                                        data,
                                        &dataSize)
                                    == ERROR_SUCCESS)
                                && (REG_SZ == type))
                        {
                            b
                                = (::_tcsnicmp(
                                            fileName,
                                            (LPCTSTR) data,
                                            fileNameLength)
                                        == 0);
                        }
                        ::RegCloseKey(hkey);

                        if (b)
                            break;
                    }
                }
                ::free(data);
            }
            else
                b = FALSE;
        }
        else
            b = FALSE;
    }
    else
        b = FALSE;
    return b;
}

jint OutOfProcessServer::JNI_OnLoad(JavaVM *vm)
{
    jint version;

    if (isMicrosoftOfficeOutlookCallIntegrationIMApplication())
    {
        ::InitializeCriticalSection(&_criticalSection);
        _vm = vm;
        version = JNI_VERSION_1_4;
    }
    else
        version = JNI_ERR;
    return version;
}

HRESULT OutOfProcessServer::loadRegTypeLib()
{
    /*
     * Microsoft Office will need the Office Communicator 2007 API to be able to
     * talk to us. Make sure it is available.
     */

    LPTYPELIB iTypeLib;
    HRESULT hr = ::LoadRegTypeLib(LIBID_CommunicatorUA, 1, 0, 0, &iTypeLib);

    Log::d(
        _T("OutOfProcessServer::loadRegTypeLib:")
        _T(" LoadRegTypeLib=%08X;")
        _T("\n"),
        hr);
    if (SUCCEEDED(hr))
        _iTypeLib = iTypeLib;
    else
    {
        HMODULE module;

        _iTypeLib = NULL;

        if (::GetModuleHandleEx(
                GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS
                    | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
                (LPCTSTR) (OutOfProcessServer::loadRegTypeLib),
                &module))
        {
            WCHAR path[MAX_PATH + 1];
            DWORD pathCapacity = sizeof(path) / sizeof(WCHAR);
            DWORD pathLength = ::GetModuleFileNameW(module, path, pathCapacity);
            if (pathLength && (pathLength < pathCapacity))
            {
                hr = ::LoadTypeLibEx(path, REGKIND_NONE, &iTypeLib);
                Log::d(
                    _T("OutOfProcessServer::loadRegTypeLib:")
                    _T(" LoadTypeLibEx(%s)=%08X;")
                    _T("\n"),
                    path,
                    hr);
                if (SUCCEEDED(hr))
                {
                    hr = ::RegisterTypeLibForUser(iTypeLib, path, NULL);
                    Log::d(
                        _T("OutOfProcessServer::loadRegTypeLib:")
                        _T(" RegisterTypeLibForUser=%08X;")
                        _T("\n"),
                        hr);
                    if (SUCCEEDED(hr))
                    {
                        /*
                        * The whole point of what has been done till
                        * now is securing the success of future calls
                        * to LoadRegTypeLib. Make sure that is indeed
                        * the case.
                        */
                        iTypeLib->Release();
                        hr = ::LoadRegTypeLib(
                                    LIBID_CommunicatorUA, 1, 0, 0, &iTypeLib);
                        Log::d(
                            _T("OutOfProcessServer::loadRegTypeLib:")
                            _T(" LoadRegTypeLib=%08X;")
                            _T("\n"),
                            hr);
                        if (SUCCEEDED(hr))
                            _iTypeLib = iTypeLib;
                    }
                    else
                        hr = E_UNEXPECTED;
                    if (iTypeLib != _iTypeLib)
                        iTypeLib->Release();
                }
            }
        }
    }
    return hr;
}

DWORD
OutOfProcessServer::regCreateKeyAndSetValue
    (LPCTSTR key, LPCTSTR valueName, DWORD data)
{
    SYSTEM_INFO systemInfo;
    REGSAM alternatives86[] = { 0 };
    REGSAM alternatives64[] = { KEY_WOW64_32KEY, KEY_WOW64_64KEY };
    REGSAM *alternatives;
    size_t alternativeCount;

    ::GetNativeSystemInfo(&systemInfo);
    if (PROCESSOR_ARCHITECTURE_INTEL == systemInfo.wProcessorArchitecture)
    {
        alternatives = alternatives86;
        alternativeCount = sizeof(alternatives86) / sizeof(REGSAM);
    }
    else
    {
        alternatives = alternatives64;
        alternativeCount = sizeof(alternatives64) / sizeof(REGSAM);
    }

    DWORD lastError;

    for (size_t i = 0; i < alternativeCount; i++)
    {
        HKEY hkey;

        lastError
            = ::RegCreateKeyEx(
                    HKEY_CURRENT_USER,
                    key,
                    0,
                    NULL,
                    REG_OPTION_VOLATILE,
                    KEY_SET_VALUE | alternatives[i],
                    NULL,
                    &hkey,
                    NULL);
        if (ERROR_SUCCESS == lastError)
        {
            lastError
                = ::RegSetValueEx(
                        hkey,
                        valueName,
                        0,
                        REG_DWORD,
                        (const BYTE *) &data,
                        sizeof(data));
            ::RegCloseKey(hkey);
        }
        if (ERROR_SUCCESS != lastError)
            break;
    }
    return lastError;
}

HRESULT OutOfProcessServer::registerClassObjects()
{
    ClassFactory *classObject = new MessengerClassFactory();
    HRESULT hresult = classObject->registerClassObject();

    if (SUCCEEDED(hresult))
        _messengerClassFactory = classObject;
    else
        classObject->Release();

    if (SUCCEEDED(hresult))
    {
        hresult = ::CoResumeClassObjects();
        if (FAILED(hresult))
            revokeClassObjects();
    }

    return hresult;
}

ULONG OutOfProcessServer::releaseTypeLib()
{
    // TODO UnRegisterTypeLibForUser
    return _iTypeLib->Release();
}

HRESULT OutOfProcessServer::revokeClassObjects()
{
    HRESULT ret = ::CoSuspendClassObjects();

    if (SUCCEEDED(ret))
    {
        ClassFactory *classObject = _messengerClassFactory;

        if (classObject)
        {
            _messengerClassFactory = NULL;

            HRESULT hr = classObject->revokeClassObject();

            classObject->Release();
            if (FAILED(hr))
                ret = hr;
        }
    }
    return ret;
}

unsigned __stdcall OutOfProcessServer::run(void *)
{
    Log::open();

    HRESULT hr = ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
    unsigned ret = 0;

    Log::d(
        _T("OutOfProcessServer::run:")
        _T(" CoInitializeEx=%08X;")
        _T("\n"), 
        hr);
    if (SUCCEEDED(hr))
    {
        hr = loadRegTypeLib();
        Log::d(
            _T("OutOfProcessServer::run:")
            _T(" loadRegTypeLib=%08X;")
            _T("\n"),
            hr);
        if (SUCCEEDED(hr))
        {
            hr = setIMProvidersCommunicatorUpAndRunning(1);
            Log::d(
                _T("OutOfProcessServer::run:")
                _T(" setIMProvidersCommunicatorUpAndRunning(1)=%08X;")
                _T("\n"),
                hr);
            if (ERROR_SUCCESS == hr)
            {
                MSG msg;

                /*
                 * Create the message queue of this thread before any other part
                 * of the code (e.g. the release method) has a chance to invoke
                 * PostThreadMessage.
                 */
                ::PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE);

                hr = registerClassObjects();
                Log::d(
                    _T("OutOfProcessServer::run:")
                    _T(" registerClassObjects=%08X;")
                    _T("\n"),
                    hr);
                if (SUCCEEDED(hr))
                {
                    hr = setIMProvidersCommunicatorUpAndRunning(2);
                    Log::d(
                        _T("OutOfProcessServer::run:")
                        _T(" setIMProvidersCommunicatorUpAndRunning(2)=%08X;")
                        _T("\n"),
                        hr);
                    if (ERROR_SUCCESS == hr)
                    {
                        HANDLE threadHandle = _threadHandle;
                        BOOL logMsgWaitForMultipleObjectsExFailed = TRUE;
                        BOOL quit = FALSE;

                        do
                        {
                            /*
                             * Enable the use of the QueueUserAPC function by
                             * entering an alertable state.
                             */
                            if ((WAIT_FAILED
                                        == ::MsgWaitForMultipleObjectsEx(
                                                1,
                                                &threadHandle,
                                                INFINITE,
                                                QS_ALLINPUT | QS_ALLPOSTMESSAGE,
                                                MWMO_ALERTABLE
                                                    | MWMO_INPUTAVAILABLE))
                                    && logMsgWaitForMultipleObjectsExFailed)
                            {
                                /*
                                 * Logging the possible failures of the calls to
                                 * MsgWaitForMultipleObjectsEx multiple times is
                                 * unlikely to be useful. Besides, the call in
                                 * question is performed inside the message loop
                                 * and the logging will be an unnecessary
                                 * performance penalty.
                                 */
                                logMsgWaitForMultipleObjectsExFailed = FALSE;
                                Log::d(
                                        _T("OutOfProcessServer::run:")
                                        _T(" MsgWaitForMultipleObjectsEx=WAIT_FAILED;")
                                        _T("\n"));
                            }
                            while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
                            {
                                if (WM_QUIT == msg.message)
                                {
                                    quit = TRUE;
                                    ret = msg.wParam;
                                    break;
                                }
                                else if (msg.hwnd)
                                {
                                    ::TranslateMessage(&msg);
                                    ::DispatchMessage(&msg);
                                }
                            }
                        }
                        while (!quit);
                    }

                    revokeClassObjects();
                }
            }

            /*
             * Even if setIMProvidersCommunicatorUpAndRunning(DWORD) failed, it
             * may have successfully set some of the multiple related registry
             * keys.
             */
            setIMProvidersCommunicatorUpAndRunning(0);

            releaseTypeLib();
        }

        ::CoUninitialize();
    }

    Log::close();
    return ret;
}

DWORD OutOfProcessServer::setIMProvidersCommunicatorUpAndRunning(DWORD dw)
{
    DWORD lastError
            = regCreateKeyAndSetValue(
                    _T("Software\\IM Providers\\Jitsi"),
                    _T("UpAndRunning"),
                    dw);

    return lastError;
}

HRESULT OutOfProcessServer::start(JNIEnv *env, jclass clazz, LPCSTR packageName)
{
    HRESULT hr;

    if (packageName)
        hr = ((_packageName = ::_strdup(packageName))) ? S_OK : E_OUTOFMEMORY;
    else
    {
        _packageName = NULL;
        hr = S_OK;
    }

    if (SUCCEEDED(hr))
    {
        hr = Messenger::start(env);
        if (SUCCEEDED(hr))
        {
            hr = MessengerContact::start(env);
            if (SUCCEEDED(hr))
            {
                unsigned threadId;
                HANDLE threadHandle
                    = (HANDLE)
                        ::_beginthreadex(
                                NULL,
                                0,
                                OutOfProcessServer::run,
                                NULL,
                                CREATE_SUSPENDED,
                                &threadId);

                if (threadHandle)
                {
                    enterCriticalSection();

                    _threadHandle = threadHandle;
                    _threadId = (DWORD) threadId;
                    if (((DWORD) -1) == ::ResumeThread(threadHandle))
                    {
                        DWORD lastError = ::GetLastError();

                        _threadHandle = NULL;

                        ::CloseHandle(threadHandle);
                        hr = HRESULT_FROM_WIN32(lastError);
                    }

                    leaveCriticalSection();
                }
                else
                    hr = E_UNEXPECTED;

                if (FAILED(hr))
                    MessengerContact::stop(env);
            }

            if (FAILED(hr))
                Messenger::stop(env);
        }

        if (FAILED(hr) && _packageName)
        {
            ::free(_packageName);
            _packageName = NULL;
        }
    }

    return hr;
}

HRESULT OutOfProcessServer::stop(JNIEnv *env, jclass clazz)
{
    DWORD lastError;

    if (::PostThreadMessage(_threadId, WM_QUIT, 0, 0))
    {
        do
        {
            DWORD exitCode;

            if (::GetExitCodeThread(_threadHandle, &exitCode))
            {
                if (STILL_ACTIVE == exitCode)
                {
                    if (WAIT_FAILED
                            == ::WaitForSingleObject(_threadHandle, INFINITE))
                        break;
                }
                else
                    break;
            }
            else
                break;
        }
        while (1);

        if (::CloseHandle(_threadHandle))
            lastError = 0;
        else
            lastError = ::GetLastError();

        MessengerContact::stop(env);
        Messenger::stop(env);

        if (_packageName)
        {
            ::free(_packageName);
            _packageName = NULL;
        }
    }
    else
        lastError = ::GetLastError();
    return lastError ? HRESULT_FROM_WIN32(lastError) : S_OK;
}
