/*
 * This file is part of mpv.
 *
 * mpv is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * mpv 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with mpv.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <windows.h>
#include <string.h>

#include "osdep/subprocess.h"

#include "osdep/io.h"
#include "osdep/windows_utils.h"

#include "mpv_talloc.h"
#include "common/common.h"
#include "stream/stream.h"
#include "misc/bstr.h"
#include "misc/thread_tools.h"

// Internal CRT FD flags
#define FOPEN (0x01)
#define FPIPE (0x08)
#define FDEV  (0x40)

static void write_arg(bstr *cmdline, char *arg)
{
    // Empty args must be represented as an empty quoted string
    if (!arg[0]) {
        bstr_xappend(NULL, cmdline, bstr0("\"\""));
        return;
    }

    // If the string doesn't have characters that need to be escaped, it's best
    // to leave it alone for the sake of Windows programs that don't process
    // quoted args correctly.
    if (!strpbrk(arg, " \t\"")) {
        bstr_xappend(NULL, cmdline, bstr0(arg));
        return;
    }

    // If there are characters that need to be escaped, write a quoted string
    bstr_xappend(NULL, cmdline, bstr0("\""));

    // Escape the argument. To match the behavior of CommandLineToArgvW,
    // backslashes are only escaped if they appear before a quote or the end of
    // the string.
    int num_slashes = 0;
    for (int pos = 0; arg[pos]; pos++) {
        switch (arg[pos]) {
        case '\\':
            // Count consecutive backslashes
            num_slashes++;
            break;
        case '"':
            // Write the argument up to the point before the quote
            bstr_xappend(NULL, cmdline, (struct bstr){arg, pos});
            arg += pos;
            pos = 0;

            // Double backslashes preceding the quote
            for (int i = 0; i < num_slashes; i++)
                bstr_xappend(NULL, cmdline, bstr0("\\"));
            num_slashes = 0;

            // Escape the quote itself
            bstr_xappend(NULL, cmdline, bstr0("\\"));
            break;
        default:
            num_slashes = 0;
        }
    }

    // Write the rest of the argument
    bstr_xappend(NULL, cmdline, bstr0(arg));

    // Double backslashes at the end of the argument
    for (int i = 0; i < num_slashes; i++)
        bstr_xappend(NULL, cmdline, bstr0("\\"));

    bstr_xappend(NULL, cmdline, bstr0("\""));
}

// Convert an array of arguments to a properly escaped command-line string
static wchar_t *write_cmdline(void *ctx, char *argv0, char **args)
{
    // argv0 should always be quoted. Otherwise, arguments may be interpreted as
    // part of the program name. Also, it can't contain escape sequences.
    bstr cmdline = {0};
    bstr_xappend_asprintf(NULL, &cmdline, "\"%s\"", argv0);

    if (args) {
        for (int i = 0; args[i]; i++) {
            bstr_xappend(NULL, &cmdline, bstr0(" "));
            write_arg(&cmdline, args[i]);
        }
    }

    wchar_t *wcmdline = mp_from_utf8(ctx, cmdline.start);
    talloc_free(cmdline.start);
    return wcmdline;
}

static void delete_handle_list(void *p)
{
    LPPROC_THREAD_ATTRIBUTE_LIST list = p;
    DeleteProcThreadAttributeList(list);
}

// Create a PROC_THREAD_ATTRIBUTE_LIST that specifies exactly which handles are
// inherited by the subprocess
static LPPROC_THREAD_ATTRIBUTE_LIST create_handle_list(void *ctx,
                                                       HANDLE *handles, int num)
{
    // Get required attribute list size
    SIZE_T size = 0;
    if (!InitializeProcThreadAttributeList(NULL, 1, 0, &size)) {
        if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
            return NULL;
    }

    // Allocate attribute list
    LPPROC_THREAD_ATTRIBUTE_LIST list = talloc_size(ctx, size);
    if (!InitializeProcThreadAttributeList(list, 1, 0, &size))
        goto error;
    talloc_set_destructor(list, delete_handle_list);

    if (!UpdateProcThreadAttribute(list, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
                                   handles, num * sizeof(HANDLE), NULL, NULL))
        goto error;

    return list;
error:
    talloc_free(list);
    return NULL;
}

// Helper method similar to sparse_poll, skips NULL handles
static int sparse_wait(HANDLE *handles, unsigned num_handles)
{
    unsigned w_num_handles = 0;
    HANDLE w_handles[MP_SUBPROCESS_MAX_FDS + 2];
    int map[MP_SUBPROCESS_MAX_FDS + 2];
    if (num_handles > MP_ARRAY_SIZE(w_handles))
        return -1;

    for (unsigned i = 0; i < num_handles; i++) {
        if (!handles[i])
            continue;

        w_handles[w_num_handles] = handles[i];
        map[w_num_handles] = i;
        w_num_handles++;
    }

    if (w_num_handles == 0)
        return -1;
    DWORD i = WaitForMultipleObjects(w_num_handles, w_handles, FALSE, INFINITE);
    i -= WAIT_OBJECT_0;

    if (i >= w_num_handles)
        return -1;
    return map[i];
}

// Wrapper for ReadFile that treats ERROR_IO_PENDING as success
static int async_read(HANDLE file, void *buf, unsigned size, OVERLAPPED* ol)
{
    if (!ReadFile(file, buf, size, NULL, ol))
        return (GetLastError() == ERROR_IO_PENDING) ? 0 : -1;
    return 0;
}

static bool is_valid_handle(HANDLE h)
{
    // _get_osfhandle can return -2 "when the file descriptor is not associated
    // with a stream"
    return h && h != INVALID_HANDLE_VALUE && (intptr_t)h != -2;
}

static wchar_t *convert_environ(void *ctx, char **env)
{
    // Environment size in wchar_ts, including the trailing NUL
    size_t env_size = 1;

    for (int i = 0; env[i]; i++) {
        int count = MultiByteToWideChar(CP_UTF8, 0, env[i], -1, NULL, 0);
        if (count <= 0)
            abort();
        env_size += count;
    }

    wchar_t *ret = talloc_array(ctx, wchar_t, env_size);
    size_t pos = 0;

    for (int i = 0; env[i]; i++) {
        int count = MultiByteToWideChar(CP_UTF8, 0, env[i], -1,
                                        ret + pos, env_size - pos);
        if (count <= 0)
            abort();
        pos += count;
    }
    ret[pos] = L'\0';

    return ret;
}

void mp_subprocess2(struct mp_subprocess_opts *opts,
                    struct mp_subprocess_result *res)
{
    wchar_t *tmp = talloc_new(NULL);
    DWORD r;

    HANDLE share_hndls[MP_SUBPROCESS_MAX_FDS] = {0};
    int share_hndl_count = 0;
    HANDLE wait_hndls[MP_SUBPROCESS_MAX_FDS + 2] = {0};
    int wait_hndl_count = 0;

    struct {
        HANDLE handle;
        bool handle_close;
        char crt_flags;

        HANDLE read;
        OVERLAPPED read_ol;
        char *read_buf;
    } fd_data[MP_SUBPROCESS_MAX_FDS] = {0};

    // The maximum target FD is limited because FDs have to fit in two sparse
    // arrays in STARTUPINFO.lpReserved2, which has a maximum size of 65535
    // bytes. The first four bytes are the handle count, followed by one byte
    // per handle for flags, and an intptr_t per handle for the HANDLE itself.
    static const int crt_fd_max = (65535 - sizeof(int)) / (1 + sizeof(intptr_t));
    int crt_fd_count = 0;

    // If the function exits before CreateProcess, there was an init error
    *res = (struct mp_subprocess_result){ .error = MP_SUBPROCESS_EINIT };

    STARTUPINFOEXW si = {
        .StartupInfo = {
            .cb = sizeof si,
            .dwFlags = STARTF_USESTDHANDLES | STARTF_FORCEOFFFEEDBACK,
        },
    };

    PROCESS_INFORMATION pi = {0};

    for (int n = 0; n < opts->num_fds; n++) {
        if (opts->fds[n].fd >= crt_fd_max) {
            // Target FD is too big to fit in the CRT FD array
            res->error = MP_SUBPROCESS_EUNSUPPORTED;
            goto done;
        }

        if (opts->fds[n].fd >= crt_fd_count)
            crt_fd_count = opts->fds[n].fd + 1;

        if (opts->fds[n].src_fd >= 0) {
            HANDLE src_handle = (HANDLE)_get_osfhandle(opts->fds[n].src_fd);

            // Invalid handles are just ignored. This is because sometimes the
            // standard handles are invalid in Windows, like in GUI processes.
            // In this case mp_subprocess2 callers should still be able to
            // blindly forward the standard FDs.
            if (!is_valid_handle(src_handle))
                continue;

            DWORD type = GetFileType(src_handle);
            bool is_console_handle = false;
            switch (type & 0xff) {
            case FILE_TYPE_DISK:
                fd_data[n].crt_flags = FOPEN;
                break;
            case FILE_TYPE_CHAR:
                fd_data[n].crt_flags = FOPEN | FDEV;
                is_console_handle = GetConsoleMode(src_handle, &(DWORD){0});
                break;
            case FILE_TYPE_PIPE:
                fd_data[n].crt_flags = FOPEN | FPIPE;
                break;
            case FILE_TYPE_UNKNOWN:
                continue;
            }

            if (is_console_handle) {
                // Some Windows versions have bugs when duplicating console
                // handles, or when adding console handles to the CreateProcess
                // handle list, so just use the handle directly for now. Console
                // handles treat inheritance weirdly, so this should still work.
                fd_data[n].handle = src_handle;
            } else {
                // Instead of making the source handle inheritable, just
                // duplicate it to an inheritable handle
                if (!DuplicateHandle(GetCurrentProcess(), src_handle,
                                     GetCurrentProcess(), &fd_data[n].handle, 0,
                                     TRUE, DUPLICATE_SAME_ACCESS))
                    goto done;
                fd_data[n].handle_close = true;

                share_hndls[share_hndl_count++] = fd_data[n].handle;
            }

        } else if (opts->fds[n].on_read && !opts->detach) {
            fd_data[n].read_ol.hEvent = CreateEventW(NULL, TRUE, FALSE, NULL);
            if (!fd_data[n].read_ol.hEvent)
                goto done;

            struct w32_create_anon_pipe_opts o = {
                .server_flags = PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED,
                .client_inheritable = true,
            };
            if (!mp_w32_create_anon_pipe(&fd_data[n].read, &fd_data[n].handle, &o))
                goto done;
            fd_data[n].handle_close = true;

            wait_hndls[n] = fd_data[n].read_ol.hEvent;
            wait_hndl_count++;

            fd_data[n].crt_flags = FOPEN | FPIPE;
            fd_data[n].read_buf = talloc_size(tmp, 4096);

            share_hndls[share_hndl_count++] = fd_data[n].handle;

        } else {
            DWORD access;
            if (opts->fds[n].fd == 0) {
                access = FILE_GENERIC_READ;
            } else if (opts->fds[n].fd <= 2) {
                access = FILE_GENERIC_WRITE | FILE_READ_ATTRIBUTES;
            } else {
                access = FILE_GENERIC_READ | FILE_GENERIC_WRITE;
            }

            SECURITY_ATTRIBUTES sa = {
                .nLength = sizeof sa,
                .bInheritHandle = TRUE,
            };
            fd_data[n].crt_flags = FOPEN | FDEV;
            fd_data[n].handle = CreateFileW(L"NUL", access,
                                            FILE_SHARE_READ | FILE_SHARE_WRITE,
                                            &sa, OPEN_EXISTING, 0, NULL);
            fd_data[n].handle_close = true;
            share_hndls[share_hndl_count++] = fd_data[n].handle;
        }

        switch (opts->fds[n].fd) {
        case 0:
            si.StartupInfo.hStdInput = fd_data[n].handle;
            break;
        case 1:
            si.StartupInfo.hStdOutput = fd_data[n].handle;
            break;
        case 2:
            si.StartupInfo.hStdError = fd_data[n].handle;
            break;
        }
    }

    // Convert the UTF-8 environment into a UTF-16 Windows environment block
    wchar_t *env = NULL;
    if (opts->env)
        env = convert_environ(tmp, opts->env);

    // Convert the args array to a UTF-16 Windows command-line string
    char **args = opts->args && opts->args[0] ? &opts->args[1] : 0;
    wchar_t *cmdline = write_cmdline(tmp, opts->exe, args);

    // Get pointers to the arrays in lpReserved2. This is an undocumented data
    // structure used by MSVCRT (and other frameworks and runtimes) to emulate
    // FD inheritance. The format is unofficially documented here:
    // <https://web.archive.org/web/20221014190010/
    // https://www.catch22.net/tuts/undocumented-createprocess>
    si.StartupInfo.cbReserved2 = sizeof(int) + crt_fd_count * (1 + sizeof(intptr_t));
    si.StartupInfo.lpReserved2 = talloc_size(tmp, si.StartupInfo.cbReserved2);
    char *crt_buf_flags = si.StartupInfo.lpReserved2 + sizeof(int);
    char *crt_buf_hndls = crt_buf_flags + crt_fd_count;

    memcpy(si.StartupInfo.lpReserved2, &crt_fd_count, sizeof(int));

    // Fill the handle array with INVALID_HANDLE_VALUE, for unassigned handles
    for (int n = 0; n < crt_fd_count; n++) {
        HANDLE h = INVALID_HANDLE_VALUE;
        memcpy(crt_buf_hndls + n * sizeof(intptr_t), &h, sizeof(intptr_t));
    }

    for (int n = 0; n < opts->num_fds; n++) {
        crt_buf_flags[opts->fds[n].fd] = fd_data[n].crt_flags;
        memcpy(crt_buf_hndls + opts->fds[n].fd * sizeof(intptr_t),
               &fd_data[n].handle, sizeof(intptr_t));
    }

    DWORD flags = CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT;

    // Specify which handles are inherited by the subprocess. If this isn't
    // specified, the subprocess inherits all inheritable handles, which could
    // include handles created by other threads. See:
    // <https://web.archive.org/web/20121127222200/
    // http://blogs.msdn.com/b/oldnewthing/archive/2011/12/16/10248328.aspx>
    si.lpAttributeList = create_handle_list(tmp, share_hndls, share_hndl_count);

    // If we have a console, the subprocess will automatically attach to it so
    // it can receive Ctrl+C events. If we don't have a console, prevent the
    // subprocess from creating its own console window by specifying
    // CREATE_NO_WINDOW. GetConsoleCP() can be used to reliably determine if we
    // have a console or not (Cygwin uses it too.)
    if (!GetConsoleCP())
        flags |= CREATE_NO_WINDOW;

    if (!CreateProcessW(NULL, cmdline, NULL, NULL, TRUE, flags, env, NULL,
                        &si.StartupInfo, &pi))
        goto done;
    talloc_free(cmdline);
    talloc_free(env);
    talloc_free(si.StartupInfo.lpReserved2);
    talloc_free(si.lpAttributeList);
    CloseHandle(pi.hThread);

    for (int n = 0; n < opts->num_fds; n++) {
        if (fd_data[n].handle_close && is_valid_handle(fd_data[n].handle))
            CloseHandle(fd_data[n].handle);
        fd_data[n].handle = NULL;

        if (fd_data[n].read) {
            // Do the first read operation on each pipe
            if (async_read(fd_data[n].read, fd_data[n].read_buf, 4096,
                           &fd_data[n].read_ol))
            {
                CloseHandle(fd_data[n].read);
                wait_hndls[n] = fd_data[n].read = NULL;
                wait_hndl_count--;
            }
        }
    }

    if (opts->detach) {
        res->error = MP_SUBPROCESS_OK;
        goto done;
    }

    res->error = MP_SUBPROCESS_EGENERIC;

    wait_hndls[MP_SUBPROCESS_MAX_FDS] = pi.hProcess;
    wait_hndl_count++;

    if (opts->cancel)
        wait_hndls[MP_SUBPROCESS_MAX_FDS + 1] = mp_cancel_get_event(opts->cancel);

    DWORD exit_code;
    while (wait_hndl_count) {
        int n = sparse_wait(wait_hndls, MP_ARRAY_SIZE(wait_hndls));

        if (n >= 0 && n < MP_SUBPROCESS_MAX_FDS) {
            // Complete the read operation on the pipe
            if (!GetOverlappedResult(fd_data[n].read, &fd_data[n].read_ol, &r, TRUE)) {
                CloseHandle(fd_data[n].read);
                wait_hndls[n] = fd_data[n].read = NULL;
                wait_hndl_count--;
            } else {
                opts->fds[n].on_read(opts->fds[n].on_read_ctx,
                                     fd_data[n].read_buf, r);

                // Begin the next read operation on the pipe
                if (async_read(fd_data[n].read, fd_data[n].read_buf, 4096,
                               &fd_data[n].read_ol))
                {
                    CloseHandle(fd_data[n].read);
                    wait_hndls[n] = fd_data[n].read = NULL;
                    wait_hndl_count--;
                }
            }

        } else if (n == MP_SUBPROCESS_MAX_FDS) { // pi.hProcess
            GetExitCodeProcess(pi.hProcess, &exit_code);
            res->exit_status = exit_code;

            CloseHandle(pi.hProcess);
            wait_hndls[n] = pi.hProcess = NULL;
            wait_hndl_count--;

        } else if (n == MP_SUBPROCESS_MAX_FDS + 1) { // opts.cancel
            if (pi.hProcess) {
                TerminateProcess(pi.hProcess, 1);
                res->error = MP_SUBPROCESS_EKILLED_BY_US;
                goto done;
            }
        } else {
            goto done;
        }
    }

    res->error = MP_SUBPROCESS_OK;

done:
    for (int n = 0; n < opts->num_fds; n++) {
        if (is_valid_handle(fd_data[n].read)) {
            // Cancel any pending I/O (if the process was killed)
            CancelIo(fd_data[n].read);
            GetOverlappedResult(fd_data[n].read, &fd_data[n].read_ol, &r, TRUE);
            CloseHandle(fd_data[n].read);
        }
        if (fd_data[n].handle_close && is_valid_handle(fd_data[n].handle))
            CloseHandle(fd_data[n].handle);
        if (fd_data[n].read_ol.hEvent)
            CloseHandle(fd_data[n].read_ol.hEvent);
    }
    if (pi.hProcess)
        CloseHandle(pi.hProcess);
    talloc_free(tmp);
}
