/* -*- 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 .
 */

#include "loader.hxx"
#include <algorithm>
#include <cassert>
#include <numeric>
#include <stdlib.h>
#include <string>
#include <string_view>
#include <vector>
#include <desktop/exithelper.h>
#include <systools/win32/extended_max_path.hxx>
#include <systools/win32/uwinapi.h>
#include <tools/pathutils.hxx>

#include <fstream>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/ini_parser.hpp>

namespace {

void fail()
{
    LPWSTR buf = nullptr;
    FormatMessageW(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr,
        GetLastError(), 0, reinterpret_cast< LPWSTR >(&buf), 0, nullptr);
    MessageBoxW(nullptr, buf, nullptr, MB_OK | MB_ICONERROR);
    HeapFree(GetProcessHeap(), 0, buf);
    TerminateProcess(GetCurrentProcess(), 255);
}

struct CommandArgs
{
    LPWSTR* argv;
    int argc;
    CommandArgs() { argv = CommandLineToArgvW(GetCommandLineW(), &argc); }
    ~CommandArgs() { LocalFree(argv); }
    auto begin() const { return argv; }
    auto end() const { return begin() + argc; }
};

// tdf#120249: quotes in arguments need to be escaped; backslashes before quotes need doubling. See
// https://docs.microsoft.com/en-us/windows/desktop/api/shellapi/nf-shellapi-commandlinetoargvw
std::wstring EscapeArg(std::wstring_view sArg)
{
    std::wstring sResult(L"\"");
    for (size_t lastPosQuote = 0; lastPosQuote <= sArg.size();)
    {
        const size_t posQuote = std::min(sArg.find(L'"', lastPosQuote), sArg.size());
        size_t posBackslash = posQuote;
        while (posBackslash != lastPosQuote && sArg[posBackslash - 1] == L'\\')
            --posBackslash;
        // 2n+1 '\' to escape internal '"'; 2n '\' before closing '"'
        const size_t nEscapes = (posQuote - posBackslash) * 2 + (posQuote < sArg.size() ? 1 : 0);

        sResult.append(sArg.begin() + lastPosQuote, sArg.begin() + posBackslash);
        sResult.append(nEscapes, L'\\');
        sResult.append(1, L'"');
        lastPosQuote = posQuote + 1;
    }
    return sResult;
}

std::wstring getCWDarg()
{
    std::wstring s(L" \"-env:OOO_CWD=");

    DWORD cwdLen = GetCurrentDirectoryW(0, nullptr);
    std::vector<WCHAR> cwd(cwdLen);
    cwdLen = GetCurrentDirectoryW(cwdLen, cwd.data());
    if (cwdLen == 0 || cwdLen >= cwd.size())
    {
        s += L'0';
    }
    else
    {
        s += L'2';

        size_t n = 0; // number of trailing backslashes
        for (auto* p = cwd.data(); *p; ++p)
        {
            WCHAR c = *p;
            if (c == L'$')
            {
                s += L"\\$";
                n = 0;
            }
            else if (c == L'\\')
            {
                s += L"\\\\";
                n += 2;
            }
            else
            {
                s += c;
                n = 0;
            }
        }
        // The command line will continue with a double quote, so double any
        // preceding backslashes as required by Windows:
        s.append(n, L'\\');
    }
    s += L'"';
    return s;
}

WCHAR* commandLineAppend(WCHAR* buffer, std::wstring_view text)
{
    auto ret = std::copy_n(text.begin(), text.size(), buffer);
    *ret = 0; // trailing null
    return ret;
}

// Set the PATH environment variable in the current (loader) process, so that a
// following CreateProcess has the necessary environment:
// returns a pair of strings { binPath, iniDirectory }
// * binPath is the full path to the "bin" file corresponding to the current executable.
// * iniDirectory is the full directory path (ending in "\") to the "ini" file corresponding to the
// current executable.
[[nodiscard]] std::pair<std::wstring, std::wstring> extendLoaderEnvironment()
{
    std::vector<wchar_t> executable_path(EXTENDED_MAX_PATH);
    DWORD exe_len;
    for (;;)
    {
        exe_len = GetModuleFileNameW(nullptr, executable_path.data(), executable_path.size());
        if (!exe_len)
            fail();
        if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
        {
            executable_path.resize(exe_len + 4); // to accommodate a possible ".bin" in the end
            break;
        }
        executable_path.resize(executable_path.size() * 2);
    }
    WCHAR* iniDirEnd = tools::filename(executable_path.data());
    std::wstring_view iniDirView(executable_path.data(), iniDirEnd);
    WCHAR* nameEnd = executable_path.data() + exe_len;
    if (!(nameEnd - iniDirEnd >= 4 && nameEnd[-4] == L'.' &&
         (((nameEnd[-3] == L'E' || nameEnd[-3] == L'e') &&
           (nameEnd[-2] == L'X' || nameEnd[-2] == L'x') &&
           (nameEnd[-1] == L'E' || nameEnd[-1] == L'e')) ||
          ((nameEnd[-3] == L'C' || nameEnd[-3] == L'c') &&
           (nameEnd[-2] == L'O' || nameEnd[-2] == L'o') &&
           (nameEnd[-1] == L'M' || nameEnd[-1] == L'm')))))
    {
        *nameEnd = L'.';
        nameEnd += 4;
    }
    nameEnd[-3] = 'b';
    nameEnd[-2] = 'i';
    nameEnd[-1] = 'n';
    std::wstring_view nameView(iniDirEnd, nameEnd);

    WCHAR env[32767];
    DWORD n = GetEnvironmentVariableW(L"PATH", env, std::size(env));
    if ((n >= std::size(env) || n == 0) && GetLastError() != ERROR_ENVVAR_NOT_FOUND) {
        fail();
    }
    std::wstring_view envView(env, n);
    // must be first in PATH to override other entries
    assert(iniDirView.back() == L'\\'); // hence -1 below
    std::wstring_view iniDirView1(iniDirView.substr(0, iniDirView.size() - 1));
    if (!envView.starts_with(iniDirView1) || env[iniDirView1.size()] != L';')
    {
        std::wstring pad(iniDirView1);
        if (n != 0) {
            pad += L';';
            pad += envView;
        }
        if (!SetEnvironmentVariableW(L"PATH", pad.data())) {
            fail();
        }
    }

    return { tools::buildPath(iniDirView, nameView), std::wstring(iniDirView) };
}

}

