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

#include "utils.h"

#include <unistd.h>
#include <sstream>

// Generated inside of the build directory
#include <config.h>

#include "../common/configuration.h"
#include "../common/notifications.h"
#include "../common/toml++.h"
#include "../common/utils.h"

namespace fs = ghc::filesystem;

// These functions are used to populate the fields in `PluginInfo`. See the
// docstrings for the corresponding fields for more information on what we're
// actually doing here.
fs::path find_plugin_library(const fs::path& this_plugin_path,
                             PluginType plugin_type,
                             bool prefer_32bit_vst3);
fs::path normalize_plugin_path(const fs::path& windows_library_path,
                               PluginType plugin_type);
std::variant<OverridenWinePrefix, fs::path, DefaultWinePrefix> find_wine_prefix(
    fs::path windows_plugin_path);

PluginInfo::PluginInfo(PluginType plugin_type,
                       const ghc::filesystem::path& plugin_path,
                       bool prefer_32bit_vst3)
    : plugin_type_(plugin_type),
      native_library_path_(plugin_path),
      // As explained in the docstring, this is the actual Windows library. For
      // VST3 plugins that come in a module we should be loading that module
      // instead of the `.vst3` file within in, which is where
      // `windows_plugin_path` comes in.
      windows_library_path_(find_plugin_library(native_library_path_,
                                                plugin_type,
                                                prefer_32bit_vst3)),
      plugin_arch_(find_dll_architecture(windows_library_path_)),
      windows_plugin_path_(
          normalize_plugin_path(windows_library_path_, plugin_type)),
      wine_prefix_(find_wine_prefix(windows_plugin_path_)) {}

ProcessEnvironment PluginInfo::create_host_env() const {
    ProcessEnvironment env(environ);

    // Only set the prefix when could auto detect it and it's not being
    // overridden (this entire `std::visit` instead of `std::has_alternative` is
    // just for clarity's sake)
    std::visit(overload{
                   [](const OverridenWinePrefix&) {},
                   [&](const ghc::filesystem::path& prefix) {
                       env.insert("WINEPREFIX", prefix.string());
                   },
                   [](const DefaultWinePrefix&) {},
               },
               wine_prefix_);

    // As of writing upstream Wine does not yet have a Wayland driver, but one
    // is in the process of being merged. If this ever becomes enabled by
    // default on distros, Wine could suddenly start using using Wayland instead
    // of X11. This would break yabridge's embedding and drag-and-drop handling.
    // So we'll preemptively avoid this by unsetting the `WAYLAND_DISPLAY`
    // environment variable.
    env.erase("WAYLAND_DISPLAY");

    return env;
}

ghc::filesystem::path PluginInfo::normalize_wine_prefix() const {
    return std::visit(
        overload{
            [](const OverridenWinePrefix& prefix) { return prefix.value; },
            [](const ghc::filesystem::path& prefix) { return prefix; },
            [](const DefaultWinePrefix&) {
                // NOLINTNEXTLINE(concurrency-mt-unsafe)
                const char* home_dir = getenv("HOME");
                assert(home_dir);

                return fs::path(home_dir) / ".wine";
            },
        },
        wine_prefix_);
}

std::string PluginInfo::wine_version() const {
    // The '*.exe' scripts generated by winegcc allow you to override the binary
    // used to run Wine, so will will handle this in the same way for our Wine
    // version detection. We'll be using `execvpe`
    std::string wine_path = "wine";
    // NOLINTNEXTLINE(concurrency-mt-unsafe)
    if (const char* wineloader_path = getenv("WINELOADER");
        wineloader_path && access(wineloader_path, X_OK) == 0) {
        wine_path = wineloader_path;
    }

    Process process(wine_path);
    process.arg("--version");
    process.environment(create_host_env());

    const auto result = process.spawn_get_stdout_line();
    return std::visit(
        overload{
            [](std::string version_string) -> std::string {
                // Strip the `wine-` prefix from the output, could potentially
                // be absent in custom Wine builds
                constexpr std::string_view version_prefix("wine-");
                if (version_string.starts_with(version_prefix)) {
                    version_string =
                        version_string.substr(version_prefix.size());
                }

                return version_string;
            },
            [](const Process::CommandNotFound&) -> std::string {
                return "<NOT FOUND>";
            },
            [](const std::error_code& err) -> std::string {
                return "<ERROR SPAWNING WINE: " + err.message() + " >";
            },
        },
        result);
}

