// yabridge: a Wine plugin bridge
// Copyright (C) 2020-2026 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 "process.h"

#include <cassert>
#include <iostream>

#include <dlfcn.h>
#include <spawn.h>
#include <sys/wait.h>

namespace fs = ghc::filesystem;

bool pid_running(pid_t pid) {
    // In theory you could `kill(0)` a process to check if it's still active,
    // but that doesn't distinguish between actually running processes and
    // unreaped zombies, and terminated group host processes will always be left
    // as zombies since there may not be anything left to reap them. Instead
    // we'll check whether one of `/proc/<pid>/{cwd,exe,root}` are valid
    // symlinks.
    std::error_code err;
    fs::canonical("/proc/" + std::to_string(pid) + "/exe", err);

    // NOTE: We can get a `EACCES` here if we don't have permissions to read
    //       this process's memory. This does mean that the process is still
    //       running.
    return !err || err.value() == EACCES;
}

std::vector<fs::path> get_augmented_search_path() {
    // HACK: `std::locale("")` would return the current locale, but this
    //       overload is implementation specific, and libstdc++ returns an error
    //       when this happens and one of the locale variables (or `LANG`) is
    //       set to a locale that doesn't exist. Because of that, you should use
    //       the default constructor instead which does fall back gracefully
    //       when using an invalid locale. Boost.Process sadly doesn't seem to
    //       do this, so some intervention is required. We can remove this once
    //       the PR linked below is merged into Boost proper and included in
    //       most distro's copy of Boost (which will probably take a while):
    //
    //       https://svn.boost.org/trac10/changeset/72855
    //
    //       https://github.com/boostorg/process/pull/179
    // FIXME: As mentioned above, we did this in the past to work around a
    //        Boost.Process bug. Since we no longer use Boost.Process, we can
    //        technically get rid of this, but we could also leave it in place
    //        since this may still cause other crashes for the user if we don't
    //        do it.
    try {
        std::locale("");
    } catch (const std::runtime_error&) {
        // We normally avoid modifying the current process' environment and
        // instead use `boost::process::environment` to only modify the
        // environment of launched child processes, but in this case we do need
        // to fix this
        // TODO: We don't have access to the logger here, so we cannot yet
        //       properly print the message inform the user that their locale is
        //       broken when this happens
        std::cerr << std::endl;
        std::cerr << "WARNING: Your locale is broken. Yabridge was kind enough "
                     "to monkey patch it for you in this DAW session, but you "
                     "should probably take a look at it ;)"
                  << std::endl;
        std::cerr << std::endl;

        setenv("LC_ALL", "C", true);  // NOLINT(concurrency-mt-unsafe)
    }

    // NOLINTNEXTLINE(concurrency-mt-unsafe)
    const char* path_env = getenv("PATH");
    assert(path_env);

    std::vector<fs::path> search_path = split_path(path_env);

    // NOLINTNEXTLINE(concurrency-mt-unsafe)
    if (const char* xdg_data_home = getenv("XDG_DATA_HOME")) {
        search_path.push_back(fs::path(xdg_data_home) / "yabridge");
        // NOLINTNEXTLINE(concurrency-mt-unsafe)
    } else if (const char* home_directory = getenv("HOME")) {
        search_path.push_back(fs::path(home_directory) / ".local" / "share" /
                              "yabridge");
    }

    return search_path;
}

std::vector<ghc::filesystem::path> split_path(
    const std::string_view& path_env) {
    // C++ has these great split range adapters. That are completely usless.
    std::vector<fs::path> search_path;

    size_t segment_begin = 0;
    while (segment_begin != path_env.size()) {
        const size_t segment_end = path_env.find(':', segment_begin);
        if (segment_end == std::string_view::npos) {
            search_path.push_back(path_env.substr(
                segment_begin, path_env.size() - segment_begin));
            break;
        } else {
            search_path.push_back(
                path_env.substr(segment_begin, segment_end - segment_begin));

            // Restart after the colon
            segment_begin = segment_end + 1;
        }
    }

    return search_path;
}

