/* Pipestream: A simple C++ interface to UNIX pipes
   Copyright (C) 2005-2014 John C. Bowman,
   with contributions from Mojca Miklavec

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU Lesser General Public License as published by
   the Free Software Foundation; either version 3 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 Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */

#include <iostream>
#include <cstring>
#include <cerrno>
#include <sstream>
#include <signal.h>

#include "pipestream.h"
#include "common.h"
#include "errormsg.h"
#include "settings.h"
#include "util.h"
#include "interact.h"
#include "lexical.h"
#include "camperror.h"
#include "pen.h"

iopipestream *instance;

void pipeHandler(int)
{
  Signal(SIGPIPE,SIG_DFL);
  instance->pipeclose();
}

void iopipestream::open(const mem::vector<string> &command, const char *hint,
                        const char *application, int out_fileno)
{
  if(pipe(in) == -1) {
    ostringstream buf;
    buf << "in pipe failed: ";
    for(size_t i=0; i < command.size(); ++i) buf << command[i];
    camp::reportError(buf);
  }

  if(pipe(out) == -1) {
    ostringstream buf;
    buf << "out pipe failed: ";
    for(size_t i=0; i < command.size(); ++i) buf << command[i];
    camp::reportError(buf);
  }
  cout.flush(); // Flush stdout to avoid duplicate output.

  if((pid=fork()) < 0) {
    ostringstream buf;
    buf << "fork failed: ";
    for(size_t i=0; i < command.size(); ++i) buf << command[i];
    camp::reportError(buf);
  }

  if(pid == 0) {
    if(interact::interactive) signal(SIGINT,SIG_IGN);
    close(in[1]);
    close(out[0]);
    close(STDIN_FILENO);
    close(out_fileno);
    dup2(in[0],STDIN_FILENO);
    dup2(out[1],out_fileno);
    close(in[0]);
    close(out[1]);
    char **argv=args(command);
    if(argv) execvp(argv[0],argv);
    execError(argv[0],hint,application);
    kill(0,SIGPIPE);
    _exit(-1);
  }
  instance=this;
  Signal(SIGPIPE,pipeHandler);
  close(out[1]);
  close(in[0]);
  *buffer=0;
  Running=true;
  pipeopen=true;
  pipein=true;
  block(false,true);
}

void iopipestream::eof()
{
  if(pipeopen && pipein) {
    close(in[1]);
    pipein=false;
  }
}

void iopipestream::pipeclose()
{
  if(pipeopen) {
    kill(pid,SIGHUP);
    eof();
    close(out[0]);
    Running=false;
    pipeopen=false;
    waitpid(pid,NULL,0); // Avoid zombies.
  }
}

void iopipestream::block(bool write, bool read)
{
  if(pipeopen) {
    int w=fcntl(in[1],F_GETFL);
    int r=fcntl(out[0],F_GETFL);
    fcntl(in[1],F_SETFL,write ? w & ~O_NONBLOCK : w | O_NONBLOCK);
    fcntl(out[0],F_SETFL,read ? r & ~O_NONBLOCK : r | O_NONBLOCK);
  }
}

ssize_t iopipestream::readbuffer()
{
  ssize_t nc;
  char *p=buffer;
  ssize_t size=BUFSIZE-1;
  errno=0;
  for(;;) {
    if((nc=read(out[0],p,size)) < 0) {
      if(errno == EAGAIN || errno == EINTR) {p[0]=0; break;}
      else {
        ostringstream buf;
        buf << "read from pipe failed: errno=" << errno;
        camp::reportError(buf);
      }
      nc=0;
    }
    p[nc]=0;
    if(nc == 0) {
      if(waitpid(pid,NULL,WNOHANG) == pid)
        Running=false;
      break;
    }
    if(nc > 0) {
      if(settings::verbose > 2) cerr << p;
      break;
    }
  }
  return nc;
}

bool iopipestream::tailequals(const char *buf, size_t len, const char *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;
}

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

void iopipestream::wait(const char *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);
}

int iopipestream::wait()
{
  for(;;) {
    int status;
    if (waitpid(pid,&status,0) == -1) {
      if (errno == ECHILD) return 0;
      if (errno != EINTR) {
        ostringstream buf;
        buf << "Process " << pid << " failed";
        camp::reportError(buf);
      }
    } else {
      if(WIFEXITED(status)) return WEXITSTATUS(status);
      else {
        ostringstream buf;
        buf << "Process " << pid << " exited abnormally";
        camp::reportError(buf);
      }
    }
  }
}

void iopipestream::Write(const string &s)
{
  ssize_t size=s.length();
  if(settings::verbose > 2) cerr << s;
  if(write(in[1],s.c_str(),size) != size) {
    camp::reportFatal("write to pipe failed");
  }
}
