// 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 <variant>

#include "../common/configuration.h"
#include "../common/logging/common.h"
#include "../common/plugins.h"
#include "../common/process.h"
#include "../common/utils.h"

/**
 * Marker struct for when we use the default Wine prefix.
 */
struct DefaultWinePrefix {};

/**
 * Marker struct for when the Wine prefix is overriden using the `WINEPREFIX`
 * environment variable.
 */
struct OverridenWinePrefix {
    ghc::filesystem::path value;
};

/**
 * This will locate the plugin we're going to host based on the location of the
 * `.so` that we're currently operating from and provides information and
 * utility functions based on that.
 */
struct PluginInfo {
   public:
    /**
     * Locate the Windows plugin based on the location of this copy of
     * `libyabridge-{clap,vst2,vst3}.so` file and the type of the plugin we're
     * going to load. For VST2 plugins this is a file with the same name but
     * with a
     * `.dll` file extension instead of `.so`. In case this file does not exist
     * and the `.so` file is a symlink, we'll also repeat this check for the
     * file it links to. This is to support the workflow described in issue #3
     * where you use symlinks to copies of `libyabridge-vst2.so`.
     *
     * For VST3 plugins there is a strict format as defined by Steinberg, and
     * we'll have yabridgectl create a 'merged bundle' that also contains the
     * Windows VST3 plugin.
     *
     * @param plugin_type The type of the plugin we're going to load. The
     *   detection works slightly differently depending on the plugin type.
     * @param plugin_path The path to the **native** plugin library `.so` file.
     *   This is used to determine the path to the Windows plugin library we
     *   should load.
     * @param prefer_32bit_vst3 If there's both a 64-bit and a 32-bit Windows
     *   VST3 module in the same bundle, then setting this to true will cause
     *   the 32-bit version to be used instead of the 64-bit version.
     *
     * @throw std::runtime_error If we cannot find a corresponding Windows
     *   plugin. The error message contains a human readable description of what
     *   went wrong.
     */
    PluginInfo(PluginType plugin_type,
               const ghc::filesystem::path& plugin_path,
               bool prefer_32bit_vst3 = false);

    /**
     * Create the environment for the plugin host based on `wine_prefix_`. If
     * `WINEPREFIX` was already set then nothing will be changed. Otherwise
     * we'll set `WINEPREFIX` to the detected Wine prefix, or it will be left
     * unset if we could not detect a prefix.
     *
     * This also unsets `WAYLAND_DISPLAY` so if Wine has been compiled with
     * Wayland support, it won't suddenly start using that over X11.
     */
    ProcessEnvironment create_host_env() const;

    /**
     * Return the path to the actual Wine prefix in use, taking into account
     * `WINEPREFIX` overrides and the default `~/.wine` fallback.
     */
    ghc::filesystem::path normalize_wine_prefix() const;

    /**
     * Return the installed Wine version. This is obtained by from `wine
     * --version` and then stripping the `wine-` prefix. This respects the
     * `WINELOADER` environment variable used in the scripts generated by
     * winegcc. The script will be run with under the same environment as the
     * Wine plugin host will be run at, since the user may use a custom
     * `WINELOADER` to change use different Wine binaries for each prefix.
     *
     * This will *not* throw when Wine can not be found, but will instead return
     * '<NOT FOUND>'. This way the user will still get some useful log files.
     */
    std::string wine_version() const;

    const PluginType plugin_type_;

    /**
     * The path to our `.so` file. For VST3 plugins this is *not* the VST3
     * module (since that has to be bundle on Linux) but rather the .so file
     * contained in that bundle.
     */
    const ghc::filesystem::path native_library_path_;

   private:
    /**
     * The path to the Windows library (`.dll` or `.vst3`, not to be confused
     * with a `.vst3` bundle) that we're targeting. This should **not** be
     * passed to the plugin host and `windows_plugin_path_` should be used
     * instead. We store this intermediate value so we can determine the
     * plugin's architecture.
     */
    const ghc::filesystem::path windows_library_path_;

   public:
    const LibArchitecture plugin_arch_;

    /**
     * The path to the plugin (`.dll` or module) we're going to in the Wine
     * plugin host.
     *
     * For VST2 plugins this will be a `.dll` file. For VST3 plugins this is
     * normally a directory called `MyPlugin.vst3` that contains
     * `MyPlugin.vst3/Contents/x86-win/MyPlugin.vst3`, but there's also an older
     * deprecated (but still ubiquitous) format where the top level
     * `MyPlugin.vst3` is not a directory but a .dll file. This points to either
     * of those things, and then `VST3::Hosting::Win32Module::create()` will be
     * able to load it.
     *
     * https://developer.steinberg.help/pages/viewpage.action?pageId=9798275
     */
    const ghc::filesystem::path windows_plugin_path_;

    /**
     * The Wine prefix to use for hosting `windows_plugin_path_`. If the
     * `WINEPREFIX` environment variable is set, then that will be used as an
     * override. Otherwise, we'll try to find the Wine prefix
     * `windows_plugin_path_` is located in. The detection works by looking for
     * a directory containing a directory called `dosdevices`. If the plugin is
     * not inside of a Wine prefix, this will be left empty, and the default
     * prefix will be used instead.
     */
    const std::
        variant<OverridenWinePrefix, ghc::filesystem::path, DefaultWinePrefix>
            wine_prefix_;
};

