/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice 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/.
 *
 * This file incorporates work covered by the following license notice:
 *
 *   Licensed to the Apache Software Foundation (ASF) under one or more
 *   contributor license agreements. See the NOTICE file distributed
 *   with this work for additional information regarding copyright
 *   ownership. The ASF licenses this file to you 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 .
 */

// Th current high-level preprocessor structure is:
//
// if !HAVE_FEATURE_UI
//   => STATIC_SAL_INSTANCE
// else
//   ? !STATIC_SAL_INSTANCE
//   ? UNIX_DESKTOP_DETECT
// endif
//
// ENABLE_HEADLESS just signifies the use of the SVP plugin!

#include <config_features.h>
#include <config_vclplug.h>

#include <cstdio>
#include <desktop/crashreport.hxx>
#include <rtl/bootstrap.hxx>
#include <rtl/process.h>
#include <salinst.hxx>
#include <sal/log.hxx>
#include <svdata.hxx>
#include <vcl/svapp.hxx>

#if HAVE_FEATURE_UI
#if USING_X11
#define UNIX_DESKTOP_DETECT 1
#include <unx/desktops.hxx>
#else
#define UNIX_DESKTOP_DETECT 0
#endif
#endif

#if defined(DISABLE_DYNLOADING) || !HAVE_FEATURE_UI
#define STATIC_SAL_INSTANCE 1
extern "C" SalInstance* create_SalInstance();
#else
#define STATIC_SAL_INSTANCE 0
#include <osl/module.hxx>
#endif

#if defined(iOS)
#include <premac.h>
#include <UIKit/UIKit.h>
#include <postmac.h>

#elif defined(ANDROID)
#include <android/androidinst.hxx>
#endif

#if defined(_WIN32)
#include <o3tl/char16_t2wchar_t.hxx>
#include <salframe.hxx>
#include <Windows.h>
#else
#include <unistd.h>
#endif

#if ENABLE_HEADLESS
#include <headless/svpdata.hxx>
#include <headless/svpinst.hxx>
#endif

