// This is an open source non-commercial project. Dear PVS-Studio, please check
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

#include <uv.h>

#include "nvim/vim.h"
#include "nvim/os/os.h"
#include "nvim/os/pty_conpty_win.h"

#ifndef EXTENDED_STARTUPINFO_PRESENT
# define EXTENDED_STARTUPINFO_PRESENT 0x00080000
#endif
#ifndef PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
# define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE 0x00020016
#endif

HRESULT (WINAPI *pCreatePseudoConsole)(COORD, HANDLE, HANDLE, DWORD, HPCON *);
HRESULT (WINAPI *pResizePseudoConsole)(HPCON, COORD);
void (WINAPI *pClosePseudoConsole)(HPCON);

bool os_has_conpty_working(void)
{
  static TriState has_conpty = kNone;
  if (has_conpty == kNone) {
    has_conpty = os_dyn_conpty_init();
  }

  return has_conpty == kTrue;
}

TriState os_dyn_conpty_init(void)
{
  uv_lib_t kernel;
  if (uv_dlopen("kernel32.dll", &kernel)) {
    uv_dlclose(&kernel);
    return kFalse;
  }
  static struct {
    char *name;
    FARPROC *ptr;
  } conpty_entry[] = {
    { "CreatePseudoConsole", (FARPROC *)&pCreatePseudoConsole },
    { "ResizePseudoConsole", (FARPROC *)&pResizePseudoConsole },
    { "ClosePseudoConsole", (FARPROC *)&pClosePseudoConsole },
    { NULL, NULL }
  };
  for (int i = 0;
       conpty_entry[i].name != NULL && conpty_entry[i].ptr != NULL; i++) {
    if (uv_dlsym(&kernel, conpty_entry[i].name, (void **)conpty_entry[i].ptr)) {
      uv_dlclose(&kernel);
      return kFalse;
    }
  }
  return kTrue;
}

conpty_t *os_conpty_init(char **in_name, char **out_name,
                         uint16_t width, uint16_t height)
{
  static int count = 0;
  conpty_t *conpty_object = xcalloc(1, sizeof(*conpty_object));
  const char *emsg = NULL;
  HANDLE in_read = INVALID_HANDLE_VALUE;
  HANDLE out_write = INVALID_HANDLE_VALUE;
  char buf[MAXPATHL];
  SECURITY_ATTRIBUTES sa = { 0 };
  const DWORD mode = PIPE_ACCESS_INBOUND
    | PIPE_ACCESS_OUTBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE;

  sa.nLength = sizeof(sa);
  snprintf(buf, sizeof(buf), "\\\\.\\pipe\\nvim-term-in-%d-%d",
           os_get_pid(), count);
  *in_name = xstrdup(buf);
  if ((in_read = CreateNamedPipeA(
      *in_name,
      mode,
      PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
      1,
      0,
      0,
      30000,
      &sa)) == INVALID_HANDLE_VALUE) {
    emsg = "create input pipe failed";
    goto failed;
  }
  snprintf(buf, sizeof(buf), "\\\\.\\pipe\\nvim-term-out-%d-%d",
           os_get_pid(), count);
  *out_name = xstrdup(buf);
  if ((out_write = CreateNamedPipeA(
      *out_name,
      mode,
      PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
      1,
      0,
      0,
      30000,
      &sa)) == INVALID_HANDLE_VALUE) {
    emsg = "create output pipe failed";
    goto failed;
  }
  assert(width <= SHRT_MAX);
  assert(height <=  SHRT_MAX);
  COORD size = { (int16_t)width, (int16_t)height };
  HRESULT hr;
  hr = pCreatePseudoConsole(size, in_read, out_write, 0, &conpty_object->pty);
  if (FAILED(hr)) {
    emsg = "create psudo console failed";
    goto failed;
  }

  conpty_object->si_ex.StartupInfo.cb = sizeof(conpty_object->si_ex);
  size_t bytes_required;
  InitializeProcThreadAttributeList(NULL, 1, 0,  & bytes_required);
  conpty_object->si_ex.lpAttributeList =
    (PPROC_THREAD_ATTRIBUTE_LIST)xmalloc(bytes_required);
  if (!InitializeProcThreadAttributeList(
      conpty_object->si_ex.lpAttributeList,
      1,
      0,
      &bytes_required)) {
    emsg = "InitializeProcThreadAttributeList failed";
    goto failed;
  }
  if (!UpdateProcThreadAttribute(
      conpty_object->si_ex.lpAttributeList,
      0,
      PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
      conpty_object->pty,
      sizeof(conpty_object->pty),
      NULL,
      NULL)) {
    emsg = "UpdateProcThreadAttribute failed";
    goto failed;
  }
  count++;
  goto finished;

failed:
  ELOG("os_conpty_init:%s : error code: %d",
       emsg, os_translate_sys_error((int)GetLastError()));
  os_conpty_free(conpty_object);
  conpty_object = NULL;
finished:
  if (in_read != INVALID_HANDLE_VALUE) {
    CloseHandle(in_read);
  }
  if (out_write != INVALID_HANDLE_VALUE) {
    CloseHandle(out_write);
  }
  return conpty_object;
}

bool os_conpty_spawn(conpty_t *conpty_object, HANDLE *process_handle,
                     wchar_t *name, wchar_t *cmd_line, wchar_t *cwd,
                     wchar_t *env)
{
  PROCESS_INFORMATION pi = { 0 };
  if (!CreateProcessW(
      name,
      cmd_line,
      NULL,
      NULL,
      false,
      EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT,
      env,
      cwd,
      &conpty_object->si_ex.StartupInfo,
      &pi)) {
    return false;
  }
  *process_handle = pi.hProcess;
  return true;
}

void os_conpty_set_size(conpty_t *conpty_object,
                        uint16_t width, uint16_t height)
{
    assert(width <= SHRT_MAX);
    assert(height <= SHRT_MAX);
    COORD size = { (int16_t)width, (int16_t)height };
    if (pResizePseudoConsole(conpty_object->pty, size) != S_OK) {
      ELOG("ResizePseudoConsoel failed: error code: %d",
           os_translate_sys_error((int)GetLastError()));
    }
}

void os_conpty_free(conpty_t *conpty_object)
{
  if (conpty_object != NULL) {
    if (conpty_object->si_ex.lpAttributeList != NULL) {
      DeleteProcThreadAttributeList(conpty_object->si_ex.lpAttributeList);
      xfree(conpty_object->si_ex.lpAttributeList);
    }
    if (conpty_object->pty != NULL) {
      pClosePseudoConsole(conpty_object->pty);
    }
  }
  xfree(conpty_object);
}