namespace desktop_win32 {

int officeloader_impl(bool bAllowConsole)
{
    const auto& [szTargetFileName, szIniDirectory] = extendLoaderEnvironment();

    STARTUPINFOW aStartupInfo;
    ZeroMemory(&aStartupInfo, sizeof(aStartupInfo));
    aStartupInfo.cb = sizeof(aStartupInfo);

    // Create process with same command line, environment and stdio handles which
    // are directed to the created pipes
    GetStartupInfoW(&aStartupInfo);

    DWORD dwExitCode = DWORD(-1);

    bool fSuccess = false;
    bool bFirst = true;

    // read limit values from fundamental.override.ini
    unsigned int nMaxMemoryInMB = 0;
    bool bExcludeChildProcesses = true;
    bool fallbackForMaxMemoryInMB = true;
    bool fallbackForExcludeChildProcesses = true;

    try
    {
        boost::property_tree::ptree pt;
        std::ifstream aFile(szIniDirectory + L"\\fundamental.override.ini");
        boost::property_tree::ini_parser::read_ini(aFile, pt);
        nMaxMemoryInMB = pt.get("Bootstrap.LimitMaximumMemoryInMB", nMaxMemoryInMB);
        fallbackForMaxMemoryInMB = !pt.get_child_optional("Bootstrap.LimitMaximumMemoryInMB");
        bExcludeChildProcesses = pt.get("Bootstrap.ExcludeChildProcessesFromLimit", bExcludeChildProcesses);
        fallbackForExcludeChildProcesses
            = !pt.get_child_optional("Bootstrap.ExcludeChildProcessesFromLimit");
    }
    catch (...)
    {
        nMaxMemoryInMB = 0;
    }
    // For backwards compatibility, for now also try to read the values from bootstrap.ini if
    // fundamental.override.ini does not provide them:
    if (fallbackForMaxMemoryInMB || fallbackForExcludeChildProcesses) {
        try
        {
            boost::property_tree::ptree pt;
            std::ifstream aFile(szIniDirectory + L"\\bootstrap.ini");
            boost::property_tree::ini_parser::read_ini(aFile, pt);
            if (fallbackForMaxMemoryInMB) {
                nMaxMemoryInMB = pt.get("Win32.LimitMaximumMemoryInMB", nMaxMemoryInMB);
            }
            if (fallbackForExcludeChildProcesses) {
                bExcludeChildProcesses = pt.get("Win32.ExcludeChildProcessesFromLimit", bExcludeChildProcesses);
            }
        }
        catch (...)
        {
        }
    }

    // create a Windows JobObject with a memory limit
    HANDLE hJobObject = nullptr;
    if (nMaxMemoryInMB > 0)
    {
        JOBOBJECT_EXTENDED_LIMIT_INFORMATION aJobLimit;
        aJobLimit.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_JOB_MEMORY;
        if (bExcludeChildProcesses)
            aJobLimit.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK;
        aJobLimit.JobMemoryLimit = nMaxMemoryInMB * 1024 * 1024;
        hJobObject = CreateJobObjectW(nullptr, nullptr);
        if (hJobObject != nullptr)
            SetInformationJobObject(hJobObject, JobObjectExtendedLimitInformation, &aJobLimit,
                                    sizeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
    }

    std::vector<std::wstring> aEscapedArgs;
    bool bHeadlessMode = false;
    for (std::wstring_view arg : CommandArgs())
    {
        // Check command line arguments for "--headless" parameter. We only set the environment
        // variable "ATTACHED_PARENT_PROCESSID" for the headless mode as self-destruction of the
        // soffice.bin process can lead to certain side-effects (log-off can result in data-loss,
        // ".lock" is not deleted). See 138244 for more information.
        if (arg == L"-headless" || arg == L"--headless")
            bHeadlessMode = true;
        // check for wildcards in arguments - Windows does not expand automatically
        else if (arg.find_first_of(L"*?") != std::wstring_view::npos)
        {
            WIN32_FIND_DATAW aFindData;
            HANDLE h = FindFirstFileW(arg.data(), &aFindData);
            if (h != INVALID_HANDLE_VALUE)
            {
                const int nPathSize = 32 * 1024;
                wchar_t drive[3];
                wchar_t dir[nPathSize];
                wchar_t path[nPathSize];
                _wsplitpath_s(arg.data(), drive, std::size(drive), dir, std::size(dir), nullptr, 0,
                              nullptr, 0);
                do
                {
                    _wmakepath_s(path, std::size(path), drive, dir, aFindData.cFileName, nullptr);
                    aEscapedArgs.push_back(EscapeArg(path));
                } while (FindNextFileW(h, &aFindData));
                FindClose(h);
                continue;
            }
        }

        aEscapedArgs.push_back(EscapeArg(arg));
    }
    size_t n = std::accumulate(aEscapedArgs.begin(), aEscapedArgs.end(), aEscapedArgs.size(),
                               [](size_t a, const std::wstring& s) { return a + s.size(); });
    std::wstring sCWDarg = getCWDarg();
    n += sCWDarg.size() + 1;
    LPWSTR lpCommandLine = new WCHAR[n];

    if (bHeadlessMode)
    {
        WCHAR szParentProcessId[64]; // This is more than large enough for a 128 bit decimal value
        if (_ltow(static_cast<long>(GetCurrentProcessId()), szParentProcessId, 10))
            SetEnvironmentVariableW(L"ATTACHED_PARENT_PROCESSID", szParentProcessId);
    }

    do
    {
        WCHAR* p = commandLineAppend(lpCommandLine, aEscapedArgs[0]);
        for (size_t i = 1; i < aEscapedArgs.size(); ++i)
        {
            const std::wstring& rArg = aEscapedArgs[i];
            if (bFirst || EXITHELPER_NORMAL_RESTART == dwExitCode || rArg.starts_with(L"\"-env:"))
            {
                p = commandLineAppend(p, L" ");
                p = commandLineAppend(p, rArg);
            }
        }

        commandLineAppend(p, sCWDarg);
        bFirst = false;

        PROCESS_INFORMATION aProcessInfo;

        fSuccess = CreateProcessW(szTargetFileName.data(), lpCommandLine, nullptr, nullptr, TRUE,
                                  bAllowConsole ? 0 : DETACHED_PROCESS, nullptr,
                                  szIniDirectory.data(), &aStartupInfo, &aProcessInfo);

        if (fSuccess)
        {
            DWORD dwWaitResult;

            if (hJobObject)
                AssignProcessToJobObject(hJobObject, aProcessInfo.hProcess);

            do
            {
                // On Windows XP it seems as the desktop calls WaitForInputIdle after "OpenWith" so
                // we have to do so as if we were processing any messages

                dwWaitResult = MsgWaitForMultipleObjects(1, &aProcessInfo.hProcess, FALSE, INFINITE,
                                                         QS_ALLEVENTS);

                if (WAIT_OBJECT_0 + 1 == dwWaitResult)
                {
                    MSG msg;

                    PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE);
                }
            } while (WAIT_OBJECT_0 + 1 == dwWaitResult);

            dwExitCode = 0;
            GetExitCodeProcess(aProcessInfo.hProcess, &dwExitCode);

            CloseHandle(aProcessInfo.hProcess);
            CloseHandle(aProcessInfo.hThread);
        }
    } while (fSuccess
             && (EXITHELPER_CRASH_WITH_RESTART == dwExitCode
                 || EXITHELPER_NORMAL_RESTART == dwExitCode));

    if (hJobObject)
        CloseHandle(hJobObject);

    delete[] lpCommandLine;

    return fSuccess ? dwExitCode : -1;
}

int unopkgloader_impl(bool bAllowConsole)
{
    const auto& [szTargetFileName, szIniDirectory] = extendLoaderEnvironment();

    STARTUPINFOW aStartupInfo{};
    aStartupInfo.cb = sizeof(aStartupInfo);
    GetStartupInfoW(&aStartupInfo);

    DWORD   dwExitCode = DWORD(-1);

    std::wstring sCWDarg = getCWDarg();
    DWORD dummy;
    std::wstring redirect = tools::buildPath(szIniDirectory, L"redirect.ini");
    bool hasRedirect = !redirect.empty() &&
            (GetBinaryTypeW(redirect.data(), &dummy) || // cheaper check for file existence?
                GetLastError() != ERROR_FILE_NOT_FOUND);
    LPWSTR cl1 = GetCommandLineW();
    std::wstring cl2 = cl1;
    if (hasRedirect)
        cl2 += L" \"-env:INIFILENAME=vnd.sun.star.pathname:" + redirect + L"\"";
    cl2 += sCWDarg;

    PROCESS_INFORMATION aProcessInfo;

    bool fSuccess = CreateProcessW(
        szTargetFileName.data(),
        cl2.data(),
        nullptr,
        nullptr,
        TRUE,
        bAllowConsole ? 0 : DETACHED_PROCESS,
        nullptr,
        szIniDirectory.data(),
        &aStartupInfo,
        &aProcessInfo);

    if (fSuccess)
    {
        DWORD   dwWaitResult;

        do
        {
            // On Windows XP it seems as the desktop calls WaitForInputIdle after "OpenWidth" so we have to do so
            // as if we were processing any messages

            dwWaitResult = MsgWaitForMultipleObjects(1, &aProcessInfo.hProcess, FALSE, INFINITE, QS_ALLEVENTS);

            if (WAIT_OBJECT_0 + 1 == dwWaitResult)
            {
                MSG msg;

                PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE);
            }
        } while (WAIT_OBJECT_0 + 1 == dwWaitResult);

        dwExitCode = 0;
        GetExitCodeProcess(aProcessInfo.hProcess, &dwExitCode);

        CloseHandle(aProcessInfo.hProcess);
        CloseHandle(aProcessInfo.hThread);
    }

    return dwExitCode;
}

}

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