namespace {

#if ENABLE_HEADLESS
SalInstance* svp_create_SalInstance()
{
    SvpSalInstance* pInstance = new SvpSalInstance(std::make_unique<SvpSalYieldMutex>());
    new SvpSalData();
    return pInstance;
}
#endif

#if HAVE_FEATURE_UI

#if !STATIC_SAL_INSTANCE
oslModule pCloseModule = nullptr;

extern "C" typedef SalInstance* (*salFactoryProc)();

SalInstance* tryInstance( const OUString& rModuleBase, bool bForce = false )
{
#if ENABLE_HEADLESS
    if (rModuleBase == "svp")
        return svp_create_SalInstance();
#endif

    SalInstance* pInst = nullptr;
    OUString aUsedModuleBase(rModuleBase);
    if (aUsedModuleBase == "kde5")
        aUsedModuleBase = "kf5";
    OUString aModule(
#ifdef SAL_DLLPREFIX
            SAL_DLLPREFIX
#endif
            "vclplug_" + aUsedModuleBase + "lo" SAL_DLLEXTENSION );

    osl::Module aMod;
    if (aMod.loadRelative(reinterpret_cast<oslGenericFunction>(&tryInstance), aModule, SAL_LOADMODULE_GLOBAL))
    {
        salFactoryProc aProc = reinterpret_cast<salFactoryProc>(aMod.getFunctionSymbol("create_SalInstance"));
        if (aProc)
        {
            pInst = aProc();
            SAL_INFO(
                "vcl.plugadapt",
                "sal plugin " << aModule << " produced instance " << pInst);
            if (pInst)
            {
                pCloseModule = static_cast<oslModule>(aMod);
                aMod.release();

                /*
                 * Recent GTK+ versions load their modules with RTLD_LOCAL, so we can
                 * not access the 'gnome_accessibility_module_shutdown' anymore.
                 * So make sure libgtk+ & co are still mapped into memory when
                 * atk-bridge's atexit handler gets called.
                 */
                if (aUsedModuleBase == "gtk4" || aUsedModuleBase == "gtk3" ||
                    aUsedModuleBase == "gtk3_kde5" || aUsedModuleBase == "kf5" ||
                    aUsedModuleBase == "kf6" ||
                    aUsedModuleBase == "qt5" || aUsedModuleBase == "qt6" ||
                    aUsedModuleBase == "win")
                {
                    pCloseModule = nullptr;
                }
            }
        }
        else
        {
            SAL_WARN(
                "vcl.plugadapt",
                "could not load symbol create_SalInstance from shared object "
                    << aModule);
        }
    }
    else if (bForce)
    {
        SAL_WARN("vcl.plugadapt", "could not load shared object " << aModule);
    }
    else
    {
        SAL_INFO("vcl.plugadapt", "could not load shared object " << aModule);
    }

    // coverity[leaked_storage] - this is on purpose
    return pInst;
}
#endif // !STATIC_SAL_INSTANCE

#if UNIX_DESKTOP_DETECT

#if !STATIC_SAL_INSTANCE
const char* const* autodetect_plugin_list()
{
    static const char* const pKDEFallbackList[] =
    {
#if ENABLE_KF5
        "kf5",
#endif
#if ENABLE_GTK3_KDE5
        "gtk3_kde5",
#endif
#if ENABLE_GTK3
        "gtk3",
#endif
#if ENABLE_GEN
        "gen",
#endif
        nullptr
    };

    static const char* const pPlasma6FallbackList[] =
    {
#if ENABLE_KF6
        "kf6",
#endif
#if ENABLE_KF5
        "kf5",
#endif
#if ENABLE_GTK3_KDE5
        "gtk3_kde5",
#endif
#if ENABLE_GTK3
        "gtk3",
#endif
#if ENABLE_GEN
        "gen",
#endif
        nullptr
    };

    static const char* const pStandardFallbackList[] =
    {
#if ENABLE_GTK3
        "gtk3",
#endif
#if ENABLE_GEN
        "gen",
#endif
        nullptr
    };

#if ENABLE_HEADLESS
    static const char* const pHeadlessFallbackList[] =
    {
        "svp",
        nullptr
    };
#endif

    DesktopType desktop = get_desktop_environment();
    const char * const * pList = pStandardFallbackList;

#if ENABLE_HEADLESS
    // no server at all: dummy plugin
    if ( desktop == DESKTOP_NONE )
        pList = pHeadlessFallbackList;
    else
#endif
        if ( desktop == DESKTOP_GNOME ||
              desktop == DESKTOP_UNITY ||
              desktop == DESKTOP_XFCE  ||
              desktop == DESKTOP_MATE )
        pList = pStandardFallbackList;
    else if (desktop == DESKTOP_PLASMA5 || desktop == DESKTOP_LXQT)
        pList = pKDEFallbackList;
    else if (desktop == DESKTOP_PLASMA6)
        pList = pPlasma6FallbackList;

    return pList;
}
#endif // !STATIC_SAL_INSTANCE
#endif // UNIX_DESKTOP_DETECT

#endif // HAVE_FEATURE_UI

// HACK to obtain Application::IsHeadlessModeEnabled early on, before
// Application::EnableHeadlessMode has potentially been called:
bool IsHeadlessModeRequested()
{
    if (Application::IsHeadlessModeEnabled()) {
        return true;
    }
    sal_uInt32 n = rtl_getAppCommandArgCount();
    for (sal_uInt32 i = 0; i < n; ++i) {
        OUString arg;
        rtl_getAppCommandArg(i, &arg.pData);
        if ( arg == "--headless" || arg == "-headless" ) {
            return true;
        }
    }
    return false;
}

} // anonymous namespace

SalInstance *CreateSalInstance()
{
    OUString aUsePlugin;
    rtl::Bootstrap::get(u"SAL_USE_VCLPLUGIN"_ustr, aUsePlugin);
    SAL_INFO_IF(!aUsePlugin.isEmpty(), "vcl.plugadapt", "Requested VCL plugin: " << aUsePlugin);

    if (Application::IsBitmapRendering() || (aUsePlugin.isEmpty() && IsHeadlessModeRequested()))
        aUsePlugin = "svp";

    if (aUsePlugin == "svp")
    {
        Application::EnableBitmapRendering();
#if ENABLE_HEADLESS
        return svp_create_SalInstance();
#else
        aUsePlugin.clear();
#endif
    }

#if STATIC_SAL_INSTANCE
    return create_SalInstance();

#else // !STATIC_SAL_INSTANCE
    SalInstance *pInst = nullptr;

    if( !aUsePlugin.isEmpty() )
        pInst = tryInstance( aUsePlugin, true );

#if UNIX_DESKTOP_DETECT
    const char* const* pPluginList = pInst ? nullptr : autodetect_plugin_list();
    for (int i = 0; !pInst && pPluginList[i]; ++i)
    {
        pInst = tryInstance(OUString::createFromAscii(pPluginList[i]));
        SAL_INFO_IF(pInst, "vcl.plugadapt", "plugin autodetection: " << pPluginList[i]);
    }
#endif

    // fallback, try everything
    static const char* const pPlugin[] = {
#ifdef _WIN32
        "win",
#elif defined(MACOSX)
        "osx",
#else // !_WIN32 && !MACOSX
#if ENABLE_GTK3
        "gtk3",
#endif
#if ENABLE_KF5
        "kf5",
#endif
#if ENABLE_GTK3_KDE5
        "gtk3_kde5",
#endif
#if ENABLE_GEN
        "gen",
#endif
#if ENABLE_QT5
        "qt5",
#endif
#if ENABLE_KF6
        "kf6",
#endif
#if ENABLE_QT6
        "qt6",
#endif
#endif // !_WIN32 && !MACOSX
        nullptr
    };

    for (int i = 0; !pInst && pPlugin[i]; ++i)
        pInst = tryInstance( OUString::createFromAscii( pPlugin[ i ] ) );

    if( ! pInst )
    {
        std::fprintf( stderr, "no suitable windowing system found, exiting.\n" );
        _exit( 1 );
    }

    return pInst;
#endif // !STATIC_SAL_INSTANCE
}

