/* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
 * Copyright 2011 Pierre Ossman for Cendio AB
 * Copyright 2017 Peter Astrand <astrand@cendio.se> for Cendio AB
 * Copyright (C) 2015-2024 m-privacy GmbH
 * 
 * This 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 software 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 software; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
 * USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <string.h>
#include <errno.h>
#ifdef _WIN32
#include <winsock2.h>
#undef errno
#define errno WSAGetLastError()
#include <os/winerrno.h>
#else
#include <sys/types.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#endif

/* Old systems have select() in sys/time.h */
#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h>
#endif

#include <rdr/FdOutStream.h>
#include <rdr/Exception.h>
#include <rfb/util.h>
#include <rfb/LogWriter.h>
#include <rdr/mutex.h>
#include <rfb/Configuration.h>

#define FDKEY 0

static rfb::LogWriter vlog("FdOutStream");

using namespace rdr;
using namespace std;

rfb::IntParameter FdKeepOutBuffersHigh("FdKeepOutBuffersHigh", "outgoing buffers to keep for high priority, Fd layer (min 1, max 255)", 128, 1, MAXBUFFERNUM);
rfb::IntParameter FdKeepOutBuffersMedium("FdKeepOutBuffersMedium", "outgoing buffers to keep for medium priority, Fd layer (min 1, max 255)", 256, 1, MAXBUFFERNUM);
rfb::IntParameter FdKeepOutBuffersLow("FdKeepOutBuffersLow", "outgoing buffers to keep for low priority, Fd layer (min 1, max 255)", 20, 1, MAXBUFFERNUM);
rfb::IntParameter FdThreshOutBuffersHigh("FdThreshOutBuffersHigh", "when to force flush outgoing buffers for high priority, TLS layer (min 1, max 255)", 1024, 1, MAXBUFFERNUM);
rfb::IntParameter FdThreshOutBuffersMedium("FdThreshOutBuffersMedium", "when to force flush outgoing buffers for medium priority, TLS layer (min 1, max 4096)", 1024, 1, MAXBUFFERNUM);
rfb::IntParameter FdThreshOutBuffersLow("FdThreshOutBuffersLow", "when to force flush outgoing buffers for low priority, TLS layer (min 1, max 255)", 40, 1, MAXBUFFERNUM);
rfb::IntParameter FdMaxOutBuffersHigh("FdMaxOutBuffersHigh", "available outgoing buffers for high priority, Fd layer (min 1, max 65535)", 4096, 1, MAXBUFFERNUM);
rfb::IntParameter FdMaxOutBuffersMedium("FdMaxOutBuffersMedium", "available outgoing buffers for medium priority, Fd layer (min 1, max 65535)", 4096, 1, MAXBUFFERNUM);
rfb::IntParameter FdMaxOutBuffersLow("FdMaxOutBuffersLow", "available outgoing buffers for low priority, Fd layer (min 1, max 65535)", 256, 1, MAXBUFFERNUM);
rfb::BoolParameter sendFdHeader("SendFdHeader", "Send every Fd buffer with checksum header", false);

static bool runThreads = true;
TGVNC_CONDITION_TYPE FdOutStream::buffersAvailableCondition;
MUTEX_TYPE FdOutStream::buffersAvailableConditionLock;
TGVNC_CONDITION_TYPE FdOutStream::flushedCondition[QPRIONUM];
MUTEX_TYPE FdOutStream::flushedConditionLock[QPRIONUM];

FdOutStream::FdOutStream(int fd_)
  : fd(fd_), flushActive(false)
{
  vlog.debug("Initializing");
  MUTEX_INIT(&buffersAvailableConditionLock);
  TGVNC_CONDITION_INIT(&buffersAvailableCondition);
  for (int q = QPRIOMIN; q <= QPRIOMAX; q++) {
    MUTEX_INIT(&flushedConditionLock[q]);
    TGVNC_CONDITION_INIT(&flushedCondition[q]);
  }
  memset(bufferStats, 0, sizeof(U32) * MAXBUFFERSIZE);
  classLog = &vlog;
  maxBuffers[QPRIOHIGH] = FdMaxOutBuffersHigh;
  maxBuffers[QPRIOMEDIUM] = FdMaxOutBuffersMedium;
  maxBuffers[QPRIOLOW] = FdMaxOutBuffersLow;
  keepBuffers[QPRIOHIGH] = FdKeepOutBuffersHigh;
  keepBuffers[QPRIOMEDIUM] = FdKeepOutBuffersMedium;
  keepBuffers[QPRIOLOW] = FdKeepOutBuffersLow;
  flushThreshBuffers[QPRIOHIGH] = FdThreshOutBuffersHigh;
  flushThreshBuffers[QPRIOMEDIUM] = FdThreshOutBuffersMedium;
  flushThreshBuffers[QPRIOLOW] = FdThreshOutBuffersLow;
  THREAD_CREATE(flushThread, flushThreadId, this);
  THREAD_SET_NAME(flushThreadId, "tg-fd-flush");
}