std::optional<ghc::filesystem::path> search_in_path(
    const std::vector<ghc::filesystem::path>& path,
    const std::string_view& target) {
    for (const auto& dir : path) {
        ghc::filesystem::path candidate = dir / target;
        if (access(candidate.c_str(), X_OK) == 0) {
            return candidate;
        }
    }

    return std::nullopt;
}

ProcessEnvironment::ProcessEnvironment(char** initial_env) {
    // We'll need to read all strings from `initial_env`. They _should_ all be
    // zero-terminated strings, with a null pointer to indicate the end of the
    // array.
    assert(initial_env);
    while (*initial_env) {
        variables_.push_back(*initial_env);
        initial_env++;
    }
}

bool ProcessEnvironment::contains(const std::string_view& key) const {
    for (const auto& variable : variables_) {
        if (variable.starts_with(key) && variable.size() > key.size() &&
            variable[key.size()] == '=') {
            return true;
        }
    }

    return false;
}

std::optional<std::string_view> ProcessEnvironment::get(
    const std::string_view& key) const {
    for (const auto& variable : variables_) {
        if (variable.starts_with(key) && variable.size() > key.size() &&
            variable[key.size()] == '=') {
            return std::string_view(variable).substr(key.size() + 1);
        }
    }

    return std::nullopt;
}

// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
void ProcessEnvironment::insert(const std::string& key,
                                const std::string& value) {
    variables_.push_back(key + "=" + value);
}

size_t ProcessEnvironment::erase(const std::string& key) {
    return std::erase_if(variables_, [&key](const std::string& variable) {
        return variable.starts_with(key) && variable.size() > key.size() &&
               variable[key.size()] == '=';
    });
}

char* const* ProcessEnvironment::make_environ() const {
    recreated_environ_.clear();

    for (const auto& variable : variables_) {
        recreated_environ_.push_back(variable.c_str());
    }
    recreated_environ_.push_back(nullptr);

    return const_cast<char* const*>(recreated_environ_.data());
}

Process::Handle::Handle(pid_t pid) : pid_(pid) {}

Process::Handle::~Handle() {
    if (!detached_) {
        // If this function has already been called then that's okay
        terminate();
    }
}

Process::Handle::Handle(Handle&& o) noexcept
    : detached_(o.detached_), pid_(o.pid_) {
    o.detached_ = true;
}

Process::Handle& Process::Handle::operator=(Handle&& o) noexcept {
    o.detached_ = true;

    detached_ = o.detached_;
    pid_ = o.pid_;

    return *this;
}

pid_t Process::Handle::pid() const noexcept {
    return pid_;
}

bool Process::Handle::running() const noexcept {
    return pid_running(pid_);
}

void Process::Handle::detach() noexcept {
    detached_ = true;
}

void Process::Handle::terminate() const noexcept {
    kill(pid_, SIGINT);
    wait();
}

std::optional<int> Process::Handle::wait() const noexcept {
    // This may fail if we've already reaped the process and terminate gets
    // called another time, so we won't check the result here
    int status = 0;
    waitpid(pid_, &status, 0);

    if (WIFEXITED(status)) {
        return WEXITSTATUS(status);
    } else {
        return std::nullopt;
    }
}

Process::Process(std::string command) : command_(command) {}

