// $Id: Rw11VirtTermTcp.cpp 1186 2019-07-12 17:49:59Z mueller $
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright 2013-2019 by Walter F.J. Mueller <W.F.J.Mueller@gsi.de>
// 
// Revision History: 
// Date         Rev Version  Comment
// 2019-02-23  1114   1.0.14 use std::bind instead of lambda
// 2018-12-22  1091   1.0.13 pfd->pfd1 (-Wshadow fix)
// 2018-12-19  1090   1.0.12 use RosPrintf(bool)
// 2018-12-18  1089   1.0.11 use c++ style casts
// 2018-12-15  1082   1.0.10 use lambda instead of boost::bind
// 2018-11-30  1075   1.0.9  use list-init
// 2018-11-11  1066   1.0.8  coverity fixup (unchecked return value)
// 2018-10-27  1059   1.0.7  coverity fixup (uncaught exception in dtor)
// 2017-04-15   875   1.0.6  Open(): set default scheme
// 2017-04-07   868   1.0.5  Dump(): add detail arg
// 2014-08-22   584   1.0.4  use nullptr
// 2013-05-17   512   1.0.3  use Rtools::String2Long
// 2013-05-05   516   1.0.2  fix mistakes in emsg generation with errno
// 2013-04-20   508   1.0.1  add fSndPreConQue handling
// 2013-03-06   495   1.0    Initial version
// 2013-02-13   488   0.1    First draft
// ---------------------------------------------------------------------------

/*!
  \brief   Implemenation of Rw11VirtTermTcp.
*/

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>

#include <sstream>
#include <functional>

#include "librtools/RosFill.hpp"
#include "librtools/RosPrintf.hpp"
#include "librtools/RlogMsg.hpp"
#include "librtools/Rtools.hpp"

#include "Rw11VirtTermTcp.hpp"

using namespace std;
using namespace std::placeholders;

/*!
  \class Retro::Rw11VirtTermTcp
  \brief FIXME_docs
*/

