/**
* @file win32pipestream.cc
* @brief A reimplementation of pipestream for win32
* @author Supakorn "Jamie" Rassameemasmuang (jamievlin [at] outlook.com)
*/
#if defined(_WIN32)
#include "win32pipestream.h"
#include "util.h"
#include "errormsg.h"

namespace w32
{

namespace cw32 = camp::w32;

Win32IoPipeStream::~Win32IoPipeStream()
{
 // this is not the best idea, but this is what the
 // original code looks like (pipestream.h)
 pipeclose();
}

void Win32IoPipeStream::open(
       mem::vector<string> const& command, char const* hint,
       char const* application, int out_fileno)
{
 if (out_fileno != STDOUT_FILENO && out_fileno != STDERR_FILENO)
 {
   camp::reportError("out_fileno must be stdout or stdin");
 }

 // creating pipes
 SECURITY_ATTRIBUTES pipeSecurityAttr = {};
 pipeSecurityAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
 pipeSecurityAttr.bInheritHandle = true;
 pipeSecurityAttr.lpSecurityDescriptor = nullptr;

 {
   cw32::HandleRaiiWrapper processStdinRd;
   cw32::checkResult(CreatePipe(processStdinRd.put(), &processStdinWr, &pipeSecurityAttr, 0));

   cw32::HandleRaiiWrapper processOutWr;
   cw32::checkResult(CreatePipe(&processOutRd, processOutWr.put(), &pipeSecurityAttr, 0));

   // building command
   string cmd= camp::w32::buildWindowsCmd(command);

   STARTUPINFOA startInfo= {};
   startInfo.cb= sizeof(startInfo);
   startInfo.dwFlags= STARTF_USESTDHANDLES;
   startInfo.hStdInput= processStdinRd.getHandle();
   startInfo.hStdOutput= out_fileno == STDOUT_FILENO ? processOutWr.getHandle() : GetStdHandle(STD_OUTPUT_HANDLE);
   startInfo.hStdError= out_fileno == STDERR_FILENO ? processOutWr.getHandle() : GetStdHandle(STD_ERROR_HANDLE);

   auto const result= CreateProcessA(
           nullptr,
           cmd.data(),
           nullptr, nullptr, true,
           0,
           nullptr, nullptr,
           &startInfo,
           &procInfo);
   if (!result)
   {
     execError(command.at(0).c_str(), hint, application);
     cw32::checkResult(result, "Cannot open application");
   }
 }

 ZeroMemory(buffer, BUFFER_LEN);
 Running=true;
 pipeopen=true;
 pipein=true;
 block(false,true);
}

void Win32IoPipeStream::block(bool write, bool read)
{
 /*
  * Side note: what the hell, microsoft?
  *
  * Why is setting mode for anonymous pipe also called
  * "SetNamedPipeHandleState", why not "SetPipeHandleState"?
  *
  * (see https://learn.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-setnamedpipehandlestate?redirectedfrom=MSDN)
  */

 DWORD blockMode = PIPE_WAIT;
 DWORD noBlockMode = PIPE_NOWAIT;

 cw32::checkResult(SetNamedPipeHandleState(
         processStdinWr,
         write ? &blockMode : &noBlockMode,
         nullptr, nullptr
         ));

 cw32::checkResult(SetNamedPipeHandleState(
         processOutRd,
         read ? &blockMode : &noBlockMode,
         nullptr, nullptr
         ));
}

void Win32IoPipeStream::eof()
{
 if(pipeopen && pipein) {
   camp::w32::checkResult(CloseHandle(&processStdinWr));
   pipein=false;
 }
}

void Win32IoPipeStream::pipeclose()
{
 if(pipeopen) {
   cw32::checkResult(CloseHandle(processOutRd));

   if (procInfo.hProcess != nullptr)
   {
     if (!TerminateProcess(procInfo.hProcess, 0) && GetLastError() != ERROR_ACCESS_DENIED)
     {
       camp::reportError("cannot terminate process");
     }
   }
   Running=false;
   pipeopen=false;
   wait();
 }
}

bool Win32IoPipeStream::isopen() const
{
 return pipeopen;
}

Win32IoPipeStream::Win32IoPipeStream(
       mem::vector<string> const& command, char const* hint, char const* application, int out_fileno)
{
 open(command, hint, application, out_fileno);
}

int Win32IoPipeStream::wait()
{
 DWORD retcode = 0;
 if (procInfo.hProcess == nullptr)
 {
   return static_cast<int>(retcode);
 }

 switch(WaitForSingleObject(procInfo.hProcess, INFINITE))
 {
   case WAIT_OBJECT_0:
     cw32::checkResult(GetExitCodeProcess(procInfo.hProcess, &retcode));
     break;
   default:
     closeProcessHandles();
     camp::reportError("Wait for process error");
     break;
 }

 closeProcessHandles();
 return static_cast<int>(retcode);
}

void Win32IoPipeStream::Write(string const& s)
{
 if (!pipeopen)
 {
   return;
 }
 if(settings::verbose > 2) cerr << s;

 DWORD bytesWritten=0;

 cw32::checkResult(WriteFile(
         processStdinWr,
         s.c_str(), s.length(),
         &bytesWritten, nullptr
         ));

 if (static_cast<DWORD>(s.length()) != bytesWritten)
 {
   camp::reportFatal("write to pipe failed");
 }

}

void Win32IoPipeStream::wait(char const* prompt)
{
 sbuffer.clear();
 size_t plen=strlen(prompt);

 do {
   readbuffer();
   if(*buffer == 0) camp::reportError(sbuffer);
   sbuffer.append(buffer);

   if(tailequals(sbuffer.c_str(),sbuffer.size(),prompt,plen)) break;
 } while(true);
}

ssize_t Win32IoPipeStream::readbuffer()
{
 if (!(Running && pipeopen))
 {
   return 0;
 }

 DWORD nc;

 if (!ReadFile(processOutRd, buffer, BUFFER_LEN - 1, &nc, nullptr))
 {
   if (GetLastError() != ERROR_BROKEN_PIPE)
   {
     // process could have exited
     camp::reportError("read failed from pipe");
   }
 }

 buffer[nc]=0;

 if (nc > 0)
 {
   if (settings::verbose > 2)
   {
     cerr << buffer;
   }
 }
 else if (procInfo.hProcess != nullptr)
 {
   switch (WaitForSingleObject(procInfo.hProcess, 0))
   {
     case WAIT_OBJECT_0:
     {
       closeProcessHandles();
       Running=false;
       break;
     }
     case WAIT_TIMEOUT:
       break;
     default:
       camp::reportError("Waiting for process failed");
       break;
   }
 }
 return nc;
}

string Win32IoPipeStream::readline()
{
 sbuffer.clear();
 int nc;
 do {
   nc=readbuffer();
   sbuffer.append(buffer);
 } while(buffer[nc-1] != '\n' && Running);
 return sbuffer;
}

bool Win32IoPipeStream::tailequals(char const* buf, size_t len, char const* prompt, size_t plen)
{
 const char *a=buf+len;
 const char *b=prompt+plen;
 while(b >= prompt) {
   if(a < buf) return false;
   if(*a != *b) return false;
   // Handle MSDOS incompatibility:
   if(a > buf && *a == '\n' && *(a-1) == '\r') --a;
   --a; --b;
 }
 return true;
}

#pragma region "private functions"
void Win32IoPipeStream::closeProcessHandles()
{
 if (procInfo.hProcess != nullptr)
 {
   cw32::checkResult(CloseHandle(procInfo.hProcess));
   procInfo.hProcess=nullptr;
 }

 if (procInfo.hThread != nullptr)
 {
   cw32::checkResult(CloseHandle(procInfo.hThread));
   procInfo.hThread=nullptr;
 }
}

Win32IoPipeStream& Win32IoPipeStream::operator<< (imanip func)
{
 return (*func)(*this);
}

Win32IoPipeStream& Win32IoPipeStream::operator>>(string& s)
{
 readbuffer();
 s=buffer;
 return *this;
}

Win32IoPipeStream& Win32IoPipeStream::operator<<(const string& s)
{
 Write(s);
 return *this;
}
string Win32IoPipeStream::getbuffer()
{
 return sbuffer;
}

bool Win32IoPipeStream::running()
{
 return Running;
}

#pragma endregion

}
#endif