Process::StringResult Process::spawn_get_stdout_line() const {
    // We'll read the results from a pipe. The child writes to the second pipe,
    // we'll read from the first one.
    int stdout_pipe_fds[2];
    assert(pipe(stdout_pipe_fds) == 0);

    const auto argv = build_argv();
    const auto envp = env_ ? env_->make_environ() : environ;

    posix_spawn_file_actions_t actions;
    posix_spawn_file_actions_init(&actions);
    posix_spawn_file_actions_adddup2(&actions, stdout_pipe_fds[1],
                                     STDOUT_FILENO);
    posix_spawn_file_actions_addopen(&actions, STDERR_FILENO, "/dev/null",
                                     O_WRONLY | O_APPEND, 0);
    posix_spawn_file_actions_addclose(&actions, stdout_pipe_fds[0]);
    posix_spawn_file_actions_addclose(&actions, stdout_pipe_fds[1]);

    pid_t child_pid = 0;
    const auto result = posix_spawnp(&child_pid, command_.c_str(), &actions,
                                     nullptr, argv, envp);

    close(stdout_pipe_fds[1]);
    if (result == 2) {
        close(stdout_pipe_fds[0]);
        return Process::CommandNotFound{};
    } else if (result != 0) {
        close(stdout_pipe_fds[0]);
        return std::error_code(result, std::system_category());
    }

    // Try to read the first line out the output until the line feed
    std::array<char, 1024> output{0};
    FILE* output_pipe_stream = fdopen(stdout_pipe_fds[0], "r");
    assert(output_pipe_stream);
    [[maybe_unused]] char* not_relevant =
        fgets(output.data(), output.size(), output_pipe_stream);
    fclose(output_pipe_stream);

    int status = 0;
    assert(waitpid(child_pid, &status, 0) > 0);
    if (!WIFEXITED(status) || WEXITSTATUS(status) == 127) {
        return Process::CommandNotFound{};
    } else {
        // `fgets()` returns the line feed, so we'll get rid of that
        std::string output_str(output.data());
        if (output_str.back() == '\n') {
            output_str.pop_back();
        }

        return output_str;
    }
}

Process::StatusResult Process::spawn_get_status() const {
    const auto argv = build_argv();
    const auto envp = env_ ? env_->make_environ() : environ;

    pid_t child_pid = 0;
    const auto result = posix_spawnp(&child_pid, command_.c_str(), nullptr,
                                     nullptr, argv, envp);
    if (result == 2) {
        return Process::CommandNotFound{};
    } else if (result != 0) {
        return std::error_code(result, std::system_category());
    }

    int status = 0;
    assert(waitpid(child_pid, &status, 0) > 0);
    if (!WIFEXITED(status) || WEXITSTATUS(status) == 127) {
        return Process::CommandNotFound{};
    } else {
        return WEXITSTATUS(status);
    }
}

/**
 * Add file handle close actions to a `posix_spawn_file_actions_t` that close
 * all non-stdio file descriptors.
 *
 * If the Wine process outlives the host, then it may cause issues if our
 * process is still keeping the host's file descriptors alive that. This can
 * prevent Ardour from restarting after an unexpected shutdown. Because of this
 * we won't use `vfork()`, but instead we'll just manually close all non-STDIO
 * file descriptors.
 */
static void close_non_stdio_file_descriptors(
    posix_spawn_file_actions_t& actions) {
#if (__GLIBC__ > 2) || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 34)
    posix_spawn_file_actions_addclosefrom_np(&actions, STDERR_FILENO + 1);
#else
    // NOTE: As of writing, yabridge is compiled on Ubuntu 20.04, where
    //       `posix_spawn_file_actions_addclosefrom_np()` is not yet available.
    //       For whatever reason that may be, on Ubuntu 24.10 closing all file
    //       handles manually becomes very slow and starts to leak memory when
    //       running yabridge under Ardour. We could bump the minimum Ubuntu
    //       version supported by the binaries and always use this function, but
    //       loading the function at runtime should be fine and gives us better
    //       compatibility even if yabridge is compiled on an older distro.
    //
    //       https://github.com/robbert-vdh/yabridge/issues/377
    int (*posix_spawn_file_actions_addclosefrom_np)(posix_spawn_file_actions_t*,
                                                    int);
    posix_spawn_file_actions_addclosefrom_np =
        reinterpret_cast<decltype(posix_spawn_file_actions_addclosefrom_np)>(
            dlsym(nullptr, "posix_spawn_file_actions_addclosefrom_np"));

    if (posix_spawn_file_actions_addclosefrom_np) {
        posix_spawn_file_actions_addclosefrom_np(&actions, STDERR_FILENO + 1);
    } else {
        const int max_fds = static_cast<int>(sysconf(_SC_OPEN_MAX));
        for (int fd = STDERR_FILENO + 1; fd < max_fds; fd++) {
            posix_spawn_file_actions_addclose(&actions, fd);
        }
    }
#endif
}