fs::path find_plugin_library(const fs::path& this_plugin_path,
                             PluginType plugin_type,
                             bool prefer_32bit_vst3) {
    // TODO: We only consider lower case extensions, and yabridgectl also
    //       explicitly ignores upper and mixed case versions. Doing a case
    //       insensitive version of this would involve checking each entry in
    //       the directory listing. That's possible, but not something we're
    //       doing right now.
    switch (plugin_type) {
        case PluginType::clap: {
            // CLAP uses the same file extension on all platforms, which is kind
            // of a problem for our setup. As a workaround we invent this new
            // 'clap-win' file extension for the Windows CLAP plugin. That
            // avoids having the actual Windows plugin in one of the plugin
            // search locations, as those will show up in the DAW's plugin
            // blacklist, and they may cause the bridged plugin to be
            // blacklisted as well.
            fs::path plugin_path(this_plugin_path);
            plugin_path.replace_extension(".clap-win");
            if (fs::exists(plugin_path)) {
                // Also resolve symlinks here, to support symlinked plugins.
                // This is the default in yabridgectl 4.0.
                return fs::canonical(plugin_path);
            }

            // In case this files does not exist and our `.so` file is a
            // symlink, we'll also repeat this check after resolving that
            // symlink to support links to copies of `libyabridge-clap.so` as
            // described in issue #3
            fs::path alternative_plugin_path = fs::canonical(this_plugin_path);
            alternative_plugin_path.replace_extension(".clap-win");
            if (fs::exists(alternative_plugin_path)) {
                return fs::canonical(alternative_plugin_path);
            }

            // This function is used in the constructor's initializer list so we
            // have to throw when the path could not be found
            throw std::runtime_error(
                "'" + plugin_path.string() +
                "' does not exist. Yabridge's CLAP support looks for a Windows "
                "CLAP plugin renamed have a '.clap-win' file extension next to "
                "a yabridge '.clap' file with the same name.");
        } break;
        case PluginType::vst2: {
            fs::path plugin_path(this_plugin_path);
            plugin_path.replace_extension(".dll");
            if (fs::exists(plugin_path)) {
                // Also resolve symlinks here, to support symlinked .dll files
                return fs::canonical(plugin_path);
            }

            // In case this files does not exist and our `.so` file is a
            // symlink, we'll also repeat this check after resolving that
            // symlink to support links to copies of `libyabridge-vst2.so` as
            // described in issue #3
            fs::path alternative_plugin_path = fs::canonical(this_plugin_path);
            alternative_plugin_path.replace_extension(".dll");
            if (fs::exists(alternative_plugin_path)) {
                return fs::canonical(alternative_plugin_path);
            }

            // This function is used in the constructor's initializer list so we
            // have to throw when the path could not be found
            throw std::runtime_error("'" + plugin_path.string() +
                                     "' does not exist, make sure to rename "
                                     "'libyabridge-vst2.so' to match a "
                                     "VST plugin .dll file.");
        } break;
        case PluginType::vst3: {
            // A VST3 plugin in Linux always has to be inside of a bundle (=
            // directory) named `X.vst3` that contains a shared object
            // `X.vst3/Contents/x86_64-linux/X.so`. On Linux `X.so` is not
            // allowed to be standalone, so for yabridge this should also always
            // be installed this way.
            // https://developer.steinberg.help/pages/viewpage.action?pageId=9798275
            const fs::path bundle_home =
                this_plugin_path.parent_path().parent_path().parent_path();
            const fs::path win_module_name =
                this_plugin_path.filename().replace_extension(".vst3");

            // Quick check in case the plugin was set up without yabridgectl,
            // since the format is very specific and any deviations from that
            // will be incorrect.
            if (bundle_home.extension() != ".vst3") {
                throw std::runtime_error(
                    "'" + this_plugin_path.string() +
                    "' is not inside of a VST3 bundle. Use yabridgectl to "
                    "set up yabridge for VST3 plugins or check the readme "
                    "for the correct format.");
            }

            // Finding the Windows plugin consists of two steps because
            // Steinberg changed the format around:
            // - First we'll find the plugin in the VST3 bundle created by
            //   yabridgectl in `~/.vst3/yabridge`. The plugin can be either
            //   32-bit or 64-bit. If both exist, then we'll take the 64-bit
            //   version, unless the `vst3_prefer_32bit` yabridge.toml option
            //   has been enabled for this plugin.
            // - After that we'll resolve the symlink to the module in the Wine
            //   prefix, and then we'll have to figure out if this module is an
            //   old style standalone module (< 3.6.10) or if it's inside of
            //   a bundle (>= 3.6.10)
            const fs::path candidate_path_64bit =
                bundle_home / "Contents" / "x86_64-win" / win_module_name;
            const fs::path candidate_path_32bit =
                bundle_home / "Contents" / "x86-win" / win_module_name;

            // After this we'll have to use `normalize_plugin_path()` to get the
            // actual module entry point in case the plugin is using a VST
            // 3.6.10 style bundle, because we need to inspect that for the
            // _actual_ (with yabridgectl `x86_64-win` should only contain a
            // 64-bit plugin and `x86-win` should only contain a 32-bit plugin,
            // but you never know!)
            // NOLINTNEXTLINE(bugprone-branch-clone)
            if (prefer_32bit_vst3 && fs::exists(candidate_path_32bit)) {
                return fs::canonical(candidate_path_32bit);
            } else if (fs::exists(candidate_path_64bit)) {
                return fs::canonical(candidate_path_64bit);
            } else if (fs::exists(candidate_path_32bit)) {
                return fs::canonical(candidate_path_32bit);
            }

            throw std::runtime_error(
                "'" + bundle_home.string() +
                "' does not contain a Windows VST3 module. Use yabridgectl to "
                "set up yabridge for VST3 plugins or check the readme "
                "for the correct format.");
        } break;
        default:
            throw std::runtime_error("How did you manage to get this?");
            break;
    }
}