/**
 * Returns equality for two strings when ignoring casing. Used for comparing
 * filenames inside of Wine prefixes since Windows/Wine does case folding for
 * filenames.
 */
bool equals_case_insensitive(const std::string& a, const std::string& b);

/**
 * Join a vector of strings with commas while wrapping the strings in quotes.
 * For example, `join_quoted_strings(std::vector<string>{"string", "another
 * string", "also a string"})` outputs `"'string', 'another string', 'also a
 * string'"`. This is used to format the initialisation message.
 */
std::string join_quoted_strings(std::vector<std::string>& strings);

/**
 * Create a logger prefix based on the endpoint base directory used for the
 * sockets for easy identification. This will result in a prefix of the form
 * `[<plugin_name>-<random_id>] `.
 *
 * @param endpoint_base_dir A directory name generated by
 *   `generate_endpoint_base()`.
 *
 * @return A prefix string for log messages.
 */
std::string create_logger_prefix(
    const ghc::filesystem::path& endpoint_base_dir);

/**
 * Finds the Wine plugin host (either `yabridge-host.exe` or
 * `yabridge-host-32.exe` depending on the plugin). For this we will search in
 * two places:
 *
 *   1. Alongside libyabridge-{clap,vst2,vst3}.so if the file got symlinked.
 *      This is useful when developing, as you can simply symlink the
 *      `libyabridge-{clap,vst2,vst3}.so` file in the build directory without
 *      having to install anything to /usr.
 *   2. In the regular search path, augmented with `~/.local/share/yabridge` to
 *      ease the setup process.
 *
 * @param this_plugin_path The path to the `.so` file this code is being run
 *   from.
 * @param plugin_arch The architecture of the plugin, either 64-bit or 32-bit.
 *   Used to determine which host application to use, if available.
 *
 * @return The a path to the VST host, if found.
 * @throw std::runtime_error If the Wine plugin host could not be found.
 */
ghc::filesystem::path find_plugin_host(
    const ghc::filesystem::path& this_plugin_path,
    LibArchitecture plugin_arch);

/**
 * Generate the group socket endpoint name used based on the name of the group,
 * the Wine prefix in use and the plugin architecture. The resulting format is
 * in the form
 * `/run/user/<uid>/yabridge-group-<group_name>-<wine_prefix_id>-<architecture>.sock`.
 * In this socket name the `wine_prefix_id` is a numerical hash based on the
 * Wine prefix in use. This way the same group name can be used for multiple
 * Wine prefixes and for both 32 and 64 bit plugins without clashes.
 *
 * @param group_name The name of the plugin group.
 * @param wine_prefix The name of the Wine prefix in use. This should be
 *   obtained from `PluginInfo::normalize_wine_prefix()`.
 * @param architecture The architecture the plugin is using, since 64-bit
 *   processes can't host 32-bit plugins and the other way around.
 *
 * @return A socket endpoint path that corresponds to the format described
 *   above.
 */
ghc::filesystem::path generate_group_endpoint(
    const std::string& group_name,
    const ghc::filesystem::path& wine_prefix,
    const LibArchitecture architecture);

/**
 * Load the configuration that belongs to a copy of or symlink to
 * `libyabridge-{clap,vst2,vst3}.so`. If no configuration file could be found
 * then this will return an empty configuration object with default settings.
 * See the docstrong on the `Configuration` class for more details on how to
 * choose the config file to load.
 *
 * If the configuration file has syntax errors, then an error will be logged and
 * the defaults will be used instead.
 *
 * This function will also take any optional compile-time features that have not
 * been enabled into account.
 *
 * @param yabridge_path The path to the .so file that's being loaded.by the VST
 *   host. This will be used both for the starting location of the search and to
 *   determine which section in the config file to use.
 *
 * @return Either a configuration object populated with values from matched glob
 *   pattern within the found configuration file, or an empty configuration
 *   object if no configuration file could be found or if the plugin could not
 *   be matched to any of the glob patterns in the configuration file.
 *
 * @see Configuration
 */
Configuration load_config_for(const ghc::filesystem::path& yabridge_path);

/**
 * Starting from the starting file or directory, go up in the directory
 * hierarchy until we find a file named `filename`.
 *
 * @param filename The name of the file we're looking for. This can also be a
 *   directory name since directories are also files.
 * @param starting_from The directory to start searching in. If this is a file,
 *   then start searching in the directory the file is located in.
 * @param predicate The predicate to use to check if the path matches a file.
 *   Needed as an easy way to limit the search to directories only since C++17
 *   does not have any built in coroutines or generators.
 *
 * @return The path to the *file* found, or `std::nullopt` if the file could not
 *   be found.
 */
template <invocable_returning<bool, const ghc::filesystem::path&> F =
              bool(const ghc::filesystem::path&)>
std::optional<ghc::filesystem::path> find_dominating_file(
    const std::string& filename,
    ghc::filesystem::path starting_dir,
    F&& predicate = ghc::filesystem::exists) {
    while (starting_dir != "/" && starting_dir != "") {
        const ghc::filesystem::path candidate = starting_dir / filename;
        if (predicate(candidate)) {
            return candidate;
        }

        starting_dir = starting_dir.parent_path();
    }

    return std::nullopt;
}
