//===-- MainLoopWindows.cpp -----------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "lldb/Host/windows/MainLoopWindows.h"
#include "lldb/Host/Config.h"
#include "lldb/Host/Socket.h"
#include "lldb/Host/windows/windows.h"
#include "lldb/Utility/Status.h"
#include "llvm/Config/llvm-config.h"
#include "llvm/Support/WindowsError.h"
#include <algorithm>
#include <atomic>
#include <cassert>
#include <ctime>
#include <io.h>
#include <synchapi.h>
#include <thread>
#include <vector>
#include <winbase.h>
#include <winerror.h>
#include <winsock2.h>

using namespace lldb;
using namespace lldb_private;

static DWORD ToTimeout(std::optional<MainLoopWindows::TimePoint> point) {
  using namespace std::chrono;

  if (!point)
    return WSA_INFINITE;

  nanoseconds dur = (std::max)(*point - steady_clock::now(), nanoseconds(0));
  return ceil<milliseconds>(dur).count();
}

namespace {

class PipeEvent : public MainLoopWindows::IOEvent {
public:
  explicit PipeEvent(HANDLE handle)
      : IOEvent(CreateEventW(NULL, /*bManualReset=*/TRUE,
                             /*bInitialState=*/FALSE, NULL)),
        m_handle(handle), m_ready(CreateEventW(NULL, /*bManualReset=*/TRUE,
                                               /*bInitialState=*/FALSE, NULL)) {
    assert(m_event && m_ready);
    m_monitor_thread = std::thread(&PipeEvent::Monitor, this);
  }

  ~PipeEvent() override {
    if (m_monitor_thread.joinable()) {
      m_stopped = true;
      SetEvent(m_ready);
      // Keep trying to cancel ReadFile() until the thread exits.
      do {
        CancelIoEx(m_handle, /*lpOverlapped=*/NULL);
      } while (WaitForSingleObject(m_monitor_thread.native_handle(), 1) ==
               WAIT_TIMEOUT);
      m_monitor_thread.join();
    }
    CloseHandle(m_event);
    CloseHandle(m_ready);
  }

  void WillPoll() override {
    if (WaitForSingleObject(m_event, /*dwMilliseconds=*/0) != WAIT_TIMEOUT) {
      // The thread has already signalled that the data is available. No need
      // for further polling until we consume that event.
      return;
    }
    if (WaitForSingleObject(m_ready, /*dwMilliseconds=*/0) != WAIT_TIMEOUT) {
      // The thread is already waiting for data to become available.
      return;
    }
    // Start waiting.
    SetEvent(m_ready);
  }

  void Disarm() override { ResetEvent(m_event); }

  /// Monitors the handle performing a zero byte read to determine when data is
  /// avaiable.
  void Monitor() {
    // Wait until the MainLoop tells us to start.
    WaitForSingleObject(m_ready, INFINITE);

    do {
      char buf[1];
      DWORD bytes_read = 0;
      OVERLAPPED ov;
      ZeroMemory(&ov, sizeof(ov));
      // Block on a 0-byte read; this will only resume when data is
      // available in the pipe. The pipe must be PIPE_WAIT or this thread
      // will spin.
      BOOL success =
          ReadFile(m_handle, buf, /*nNumberOfBytesToRead=*/0, &bytes_read, &ov);
      DWORD bytes_available = 0;
      DWORD err = GetLastError();
      if (!success && err == ERROR_IO_PENDING) {
        success = GetOverlappedResult(m_handle, &ov, &bytes_read,
                                      /*bWait=*/TRUE);
        err = GetLastError();
      }
      if (success) {
        success =
            PeekNamedPipe(m_handle, NULL, 0, NULL, &bytes_available, NULL);
        err = GetLastError();
      }
      if (success) {
        if (bytes_available == 0) {
          // This can happen with a zero-byte write. Try again.
          continue;
        }
      } else if (err == ERROR_NO_DATA) {
        // The pipe is nonblocking. Try again.
        Sleep(0);
        continue;
      } else if (err == ERROR_OPERATION_ABORTED) {
        // Read may have been cancelled, try again.
        continue;
      }

      // Notify that data is available on the pipe. It's important to set this
      // before clearing m_ready to avoid a race with WillPoll.
      SetEvent(m_event);
      // Stop polling until we're told to resume.
      ResetEvent(m_ready);

      // Wait until the current read is consumed before doing the next read.
      WaitForSingleObject(m_ready, INFINITE);
    } while (!m_stopped);
  }

private:
  HANDLE m_handle;
  HANDLE m_ready;
  std::thread m_monitor_thread;
  std::atomic<bool> m_stopped = false;
};

class SocketEvent : public MainLoopWindows::IOEvent {
public:
  explicit SocketEvent(SOCKET socket)
      : IOEvent(WSACreateEvent()), m_socket(socket) {
    assert(m_event != WSA_INVALID_EVENT);
  }

