/*
  Simple DirectMedia Layer
  Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>

  This software is provided 'as-is', without any express or implied
  warranty.  In no event will the authors be held liable for any damages
  arising from the use of this software.

  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it
  freely, subject to the following restrictions:

  1. The origin of this software must not be misrepresented; you must not
     claim that you wrote the original software. If you use this software
     in a product, an acknowledgment in the product documentation would be
     appreciated but is not required.
  2. Altered source versions must be plainly marked as such, and must not be
     misrepresented as being the original software.
  3. This notice may not be removed or altered from any source distribution.
*/
#include "SDL_internal.h"

#ifdef SDL_PROCESS_POSIX

#include <dirent.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <spawn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#include "../SDL_sysprocess.h"
#include "../../io/SDL_iostream_c.h"


#if defined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP) && \
    !defined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR)
#define HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR
#define posix_spawn_file_actions_addchdir posix_spawn_file_actions_addchdir_np
#endif

#define READ_END 0
#define WRITE_END 1

struct SDL_ProcessData {
    pid_t pid;
};

static void CleanupStream(void *userdata, void *value)
{
    SDL_Process *process = (SDL_Process *)value;
    const char *property = (const char *)userdata;

    SDL_ClearProperty(process->props, property);
}

static bool SetupStream(SDL_Process *process, int fd, const char *mode, const char *property)
{
    // Set the file descriptor to non-blocking mode
    fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);

    SDL_IOStream *io = SDL_IOFromFD(fd, true);
    if (!io) {
        return false;
    }

    SDL_SetPointerPropertyWithCleanup(SDL_GetIOProperties(io), "SDL.internal.process", process, CleanupStream, (void *)property);
    SDL_SetPointerProperty(process->props, property, io);
    return true;
}