FdOutStream::~FdOutStream()
{
  runThreads = false;
  THREAD_JOIN(flushThreadId);
  printBufferUsage();
  if(vlog.getLevel() >= vlog.LEVEL_DEBUG)
    for (U32 i=0; i<MAXBUFFERSIZE; i++)
      if(bufferStats[i] >= 100)
        vlog.debug("buffer size %u used %u times", i, bufferStats[i]);
  try {
    flush(storedKey);
  } catch (Exception&) {
  }
  ::close(fd);

#if 0
  TGVNC_CONDITION_DESTROY(&buffersAvailableCondition);
  MUTEX_DESTROY(&buffersAvailableConditionLock);
  for (int q = QPRIOMIN; q <= QPRIOMAX; q++) {
    TGVNC_CONDITION_DESTROY(&flushedCondition[q]);
    MUTEX_DESTROY(&flushedConditionLock[q]);
  }
#endif
  vlog.debug("~FdOutStream(): exiting after %llu flush calls with serial %u.", flushCalls, serial);
//  resetClassLog();
}

void FdOutStream::cork(bool enable, int key, QPrio prio)
{
  OutStream::cork(enable, FDKEY, prio);
#ifdef TCP_CORK
  int one = enable ? 1 : 0;
  setsockopt(fd, IPPROTO_TCP, TCP_CORK, (char *)&one, sizeof(one));
#endif
}

THREAD_FUNC FdOutStream::flushThread(void* param) {
  FdOutStream * myself = (FdOutStream *) param;

  struct queueBuffer * buffer;
  unsigned sent;
  bool restart;
  int n = 0;
  struct timeval nowTime;
  int currentPrio;
  U64 sleepCount = 0;
  ssize_t writtenBytes[QPRIONUM];
  U8 headerBuf[CHECKHEADERSIZE];

#ifdef WIN32
  vlog.debug("flushThread (tid %lu) created", GetCurrentThreadId());
#else
#if defined(__APPLE__)
  vlog.debug("flushThread (tid %u) created", gettid());
#else
  vlog.debug("flushThread (tid %lu) created", gettid());
#endif
#endif

  for (int q = QPRIOMIN; q <= QPRIOMAX; q++)
    writtenBytes[q] = 0;

#ifdef WIN32
  vlog.debug("flushThread %lu: trying to raise thread priority", GetCurrentThreadId());
  SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL);

#else

#if defined(__APPLE__)
  vlog.debug("flushThread %u: current priority is %i, now set nice(-10)", gettid(), getpriority(PRIO_PROCESS, 0));
#else
  vlog.debug("flushThread %lu: current priority is %i, now set nice(-10)", gettid(), getpriority(PRIO_PROCESS, 0));
#endif
  errno = 0;
  n = nice(-10);
  if (n == -1 && errno != 0) {
#if defined(__APPLE__)
    vlog.error("flushThread %u: failed to set nice(-10), error: %s", gettid(), strerror(errno));
#else
    vlog.error("flushThread %lu: failed to set nice(-10), error: %s", gettid(), strerror(errno));
#endif
  } else {
#if defined(__APPLE__)
    vlog.debug("flushThread %u: new priority is %i", gettid(), getpriority(PRIO_PROCESS, 0));
#else
    vlog.debug("flushThread %lu: new priority is %i", gettid(), getpriority(PRIO_PROCESS, 0));
#endif
  }
