/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
 * This file is part of the Collabora Office project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#ifndef INCLUDED_COKIT_COKIT_INIT_H
#define INCLUDED_COKIT_COKIT_INIT_H

#if defined __GNUC__ || defined __clang__
#  define KIT_TOLERATE_UNUSED __attribute__((used))
#else
#  define KIT_TOLERATE_UNUSED
#endif

#if defined(__linux__) || defined (__FreeBSD__) ||\
    defined(_WIN32) || defined(__APPLE__) || defined (__NetBSD__) ||\
    defined (__sun) || defined(__OpenBSD__) || defined(__EMSCRIPTEN__)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

#include "COKit.hxx"

#ifndef _WIN32

    #include <dlfcn.h>

    #ifdef __APPLE__
        #include <TargetConditionals.h>
        #define SOFFICEAPP_LIB "libsofficeapp.dylib"
        #define MERGED_LIB "libmergedlo.dylib"

        #if (!defined TARGET_OS_IPHONE || TARGET_OS_IPHONE == 0) && (!defined TARGET_OS_OSX || TARGET_OS_OSX == 0)
            #error COKit is not supported on tvOS, visionOS or watchOS
        #endif
    #else
        #define SOFFICEAPP_LIB "libsofficeapp.so"
        #define MERGED_LIB "libmergedlo.so"
    #endif
    #define SEPARATOR         '/'

#else

    #if !defined WIN32_LEAN_AND_MEAN
        #define WIN32_LEAN_AND_MEAN
    #endif
    #include <windows.h>
    #define SOFFICEAPP_LIB "sofficeapp.dll"
    #define MERGED_LIB "mergedlo.dll"
    #define SEPARATOR '\\'

    #undef DELETE

#endif

#ifdef __cplusplus
extern "C"
{
#endif

#ifndef _WIN32

#if !defined(IOS)
        static void *cok_loadlib(const char *pFN)
    {
        return dlopen(pFN, RTLD_LAZY
#if defined KIT_LOADLIB_GLOBAL
                      | RTLD_GLOBAL
#endif
                      );
    }

    static char *cok_dlerror(void)
    {
        return dlerror();
    }

    // This function must be called to release memory allocated by cok_dlerror()
    static void cok_dlerror_free(char *pErrMessage)
    {
        (void)pErrMessage;
        // Do nothing for return of dlerror()
    }

    static void *cok_dlsym(void *Hnd, const char *pName)
    {
        return dlsym(Hnd, pName);
    }

    static int cok_dlclose(void *Hnd)
    {
        return dlclose(Hnd);
    }
#endif // IOS


#else // _WIN32

    static wchar_t* cok_string_to_wide_string(const char* string)
    {
        const size_t len = strlen(string);
        if (len == 0)
        {
            return _wcsdup(L"");
        }

        if (len > INT_MAX)
        {
            /* Trying to be funny, eh? */
            return _wcsdup(L"");
        }

        const int wlen = MultiByteToWideChar(CP_UTF8, 0, string, (int)len, NULL, 0);
        if (wlen <= 0)
        {
            return NULL;
        }

        wchar_t* result = (wchar_t*)malloc(wlen * 2 + 2);
        assert(result);
        MultiByteToWideChar(CP_UTF8, 0, string, (int)len, result, wlen);
        result[wlen] = L'\0';

        return result;
    }

    static char* cok_wide_string_to_string(const wchar_t* wstring)
    {
        if (wstring == NULL)
            return NULL;

        int len = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wstring, -1, NULL, 0, NULL, NULL);
        if (len <= 0)
            return NULL;

        char* result = (char*)malloc(len);
        if (result == NULL)
            return NULL;

        len = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wstring, -1, (char*)result, len, NULL,
                                  NULL);
        if (len <= 0)
        {
            free(result);
            return NULL;
        }

        return result;
    }

    static void *cok_loadlib(const char *pFN)
    {
        wchar_t* wpFN = cok_string_to_wide_string(pFN);
        void* result = LoadLibraryW(wpFN);
        free(wpFN);
        return result;
    }

    static char *cok_dlerror(void)
    {
        LPWSTR wbuf = NULL;
        int rc = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
            NULL, GetLastError(), 0, reinterpret_cast<LPWSTR>(&wbuf), 0, NULL);
        if (rc == 0)
            return _strdup("");

        /* Strip trailing CRLF */
        if (wbuf[wcslen(wbuf)-1] == L'\n')
            wbuf[wcslen(wbuf)-1] = L'\0';
        if (wbuf[wcslen(wbuf)-1] == L'\r')
            wbuf[wcslen(wbuf)-1] = L'\0';

        char* result = cok_wide_string_to_string(wbuf);
        HeapFree(GetProcessHeap(), 0, wbuf);
        return result;
    }

    // This function must be called to release memory allocated by cok_dlerror()
    static void cok_dlerror_free(char *pErrMessage)
    {
        free(pErrMessage);
    }

    static void *cok_dlsym(void *Hnd, const char *pName)
    {
        return reinterpret_cast<void *>(GetProcAddress((HINSTANCE) Hnd, pName));
    }

    static int cok_dlclose(void *Hnd)
    {
        return FreeLibrary((HINSTANCE) Hnd);
    }