fs::path normalize_plugin_path(const fs::path& windows_library_path,
                               PluginType plugin_type) {
    switch (plugin_type) {
        case PluginType::clap:
        case PluginType::vst2:
            return windows_library_path;
            break;
        case PluginType::vst3: {
            // Now we'll have to figure out if this is a new-style bundle or
            // an old standalone module
            const fs::path win_module_name =
                windows_library_path.filename().replace_extension(".vst3");
            // NOTE: This extra check is needed to prevent
            //       `<plugin_dir>/foo/X/Y/foo.vst3` from being recognized as a
            //       bundle when it isn't in fact a bundle.
            const fs::path windows_bundle_contents =
                windows_library_path.parent_path().parent_path();
            const fs::path windows_bundle_home =
                windows_bundle_contents.parent_path();
            if (equals_case_insensitive(windows_bundle_contents.string(),
                                        "Contents") &&
                equals_case_insensitive(windows_bundle_home.filename().string(),
                                        win_module_name.string())) {
                return windows_bundle_home;
            } else {
                return windows_library_path;
            }
        } break;
        default:
            throw std::runtime_error("How did you manage to get this?");
            break;
    }
}

std::variant<OverridenWinePrefix, fs::path, DefaultWinePrefix> find_wine_prefix(
    fs::path windows_plugin_path) {
    // NOLINTNEXTLINE(concurrency-mt-unsafe)
    if (const auto prefix = getenv("WINEPREFIX")) {
        return OverridenWinePrefix{prefix};
    }

    const std::optional<fs::path> dosdevices_dir = find_dominating_file(
        "dosdevices", windows_plugin_path, fs::is_directory);
    if (!dosdevices_dir) {
        return DefaultWinePrefix{};
    }

    return dosdevices_dir->parent_path();
}