#endif

  while (runThreads) {
    myself->flushActive = false;
    /* sleep */
//    vlog.verbose("flushThread(): sleep");
    MUTEX_LOCK(&buffersAvailableConditionLock);
#ifdef WIN32
    TGVNC_CONDITION_TIMED_WAIT(&buffersAvailableCondition, &buffersAvailableConditionLock, 500);
#else
    int err = TGVNC_CONDITION_TIMED_WAIT(&buffersAvailableCondition, &buffersAvailableConditionLock, 500);
    if (err == EINTR)
      vlog.verbose("flushThread(): TGVNC_CONDITION_TIMED_WAIT was interrupted");
    else if (err != 0 && err != ETIMEDOUT)
      vlog.verbose("flushThread(): TGVNC_CONDITION_TIMED_WAIT returned error %i", err);
#endif
    MUTEX_UNLOCK(&buffersAvailableConditionLock);
//    vlog.verbose("flushThread(): woken up");
    myself->flushActive = true;
    sleepCount++;

    gettimeofday(&nowTime, NULL);
    currentPrio = QPRIOMAX;

    while(currentPrio >= QPRIOMIN) {
      if (myself->queueEmpty((QPrio)currentPrio)) {
        currentPrio--;
        continue;
      }
      restart = false;
      while (( buffer = myself->popBuffer((QPrio)currentPrio) )) {
//      vlog.debug("flushThread(): send buffer %u, prio %u, used %u", buffer->number, currentPrio, buffer->used);
        myself->bufferStats[buffer->used]++;
        if(sendFdHeader) {
          myself->fillCheckHeader(buffer, headerBuf);
          sent = 0;
          while (sent < CHECKHEADERSIZE) {
            n = ::send(myself->fd, (const char*) headerBuf + sent, CHECKHEADERSIZE - sent, 0);
            if (n < 0) {
              vlog.error("flushThread(): failed to send check header, prio %u, push buffer and break, error: %s", currentPrio, strerror(errno));
              myself->pushBuffer(buffer, (QPrio)currentPrio);
              break;
            }
            sent += n;
          }
          if (n < 0)
            break;
          writtenBytes[currentPrio] += CHECKHEADERSIZE;
        }
        /* buffer splitting can happen in case of congestion */
        sent = 0;
        while (sent < buffer->used) {
          n = ::send(myself->fd, (const char*) buffer->data + sent, buffer->used - sent, 0);
          if (n < 0) {
            vlog.error("flushThread(): failed to send, prio %u, push buffer and break, error: %s", currentPrio, strerror(errno));
            myself->pushBuffer(buffer, (QPrio)currentPrio);
            break;
          }
          if (n < (int) (buffer->used - sent))
            vlog.verbose("flushThread(): send buffer %u, prio %u, used %u, only sent %u of requested %u bytes", buffer->number, currentPrio, buffer->used, n, buffer->used - sent);
          sent += n;
        }
        if (n < 0)
          break;
        writtenBytes[currentPrio] += buffer->used;
        myself->returnQueueBuffer(buffer, (QPrio)currentPrio, nowTime.tv_sec);

        /* new high prio buffers must be sent first */
        if (currentPrio < QPRIOMAX && !myself->queueEmpty(QPRIOMAX)) {
          vlog.verbose("flushThread(): new high prio %u buffer at prio %u, restart at high prio", QPRIOMAX, currentPrio);
          restart = true;
          break;
        }
        /* new medium prio buffers must be sent before low */
        if (currentPrio < QPRIOMEDIUM && !myself->queueEmpty(QPRIOMEDIUM)) {
          vlog.verbose("flushThread(): new medium prio buffer at prio %u, restart at high prio", currentPrio);
          restart = true;
          break;
        }
      }
      if (n < 0)
        break;
      if (myself->queueEmpty((QPrio)currentPrio)) {
        /* wake up all flushers at prio currentPrio */
        MUTEX_LOCK(&flushedConditionLock[currentPrio]);
        TGVNC_CONDITION_BROADCAST(&flushedCondition[currentPrio]);
        MUTEX_UNLOCK(&flushedConditionLock[currentPrio]);
      }
      if (restart)
        currentPrio = QPRIOMAX;
      else
        currentPrio--;
    }
  }
#ifndef WIN32
#if defined(__APPLE__)
  vlog.debug("flushThread (tid %u) exiting, slept %llu times, %llu flush calls", gettid(), sleepCount, myself->flushCalls);
#else
  vlog.debug("flushThread (tid %lu) exiting, slept %llu times, %llu flush calls", gettid(), sleepCount, myself->flushCalls);
#endif
#else
  vlog.debug("flushThread (tid %lu) exiting, slept %llu times, %llu flush calls", GetCurrentThreadId(), sleepCount, myself->flushCalls);
#endif
  for (int q = QPRIOMIN; q <= QPRIOMAX; q++) {
    if (writtenBytes[q] > 0)
      vlog.debug("flushThread() wrote %zu bytes for prio %u", writtenBytes[q], q);
  }

  THREAD_EXIT(THREAD_NULL);
}

void FdOutStream::flush(int key, QPrio prio, bool wait)
{
//  if(!check_key(key, __func__))
//    return;
  if(!check_prio(prio, __func__))
    return;

//  if (key == MASTERKEY)
//    vlog.verbose("flush(): called with key %u, prio %u, wait=%u", key, prio, wait);

  if (queueEmpty(prio) || (flushActive && !wait))
    return;

  flushCalls++;

  /* wake up flushThread */
  MUTEX_LOCK(&buffersAvailableConditionLock);
  TGVNC_CONDITION_SEND_SIG(&buffersAvailableCondition);
  MUTEX_UNLOCK(&buffersAvailableConditionLock);

  if (wait) {
    /* sleep for prio prio */
    MUTEX_LOCK(&flushedConditionLock[prio]);
    TGVNC_CONDITION_TIMED_WAIT(&flushedCondition[prio], &flushedConditionLock[prio], 500);
    MUTEX_UNLOCK(&flushedConditionLock[prio]);
  }
}