// all method definitions in namespace Retro
namespace Retro {

//------------------------------------------+-----------------------------------
// constants definitions

const uint8_t  Rw11VirtTermTcp::kCode_NULL;
const uint8_t  Rw11VirtTermTcp::kCode_LF;   
const uint8_t  Rw11VirtTermTcp::kCode_CR;   
const uint8_t  Rw11VirtTermTcp::kCode_ESC;  
const uint8_t  Rw11VirtTermTcp::kCode_SE;   
const uint8_t  Rw11VirtTermTcp::kCode_NOP;  
const uint8_t  Rw11VirtTermTcp::kCode_IP;   
const uint8_t  Rw11VirtTermTcp::kCode_GA;   
const uint8_t  Rw11VirtTermTcp::kCode_SB;   
const uint8_t  Rw11VirtTermTcp::kCode_WILL; 
const uint8_t  Rw11VirtTermTcp::kCode_WONT; 
const uint8_t  Rw11VirtTermTcp::kCode_DO;   
const uint8_t  Rw11VirtTermTcp::kCode_DONT; 
const uint8_t  Rw11VirtTermTcp::kCode_IAC;  

const uint8_t  Rw11VirtTermTcp::kOpt_BIN;   
const uint8_t  Rw11VirtTermTcp::kOpt_ECHO;  
const uint8_t  Rw11VirtTermTcp::kOpt_SGA;   
const uint8_t  Rw11VirtTermTcp::kOpt_TTYP;  
const uint8_t  Rw11VirtTermTcp::kOpt_LINE;  

const size_t   Rw11VirtTermTcp::kPreConQue_limit;

//------------------------------------------+-----------------------------------
//! Default constructor

Rw11VirtTermTcp::Rw11VirtTermTcp(Rw11Unit* punit)
  : Rw11VirtTerm(punit),
    fFdListen(-1),
    fFd(-1),
    fState(ts_Closed),
    fTcpTrace(false),
    fSndPreConQue()
{
  fStats.Define(kStatNVTPreConSave , "NVTPreConSave" ,
                "VT snd bytes saved prior connect");
  fStats.Define(kStatNVTPreConDrop , "NVTPreConDrop" ,
                "VT snd bytes dropped prior connect");
  fStats.Define(kStatNVTListenPoll , "NVTListenPoll" ,
                "VT ListenPollHandler() calls");
  fStats.Define(kStatNVTAccept,      "NVTAccept",     "VT socket accepts");
  fStats.Define(kStatNVTRcvRaw,      "NVTRcvRaw",     "VT raw bytes received");
  fStats.Define(kStatNVTSndRaw,      "NVTSndRaw",     "VT raw bytes send");
}

//------------------------------------------+-----------------------------------
//! Destructor

Rw11VirtTermTcp::~Rw11VirtTermTcp()
{
  if (fFdListen > 2) {
    Rtools::Catch2Cerr(__func__,
                       [this](){ Server().RemovePollHandler(fFdListen); } );
    ::close(fFdListen);
  }
  if (Connected()) {
    Rtools::Catch2Cerr(__func__,
                       [this](){ Server().RemovePollHandler(fFd); } );
    ::close(fFd);
  }
}

//------------------------------------------+-----------------------------------
//! FIXME_docs

bool Rw11VirtTermTcp::Open(const std::string& url, RerrMsg& emsg)
{
  if (!fUrl.Set(url, "|port=|trace|", "tcp", emsg)) return false;
  
  string port;
  if (!fUrl.FindOpt("port", port)) {
    emsg.Init("Rw11VirtTermTcp::Open", "port= option not specified");
    return false;
  }

  fTcpTrace = fUrl.FindOpt("trace");

  unsigned long portno;
  if (!Rtools::String2Long(port, portno, emsg)) return false;

  protoent* pe = getprotobyname("tcp");
  if (pe == nullptr) {
    emsg.Init("Rw11VirtTermTcp::Open","getprotobyname(\"tcp\") failed");
    return false;
  }

  int fd = socket(AF_INET, SOCK_STREAM|SOCK_NONBLOCK, pe->p_proto);
  if (fd < 0) {
    emsg.InitErrno("Rw11VirtTermTcp::Open","socket() failed: ", errno);
    return false;
  }

  int on = 1;
  if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) {
    emsg.InitErrno("Rw11VirtTermTcp::Open","setsockop() failed: ", errno);
    ::close(fd);
    return false;
  }

  sockaddr_in sa = {};
  sa.sin_family = AF_INET;
  sa.sin_port   = htons(static_cast<unsigned short>(portno));
  sa.sin_addr.s_addr = htonl(INADDR_ANY);

  // Note: ::bind needed below to avoid collision with std::bind... 
  if (::bind(fd, reinterpret_cast<sockaddr*>(&sa), sizeof(sa)) < 0) {
    emsg.InitErrno("Rw11VirtTermTcp::Open","bind() failed: ", errno);
    ::close(fd);
    return false;
  }

  if (listen(fd, 1) <0) {
    emsg.InitErrno("Rw11VirtTermTcp::Open","listen() failed: ", errno);
    ::close(fd);
    return false;    
  }

  fFdListen = fd;
  fChannelId = port;
  fState = ts_Listen;

  if (fTcpTrace) {
    RlogMsg lmsg(LogFile(),'I');
    lmsg << "TermTcp: listen on " << fChannelId << " for " << Unit().Name();
  }

  Server().AddPollHandler(bind(&Rw11VirtTermTcp::ListenPollHandler, this, _1), 
                          fFdListen, POLLIN);

  return true;
}

//------------------------------------------+-----------------------------------
//! FIXME_docs

