/**
 * Looking Glass
 * Copyright © 2017-2026 The Looking Glass Authors
 * https://looking-glass.io
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program 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 General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc., 59
 * Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

#include "CPipeServer.h"
#include "CDebug.h"
#include "CIndirectDeviceContext.h"

CPipeServer g_pipe;

bool CPipeServer::Init()
{
  _DeInit();

  m_pipe.Attach(CreateNamedPipeA(
    LG_PIPE_NAME,
    PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
    PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
    1,
    1024,
    1024,
    0,
    NULL));

  if (!m_pipe.IsValid())
  {
    DEBUG_ERROR_HR(GetLastError(), "Failed to create the named pipe");
    return false;
  }

  m_signal.Attach(CreateEvent(NULL, TRUE, FALSE, NULL));
  if (!m_signal.IsValid())
  {
    DEBUG_ERROR_HR(GetLastError(), "Failed to create pipe signal event");
    return false;
  }

  m_running = true;
  m_thread.Attach(CreateThread(
    NULL,
    0,
    _pipeThread,
    (LPVOID)this,
    0,
    NULL));

  if (!m_thread.IsValid())
  {
    DEBUG_ERROR_HR(GetLastError(), "Failed to create the pipe thread");
    return false;
  }

  DEBUG_TRACE("Pipe Initialized");
  return true;
}

void CPipeServer::_DeInit()
{
  m_running = false;
  m_connected = false;
  if (m_signal.IsValid())
    SetEvent(m_signal.Get());

  if (m_thread.IsValid())
  {
    WaitForSingleObject(m_thread.Get(), INFINITE);
    m_thread.Close();
  }

  if (m_pipe.IsValid())
  {
    FlushFileBuffers(m_pipe.Get());
    m_pipe.Close();
  }

  m_signal.Close();
}

void CPipeServer::DeInit()
{  
  DEBUG_TRACE("Pipe Stopping");
  _DeInit();
  DEBUG_TRACE("Pipe Stopped");
}

void CPipeServer::Thread()
{
  DEBUG_TRACE("Pipe thread started");

  HandleT<EventTraits> ioEvent(CreateEvent(NULL, TRUE, FALSE, NULL));
  if (!ioEvent.IsValid())
  {
    DEBUG_ERROR_HR(GetLastError(), "Can't create event for overlapped I/O!");
    WaitForSingleObject(m_signal.Get(), 5000);
    return;
  }

  while(m_running)
  {
    m_connected = false;

    OVERLAPPED overlapped = { 0 };
    overlapped.hEvent = ioEvent.Get();

    if (!ConnectNamedPipe(m_pipe.Get(), &overlapped))
    {
      DWORD dwError = GetLastError();
      switch (dwError) {
      case ERROR_PIPE_CONNECTED:
        break;
      case ERROR_IO_PENDING:
      {
        HANDLE hWait[] = { ioEvent.Get(), m_signal.Get() };
        switch (WaitForMultipleObjects(2, hWait, FALSE, INFINITE))
        {
        case WAIT_OBJECT_0:
          break;
        case WAIT_OBJECT_0 + 1:
          DEBUG_INFO("Connect interrupted by signal");
          CancelIo(m_pipe.Get());
          WaitForSingleObject(ioEvent.Get(), INFINITE);
          continue;
        }
        break;
      }
      default:
        DEBUG_ERROR_HR(dwError, "Error connecting to the named pipe");
        goto end;
      }
    }

    DEBUG_TRACE("Client connected");

    m_connected = true;

    for (const auto& msg : m_queue)
      WriteMsg(msg);
    m_queue.clear();

    while (m_running && m_connected)
    {
      LGPipeMsg msg;

      if (!ReadFile(m_pipe.Get(), &msg, sizeof(msg), NULL, &overlapped))
      {
        DWORD dwError = GetLastError();
        if (dwError != ERROR_IO_PENDING)
        {
          DEBUG_ERROR_HR(dwError, "ReadFile Failed");
          break;
        }

        HANDLE hWait[] = { ioEvent.Get(), m_signal.Get() };
        switch (WaitForMultipleObjects(2, hWait, FALSE, INFINITE))
        {
        case WAIT_OBJECT_0:
          break;
        case WAIT_OBJECT_0 + 1:
          DEBUG_INFO("I/O interrupted by signal");
          CancelIo(m_pipe.Get());
          WaitForSingleObject(ioEvent.Get(), INFINITE);
          continue;
        }
      }

      DWORD bytesRead;
      GetOverlappedResult(m_pipe.Get(), &overlapped, &bytesRead, TRUE);

      if (bytesRead != sizeof(msg))
      {
        DEBUG_ERROR("Corrupted data, expected %lld bytes, read %lld bytes", sizeof msg, bytesRead);
        break;
      }

      if (msg.size != sizeof(msg))
      {
        DEBUG_ERROR("Corrupted data, expected %lld bytes, actual message size: %lld bytes", sizeof msg, msg.size);
        break;
      }

      switch (msg.type)
      {
      case LGPipeMsg::RELOADSETTINGS:
        HandleReloadSettings();
        break;

      default:
        DEBUG_ERROR("Unknown message type %d", msg.type);
        break;
      }
    }

    DEBUG_TRACE("Client disconnected");
    DisconnectNamedPipe(m_pipe.Get());

    if (m_running)
      ResetEvent(m_signal.Get());
  }

end:
  m_running   = false;
  m_connected = false;
  DEBUG_TRACE("Pipe thread shutdown");
}

void CPipeServer::WriteMsg(const LGPipeMsg & msg)
{
  if (!m_connected)
  {
    // Not connected yet: keep only the latest message of each type. These are
    // all latest-state-wins messages, so a burst (e.g. display mode changes
    // while resizing) must collapse to the final state rather than replay every
    // intermediate value when the helper reconnects.
    for (auto & queued : m_queue)
      if (queued.type == msg.type)
      {
        queued = msg;
        return;
      }
    m_queue.push_back(msg);
    return;
  }

  DWORD written;
  if (!WriteFile(m_pipe.Get(), &msg, sizeof(msg), &written, NULL))
  {
    DWORD err = GetLastError();
    if (err == ERROR_BROKEN_PIPE || err == ERROR_NO_DATA)
    {
      DEBUG_WARN_HR(err, "Client disconnected, failed to write");
      m_connected = false;
      SetEvent(m_signal.Get());
      return;
    }

    DEBUG_WARN_HR(err, "WriteFile failed on the pipe");
    return;
  }

  FlushFileBuffers(m_pipe.Get());
}

void CPipeServer::HandleReloadSettings()
{
  DEBUG_INFO("Reloading settings");

  AcquireSRWLockShared(&m_deviceContextLock);
  if (m_deviceContext)
    m_deviceContext->ReplugMonitor();
  ReleaseSRWLockShared(&m_deviceContextLock);
}

void CPipeServer::SetDeviceContext(CIndirectDeviceContext* context)
{
  AcquireSRWLockExclusive(&m_deviceContextLock);
  m_deviceContext = context;
  ReleaseSRWLockExclusive(&m_deviceContextLock);
}

void CPipeServer::SetCursorPos(uint32_t x, uint32_t y)
{
  // do not send cursor messages if we are not connected or they will end up queued
  if (!m_connected)
    return;

  LGPipeMsg msg = {};
  msg.size       = sizeof(msg);
  msg.type       = LGPipeMsg::SETCURSORPOS;
  msg.curorPos.x = x;
  msg.curorPos.y = y;
  WriteMsg(msg);
}

void CPipeServer::SetDisplayMode(uint32_t width, uint32_t height, uint32_t refresh)
{
  LGPipeMsg msg = {};
  msg.size                = sizeof(msg);
  msg.type                = LGPipeMsg::SETDISPLAYMODE;
  msg.displayMode.width   = width;
  msg.displayMode.height  = height;
  msg.displayMode.refresh = refresh;
  WriteMsg(msg);
}

void CPipeServer::SetGPUStatus(bool software)
{
  LGPipeMsg msg = {};
  msg.size               = sizeof(msg);
  msg.type               = LGPipeMsg::GPUSTATUS;
  msg.gpuStatus.software = software;
  WriteMsg(msg);
}

void CPipeServer::ResolutionRejected(uint32_t width, uint32_t height,
  uint32_t requiredSizeMiB)
{
  LGPipeMsg msg = {};
  msg.size = sizeof(msg);
  msg.type = LGPipeMsg::RESOLUTIONREJECTED;
  msg.resolutionRejected.width = width;
  msg.resolutionRejected.height = height;
  msg.resolutionRejected.requiredSizeMiB = requiredSizeMiB;
  WriteMsg(msg);
}