  ~SocketEvent() override { WSACloseEvent(m_event); }

  void WillPoll() override {
    int result =
        WSAEventSelect(m_socket, m_event, FD_READ | FD_ACCEPT | FD_CLOSE);
    assert(result == 0);
    UNUSED_IF_ASSERT_DISABLED(result);
  }

  void DidPoll() override {
    int result = WSAEventSelect(m_socket, WSA_INVALID_EVENT, 0);
    assert(result == 0);
    UNUSED_IF_ASSERT_DISABLED(result);
  }

  void Disarm() override { WSAResetEvent(m_event); }

  SOCKET m_socket;
};

} // namespace

MainLoopWindows::MainLoopWindows() {
  m_interrupt_event = WSACreateEvent();
  assert(m_interrupt_event != WSA_INVALID_EVENT);
}

MainLoopWindows::~MainLoopWindows() {
  assert(m_read_fds.empty());
  BOOL result = WSACloseEvent(m_interrupt_event);
  assert(result == TRUE);
  UNUSED_IF_ASSERT_DISABLED(result);
}

llvm::Expected<size_t> MainLoopWindows::Poll() {
  std::vector<HANDLE> events;
  events.reserve(m_read_fds.size() + 1);
  for (auto &[_, fd_info] : m_read_fds) {
    fd_info.event->WillPoll();
    events.push_back(fd_info.event->GetHandle());
  }
  events.push_back(m_interrupt_event);

  DWORD result =
      WSAWaitForMultipleEvents(events.size(), events.data(), FALSE,
                               ToTimeout(GetNextWakeupTime()), FALSE);

  for (auto &[_, fd_info] : m_read_fds)
    fd_info.event->DidPoll();

  if (result >= WSA_WAIT_EVENT_0 && result < WSA_WAIT_EVENT_0 + events.size())
    return result - WSA_WAIT_EVENT_0;

  // A timeout is treated as a (premature) signalization of the interrupt event.
  if (result == WSA_WAIT_TIMEOUT)
    return events.size() - 1;

  return llvm::createStringError(llvm::inconvertibleErrorCode(),
                                 "WSAWaitForMultipleEvents failed");
}

MainLoopWindows::ReadHandleUP
MainLoopWindows::RegisterReadObject(const IOObjectSP &object_sp,
                                    const Callback &callback, Status &error) {
  if (!object_sp || !object_sp->IsValid()) {
    error = Status::FromErrorString("IO object is not valid.");
    return nullptr;
  }

  IOObject::WaitableHandle waitable_handle = object_sp->GetWaitableHandle();
  assert(waitable_handle != IOObject::kInvalidHandleValue);

  if (m_read_fds.find(waitable_handle) != m_read_fds.end()) {
    error = Status::FromErrorStringWithFormat(
        "File descriptor %p already monitored.", waitable_handle);
    return nullptr;
  }

  if (object_sp->GetFdType() == IOObject::eFDTypeSocket) {
    m_read_fds[waitable_handle] = {
        std::make_unique<SocketEvent>(
            reinterpret_cast<SOCKET>(waitable_handle)),
        callback};
  } else {
    DWORD file_type = GetFileType(waitable_handle);
    if (file_type != FILE_TYPE_PIPE) {
      error = Status::FromErrorStringWithFormat("Unsupported file type %ld",
                                                file_type);
      return nullptr;
    }

    m_read_fds[waitable_handle] = {std::make_unique<PipeEvent>(waitable_handle),
                                   callback};
  }

  return CreateReadHandle(object_sp);
}

void MainLoopWindows::UnregisterReadObject(IOObject::WaitableHandle handle) {
  auto it = m_read_fds.find(handle);
  assert(it != m_read_fds.end());
  m_read_fds.erase(it);
}

Status MainLoopWindows::Run() {
  m_terminate_request = false;

  Status error;

  while (!m_terminate_request) {
    llvm::Expected<size_t> signaled_event = Poll();
    if (!signaled_event)
      return Status::FromError(signaled_event.takeError());

    if (*signaled_event < m_read_fds.size()) {
      auto &KV = *std::next(m_read_fds.begin(), *signaled_event);
      KV.second.event->Disarm();
      KV.second.callback(*this); // Do the work.
    } else {
      assert(*signaled_event == m_read_fds.size());
      WSAResetEvent(m_interrupt_event);
    }
    ProcessCallbacks();
  }
  return Status();
}

void MainLoopWindows::Interrupt() { WSASetEvent(m_interrupt_event); }
