/* GStreamer
 * Copyright (C) 2022 Seungha Yang <seungha@centricular.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 * SPDX-License-Identifier: MIT
 */

#include "win32ipcpipeclient.h"
#include "win32ipcutils.h"
#include <mutex>
#include <condition_variable>
#include <memory>
#include <thread>
#include <queue>
#include <string>

GST_DEBUG_CATEGORY_EXTERN (gst_win32_ipc_debug);
#define GST_CAT_DEFAULT gst_win32_ipc_debug

#define CONN_BUFFER_SIZE 1024

struct MmfInfo
{
  Win32IpcMmf *mmf;
  Win32IpcVideoInfo info;
};

struct ClientConnection : public OVERLAPPED
{
  ClientConnection () : pipe (INVALID_HANDLE_VALUE), to_read (0), to_write (0),
      seq_num (0)
  {
    OVERLAPPED *parent = dynamic_cast<OVERLAPPED *> (this);
    parent->Internal = 0;
    parent->InternalHigh = 0;
    parent->Offset = 0;
    parent->OffsetHigh = 0;
  }

  Win32IpcPipeClient *self;
  HANDLE pipe;
  UINT8 client_msg[CONN_BUFFER_SIZE];
  UINT32 to_read;
  UINT8 server_msg[CONN_BUFFER_SIZE];
  UINT32 to_write;
  UINT64 seq_num;
};

struct Win32IpcPipeClient
{
  explicit Win32IpcPipeClient (const std::string & n)
    : name (n), ref_count(1), last_err (ERROR_SUCCESS), flushing (FALSE)
    , stopped (FALSE), io_pending (FALSE)
  {
    release_event = CreateEventA (nullptr, FALSE, FALSE, nullptr);
    cancellable = CreateEventA (nullptr, TRUE, FALSE, nullptr);
    conn.pipe = INVALID_HANDLE_VALUE;
    conn.self = this;
  }

  ~Win32IpcPipeClient ()
  {
    GST_DEBUG ("Free client %p", this);
    SetEvent (cancellable);
    if (thread) {
      thread->join ();
      thread = nullptr;
    }

    last_err = ERROR_OPERATION_ABORTED;
    while (!queue.empty ()) {
      MmfInfo info = queue.front ();

      queue.pop ();
      win32_ipc_mmf_unref (info.mmf);
    }

    CloseHandle (release_event);
    CloseHandle (cancellable);
  }

  std::mutex lock;
  std::condition_variable cond;
  std::unique_ptr<std::thread> thread;
  std::queue<MmfInfo> queue;
  std::queue<std::string> unused_mmf;
  std::string name;

  ULONG ref_count;
  HANDLE release_event;
  HANDLE cancellable;
  UINT last_err;
  BOOL flushing;
  BOOL stopped;
  BOOL io_pending;
  ClientConnection conn;
};

static DWORD
win32_ipc_pipe_client_send_need_data_async (Win32IpcPipeClient * self);
static DWORD
win32_ipc_pipe_client_send_release_data_async (Win32IpcPipeClient * self,
    const char * mmf_name);

static VOID WINAPI
win32_ipc_pipe_client_send_finish (DWORD error_code, DWORD n_bytes,
    LPOVERLAPPED overlapped)
{
  ClientConnection *conn = (ClientConnection *) overlapped;
  Win32IpcPipeClient *self = conn->self;
  std::string unused_mmf;

  if (error_code != ERROR_SUCCESS) {
    std::string msg = win32_ipc_error_message (error_code);
    self->last_err = error_code;
    GST_WARNING ("Failed with 0x%x (%s)", self->last_err, msg.c_str ());
    goto error;
  }

  self->lock.lock ();
  if (!self->unused_mmf.empty ()) {
    unused_mmf = self->unused_mmf.front ();
    self->unused_mmf.pop ();
  }
  self->lock.unlock ();

  if (unused_mmf.size () > 0) {
    self->last_err = win32_ipc_pipe_client_send_release_data_async (self,
        unused_mmf.c_str ());
    if (self->last_err != ERROR_SUCCESS)
      goto error;

    return;
  }

  /* Don't request data anymore if we are stopped, but keep connection
   * to send release data message later */
  if (self->stopped) {
    GST_DEBUG ("We are stopped");
    self->io_pending = FALSE;
    return;
  }

  self->last_err = win32_ipc_pipe_client_send_need_data_async (self);
  if (self->last_err != ERROR_SUCCESS)
    goto error;

  /* All done, back to need-data state */
  return;

error:
  SetEvent (self->cancellable);
}