#endif

#if !defined(IOS)
static int cok_stat(const char* path, struct stat* st)
{
#ifdef _WIN32
    struct _stat32 st32;
    wchar_t* wpath = cok_string_to_wide_string(path);
    int result = _wstat32(wpath, &st32);
    free(wpath);
    if (result != -1)
    {
        st->st_dev = st32.st_dev;
        st->st_ino = st32.st_ino;
        st->st_mode = st32.st_mode;
        st->st_nlink = st32.st_nlink;
        st->st_uid = st32.st_uid;
        st->st_gid = st32.st_gid;
        st->st_rdev = st32.st_rdev;
        st->st_size = st32.st_size;
        st->st_atime = st32.st_atime;
        st->st_mtime = st32.st_mtime;
        st->st_ctime = st32.st_ctime;
    }
    return result;
#else
    return stat(path, st);
#endif
}

static void *cok_dlopen( const char *install_path, char ** _imp_lib )
{
    char *imp_lib;
    void *dlhandle;

    size_t partial_length, imp_lib_size;
    struct stat dir_st;

    *_imp_lib = NULL;

    if (!install_path)
        return NULL;

    if (cok_stat(install_path, &dir_st) != 0)
    {
        fprintf(stderr, "installation path \"%s\" does not exist\n", install_path);
        return NULL;
    }

    // allocate large enough buffer
    partial_length = strlen(install_path);
    imp_lib_size = partial_length + sizeof(SOFFICEAPP_LIB) + sizeof(MERGED_LIB) + 2;
    imp_lib = (char *) malloc(imp_lib_size);
    if (!imp_lib)
    {
        fprintf( stderr, "failed to open library : not enough memory\n");
        return NULL;
    }

    memcpy(imp_lib, install_path, partial_length);

    imp_lib[partial_length++] = SEPARATOR;
    strncpy(imp_lib + partial_length, SOFFICEAPP_LIB, imp_lib_size - partial_length);

    struct stat st;
    // If SOFFICEAPP_LIB exists but is ridiculously small, it is the
    // one-line text stub as in the --enable-mergedlib case.
    if (cok_stat(imp_lib, &st) == 0 && st.st_size > 1000)
    {
        dlhandle = cok_loadlib(imp_lib);
        if (!dlhandle)
        {
            char *pErrMessage = cok_dlerror();
            fprintf(stderr, "failed to open library '%s': %s\n",
                    imp_lib, pErrMessage);
            cok_dlerror_free(pErrMessage);
            free(imp_lib);
            return NULL;
        }
    }
    else
    {
        strncpy(imp_lib + partial_length, MERGED_LIB, imp_lib_size - partial_length);

        dlhandle = cok_loadlib(imp_lib);
        if (!dlhandle)
        {
            char *pErrMessage = cok_dlerror();
            fprintf(stderr, "failed to open library '%s': %s\n",
                    imp_lib, pErrMessage);
            cok_dlerror_free(pErrMessage);
            free(imp_lib);
            return NULL;
        }
    }
    *_imp_lib = imp_lib;
    return dlhandle;
}
#endif

typedef COKit *(CokHookFunction)( const char *install_path);

typedef COKit *(CokHookFunction2)( const char *install_path, const char *user_profile_url );

typedef int             (CokHookPreInit)  ( const char *install_path, const char *user_profile_url );

// For client code directly accessing the exported cok_preinit_2 via cok_dlsym:
typedef int             (CokHookPreInit2) ( const char *install_path, const char *user_profile_url, COKit** kit);

#if defined(IOS) || defined(ANDROID) || defined(__EMSCRIPTEN__)
COKit *cokit_hook_2(const char* install_path, const char* user_profile_path);
#endif

