/*
 * 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 <sddl.h>

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

#include "common/common.h"
#include "common/global.h"
#include "common/msg.h"
#include "input/input.h"
#include "mpv/client.h"
#include "options/m_config.h"
#include "options/options.h"
#include "player/client.h"

struct mp_ipc_ctx {
    struct mp_log *log;
    struct mp_client_api *client_api;
    const wchar_t *path;

    mp_thread thread;
    HANDLE death_event;
};

struct client_arg {
    struct mp_log *log;
    struct mpv_handle *client;

    char *client_name;
    HANDLE client_h;
    bool writable;
    OVERLAPPED write_ol;
};

// Get a string SID representing the current user. Must be freed by LocalFree.
static char *get_user_sid(void)
{
    char *ssid = NULL;
    TOKEN_USER *info = NULL;
    HANDLE t = INVALID_HANDLE_VALUE;
    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &t))
        goto done;

    DWORD info_len;
    if (!GetTokenInformation(t, TokenUser, NULL, 0, &info_len) &&
        GetLastError() != ERROR_INSUFFICIENT_BUFFER)
        goto done;

    info = talloc_size(NULL, info_len);
    if (!GetTokenInformation(t, TokenUser, info, info_len, &info_len))
        goto done;
    if (!info->User.Sid)
        goto done;

    ConvertSidToStringSidA(info->User.Sid, &ssid);
done:
    if (t != INVALID_HANDLE_VALUE)
        CloseHandle(t);
    talloc_free(info);
    return ssid;
}

// Get a string SID for the process integrity level. Must be freed by LocalFree.
static char *get_integrity_sid(void)
{
    char *ssid = NULL;
    TOKEN_MANDATORY_LABEL *info = NULL;
    HANDLE t = INVALID_HANDLE_VALUE;
    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &t))
        goto done;

    DWORD info_len;
    if (!GetTokenInformation(t, TokenIntegrityLevel, NULL, 0, &info_len) &&
        GetLastError() != ERROR_INSUFFICIENT_BUFFER)
        goto done;

    info = talloc_size(NULL, info_len);
    if (!GetTokenInformation(t, TokenIntegrityLevel, info, info_len, &info_len))
        goto done;
    if (!info->Label.Sid)
        goto done;

    ConvertSidToStringSidA(info->Label.Sid, &ssid);
done:
    if (t != INVALID_HANDLE_VALUE)
        CloseHandle(t);
    talloc_free(info);
    return ssid;
}

// Create a security descriptor that only grants access to processes running
// under the current user at the current integrity level or higher
static PSECURITY_DESCRIPTOR create_restricted_sd(void)
{
    char *user_sid = get_user_sid();
    char *integrity_sid = get_integrity_sid();
    if (!user_sid || !integrity_sid)
        return NULL;

    char *sddl = talloc_asprintf(NULL,
        "O:%s"                 // Set the owner to user_sid
        "D:(A;;GRGW;;;%s)"     // Grant GENERIC_{READ,WRITE} access to user_sid
        "S:(ML;;NRNWNX;;;%s)", // Disallow read, write and execute permissions
                               // to integrity levels below integrity_sid
        user_sid, user_sid, integrity_sid);
    LocalFree(user_sid);
    LocalFree(integrity_sid);

    PSECURITY_DESCRIPTOR sd = NULL;
    ConvertStringSecurityDescriptorToSecurityDescriptorA(sddl, SDDL_REVISION_1,
        &sd, NULL);
    talloc_free(sddl);

    return sd;
}

static void wakeup_cb(void *d)
{
    HANDLE event = d;
    SetEvent(event);
}

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

// Wrapper for WriteFile that treats ERROR_IO_PENDING as success
static DWORD async_write(HANDLE file, const void *buf, unsigned size, OVERLAPPED* ol)
{
    DWORD err = WriteFile(file, buf, size, NULL, ol) ? 0 : GetLastError();
    return err == ERROR_IO_PENDING ? 0 : err;
}

static bool pipe_error_is_fatal(DWORD error)
{
    switch (error) {
    case 0:
    case ERROR_HANDLE_EOF:
    case ERROR_BROKEN_PIPE:
    case ERROR_PIPE_NOT_CONNECTED:
    case ERROR_NO_DATA:
        return false;
    }
    return true;
}

static DWORD ipc_write_str(struct client_arg *arg, const char *buf)
{
    DWORD error = 0;

    if ((error = async_write(arg->client_h, buf, strlen(buf), &arg->write_ol)))
        goto done;
    if (!GetOverlappedResult(arg->client_h, &arg->write_ol, &(DWORD){0}, TRUE)) {
        error = GetLastError();
        goto done;
    }

done:
    if (pipe_error_is_fatal(error)) {
        MP_VERBOSE(arg, "Error writing to pipe: %s\n",
            mp_HRESULT_to_str(HRESULT_FROM_WIN32(error)));
    }

    if (error)
        arg->writable = false;
    return error;
}

static void report_read_error(struct client_arg *arg, DWORD error)
{
    // Only report the error if it's not just due to the pipe closing
    if (pipe_error_is_fatal(error)) {
        MP_ERR(arg, "Error reading from pipe: %s\n",
            mp_HRESULT_to_str(HRESULT_FROM_WIN32(error)));
    } else {
        MP_VERBOSE(arg, "Client disconnected\n");
    }
}

static MP_THREAD_VOID client_thread(void *p)
{
    struct client_arg *arg = p;
    char buf[4096];
    HANDLE wakeup_event = CreateEventW(NULL, TRUE, FALSE, NULL);
    OVERLAPPED ol = { .hEvent = CreateEventW(NULL, TRUE, TRUE, NULL) };
    bstr client_msg = { talloc_strdup(NULL, ""), 0 };
    DWORD ioerr = 0;
    DWORD r;

    char *tname = talloc_asprintf(NULL, "ipc/%s", arg->client_name);
    mp_thread_set_name(tname);
    talloc_free(tname);

    arg->write_ol.hEvent = CreateEventW(NULL, TRUE, TRUE, NULL);
    if (!wakeup_event || !ol.hEvent || !arg->write_ol.hEvent) {
        MP_ERR(arg, "Couldn't create events\n");
        goto done;
    }

    MP_VERBOSE(arg, "Client connected\n");

    mpv_set_wakeup_callback(arg->client, wakeup_cb, wakeup_event);

    // Do the first read operation on the pipe
    if ((ioerr = async_read(arg->client_h, buf, 4096, &ol))) {
        report_read_error(arg, ioerr);
        goto done;
    }

    while (1) {
        HANDLE handles[] = { wakeup_event, ol.hEvent };
        int n = WaitForMultipleObjects(2, handles, FALSE, 0);
        if (n == WAIT_TIMEOUT)
            n = WaitForMultipleObjects(2, handles, FALSE, INFINITE);

        switch (n) {
        case WAIT_OBJECT_0: // wakeup_event
            ResetEvent(wakeup_event);

            while (1) {
                mpv_event *event = mpv_wait_event(arg->client, 0);

                if (event->event_id == MPV_EVENT_NONE)
                    break;

                if (event->event_id == MPV_EVENT_SHUTDOWN)
                    goto done;

                if (!arg->writable)
                    continue;

                char *event_msg = mp_json_encode_event(event);
                if (!event_msg) {
                    MP_ERR(arg, "Encoding error\n");
                    goto done;
                }

                ipc_write_str(arg, event_msg);
                talloc_free(event_msg);
            }

            break;
        case WAIT_OBJECT_0 + 1: // ol.hEvent
            // Complete the read operation on the pipe
            if (!GetOverlappedResult(arg->client_h, &ol, &r, TRUE)) {
                report_read_error(arg, GetLastError());
                goto done;
            }

            bstr_xappend(NULL, &client_msg, (bstr){buf, r});
            while (bstrchr(client_msg, '\n') != -1) {
                char *reply_msg = mp_ipc_consume_next_command(arg->client,
                    NULL, &client_msg);
                if (reply_msg && arg->writable)
                    ipc_write_str(arg, reply_msg);
                talloc_free(reply_msg);
            }

            // Begin the next read operation on the pipe
            if ((ioerr = async_read(arg->client_h, buf, 4096, &ol))) {
                report_read_error(arg, ioerr);
                goto done;
            }
            break;
        default:
            MP_ERR(arg, "WaitForMultipleObjects failed\n");
            goto done;
        }
    }

done:
    if (client_msg.len > 0)
        MP_WARN(arg, "Ignoring unterminated command on disconnect.\n");

    if (CancelIoEx(arg->client_h, &ol) || GetLastError() != ERROR_NOT_FOUND)
        GetOverlappedResult(arg->client_h, &ol, &(DWORD){0}, TRUE);
    if (wakeup_event)
        CloseHandle(wakeup_event);
    if (ol.hEvent)
        CloseHandle(ol.hEvent);
    if (arg->write_ol.hEvent)
        CloseHandle(arg->write_ol.hEvent);

    CloseHandle(arg->client_h);
    mpv_handle *client = arg->client;
    talloc_free(arg);
    mpv_destroy(client);
    MP_THREAD_RETURN();
}

static void ipc_start_client(struct mp_ipc_ctx *ctx, struct client_arg *client)
{
    client->client = mp_new_client(ctx->client_api, client->client_name);
    if (!client->client)
        goto err;
    client->log = mp_client_get_log(client->client);

    mp_thread client_thr;
    if (mp_thread_create(&client_thr, client_thread, client))
        goto err;
    mp_thread_detach(client_thr);
    return;

err:
    mpv_destroy(client->client);
    CloseHandle(client->client_h);
    talloc_free(client);
}

static void ipc_start_client_json(struct mp_ipc_ctx *ctx, int id, HANDLE h)
{
    struct client_arg *client = talloc_ptrtype(NULL, client);
    *client = (struct client_arg){
        .client_name = talloc_asprintf(client, "ipc-%d", id),
        .client_h = h,
        .writable = true,
    };

    ipc_start_client(ctx, client);
}

bool mp_ipc_start_anon_client(struct mp_ipc_ctx *ctx, struct mpv_handle *h,
                              int out_fd[2])
{
    return false;
}

static MP_THREAD_VOID ipc_thread(void *p)
{
    // Use PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE so message framing is
    // maintained for message-mode clients, but byte-mode clients can still
    // connect, send and receive data. This is the most compatible mode.
    static const DWORD state =
        PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE | PIPE_WAIT |
        PIPE_REJECT_REMOTE_CLIENTS;
    static const DWORD mode =
        PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED;
    static const DWORD bufsiz = 4096;

    struct mp_ipc_ctx *arg = p;
    HANDLE server = INVALID_HANDLE_VALUE;
    HANDLE client = INVALID_HANDLE_VALUE;
    int client_num = 0;

    mp_thread_set_name("ipc/named-pipe");
    MP_VERBOSE(arg, "Starting IPC master\n");

    OVERLAPPED ol = {0};
    SECURITY_ATTRIBUTES sa = {
        .nLength = sizeof sa,
        .lpSecurityDescriptor = create_restricted_sd(),
    };
    if (!sa.lpSecurityDescriptor) {
        MP_ERR(arg, "Couldn't create security descriptor");
        goto done;
    }

    ol = (OVERLAPPED){ .hEvent = CreateEventW(NULL, TRUE, TRUE, NULL) };
    if (!ol.hEvent) {
        MP_ERR(arg, "Couldn't create event");
        goto done;
    }

    server = CreateNamedPipeW(arg->path, mode | FILE_FLAG_FIRST_PIPE_INSTANCE,
        state, PIPE_UNLIMITED_INSTANCES, bufsiz, bufsiz, 0, &sa);
    if (server == INVALID_HANDLE_VALUE) {
        MP_ERR(arg, "Couldn't create first pipe instance: %s\n",
            mp_LastError_to_str());
        goto done;
    }

    MP_VERBOSE(arg, "Listening to IPC pipe.\n");

    while (1) {
        DWORD err = ConnectNamedPipe(server, &ol) ? 0 : GetLastError();

        if (err == ERROR_IO_PENDING) {
            int n = WaitForMultipleObjects(2, (HANDLE[]) {
                arg->death_event,
                ol.hEvent,
            }, FALSE, INFINITE) - WAIT_OBJECT_0;

            switch (n) {
            case 0:
                // Stop waiting for new clients
                CancelIo(server);
                GetOverlappedResult(server, &ol, &(DWORD){0}, TRUE);
                goto done;
            case 1:
                // Complete the ConnectNamedPipe request
                err = GetOverlappedResult(server, &ol, &(DWORD){0}, TRUE)
                    ? 0 : GetLastError();
                break;
            default:
                MP_ERR(arg, "WaitForMultipleObjects failed\n");
                goto done;
            }
        }

        // ERROR_PIPE_CONNECTED is returned if a client connects before
        // ConnectNamedPipe is called. ERROR_NO_DATA is returned if a client
        // connects, (possibly) writes data and exits before ConnectNamedPipe
        // is called. Both cases should be handled as normal connections.
        if (err == ERROR_PIPE_CONNECTED || err == ERROR_NO_DATA)
            err = 0;

        if (err) {
            MP_ERR(arg, "ConnectNamedPipe failed: %s\n",
                mp_HRESULT_to_str(HRESULT_FROM_WIN32(err)));
            goto done;
        }

        // Create the next pipe instance before the client thread to avoid the
        // theoretical race condition where the client thread immediately
        // closes the handle and there are no active instances of the pipe
        client = server;
        server = CreateNamedPipeW(arg->path, mode, state,
            PIPE_UNLIMITED_INSTANCES, bufsiz, bufsiz, 0, &sa);
        if (server == INVALID_HANDLE_VALUE) {
            MP_ERR(arg, "Couldn't create additional pipe instance: %s\n",
                mp_LastError_to_str());
            goto done;
        }

        ipc_start_client_json(arg, client_num++, client);
        client = NULL;
    }

done:
    if (sa.lpSecurityDescriptor)
        LocalFree(sa.lpSecurityDescriptor);
    if (client != INVALID_HANDLE_VALUE)
        CloseHandle(client);
    if (server != INVALID_HANDLE_VALUE)
        CloseHandle(server);
    if (ol.hEvent)
        CloseHandle(ol.hEvent);
    MP_THREAD_RETURN();
}

struct mp_ipc_ctx *mp_init_ipc(struct mp_client_api *client_api,
                               struct mpv_global *global)
{
    struct MPOpts *opts = mp_get_config_group(NULL, global, &mp_opt_root);

    struct mp_ipc_ctx *arg = talloc_ptrtype(NULL, arg);
    *arg = (struct mp_ipc_ctx){
        .log = mp_log_new(arg, global->log, "ipc"),
        .client_api = client_api,
    };

    if (opts->ipc_client && opts->ipc_client[0]) {
        int fd = -1;
        bstr str = bstr0(opts->ipc_client);
        if (bstr_eatstart0(&str, "fd://") && str.len) {
            long long ll = bstrtoll(str, &str, 0);
            if (!str.len && ll >= 0 && ll <= INT_MAX)
                fd = ll;
        }
        if (fd < 0) {
            MP_ERR(arg, "Invalid IPC client argument: '%s'\n", opts->ipc_client);
        } else {
            HANDLE h = (HANDLE)_get_osfhandle(fd);
            if (h && h != INVALID_HANDLE_VALUE && (intptr_t)h != -2)
                ipc_start_client_json(arg, -1, h);
            else
                MP_ERR(arg, "Invalid IPC client fd: '%d'\n", fd);
        }
    }

    if (!opts->ipc_path || !*opts->ipc_path)
        goto out;

    // Ensure the path is a legal Win32 pipe name by prepending \\.\pipe\ if
    // it's not already present. Qt's QLocalSocket uses the same logic, so
    // cross-platform programs that use paths like /tmp/mpv-socket should just
    // work. (Win32 converts this path to \Device\NamedPipe\tmp\mpv-socket)
    if (!strncmp(opts->ipc_path, "\\\\.\\pipe\\", 9)) {
        arg->path = mp_from_utf8(arg, opts->ipc_path);
    } else {
        char *path = talloc_asprintf(NULL, "\\\\.\\pipe\\%s", opts->ipc_path);
        arg->path = mp_from_utf8(arg, path);
        talloc_free(path);
    }

    if (!(arg->death_event = CreateEventW(NULL, TRUE, FALSE, NULL)))
        goto out;

    if (mp_thread_create(&arg->thread, ipc_thread, arg))
        goto out;

    talloc_free(opts);
    return arg;

out:
    if (arg->death_event)
        CloseHandle(arg->death_event);
    talloc_free(arg);
    talloc_free(opts);
    return NULL;
}

void mp_uninit_ipc(struct mp_ipc_ctx *arg)
{
    if (!arg)
        return;

    SetEvent(arg->death_event);
    mp_thread_join(arg->thread);

    CloseHandle(arg->death_event);
    talloc_free(arg);
}
