// yabridge: a Wine plugin bridge
// Copyright (C) 2020-2024 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program 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.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

#pragma once

#include <thread>

#include <asio/local/stream_protocol.hpp>
#include <asio/posix/stream_descriptor.hpp>
#include <asio/streambuf.hpp>
#include <ghc/filesystem.hpp>

#include "../common/communication/common.h"
#include "../common/logging/common.h"
#include "../common/plugins.h"
#include "../common/serialization/common.h"
#include "utils.h"

/**
 * Encapsulates the behavior of launching a host process or connecting to an
 * existing one. This is needed because plugins groups require slightly
 * different handling. All derived classes are set up to pipe their STDOUT and
 * STDERR streams to the provided IO context instance.
 */
class HostProcess {
   public:
    virtual ~HostProcess() noexcept = default;

    /**
     * Return the full path to the host application in use. The host application
     * is chosen depending on the architecture of the plugin's `.dll` file and
     * on the hosting mode.
     */
    virtual ghc::filesystem::path path() = 0;

    /**
     * Return true if the host process is still running. Used during startup to
     * abort connecting to sockets if the Wine process has crashed.
     */
    virtual bool running() = 0;

    /**
     * Kill the process or cause the plugin that's being hosted to exit.
     */
    virtual void terminate() = 0;

   protected:
    /**
     * The actual process initialization and everything involved in that process
     * is done in `launch_host()` since a new process may not be required
     * when using plugin groups.
     *
     * @param io_context The IO context that the STDIO redirection pipes will be
     *   bound to. The logging for these pipes is set up in `launch_host()`.
     * @param sockets The socket endpoints that will be used for communication
     *   with the plugin. When the plugin shuts down, we'll close all of the
     *   sockets used by the plugin.
     */
    HostProcess(asio::io_context& io_context, Sockets& sockets);

    /**
     * Helper function that launches the Wine host application (`*.exe`) with
     * all of the correct environment setup. This includes setting the correct
     * environment variables for the Wine prefix the plugin is in, setting up
     * pipes or files for STDIO redirection, closing file descriptors to prevent
     * leaks, and wrapping all of that in a terminal process running winedbg if
     * we're compiling with `-Dwinedbg=true`. Keep in mind that winedbg does not
     * handle arguments containing spaces, so most Windows paths will be split
     * up into multiple arguments.
     *
     * @param logger The `Logger` instance the redirected STDIO streams will be
     *   written to.
     * @param config The plugin's configuration, used to determine whether to
     *   use pipes or to redirect the output to a file instead.
     * @param plugin_info Information about the plugin, used to determine the
     *   plugin's Wine prefix.
     */
    Process::Handle launch_host(const ghc::filesystem::path& host_path,
                                std::initializer_list<std::string> args,
                                Logger& logger,
                                const Configuration& config,
                                const PluginInfo& plugin_info);

    /**
     * The associated sockets for the plugin we're hosting. This is used to
     * terminate the plugin.
     */
    Sockets& sockets_;

   private:
    /**
     * The STDOUT stream of the Wine process we can forward to the logger.
     */
    asio::posix::stream_descriptor stdout_pipe_;
    /**
     * The STDERR stream of the Wine process we can forward to the logger.
     */
    asio::posix::stream_descriptor stderr_pipe_;

    asio::streambuf stdout_buffer_;
    asio::streambuf stderr_buffer_;
};

/**
 * Launch a group host process for hosting a single plugin.
 */
class IndividualHost : public HostProcess {
   public:
    /**
     * Start a host process that loads the plugin and connects back to this
     * yabridge instance over the specified socket.
     *
     * @param io_context The IO context that the STDIO redurection will be
     *   handled on.
     * @param logger The `Logger` instance the redirected STDIO streams will be
     *   written to.
     * @param config The configuration for this plugin instance.
     * @param sockets The socket endpoints that will be used for communication
     *   with the plugin. When the plugin shuts down, we'll close all of the
     *   sockets used by the plugin.
     * @param plugin_info Information about the plugin we're going to use. Used
     *   to retrieve the Wine prefix and the plugin's architecture.
     * @param host_request The information about the plugin we should launch a
     *   host process for. The values in the struct will be used as command line
     *   arguments.
     *
     * @throw std::runtime_error When `plugin_path` does not point to a valid
     *   32-bit or 64-bit .dll file.
     */
    IndividualHost(asio::io_context& io_context,
                   Logger& logger,
                   const Configuration& config,
                   Sockets& sockets,
                   const PluginInfo& plugin_info,
                   const HostRequest& host_request);

    ghc::filesystem::path path() override;
    bool running() override;
    void terminate() override;

   private:
    const PluginInfo& plugin_info_;
    ghc::filesystem::path host_path_;
    Process::Handle handle_;
};

/**
 * Either launch a new group host process, or connect to an existing one. This
 * will first try to connect to the plugin group's socket (determined based on
 * group name, Wine prefix and architecture). If that fails, it will launch a
 * new, detached group host process. This will likely outlive this plugin
 * instance if multiple instances of yabridge using the same plugin group are in
 * use. In the event that two yabridge instances are initialized at the same
 * time and both instances spawn their own group host process, then the later
 * one will simply terminate gracefully after it fails to listen on the socket.
 */
class GroupHost : public HostProcess {
   public:
    /**
     * Start a new group host process or connect to an existing one. The actual
     * host request is deferred until the process has actually started using a
     * thread.
     *
     * @param io_context The IO context that the STDIO redurection will be
     *   handled on.
     * @param logger The `Logger` instance the redirected STDIO streams will be
     *   written to.
     * @param config The configuration for this plugin instance. The group name
     *   will be retrieved from here.
     * @param sockets The socket endpoints that will be used for communication
     *   with the plugin. When the plugin shuts down, we'll close all of the
     *   sockets used by the plugin.
     * @param plugin_info Information about the plugin we're going to use. Used
     *   to retrieve the Wine prefix and the plugin's architecture.
     * @param host_request The information about the plugin we should launch a
     *   host process for. This object will be sent to the group host process.
     */
    GroupHost(asio::io_context& io_context,
              Logger& logger,
              const Configuration& config,
              Sockets& sockets,
              const PluginInfo& plugin_info,
              const HostRequest& host_request);

    ghc::filesystem::path path() override;
    bool running() noexcept override;
    void terminate() override;

   private:
    const PluginInfo& plugin_info_;
    ghc::filesystem::path host_path_;

    /**
     * We want to either connect to an existing group host process, or spawn a
     * new one. This can result in some interesting scenarios when multiple
     * plugins within the same plugin host get initialized at once (e.g. when
     * loading a project). On startup we'll go through the following sequence:
     *
     * 1. Try to connect to an existing group host process.
     * 2. Spawn a new group host process and connect to it. When multiple
     *    plugins launch at the same time the first to start listening on the
     *    socket wins and the other processes will shut down gracefully.
     * 3.  When the group host process exits, try to connect again (potentially
     *     to a group host process spawned by another instance).
     *
     * When this last step also fails, then we'll say that startup has failed
     * and we will terminate the plugin initialization process.
     */
    std::atomic_bool startup_failed_;

    /**
     * A thread that waits for the group host to have started and then ask it to
     * host our plugin. This is used to defer the request since it may take a
     * little while until the group host process is up and running. This way we
     * don't have to delay the rest of the initialization process.
     *
     * TODO: Replace the polling with inotify to prevent delays and to reduce
     *       wasting resources
     */
    std::jthread group_host_connect_handler_;
};