static DWORD
win32_ipc_pipe_client_send_release_data_async (Win32IpcPipeClient * self,
    const char * mmf_name)
{
  ClientConnection *conn = &self->conn;

  conn->to_write = win32_ipc_pkt_build_release_data (conn->client_msg,
      CONN_BUFFER_SIZE, conn->seq_num, mmf_name);
  if (conn->to_write == 0) {
    GST_ERROR ("Couldn't build RELEASE-DATA pkt");
    return ERROR_BAD_FORMAT;
  }

  GST_TRACE ("Sending RELEASE-DATA");

  if (!WriteFileEx (conn->pipe, conn->client_msg, conn->to_write,
      (OVERLAPPED *) conn, win32_ipc_pipe_client_send_finish)) {
    UINT last_err = GetLastError ();
    std::string msg = win32_ipc_error_message (last_err);
    GST_WARNING ("WriteFileEx failed with 0x%x (%s)", last_err, msg.c_str ());
    return last_err;
  }

  return ERROR_SUCCESS;
}

static DWORD
win32_ipc_pipe_client_send_read_done_async (Win32IpcPipeClient * self)
{
  ClientConnection *conn = &self->conn;

  conn->to_write = win32_ipc_pkt_build_read_done (conn->client_msg,
      CONN_BUFFER_SIZE, conn->seq_num);
  if (conn->to_write == 0) {
    GST_ERROR ("Couldn't build READ-DONE pkt");
    return ERROR_BAD_FORMAT;
  }

  GST_TRACE ("Sending READ-DONE");

  if (!WriteFileEx (conn->pipe, conn->client_msg, conn->to_write,
      (OVERLAPPED *) conn, win32_ipc_pipe_client_send_finish)) {
    UINT last_err = GetLastError ();
    std::string msg = win32_ipc_error_message (last_err);

    GST_WARNING ("WriteFileEx failed with 0x%x (%s)", last_err, msg.c_str ());
    return last_err;
  }

  return ERROR_SUCCESS;
}

static VOID WINAPI
win32_ipc_pipe_client_receive_have_data_finish (DWORD error_code, DWORD n_bytes,
    LPOVERLAPPED overlapped)
{
  ClientConnection *conn = (ClientConnection *) overlapped;
  Win32IpcPipeClient *self = conn->self;
  char mmf_name[1024] = { '\0', };
  Win32IpcVideoInfo info;
  Win32IpcMmf *mmf;
  MmfInfo minfo;

  if (error_code != ERROR_SUCCESS) {
    std::string msg = win32_ipc_error_message (error_code);
    self->last_err = error_code;
    GST_WARNING ("HAVE-DATA failed with 0x%x (%s)",
        self->last_err, msg.c_str ());
    goto error;
  }

  if (!win32_ipc_pkt_parse_have_data (conn->server_msg, n_bytes,
      &conn->seq_num, mmf_name, &info)) {
    self->last_err = ERROR_BAD_FORMAT;
    GST_WARNING ("Couldn't parse HAVE-DATA pkt");
    goto error;
  }

  mmf = win32_ipc_mmf_open (info.size, mmf_name);
  if (!mmf) {
    GST_ERROR ("Couldn't open file %s", mmf_name);
    self->last_err = ERROR_BAD_FORMAT;
    goto error;
  }

  GST_TRACE ("Got HAVE-DATA %s", mmf_name);

  minfo.mmf = mmf;
  minfo.info = info;

  {
    std::lock_guard<std::mutex> lk (self->lock);
    /* Drops too old data */
    while (self->queue.size () > 5) {
      MmfInfo info = self->queue.front ();

      self->queue.pop ();
      win32_ipc_mmf_unref (info.mmf);
    }

    self->queue.push (minfo);
    self->cond.notify_all ();
  }

  self->last_err = win32_ipc_pipe_client_send_read_done_async (self);
  if (self->last_err != ERROR_SUCCESS)
    goto error;

  return;

error:
  SetEvent (self->cancellable);
}