bool equals_case_insensitive(const std::string& a, const std::string& b) {
    return std::equal(a.begin(), a.end(), b.begin(),
                      [](const char& a_char, const char& b_char) {
                          return std::tolower(a_char) == std::tolower(b_char);
                      });
}

std::string join_quoted_strings(std::vector<std::string>& strings) {
    bool is_first = true;
    std::ostringstream joined_strings{};
    for (const auto& option : strings) {
        joined_strings << (is_first ? "'" : ", '") << option << "'";
        is_first = false;
    }

    return joined_strings.str();
}

std::string create_logger_prefix(const fs::path& endpoint_base_dir) {
    // Use the name of the base directory used for our sockets as the logger
    // prefix, but strip the `yabridge-` part since that's redundant
    std::string endpoint_name = endpoint_base_dir.filename().string();

    constexpr std::string_view socket_prefix("yabridge-");
    assert(endpoint_name.starts_with(socket_prefix));
    endpoint_name = endpoint_name.substr(socket_prefix.size());

    return "[" + endpoint_name + "] ";
}

fs::path find_plugin_host(const ghc::filesystem::path& this_plugin_path,
                          LibArchitecture plugin_arch) {
    auto host_name = yabridge_host_name;
    if (plugin_arch == LibArchitecture::dll_32) {
        host_name = yabridge_host_name_32bit;
    }

    // If our `.so` file is a symlink, then search for the host in the directory
    // of the file that symlink points to
    fs::path host_path =
        fs::canonical(this_plugin_path).remove_filename() / host_name;
    if (fs::exists(host_path)) {
        return host_path;
    }

    if (const std::optional<fs::path> plugin_host_path =
            search_in_path(get_augmented_search_path(), host_name)) {
        return *plugin_host_path;
    } else {
        throw std::runtime_error("Could not locate '" + std::string(host_name) +
                                 "'");
    }
}

ghc::filesystem::path generate_group_endpoint(
    const std::string& group_name,
    const ghc::filesystem::path& wine_prefix,
    const LibArchitecture architecture) {
    std::ostringstream socket_name;
    socket_name << "yabridge-group-" << group_name << "-"
                << std::to_string(
                       std::hash<std::string>{}(wine_prefix.string()))
                << "-";
    switch (architecture) {
        case LibArchitecture::dll_32:
            socket_name << "x32";
            break;
        case LibArchitecture::dll_64:
            socket_name << "x64";
            break;
    }
    socket_name << ".sock";

    return get_temporary_directory() / socket_name.str();
}

Configuration load_config_for(const fs::path& yabridge_path) {
    // First find the closest `yabridge.tmol` file for the plugin, falling back
    // to default configuration settings if it doesn't exist
    const std::optional<fs::path> config_file =
        find_dominating_file("yabridge.toml", yabridge_path);
    if (!config_file) {
        return Configuration();
    }

    try {
        return Configuration(*config_file, yabridge_path);
    } catch (const toml::parse_error& error) {
        Logger logger = Logger::create_exception_logger();

        // Parsing failures should be non-fatal since that leads to a pretty
        // confusing user experience (see
        // https://github.com/robbert-vdh/yabridge/issues/282). They should,
        // however, still result in a visible error.
        logger.log("");
        logger.log("The configuration file at '" + config_file->string() +
                   "' could not be parsed:");
        logger.log(error.what());
        logger.log("");

        send_notification(
            "Failed to parse yabridge.toml file",
            "The configuration file at '" + config_file->string() +
                "' could not be parsed: " + std::string(error.description()),
            std::nullopt);

        return Configuration();
    }
}