bool Rw11VirtTermTcp::Snd(const uint8_t* data, size_t count, RerrMsg& /*emsg*/)
{
  fStats.Inc(kStatNVTSnd);
  const uint8_t* pdata = data;
  const uint8_t* pdataend = data+count;
  if (count == 0) return true;              // quit if nothing to do

  if (!Connected()) {                       // if not connected keep last chars
    for (size_t i=0; i<count; i++) fSndPreConQue.push_back(data[i]);
    fStats.Inc(kStatNVTPreConSave, double(count));
    while (fSndPreConQue.size() > kPreConQue_limit) {
      fSndPreConQue.pop_front();
      fStats.Inc(kStatNVTPreConDrop);
    }
    return true;
  }

  const size_t c_bufsiz=1024;
  uint8_t  obuf[c_bufsiz];
  while (pdata < pdataend) {
    uint8_t* pobuf = obuf;
    uint8_t* pobufend = obuf+sizeof(obuf);
    while (pdata < pdataend && pobuf < pobufend-1) {
      if (*pdata == kCode_IAC) *pobuf++ = kCode_IAC;
      *pobuf++ = *pdata++;
    }

    int irc = ::write(fFd, obuf, pobuf-obuf);
    if (irc < 0) {
      RlogMsg lmsg(LogFile(),'E');
      RerrMsg emsg("Rw11VirtTermTcp::Snd", 
                   string("write() for port ") + fChannelId + " failed: ", 
                   errno);
      lmsg << emsg;
    } else {
      fStats.Inc(kStatNVTSndRaw, double(irc));
    }
  }

  fStats.Inc(kStatNVTSndByt, double(count));
  return true;
}

//------------------------------------------+-----------------------------------
//! FIXME_docs

void Rw11VirtTermTcp::Dump(std::ostream& os, int ind, const char* text,
                           int detail) const
{
  RosFill bl(ind);
  os << bl << (text?text:"--") << "Rw11VirtTermTcp @ " << this << endl;

  os << bl << "  fFdListen:       " << fFdListen << endl;
  os << bl << "  fFd:             " << fFd << endl;
  const char* t_state = "";
  switch (fState) {
  case ts_Closed: t_state = "ts_Closed";  break;
  case ts_Listen: t_state = "ts_Listen";  break;
  case ts_Stream: t_state = "ts_Stream";  break;
  case ts_Iac:    t_state = "ts_Iac";     break;
  case ts_Cmd:    t_state = "ts_Cmd";     break;
  case ts_Subneg: t_state = "ts_Subneg";  break;
  case ts_Subiac: t_state = "ts_Subiac";  break;
  default: t_state = "???";
  }
  os << bl << "  fState:            " << t_state    << endl;
  os << bl << "  fTcpTrace:         " << RosPrintf(fTcpTrace)  << endl;
  os << bl << "  fSndPreConQue.size:" << fSndPreConQue.size()  << endl;
  Rw11VirtTerm::Dump(os, ind, " ^", detail);
  return;
}

//------------------------------------------+-----------------------------------
//! FIXME_docs

int Rw11VirtTermTcp::ListenPollHandler(const pollfd& pfd)
{
  // bail-out and cancel handler if poll returns an error event
  if (pfd.revents & (~pfd.events)) return -1;

  fFd = ::accept(fFdListen, nullptr, 0);

  if (fFd < 0) {
    RlogMsg lmsg(LogFile(),'E');
    RerrMsg emsg("Rw11VirtTermTcp::ListenPollHandler", 
                 string("accept() for port ") + fChannelId + " failed: ", 
                 errno);
    lmsg << emsg;    
    // FIXME_code: proper error handling
    return 0;
  } 

  fStats.Inc(kStatNVTAccept);

  uint8_t buf_1[3] = {kCode_IAC, kCode_WILL, kOpt_LINE};
  uint8_t buf_2[3] = {kCode_IAC, kCode_WILL, kOpt_SGA};
  uint8_t buf_3[3] = {kCode_IAC, kCode_WILL, kOpt_ECHO};
  uint8_t buf_4[3] = {kCode_IAC, kCode_WILL, kOpt_BIN};
  uint8_t buf_5[3] = {kCode_IAC, kCode_DO  , kOpt_BIN};

  int nerr = 0;

  // send initial negotiation WILLs and DOs
  if (::write(fFd, buf_1, sizeof(buf_1)) < 0) nerr += 1;
  if (::write(fFd, buf_2, sizeof(buf_2)) < 0) nerr += 1;
  if (::write(fFd, buf_3, sizeof(buf_3)) < 0) nerr += 1;
  if (::write(fFd, buf_4, sizeof(buf_4)) < 0) nerr += 1;
  if (::write(fFd, buf_5, sizeof(buf_5)) < 0) nerr += 1;

  // send connect message
  if (nerr==0) {
    stringstream msg;
    msg << "\r\nconnect on port " << fChannelId 
        << " for " << Unit().Name() << "\r\n\r\n";
    string str = msg.str();
    if (::write(fFd, str.c_str(), str.length()) < 0) nerr += 1;
  }

  // send chars buffered while attached but not connected
  if (nerr==0 && fSndPreConQue.size()) {
    stringstream msg;
    while (!fSndPreConQue.empty()) {
      msg << char(fSndPreConQue.front());
      fSndPreConQue.pop_front();
    }
    string str = msg.str();
    if (::write(fFd, str.c_str(), str.length()) < 0) nerr += 1;
  }

  if (nerr) {
    ::close(fFd);
    fFd = -1;
    RlogMsg lmsg(LogFile(),'E');
    RerrMsg emsg("Rw11VirtTermTcp::ListenPollHandler", 
                 string("initial write()s for port ") + fChannelId + 
                 " failed: ", errno);
    lmsg << emsg;    
    return 0;
  }

  if (fTcpTrace) {
    RlogMsg lmsg(LogFile(),'I');
    lmsg << "TermTcp: accept on " << fChannelId << " for " << Unit().Name();
  }

  fState = ts_Stream;

  Server().RemovePollHandler(fFdListen);
  Server().AddPollHandler(bind(&Rw11VirtTermTcp::RcvPollHandler, this, _1), 
                          fFd, POLLIN);
  return 0;
}
  