static DWORD
win32_ipc_pipe_client_receive_have_data_async (Win32IpcPipeClient * self)
{
  ClientConnection *conn = &self->conn;

  GST_TRACE ("Waiting HAVE-DATA");

  if (!ReadFileEx (conn->pipe, conn->server_msg, CONN_BUFFER_SIZE,
      (OVERLAPPED *) conn, win32_ipc_pipe_client_receive_have_data_finish)) {
    UINT last_err = GetLastError ();
    std::string msg = win32_ipc_error_message (last_err);
    GST_WARNING ("ReadFileEx failed with 0x%x (%s)", last_err, msg.c_str ());
    return last_err;
  }

  return ERROR_SUCCESS;
}

static VOID WINAPI
pipe_clinet_send_need_data_finish (DWORD error_code, DWORD n_bytes,
    LPOVERLAPPED overlapped)
{
  ClientConnection *conn = (ClientConnection *) overlapped;
  Win32IpcPipeClient *self = conn->self;

  if (error_code != ERROR_SUCCESS) {
    std::string msg = win32_ipc_error_message (error_code);
    self->last_err = error_code;
    GST_WARNING ("NEED-DATA failed with 0x%x (%s)",
        self->last_err, msg.c_str ());
    goto error;
  }

  self->last_err = win32_ipc_pipe_client_receive_have_data_async (self);
  if (self->last_err != ERROR_SUCCESS)
    goto error;

  return;

error:
  SetEvent (self->cancellable);
}

static DWORD
win32_ipc_pipe_client_send_need_data_async (Win32IpcPipeClient * self)
{
  ClientConnection *conn = &self->conn;

  conn->to_write = win32_ipc_pkt_build_need_data (conn->client_msg,
      CONN_BUFFER_SIZE, conn->seq_num);
  if (conn->to_write == 0) {
    GST_ERROR ("Couldn't build NEED-DATA pkt");
    return ERROR_BAD_FORMAT;
  }

  GST_TRACE ("Sending NEED-DATA");

  if (!WriteFileEx (conn->pipe, conn->client_msg, conn->to_write,
      (OVERLAPPED *) conn, pipe_clinet_send_need_data_finish)) {
    UINT last_err = GetLastError ();
    std::string msg = win32_ipc_error_message (last_err);
    GST_WARNING ("WriteFileEx failed with 0x%x (%s)", last_err, msg.c_str ());
    return last_err;
  }

  return ERROR_SUCCESS;
}

static VOID
win32_ipc_pipe_client_loop (Win32IpcPipeClient * self)
{
  DWORD mode = PIPE_READMODE_MESSAGE;
  std::unique_lock<std::mutex> lk (self->lock);
  ClientConnection *conn = &self->conn;
  HANDLE waitables[2];
  DWORD wait_ret;

  conn->pipe = CreateFileA (self->name.c_str (),
        GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING,
        FILE_FLAG_OVERLAPPED, nullptr);
  self->last_err = GetLastError ();
  if (conn->pipe == INVALID_HANDLE_VALUE) {
    std::string msg = win32_ipc_error_message (self->last_err);
    GST_WARNING ("CreateFileA failed with 0x%x (%s)", self->last_err,
        msg.c_str ());
    self->cond.notify_all ();
    return;
  }

  if (!SetNamedPipeHandleState (conn->pipe, &mode, nullptr, nullptr)) {
    self->last_err = GetLastError ();
    std::string msg = win32_ipc_error_message (self->last_err);
    GST_WARNING ("SetNamedPipeHandleState failed with 0x%x (%s)",
        self->last_err, msg.c_str ());
    CloseHandle (conn->pipe);
    conn->pipe = INVALID_HANDLE_VALUE;
    self->cond.notify_all ();
    return;
  }

  self->last_err = ERROR_SUCCESS;
  self->cond.notify_all ();
  lk.unlock ();

  /* Once connection is established, send NEED-DATA message to server,
   * and then it will loop NEED-DATA -> HAVE-DATA -> READ-DONE */
  self->last_err = win32_ipc_pipe_client_send_need_data_async (self);
  if (self->last_err != ERROR_SUCCESS)
    goto out;

  self->io_pending = TRUE;
  waitables[0] = self->cancellable;
  waitables[1] = self->release_event;

  do {
    /* Enters alertable thread state and wait for I/O completion event
     * or cancellable event */
    wait_ret = WaitForMultipleObjectsEx (2, waitables, FALSE, INFINITE, TRUE);
    if (wait_ret == WAIT_OBJECT_0) {
      GST_DEBUG ("Operation cancelled");
      goto out;
    }

    switch (wait_ret) {
      case WAIT_OBJECT_0 + 1:
      case WAIT_IO_COMPLETION:
      {
        std::string unused_mmf;
        /* If I/O chain is stopped, send release data message here */
        if (!self->io_pending) {
          lk.lock ();
          if (!self->unused_mmf.empty ()) {
            unused_mmf = self->unused_mmf.front ();
            self->unused_mmf.pop ();
          }
          lk.unlock ();
        }

        if (unused_mmf.size () > 0) {
          GST_DEBUG ("Sending release data for %s", unused_mmf.c_str ());
          self->io_pending = TRUE;
          self->last_err = win32_ipc_pipe_client_send_release_data_async (self,
              unused_mmf.c_str ());
          if (self->last_err != ERROR_SUCCESS)
            goto out;
        }
        break;
      }
      default:
        GST_WARNING ("Unexpected wait return 0x%x", (UINT) wait_ret);
        goto out;
    }
  } while (true);

out:
  if (conn->pipe != INVALID_HANDLE_VALUE) {
    CancelIoEx (conn->pipe, (OVERLAPPED *) &conn);
    CloseHandle (conn->pipe);
  }

  lk.lock ();
  self->last_err = ERROR_OPERATION_ABORTED;
  conn->pipe = INVALID_HANDLE_VALUE;
  self->io_pending = FALSE;
  self->cond.notify_all ();
}