// install_path is the pathname to the CollaboraOffice installation
// directory, the one with the subdirectories "program", "share" etc.
// On Linux there is nothing special here, you just pass such a
// pathname.
//
// On Windows, in the actual file system names are in UTF-16. To work
// in arbitrary situations, on Windows one should use APIs that take
// wide characters when accessing the file system. Here you pass in
// the pathname in UTF-8. *Not* in the system codepage (or some other
// codepage). Of course, in the common (lucky?) case of a pathname
// that is just ASCII anyway, that is already UTF-8.
//
// On macOS it should be the pathname of the bundle (.app directory)
// the code is in.
//
// user_profile_url is a file: URI for the user profile. Can be NULL.

static COKit *cok_init_2( const char *install_path,  const char *user_profile_url )
{
#if !defined(IOS) && !defined(ANDROID) && !defined(__EMSCRIPTEN__)
    void *dlhandle;
    char *imp_lib;
    CokHookFunction *pSym;
    CokHookFunction2 *pSym2;

    dlhandle = cok_dlopen(install_path, &imp_lib);
    if (!dlhandle)
        return NULL;

    pSym2 = (CokHookFunction2 *) cok_dlsym(dlhandle, "cokit_hook_2");
    if (!pSym2)
    {
        if (user_profile_url != NULL)
        {
            fprintf( stderr, "the CollaboraOffice version in '%s' does not support passing a user profile to the hook function\n",
                     imp_lib );
            cok_dlclose( dlhandle );
            free( imp_lib );
            return NULL;
        }
        pSym = (CokHookFunction *) cok_dlsym( dlhandle, "cokit_hook" );
        if (!pSym)
        {
            fprintf( stderr, "failed to find hook in library '%s'\n", imp_lib );
            cok_dlclose( dlhandle );
            free( imp_lib );
            return NULL;
        }
        free( imp_lib );
        // dlhandle is "leaked"
        // coverity[leaked_storage] - on purpose
        return pSym( install_path );
    }

    if (user_profile_url != NULL && user_profile_url[0] == '/')
    {
        // It should be either a file: URL or a vnd.sun.star.pathname: URL.
        fprintf( stderr, "second parameter to cok_init_2 '%s' should be a URL, not a pathname\n", user_profile_url );
        cok_dlclose( dlhandle );
        free( imp_lib );
        return NULL;
    }

    free( imp_lib );
    // dlhandle is "leaked"
    // coverity[leaked_storage] - on purpose
    return pSym2( install_path, user_profile_url );
#else
    return cokit_hook_2( install_path, user_profile_url );
#endif
}

static KIT_TOLERATE_UNUSED
COKit *cok_init( const char *install_path )
{
    return cok_init_2( install_path, NULL );
}

#if !defined(IOS)
static KIT_TOLERATE_UNUSED
int cok_preinit( const char *install_path,  const char *user_profile_url )
{
    void *dlhandle;
    char *imp_lib;
    CokHookPreInit *pSym;

    dlhandle = cok_dlopen(install_path, &imp_lib);
    if (!dlhandle)
        return -1;

    pSym = (CokHookPreInit *) cok_dlsym(dlhandle, "cok_preinit");
    if (!pSym)
    {
        fprintf( stderr, "failed to find pre-init hook in library '%s'\n", imp_lib );
        cok_dlclose( dlhandle );
        free( imp_lib );
        return -1;
    }

    free( imp_lib );

    // dlhandle is "leaked"
    // coverity[leaked_storage] - on purpose
    return pSym( install_path, user_profile_url );
}
#endif

#undef SEPARATOR // It is used at least in enum class MenuItemType

#ifdef __cplusplus
}
#endif

namespace kit
{

/// Create a kit::Office instance.
///
/// Presumably you are not supposed to create multiple kit::Office
/// instances in the same process. Possibly not even a new one after
/// destroying a previous one.
///
/// For information on the parameters, see writeup for cok_init_2 above.
inline Office* kit_cpp_init(const char* pInstallPath, const char* pUserProfileUrl = NULL)
{
    COKit* pThis = cok_init_2(pInstallPath, pUserProfileUrl);
    if (pThis == NULL)
        return NULL;
    return new ::kit::Office(pThis);
}

}

#endif // defined(__linux__) || defined (__FreeBSD__) || defined(_WIN32) || defined(__APPLE__)

#endif // INCLUDED_COKIT_COKIT_INIT_H

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