//------------------------------------------+-----------------------------------
//! FIXME_docs

int Rw11VirtTermTcp::RcvPollHandler(const pollfd& pfd)
{
  fStats.Inc(kStatNVTRcvPoll);

  int irc = -1;

  if (pfd.revents & POLLIN) {
    const size_t c_bufsiz=1024;
    uint8_t ibuf[c_bufsiz];
    uint8_t obuf[c_bufsiz];
    uint8_t* pobuf = obuf;

    irc = ::read(fFd, ibuf, sizeof(ibuf));

    if (irc < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return 0;

    if (irc > 0) {
      fStats.Inc(kStatNVTRcvRaw, double(irc));
      for (int i=0; i<irc; i++) {
        uint8_t byt = ibuf[i];
        switch (fState) {
        case ts_Stream:
          if (byt == kCode_IAC) {
            fState = ts_Iac;
          } else {
            *pobuf++ = byt;
            fStats.Inc(kStatNVTRcvByt, 1.);
          }
          break;

        case ts_Iac:
          if (byt == kCode_WILL || byt == kCode_WONT ||
              byt == kCode_DO   || byt == kCode_DONT) {
            fState = ts_Cmd;
          } else if (byt == kCode_SB) {
            fState = ts_Subneg;
          } else {
            fState = ts_Stream;
          }
          break;

        case ts_Cmd:
          fState = ts_Stream;
          break;

        case ts_Subneg:
          if (byt == kCode_IAC) {
            fState = ts_Subiac;
          }
          break;

        case ts_Subiac:
          fState = ts_Stream;
          break;
        default:
          break;
        } 

      }
    }

    if (pobuf > obuf) fRcvCb(obuf, pobuf - obuf);
  }


  if (irc <= 0) {
    if (irc < 0) {
      RlogMsg lmsg(LogFile(),'E');
      RerrMsg emsg("Rw11VirtTermTcp::RcvPollHandler", 
                   string("read() for port ") + fChannelId + " failed: ", 
                   errno);
      lmsg << emsg;
    }
    if (fTcpTrace) {
      RlogMsg lmsg(LogFile(),'I');
      lmsg << "TermTcp: close on " << fChannelId << " for " << Unit().Name();
    }
    ::close(fFd);
    fFd = -1;
    Server().AddPollHandler(bind(&Rw11VirtTermTcp::ListenPollHandler, this, _1), 
                            fFdListen, POLLIN);    
    fState = ts_Listen;
    return -1;
  }

  return 0;
}
  

} // end namespace Retro