static BOOL
win32_ipc_pipe_client_run (Win32IpcPipeClient * self)
{
  std::unique_lock<std::mutex> lk (self->lock);

  self->thread = std::make_unique<std::thread>
      (std::thread (win32_ipc_pipe_client_loop, self));
  self->cond.wait (lk);

  if (self->last_err != ERROR_SUCCESS) {
    self->thread->join ();
    self->thread = nullptr;
    return FALSE;
  }

  return TRUE;
}

Win32IpcPipeClient *
win32_ipc_pipe_client_new (const char * pipe_name)
{
  Win32IpcPipeClient *self;

  if (!pipe_name) {
    GST_ERROR ("Pipe name must be specified");
    return nullptr;
  }

  self = new Win32IpcPipeClient (pipe_name);

  if (!win32_ipc_pipe_client_run (self)) {
    win32_ipc_pipe_client_unref (self);
    return nullptr;
  }

  return self;
}

Win32IpcPipeClient *
win32_ipc_pipe_client_ref (Win32IpcPipeClient * client)
{
  InterlockedIncrement (&client->ref_count);

  return client;
}

void
win32_ipc_pipe_client_unref (Win32IpcPipeClient * client)
{
  ULONG ref_count;

  ref_count = InterlockedDecrement (&client->ref_count);
  if (ref_count == 0)
    delete client;
}

void
win32_ipc_pipe_client_set_flushing (Win32IpcPipeClient * client, BOOL flushing)
{
  std::lock_guard<std::mutex> lk (client->lock);
  client->flushing = flushing;
  client->cond.notify_all ();
}

BOOL
win32_ipc_pipe_client_get_mmf (Win32IpcPipeClient * client, Win32IpcMmf ** mmf,
    Win32IpcVideoInfo * info)
{
  std::unique_lock<std::mutex> lk (client->lock);
  if (client->last_err != ERROR_SUCCESS) {
    GST_WARNING ("Last error code was 0x%x", client->last_err);
    return FALSE;
  }

  while (client->queue.empty () && client->last_err == ERROR_SUCCESS &&
      !client->flushing && !client->stopped) {
    client->cond.wait (lk);
  }

  if (client->queue.empty ())
    return FALSE;

  MmfInfo mmf_info = client->queue.front ();
  client->queue.pop ();

  *mmf = mmf_info.mmf;
  *info = mmf_info.info;

  return TRUE;
}

void
win32_ipc_pipe_client_release_mmf (Win32IpcPipeClient * client,
    Win32IpcMmf * mmf)
{
  std::string name = win32_ipc_mmf_get_name (mmf);

  win32_ipc_mmf_unref (mmf);

  std::lock_guard<std::mutex> lk (client->lock);
  if (client->last_err != ERROR_SUCCESS)
    return;

  GST_LOG ("Enqueue release data %s", name.c_str ());
  client->unused_mmf.push (name);
  SetEvent (client->release_event);
}

void
win32_ipc_pipe_client_stop (Win32IpcPipeClient * client)
{
  GST_DEBUG ("Stopping %p", client);

  std::lock_guard<std::mutex> lk (client->lock);
  client->stopped = TRUE;
  client->cond.notify_all ();
}