static void IgnoreSignal(int sig)
{
    struct sigaction action;

    sigaction(SIGPIPE, NULL, &action);
#ifdef HAVE_SA_SIGACTION
    if (action.sa_handler == SIG_DFL && (void (*)(int))action.sa_sigaction == SIG_DFL) {
#else
    if (action.sa_handler == SIG_DFL) {
#endif
        action.sa_handler = SIG_IGN;
        sigaction(sig, &action, NULL);
    }
}

static bool CreatePipe(int fds[2])
{
    if (pipe(fds) < 0) {
        return false;
    }

    // Make sure the pipe isn't accidentally inherited by another thread creating a process
    fcntl(fds[READ_END], F_SETFD, fcntl(fds[READ_END], F_GETFD) | FD_CLOEXEC);
    fcntl(fds[WRITE_END], F_SETFD, fcntl(fds[WRITE_END], F_GETFD) | FD_CLOEXEC);

    // Make sure we don't crash if we write when the pipe is closed
    IgnoreSignal(SIGPIPE);

    return true;
}

static bool GetStreamFD(SDL_PropertiesID props, const char *property, int *result)
{
    SDL_IOStream *io = (SDL_IOStream *)SDL_GetPointerProperty(props, property, NULL);
    if (!io) {
        SDL_SetError("%s is not set", property);
        return false;
    }

    int fd = (int)SDL_GetNumberProperty(SDL_GetIOProperties(io), SDL_PROP_IOSTREAM_FILE_DESCRIPTOR_NUMBER, -1);
    if (fd < 0) {
        SDL_SetError("%s doesn't have SDL_PROP_IOSTREAM_FILE_DESCRIPTOR_NUMBER available", property);
        return false;
    }

    *result = fd;
    return true;
}

static bool AddFileDescriptorCloseActions(posix_spawn_file_actions_t *fa)
{
    DIR *dir = opendir("/proc/self/fd");
    if (dir) {
        struct dirent *entry;
        while ((entry = readdir(dir)) != NULL) {
            int fd = SDL_atoi(entry->d_name);
            if (fd <= STDERR_FILENO) {
                continue;
            }

            int flags = fcntl(fd, F_GETFD);
            if (flags < 0 || (flags & FD_CLOEXEC)) {
                continue;
            }
            if (posix_spawn_file_actions_addclose(fa, fd) != 0) {
                closedir(dir);
                return SDL_SetError("posix_spawn_file_actions_addclose failed: %s", strerror(errno));
            }
        }
        closedir(dir);
    } else {
        for (int fd = (int)(sysconf(_SC_OPEN_MAX) - 1); fd > STDERR_FILENO; --fd) {
            int flags = fcntl(fd, F_GETFD);
            if (flags < 0 || (flags & FD_CLOEXEC)) {
                continue;
            }
            if (posix_spawn_file_actions_addclose(fa, fd) != 0) {
                return SDL_SetError("posix_spawn_file_actions_addclose failed: %s", strerror(errno));
            }
        }
    }
    return true;
}

bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID props)
{
    char * const *args = SDL_GetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, NULL);
    SDL_Environment *env = SDL_GetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ENVIRONMENT_POINTER, SDL_GetEnvironment());
    char **envp = NULL;
    const char *working_directory = SDL_GetStringProperty(props, SDL_PROP_PROCESS_CREATE_WORKING_DIRECTORY_STRING, NULL);
    SDL_ProcessIO stdin_option = (SDL_ProcessIO)SDL_GetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDIN_NUMBER, SDL_PROCESS_STDIO_NULL);
    SDL_ProcessIO stdout_option = (SDL_ProcessIO)SDL_GetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_INHERITED);
    SDL_ProcessIO stderr_option = (SDL_ProcessIO)SDL_GetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER, SDL_PROCESS_STDIO_INHERITED);
    bool redirect_stderr = SDL_GetBooleanProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_TO_STDOUT_BOOLEAN, false) &&
                           !SDL_HasProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER);
    int stdin_pipe[2] = { -1, -1 };
    int stdout_pipe[2] = { -1, -1 };
    int stderr_pipe[2] = { -1, -1 };
    int fd = -1;

    // Keep the malloc() before exec() so that an OOM won't run a process at all
    envp = SDL_GetEnvironmentVariables(env);
    if (!envp) {
        return false;
    }

    SDL_ProcessData *data = SDL_calloc(1, sizeof(*data));
    if (!data) {
        SDL_free(envp);
        return false;
    }
    process->internal = data;

    posix_spawnattr_t attr;
    posix_spawn_file_actions_t fa;

    if (posix_spawnattr_init(&attr) != 0) {
        SDL_SetError("posix_spawnattr_init failed: %s", strerror(errno));
        goto posix_spawn_fail_none;
    }

    if (posix_spawn_file_actions_init(&fa) != 0) {
        SDL_SetError("posix_spawn_file_actions_init failed: %s", strerror(errno));
        goto posix_spawn_fail_attr;
    }

    if (working_directory) {
#ifdef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR
#ifdef SDL_PLATFORM_APPLE
        if (__builtin_available(macOS 10.15, *)) {
            if (posix_spawn_file_actions_addchdir_np(&fa, working_directory) != 0) {
                SDL_SetError("posix_spawn_file_actions_addchdir failed: %s", strerror(errno));
                goto posix_spawn_fail_all;
            }
        } else {
            SDL_SetError("Setting the working directory is only supported on macOS 10.15 and newer");
            goto posix_spawn_fail_all;
        }
#else
        if (posix_spawn_file_actions_addchdir(&fa, working_directory) != 0) {
            SDL_SetError("posix_spawn_file_actions_addchdir failed: %s", strerror(errno));
            goto posix_spawn_fail_all;
        }
#endif // SDL_PLATFORM_APPLE
#else
        SDL_SetError("Setting the working directory is not supported");
        goto posix_spawn_fail_all;
#endif
    }

    // Background processes don't have access to the terminal
    if (process->background) {
        if (stdin_option == SDL_PROCESS_STDIO_INHERITED) {
            stdin_option = SDL_PROCESS_STDIO_NULL;
        }
        if (stdout_option == SDL_PROCESS_STDIO_INHERITED) {
            stdout_option = SDL_PROCESS_STDIO_NULL;
        }
        if (stderr_option == SDL_PROCESS_STDIO_INHERITED) {
            stderr_option = SDL_PROCESS_STDIO_NULL;
        }
    }

    switch (stdin_option) {
    case SDL_PROCESS_STDIO_REDIRECT:
        if (!GetStreamFD(props, SDL_PROP_PROCESS_CREATE_STDIN_POINTER, &fd)) {
            goto posix_spawn_fail_all;
        }
        if (posix_spawn_file_actions_adddup2(&fa, fd, STDIN_FILENO) != 0) {
            SDL_SetError("posix_spawn_file_actions_adddup2 failed: %s", strerror(errno));
            goto posix_spawn_fail_all;
        }
        break;
    case SDL_PROCESS_STDIO_APP:
        if (!CreatePipe(stdin_pipe)) {
            goto posix_spawn_fail_all;
        }
        if (posix_spawn_file_actions_adddup2(&fa, stdin_pipe[READ_END], STDIN_FILENO) != 0) {
            SDL_SetError("posix_spawn_file_actions_adddup2 failed: %s", strerror(errno));
            goto posix_spawn_fail_all;
        }
        break;
    case SDL_PROCESS_STDIO_NULL:
        if (posix_spawn_file_actions_addopen(&fa, STDIN_FILENO, "/dev/null", O_RDONLY, 0) != 0) {
            SDL_SetError("posix_spawn_file_actions_addopen failed: %s", strerror(errno));
            goto posix_spawn_fail_all;
        }
        break;
    case SDL_PROCESS_STDIO_INHERITED:
    default:
        break;
    }

    switch (stdout_option) {
    case SDL_PROCESS_STDIO_REDIRECT:
        if (!GetStreamFD(props, SDL_PROP_PROCESS_CREATE_STDOUT_POINTER, &fd)) {
            goto posix_spawn_fail_all;
        }
        if (posix_spawn_file_actions_adddup2(&fa, fd, STDOUT_FILENO) != 0) {
            SDL_SetError("posix_spawn_file_actions_adddup2 failed: %s", strerror(errno));
            goto posix_spawn_fail_all;
        }
        break;
    case SDL_PROCESS_STDIO_APP:
        if (!CreatePipe(stdout_pipe)) {
            goto posix_spawn_fail_all;
        }
        if (posix_spawn_file_actions_adddup2(&fa, stdout_pipe[WRITE_END], STDOUT_FILENO) != 0) {
            SDL_SetError("posix_spawn_file_actions_adddup2 failed: %s", strerror(errno));
            goto posix_spawn_fail_all;
        }
        break;
    case SDL_PROCESS_STDIO_NULL:
        if (posix_spawn_file_actions_addopen(&fa, STDOUT_FILENO, "/dev/null", O_WRONLY, 0644) != 0) {
            SDL_SetError("posix_spawn_file_actions_addopen failed: %s", strerror(errno));
            goto posix_spawn_fail_all;
        }
        break;
    case SDL_PROCESS_STDIO_INHERITED:
    default:
        break;
    }

    if (redirect_stderr) {
        if (posix_spawn_file_actions_adddup2(&fa, STDOUT_FILENO, STDERR_FILENO) != 0) {
            SDL_SetError("posix_spawn_file_actions_adddup2 failed: %s", strerror(errno));
            goto posix_spawn_fail_all;
        }
    } else {
        switch (stderr_option) {
        case SDL_PROCESS_STDIO_REDIRECT:
            if (!GetStreamFD(props, SDL_PROP_PROCESS_CREATE_STDERR_POINTER, &fd)) {
                goto posix_spawn_fail_all;
            }
            if (posix_spawn_file_actions_adddup2(&fa, fd, STDERR_FILENO) != 0) {
                SDL_SetError("posix_spawn_file_actions_adddup2 failed: %s", strerror(errno));
                goto posix_spawn_fail_all;
            }
            break;
        case SDL_PROCESS_STDIO_APP:
            if (!CreatePipe(stderr_pipe)) {
                goto posix_spawn_fail_all;
            }
            if (posix_spawn_file_actions_adddup2(&fa, stderr_pipe[WRITE_END], STDERR_FILENO) != 0) {
                SDL_SetError("posix_spawn_file_actions_adddup2 failed: %s", strerror(errno));
                goto posix_spawn_fail_all;
            }
            break;
        case SDL_PROCESS_STDIO_NULL:
            if (posix_spawn_file_actions_addopen(&fa, STDERR_FILENO, "/dev/null", O_WRONLY, 0644) != 0) {
                SDL_SetError("posix_spawn_file_actions_addopen failed: %s", strerror(errno));
                goto posix_spawn_fail_all;
            }
            break;
        case SDL_PROCESS_STDIO_INHERITED:
        default:
            break;
        }
    }

    if (!AddFileDescriptorCloseActions(&fa)) {
        goto posix_spawn_fail_all;
    }

    // Spawn the new process
    if (process->background) {
        int status = -1;
        #ifdef SDL_PLATFORM_APPLE  // Apple has vfork marked as deprecated and (as of macOS 10.12) is almost identical to calling fork() anyhow.
        const pid_t pid = fork();
        const char *forkname = "fork";
        #else
        const pid_t pid = vfork();
        const char *forkname = "vfork";
        #endif
        switch (pid) {
        case -1:
            SDL_SetError("%s() failed: %s", forkname, strerror(errno));
            goto posix_spawn_fail_all;

        case 0:
            // Detach from the terminal and launch the process
            setsid();
            if (posix_spawnp(&data->pid, args[0], &fa, &attr, args, envp) != 0) {
                _exit(errno);
            }
            _exit(0);

        default:
            if (waitpid(pid, &status, 0) < 0) {
                SDL_SetError("waitpid() failed: %s", strerror(errno));
                goto posix_spawn_fail_all;
            }
            if (status != 0) {
                SDL_SetError("posix_spawn() failed: %s", strerror(status));
                goto posix_spawn_fail_all;
            }
            break;
        }
    } else {
        if (posix_spawnp(&data->pid, args[0], &fa, &attr, args, envp) != 0) {
            SDL_SetError("posix_spawn() failed: %s", strerror(errno));
            goto posix_spawn_fail_all;
        }
    }
    SDL_SetNumberProperty(process->props, SDL_PROP_PROCESS_PID_NUMBER, data->pid);

    if (stdin_option == SDL_PROCESS_STDIO_APP) {
        if (!SetupStream(process, stdin_pipe[WRITE_END], "wb", SDL_PROP_PROCESS_STDIN_POINTER)) {
            close(stdin_pipe[WRITE_END]);
        }
        close(stdin_pipe[READ_END]);
    }

    if (stdout_option == SDL_PROCESS_STDIO_APP) {
        if (!SetupStream(process, stdout_pipe[READ_END], "rb", SDL_PROP_PROCESS_STDOUT_POINTER)) {
            close(stdout_pipe[READ_END]);
        }
        close(stdout_pipe[WRITE_END]);
    }

    if (stderr_option == SDL_PROCESS_STDIO_APP) {
        if (!SetupStream(process, stderr_pipe[READ_END], "rb", SDL_PROP_PROCESS_STDERR_POINTER)) {
            close(stderr_pipe[READ_END]);
        }
        close(stderr_pipe[WRITE_END]);
    }

    posix_spawn_file_actions_destroy(&fa);
    posix_spawnattr_destroy(&attr);
    SDL_free(envp);

    return true;

    /* --------------------------------------------------------------------- */