void DestroySalInstance( SalInstance *pInst )
{
    delete pInst;
#if !STATIC_SAL_INSTANCE
    if( pCloseModule )
        osl_unloadModule( pCloseModule );
#endif
}

void SalAbort( const OUString& rErrorText, bool bDumpCore )
{
    if (GetSalInstance())
        GetSalInstance()->BeforeAbort(rErrorText, bDumpCore);

#if defined _WIN32
    if( rErrorText.isEmpty() )
    {
        // make sure crash reporter is triggered
        RaiseException( 0, EXCEPTION_NONCONTINUABLE, 0, nullptr );
        FatalAppExitW( 0, L"Application Error" );
    }
    else
    {
        CrashReporter::addKeyValue("AbortMessage", rErrorText, CrashReporter::Write);
        // make sure crash reporter is triggered
        RaiseException( 0, EXCEPTION_NONCONTINUABLE, 0, nullptr );
        FatalAppExitW( 0, o3tl::toW(rErrorText.getStr()) );
    }
#else // !_WIN32
#if defined ANDROID
    OUString aError(rErrorText.isEmpty() ? "Unspecified application error" : rErrorText);
    LOGE("SalAbort: '%s'", OUStringToOString(aError, osl_getThreadTextEncoding()).getStr());
#elif defined(iOS)
    NSLog(@"SalAbort: %s", OUStringToOString(rErrorText, osl_getThreadTextEncoding()).getStr());
#else
    if( rErrorText.isEmpty() )
        std::fprintf( stderr, "Unspecified Application Error\n" );
    else
    {
        CrashReporter::addKeyValue(u"AbortMessage"_ustr, rErrorText, CrashReporter::Write);
        std::fprintf( stderr, "%s\n", OUStringToOString(rErrorText, osl_getThreadTextEncoding()).getStr() );
    }
#endif
    if( bDumpCore )
        abort();
    else
        _exit(1);
#endif // !_WIN32
}

const OUString& SalGetDesktopEnvironment()
{
#if !HAVE_FEATURE_UI
    static OUString aDesktopEnvironment("headless");
#elif defined(_WIN32)
    static OUString aDesktopEnvironment( "Windows" );
#elif defined(MACOSX)
    static OUString aDesktopEnvironment( "MacOSX" );
#elif defined(EMSCRIPTEN)
    static OUString aDesktopEnvironment("WASM");
#elif defined(ANDROID)
    static OUString aDesktopEnvironment("android");
#elif defined(iOS)
    static OUString aDesktopEnvironment("iOS");
#elif UNIX_DESKTOP_DETECT
    // Order to match desktops.hxx' DesktopType
    static constexpr OUString desktop_strings[] = {
        u"none"_ustr, u"unknown"_ustr, u"GNOME"_ustr, u"UNITY"_ustr,
        u"XFCE"_ustr, u"MATE"_ustr, u"PLASMA5"_ustr, u"PLASMA6"_ustr, u"LXQT"_ustr };
    static OUString aDesktopEnvironment;
    if( aDesktopEnvironment.isEmpty())
    {
        aDesktopEnvironment = desktop_strings[get_desktop_environment()];
    }
#else
    static OUString aDesktopEnvironment("unknown");
#endif
    return aDesktopEnvironment;
}

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