#ifndef WITHOUT_ASIO
Process::HandleResult Process::spawn_child_piped(
    // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
    asio::posix::stream_descriptor& stdout_pipe,
    asio::posix::stream_descriptor& stderr_pipe) const {
    // We'll reopen the child process' STDOUT and STDERR stream from a pipe, and
    // we'll assign the other ends of those pipes to the stream descriptors
    // passed to this function so they can be read from asynchronously in an
    // Asio IO context loop. We'll read from the first elements of these pipes,
    // and the child process will write to the second elements.
    int stdout_pipe_fds[2];
    int stderr_pipe_fds[2];
    assert(pipe(stdout_pipe_fds) == 0);
    assert(pipe(stderr_pipe_fds) == 0);

    const auto argv = build_argv();
    const auto envp = env_ ? env_->make_environ() : environ;

    posix_spawn_file_actions_t actions;
    posix_spawn_file_actions_init(&actions);
    posix_spawn_file_actions_adddup2(&actions, stdout_pipe_fds[1],
                                     STDOUT_FILENO);
    posix_spawn_file_actions_adddup2(&actions, stderr_pipe_fds[1],
                                     STDERR_FILENO);
    // We'll close the four pipe fds along with the rest of the file descriptors
    close_non_stdio_file_descriptors(actions);

    pid_t child_pid = 0;
    const auto result = posix_spawnp(&child_pid, command_.c_str(), &actions,
                                     nullptr, argv, envp);

    // We'll assign the read ends of the pipes to the Asio stream descriptors
    // passed to this function, even if launching the process failed.
    // `asio::posix::stream_descriptor::assign()` will take ownership of the FD
    // and close it when the object gets dropped.
    stdout_pipe.assign(stdout_pipe_fds[0]);
    stderr_pipe.assign(stderr_pipe_fds[0]);
    close(stdout_pipe_fds[1]);
    close(stderr_pipe_fds[1]);

    if (result == 2) {
        return Process::CommandNotFound{};
    } else if (result != 0) {
        return std::error_code(result, std::system_category());
    }

    // With glibc `posix_spawn*()` will return 2/`ENOENT` when the file does not
    // exist, but the specification says that it should return a PID that exits
    // with status 127 instead. I have no idea how we'd check for that without
    // waiting here though, so this check may not work
    int status = 0;
    assert(waitpid(child_pid, &status, WNOHANG) >= 0);
    if (WIFEXITED(status) && WEXITSTATUS(status) == 127) {
        return Process::CommandNotFound{};
    } else {
        return Handle(child_pid);
    }
}
#endif  // WITHOUT_ASIO

Process::HandleResult Process::spawn_child_redirected(
    const ghc::filesystem::path& filename) const {
    const auto argv = build_argv();
    const auto envp = env_ ? env_->make_environ() : environ;

    posix_spawn_file_actions_t actions;
    posix_spawn_file_actions_init(&actions);
    posix_spawn_file_actions_addopen(&actions, STDOUT_FILENO, filename.c_str(),
                                     O_WRONLY | O_CREAT | O_APPEND, 0640);
    posix_spawn_file_actions_addopen(&actions, STDERR_FILENO, filename.c_str(),
                                     O_WRONLY | O_CREAT | O_APPEND, 0640);
    close_non_stdio_file_descriptors(actions);

    pid_t child_pid = 0;
    const auto result = posix_spawnp(&child_pid, command_.c_str(), &actions,
                                     nullptr, argv, envp);
    if (result == 2) {
        return Process::CommandNotFound{};
    } else if (result != 0) {
        return std::error_code(result, std::system_category());
    }

    int status = 0;
    assert(waitpid(child_pid, &status, WNOHANG) >= 0);
    if (WIFEXITED(status) && WEXITSTATUS(status) == 127) {
        return Process::CommandNotFound{};
    } else {
        return Handle(child_pid);
    }
}

char* const* Process::build_argv() const {
    argv_.clear();

    argv_.push_back(command_.c_str());
    for (const auto& arg : args_) {
        argv_.push_back(arg.c_str());
    }
    argv_.push_back(nullptr);

    return const_cast<char* const*>(argv_.data());
}