posix_spawn_fail_all:
    posix_spawn_file_actions_destroy(&fa);

posix_spawn_fail_attr:
    posix_spawnattr_destroy(&attr);

posix_spawn_fail_none:
    if (stdin_pipe[READ_END] >= 0) {
        close(stdin_pipe[READ_END]);
    }
    if (stdin_pipe[WRITE_END] >= 0) {
        close(stdin_pipe[WRITE_END]);
    }
    if (stdout_pipe[READ_END] >= 0) {
        close(stdout_pipe[READ_END]);
    }
    if (stdout_pipe[WRITE_END] >= 0) {
        close(stdout_pipe[WRITE_END]);
    }
    if (stderr_pipe[READ_END] >= 0) {
        close(stderr_pipe[READ_END]);
    }
    if (stderr_pipe[WRITE_END] >= 0) {
        close(stderr_pipe[WRITE_END]);
    }
    SDL_free(envp);
    return false;
}

bool SDL_SYS_KillProcess(SDL_Process *process, bool force)
{
    int ret = kill(process->internal->pid, force ? SIGKILL : SIGTERM);
    if (ret == 0) {
        return true;
    } else {
        return SDL_SetError("Could not kill(): %s", strerror(errno));
    }
}

bool SDL_SYS_WaitProcess(SDL_Process *process, bool block, int *exitcode)
{
    int wstatus = 0;
    int ret;
    pid_t pid = process->internal->pid;

    if (process->background) {
        // We can't wait on the status, so we'll poll to see if it's alive
        if (block) {
            while (kill(pid, 0) == 0) {
                SDL_Delay(10);
            }
        } else {
            if (kill(pid, 0) == 0) {
                return false;
            }
        }
        *exitcode = 0;
        return true;
    } else {
        ret = waitpid(pid, &wstatus, block ? 0 : WNOHANG);
        if (ret < 0) {
            return SDL_SetError("Could not waitpid(): %s", strerror(errno));
        }

        if (ret == 0) {
            SDL_ClearError();
            return false;
        }

        if (WIFEXITED(wstatus)) {
            *exitcode = WEXITSTATUS(wstatus);
        } else if (WIFSIGNALED(wstatus)) {
            *exitcode = -WTERMSIG(wstatus);
        } else {
            *exitcode = -255;
        }

        return true;
    }
}

void SDL_SYS_DestroyProcess(SDL_Process *process)
{
    SDL_IOStream *io;

    io = (SDL_IOStream *)SDL_GetPointerProperty(process->props, SDL_PROP_PROCESS_STDIN_POINTER, NULL);
    if (io) {
        SDL_CloseIO(io);
    }

    io = (SDL_IOStream *)SDL_GetPointerProperty(process->props, SDL_PROP_PROCESS_STDOUT_POINTER, NULL);
    if (io) {
        SDL_CloseIO(io);
    }

    io = (SDL_IOStream *)SDL_GetPointerProperty(process->props, SDL_PROP_PROCESS_STDERR_POINTER, NULL);
    if (io) {
        SDL_CloseIO(io);
    }

    SDL_free(process->internal);
}

#endif // SDL_PROCESS_POSIX
