/* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
 * Copyright (C) 2005 Martin Koegler
 * Copyright (C) 2010 TigerVNC Team
 * 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 <unistd.h>
#include <stdlib.h>

#include <rdr/Exception.h>
#include <rdr/MultiOutStream.h>
#include <rfb/LogWriter.h>
#include <rfb/Configuration.h>
#include <rfb/MP.h>
#include <rdr/MultiStream.h>
#include <errno.h>
#include <zstd.h>

#define MULTIHEADERSIZE 3
#define OLDMAXCHUNK (16*1024 - MULTIHEADERSIZE)
#define PRINTMAXCHUNK (4*1024 - MULTIHEADERSIZE)

static unsigned maxchunk = OLDMAXCHUNK;
static int multiMaxVNCChunkInt;

using namespace rdr;

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

#define MAXVNCDEF (2048 - MULTIHEADERSIZE)
#define MAXVNCDEFSMALL (256 - MULTIHEADERSIZE)
#define MAXVNCMIN (128 - MULTIHEADERSIZE)
#define MAXVNCMAX (16 * 1024 - MULTIHEADERSIZE)
#define DEFTHRESH (7 * 1024)

/* VNC data blobs can be huge, so split them into chunks fitting well into TLS buffer */
rfb::IntParameter MultiOutStream::multiMaxVNCChunk("MultiMaxVNCChunk", "Maximum VNC chunk size sent through multiplexer", MAXVNCDEF, MAXVNCMIN, MAXVNCMAX);
rfb::IntParameter MultiOutStream::multiMaxVNCChunkSmall("MultiMaxVNCChunkSmall", "Maximum VNC chunk size sent when small size requested", MAXVNCDEFSMALL, MAXVNCMIN, MAXVNCMAX);
rfb::IntParameter MultiOutStream::pingThresh("PingThresh", "Min. VNC data packet size to trigger turn-around time tests", DEFTHRESH, MAXVNCMIN, MAXVNCMAX);
rfb::IntParameter pingInterval("PingInterval", "Interval in milliseconds between two turn-around time tests (0 to disable)", 1000, 0, 60000);
rfb::IntParameter MultiOutStream::videoBoostDelay("VideoBoostDelay", "Time in seconds after last audio packet before boosting VNC package size", 30, 0, 3600);
rfb::IntParameter multiKeepOutBuffersHigh("MultiKeepOutBuffersHigh", "outgoing buffers to keep for high priority, Multi layer", 32, 4, MAXBUFFERNUM);
rfb::IntParameter multiKeepOutBuffersMedium("MultiKeepOutBuffersMedium", "outgoing buffers to keep for medium priority, Multi layer", 16, 4, MAXBUFFERNUM);
rfb::IntParameter multiKeepOutBuffersLow("MultiKeepOutBuffersLow", "outgoing buffers to keep for low priority, Multi layer", 128, 2, MAXBUFFERNUM);
rfb::IntParameter multiThreshOutBuffersHigh("MultiThreshOutBuffersHigh", "when to force flush outgoing buffers for high priority, Multi layer", 64, 8, MAXBUFFERNUM);
rfb::IntParameter multiThreshOutBuffersMedium("MultiThreshOutBuffersMedium", "when to force flush outgoing buffers for medium priority, Multi layer", 128, 32, MAXBUFFERNUM);
rfb::IntParameter multiThreshOutBuffersLow("MultiThreshOutBuffersLow", "when to force flush outgoing buffers for low priority, Multi layer", 256, 4, MAXBUFFERNUM);
rfb::IntParameter multiMaxOutBuffersHigh("MultiMaxOutBuffersHigh", "available outgoing buffers for high priority, Multi layer", 256, 4, MAXBUFFERNUM);
rfb::IntParameter multiMaxOutBuffersMedium("MultiMaxOutBuffersMedium", "available outgoing buffers for medium priority, Multi layer", 16384, 50, MAXBUFFERNUM);
rfb::IntParameter multiMaxOutBuffersLow("MultiMaxOutBuffersLow", "available outgoing buffers for low priority, Multi layer", 1024, 2, MAXBUFFERNUM);
rfb::IntParameter MultiOutStream::pulseZstdLevel("PulseZstdLevel", "Pulseaudio ZSTD compression level (0-22, 0 to disable)", 0, 0, 22);
rfb::BoolParameter MultiOutStream::pulseZstdEnabled("PulseZstdEnabled", "Enable Pulseaudio ZSTD compression", true);

time_t MultiOutStream::lastAudioTime;

/* Limit signals to 4K, MAXSIGNAL is the max data size */
#define MAXSIGNAL (4 * 1024 - MULTIHEADERSIZE - 4)

MultiOutStream::MultiOutStream(OutStream* _out)
	: out(_out), lastPing(0), multiPartSupport(false),
	remoteSupportsPulseZstd(false), pulseZstdSentRaw(0), pulseZstdSentCompressed(0) {
	struct timeval now;

#ifdef WIN32
	vlog.debug("Init in thread %lu", GetCurrentThreadId());
#elif defined(__APPLE__)
	vlog.debug("Init in thread %u", gettid());
#else
	vlog.debug("Init in thread %lu", gettid());
#endif
	/* enforce sanity limits: max is TLS buffer size - multi header */
	vlog.debug("multiMaxVNCChunk is %u", (int) multiMaxVNCChunk);
	vlog.debug("multiMaxVNCChunkSmall is %u", (int) multiMaxVNCChunkSmall);
	multiMaxVNCChunkInt = multiMaxVNCChunk;
	videoBoostDelayInt = videoBoostDelay;
	lastAudioTime = 0;
	classLog = &vlog;
	maxBuffers[QPRIOHIGH] = multiMaxOutBuffersHigh;
	maxBuffers[QPRIOMEDIUM] = multiMaxOutBuffersMedium;
	maxBuffers[QPRIOLOW] = multiMaxOutBuffersLow;
	keepBuffers[QPRIOHIGH] = multiKeepOutBuffersHigh;
	keepBuffers[QPRIOMEDIUM] = multiKeepOutBuffersMedium;
	keepBuffers[QPRIOLOW] = multiKeepOutBuffersLow;
	flushThreshBuffers[QPRIOHIGH] = multiThreshOutBuffersHigh;
	flushThreshBuffers[QPRIOMEDIUM] = multiThreshOutBuffersMedium;
	flushThreshBuffers[QPRIOLOW] = multiThreshOutBuffersLow;
	maxBufferSize = out->getMaxBufferSize() - MULTIHEADERSIZE;

	if (!gettimeofday(&now, NULL)) {
		lastAudioTime = now.tv_sec;
	}

	out->setAllowMasterKey(false);
}


MultiOutStream::~MultiOutStream() {
	if (pulseZstdSentRaw > 0)
		vlog.debug("~MultiOutStream(): Pulse ZSTD with current level %u sent compressed %llu, raw %llu, reduced to %llu%%", (int) pulseZstdLevel, pulseZstdSentCompressed, pulseZstdSentRaw, pulseZstdSentCompressed * 100 / pulseZstdSentRaw);
	delete out;
	resetClassLog();
}

inline void MultiOutStream::copyHeader(struct queueBuffer * buffer, const U16 len, const U8 streamId) {
	copyU8(buffer->data, streamId);
	copyU16(buffer->data + 1, len);
}

// Submit a chain of buffers to this layer for sending.
// Last buffer in chain must have next == NULL (preset by getQueueBuffer())
// Returns true if success.
// Queues are per layer == prio.
bool MultiOutStream::submitQueueBuffer(struct queueBuffer * firstBuffer, struct queueBuffer * lastBuffer, int key, QPrio prio) {
	struct queueBuffer * outBuffer;
	struct queueBuffer * lastOutBuffer = NULL;
	struct queueBuffer * firstOutBuffer = NULL;
	struct queueBuffer * nextBuffer;

	if(unlikely(!firstBuffer))
		return false;
	if(unlikely(!check_key(key, __func__)))
		return false;
	if(unlikely(!check_prio(prio, __func__)))
		return false;
	if (unlikely(firstBuffer->used == 0)) {
		vlog.error("submitQueueBuffer(): buffer->used == 0 for key %u, prio %u, buffer %u, rejecting", key, prio, firstBuffer->number);
		return false;
	}

//	vlog.verbose("submitQueueBuffer(): buffer %u, prio %u, size %u, used %u", firstBuffer->number, prio, firstBuffer->size, firstBuffer->used);

	setAllowMasterKey(false);

//	Sanity checks
#if 0
	nextBuffer = firstBuffer;
	while (nextBuffer->next) {
		nextBuffer = nextBuffer->next;
		if (unlikely(nextBuffer->used == 0)) {
			vlog.debug("submitQueueBuffer(): buffer->used == 0 for key %u, prio %u, buffer %u, rejecting", key, prio, nextBuffer->number);
			return false;
		}
		if (unlikely(nextBuffer->used > nextBuffer->size)) {
			vlog.debug("submitQueueBuffer(): buffer->used > buffer->size for key %u, prio %u, buffer %u, rejecting", key, prio, nextBuffer->number);
			return false;
		}
	}
#endif
//	Transfer this buffer chain into output buffer chain
	nextBuffer = firstBuffer;
	while (nextBuffer) {
		outBuffer = out->getQueueBuffer(nextBuffer->used + MULTIHEADERSIZE, prio);
		if (unlikely(!outBuffer)) {
			vlog.error("submitQueueBuffer(): out->getQueueBuffer() for size %u, prio %u failed - sleep and retry!", nextBuffer->used + MULTIHEADERSIZE, prio);
			Sleep(100);
			continue;
		}
		copyHeader(outBuffer, nextBuffer->used);
		memcpy(outBuffer->data + MULTIHEADERSIZE, nextBuffer->data, nextBuffer->used);
		outBuffer->used = nextBuffer->used + MULTIHEADERSIZE;

		if (!firstOutBuffer) {
			firstOutBuffer = outBuffer;
		} else {
			lastOutBuffer->next = outBuffer;
		}
		lastOutBuffer = outBuffer;

		nextBuffer = nextBuffer->next;
	}
	lastOutBuffer->next = NULL;
	if (likely(out->submitQueueBuffer(firstOutBuffer, lastOutBuffer, MULTIKEY, prio))) {
		// success, cleanup input buffer chain
		returnQueueBuffer(firstBuffer, prio);
		return true;
	} else {
		// failed, cleanup output buffer chain
		vlog.error("submitQueueBuffer(): out->submitQueueBuffer() for buffer %u, prio %u failed!", firstOutBuffer->number, prio);
		out->returnQueueBuffer(firstOutBuffer, prio);
		return false;
	}
}


void MultiOutStream::flush(int key, QPrio prio, bool wait) {
	out->flush(MULTIKEY, prio, wait);
}

void MultiOutStream::writeBuffer(const U8* buf, const U32 len, const U8 streamId, bool flush) {
	U8* sendBuf = (U8*) buf;
	U32 sendLen = len;
	U8 sendStreamId = streamId;

	if (unlikely(streamId >= UNKNOWN_STREAM_ID)) {
		vlog.error("writeBuffer(): Received unknown multiplex stream id %u data of len %u, call savebacktrace() and drop.", streamId, len);
		savebacktrace(__func__);
		return;
	}
	if (unlikely(len == 0)) {
		vlog.error("writeBuffer(): called with len 0 for stream id %u, call savebacktrace() and drop.", streamId);
		savebacktrace(__func__);
		sleep(1);
		return;
	}

	QPrio sendPrio;
	unsigned sendMaxchunk;

	switch(streamId) {
		case PULSEAUDIO_ZSTD_STREAM_ID:
			vlog.error("writeBuffer(): PULSEAUDIO_ZSTD_STREAM_ID not allowed here!");
			return;
		case PULSEAUDIO_STREAM_ID:
			struct timeval now;

			sendPrio = QPRIOHIGH;
			sendMaxchunk = maxchunk;
			if (likely(!gettimeofday(&now, NULL))) {
				lastAudioTime = now.tv_sec;
			}
			if (remoteSupportsPulseZstd && pulseZstdEnabled && pulseZstdLevel > 0 && len > 128) {
				const int compressedDataSize = ZSTD_compressBound(len);
				int compressedSize;

				sendBuf = (U8*) malloc(compressedDataSize);
				if (unlikely(!sendBuf)) {
					sendBuf = (U8*) buf;
					break;
				}
				compressedSize = ZSTD_compress(sendBuf, compressedDataSize, buf, len, pulseZstdLevel);
				if (likely(compressedSize > 0)) {
					if ((U32) compressedSize < len) {
						pulseZstdSentRaw += len;
						pulseZstdSentCompressed += compressedSize;
						sendLen = compressedSize;
						sendStreamId = PULSEAUDIO_ZSTD_STREAM_ID;
					} else {
						vlog.verbose("pulse zstd compression not beneficial: %u >= %u", compressedSize, len);
						free(sendBuf);
						sendBuf = (U8*) buf;
					}
				} else {
					vlog.error("ZSTD_compress() failed with error %i", compressedSize);
					free(sendBuf);
					sendBuf = (U8*) buf;
				}
			}
			break;
		case FILE_TRANSFER_STREAM_ID:
			sendPrio = QPRIOLOW;
			sendMaxchunk = PRINTMAXCHUNK;
			break;
		default:
			sendPrio = QPRIOMEDIUM;
			sendMaxchunk = multiMaxVNCChunkInt;
	}
	if (sendLen > sendMaxchunk) {
		if (likely(multiPartSupport))
			vlog.verbose("writeBuffer(): need to split stream id %u buffer of len %u to MultiPart chunks of size %u", sendStreamId, sendLen, sendMaxchunk);
		else
			vlog.debug("writeBuffer(): need to split stream id %u buffer of len %u to separate chunks of size %u", sendStreamId, sendLen, sendMaxchunk);
	}

	U16 chunksize;
	U32 left = sendLen;
	U32 pos = 0;
	U8 multiPartVal = 0;
	struct queueBuffer * buffer;
	struct queueBuffer * firstBuffer = NULL;
	struct queueBuffer * lastBuffer = NULL;

	while (left > 0) {
		if (left > sendMaxchunk) {
			chunksize = sendMaxchunk;
			if (likely(multiPartSupport))
				multiPartVal = MULTIPARTMARKER;
		} else {
			chunksize = left;
			multiPartVal = 0;
		}
		buffer = out->getQueueBuffer(MULTIHEADERSIZE + chunksize, sendPrio);
		if (unlikely(!buffer)) {
			vlog.error("writeBuffer(): out->getQueueBuffer() for size %u, prio %u failed - sleep and retry!", MULTIHEADERSIZE + chunksize, sendPrio);
			Sleep(100);
			continue;
		}
		copyHeader(buffer, chunksize, sendStreamId | multiPartVal);
		memcpy(buffer->data + MULTIHEADERSIZE, sendBuf + pos, chunksize);
		buffer->used = MULTIHEADERSIZE + chunksize;

		if (!firstBuffer) {
			firstBuffer = buffer;
		} else {
			lastBuffer->next = buffer;
		}
		lastBuffer = buffer;

		left -= chunksize;
		pos += chunksize;
	}
	lastBuffer->next = NULL;
	if (sendBuf != buf)
		free(sendBuf);
	if (unlikely(!out->submitQueueBuffer(firstBuffer, lastBuffer, MULTIKEY, sendPrio))) {
		vlog.error("writeBuffer(): out->submitQueueBuffer failed for buffer %u, prio %u, size %u, dropping data!", firstBuffer->number, sendPrio, chunksize);
		out->returnQueueBuffer(firstBuffer, sendPrio);
	}
	if (flush)
		out->flush(MULTIKEY, sendPrio);
}

void MultiOutStream::overrun(size_t needed, int key, QPrio prio) {
	out->overrun(needed + MULTIHEADERSIZE, MULTIKEY, prio);
}

void MultiOutStream::check(size_t length, int key, QPrio prio) {
	out->check(length + MULTIHEADERSIZE, MULTIKEY, prio);
}

void MultiOutStream::writeU8(U8 u, int key, QPrio prio) {
//	if(!check_key(key, __func__))
//		return;
	if(!check_prio(prio, __func__))
		return;

	struct queueBuffer * buffer = out->getQueueBuffer(MULTIHEADERSIZE + 1, prio);
	if (likely(buffer)) {
		copyHeader(buffer, 1);
		copyU8(buffer->data + MULTIHEADERSIZE, u);
		buffer->used = MULTIHEADERSIZE + 1;
		if (unlikely(!out->submitQueueBuffer(buffer, buffer, MULTIKEY, prio))) {
			vlog.error("writeU8(): failed to submit buffer %u for prio %u", buffer->number, prio);
			out->returnQueueBuffer(buffer, prio);
		}

	} else {
		vlog.error("writeU8(): failed to get buffer for prio %u, size %u", prio, MULTIHEADERSIZE + 1);
	}
}

void MultiOutStream::writeU16(U16 u, int key, QPrio prio) {
//	if(!check_key(key, __func__))
//		return;
	if(!check_prio(prio, __func__))
		return;

	struct queueBuffer * buffer = out->getQueueBuffer(MULTIHEADERSIZE + 2, prio);
	if (likely(buffer)) {
		copyHeader(buffer, 2);
		copyU16(buffer->data + MULTIHEADERSIZE, u);
		buffer->used = MULTIHEADERSIZE + 2;
		if (unlikely(!out->submitQueueBuffer(buffer, buffer, MULTIKEY, prio))) {
			vlog.error("writeU16(): failed to submit buffer %u for prio %u", buffer->number, prio);
			out->returnQueueBuffer(buffer, prio);
		}
	} else {
		vlog.error("writeU16(): failed to get buffer for prio %u, size %u", prio, MULTIHEADERSIZE + 2);
	}
}

void MultiOutStream::writeU32(U32 u, int key, QPrio prio) {
//	if(!check_key(key, __func__))
//		return;
	struct queueBuffer * buffer = out->getQueueBuffer(MULTIHEADERSIZE + 4, prio);
	if (likely(buffer)) {
		copyHeader(buffer, 4);
		copyU32(buffer->data + MULTIHEADERSIZE, u);
		buffer->used = MULTIHEADERSIZE + 4;
		if (unlikely(!out->submitQueueBuffer(buffer, buffer, MULTIKEY, prio))) {
			vlog.error("writeU32(): failed to submit buffer %u for prio %u", buffer->number, prio);
			out->returnQueueBuffer(buffer, prio);
		}
	} else {
		vlog.error("writeU32(): failed to get buffer for prio %u, size %u", prio, MULTIHEADERSIZE + 4);
	}
}

void MultiOutStream::pad(size_t bytes, int key, QPrio prio) {
//	if(!check_key(key, __func__))
//		return;
	if (unlikely(!check_prio(prio, __func__)))
		return;

	if (unlikely(bytes <= 0 || bytes > maxBufferSize)) {
		vlog.error("pad(): called with bytes %zu, returning", bytes);
		return;
	}
	struct queueBuffer * buffer = out->getQueueBuffer(MULTIHEADERSIZE + bytes, prio);
	if (likely(buffer)) {
		copyHeader(buffer, bytes);
		memset(buffer->data + MULTIHEADERSIZE, 0, bytes);
		buffer->used = MULTIHEADERSIZE + bytes;
		if (unlikely(!out->submitQueueBuffer(buffer, buffer, MULTIKEY, prio))) {
			vlog.error("pad(): failed to submit buffer %u for prio %u", buffer->number, prio);
			out->returnQueueBuffer(buffer, prio);
		}
	} else {
		vlog.error("pad(): failed to get buffer for prio %u, size %zu", prio, MULTIHEADERSIZE + bytes);
	}
}

/* writeBytes() writes an exact number of bytes. This must be VNC data. */
void MultiOutStream::writeBytes(const void* data, size_t numBytes, int key, QPrio prio) {
	unsigned chunksize;
	unsigned maxWriteChunk;
	int bytesToSend;
	struct queueBuffer * buffer;
	struct queueBuffer * firstBuffer = NULL;
	struct queueBuffer * lastBuffer = NULL;
	bool sendPing = false;
	U8 * u8Data = (U8 *) data;
	struct timeval startTime;
	time_t audioDiff;

//	if(!check_key(key, __func__))
//		return;
	if(!check_prio(prio, __func__))
		return;
	if((numBytes <= 0) || !data) {
		vlog.debug("writeBytes(): called without data, returning");
		return;
	}
	if (unlikely(gettimeofday(&startTime, NULL) < 0)) {
		vlog.error("writeBytes(): gettimeofday() failed");
		return;
	}
	bytesToSend = numBytes;
	if (pingInterval > 0 && bytesToSend >= pingThresh && (U64) startTime.tv_sec * 1000 > (U64) lastPing * 1000 + pingInterval) {
		lastPing = startTime.tv_sec;
		sendPing = true;
	}
	audioDiff = startTime.tv_sec - lastAudioTime;
	if (audioDiff > videoBoostDelayInt) {
		if (sendPing) {
			vlog.verbose("writeBytes(): audioDiff %lu > videoBoostDelay %u, boosting VNC maxWriteChunk for size %u buffer from %u to %u",
				audioDiff, videoBoostDelayInt, bytesToSend, multiMaxVNCChunkInt, maxchunk);
		}
		maxWriteChunk = maxchunk;
	} else {
		maxWriteChunk = multiMaxVNCChunkInt;
	}
	while (bytesToSend > 0) {
		if ((unsigned) bytesToSend > maxWriteChunk) {
			chunksize = maxWriteChunk;
		} else {
			chunksize = bytesToSend;
		}

		buffer = out->getQueueBuffer(MULTIHEADERSIZE + chunksize, prio);
		if (unlikely(!buffer)) {
			vlog.error("writeBytes(): out->getQueueBuffer() for size %u, prio %u failed - sleep and retry!", MULTIHEADERSIZE + chunksize, prio);
			Sleep(100);
			continue;
		}
		copyHeader(buffer, chunksize);
		memcpy(buffer->data + MULTIHEADERSIZE, u8Data, chunksize);
		buffer->used = MULTIHEADERSIZE + chunksize;

		if (!firstBuffer) {
			firstBuffer = buffer;
		} else {
			lastBuffer->next = buffer;
		}
		lastBuffer = buffer;

		bytesToSend -= chunksize;
		u8Data += chunksize;
	}
	lastBuffer->next = NULL;
	if (unlikely(!out->submitQueueBuffer(firstBuffer, lastBuffer, MULTIKEY, prio))) {
		vlog.error("writeBytes(): failed to submit buffer %u for prio %u, size %u, dropping data!", firstBuffer->number, prio, chunksize);
		out->returnQueueBuffer(firstBuffer, prio);
	}
	if (sendPing) {
		U64 pingTime;
		struct timeval postTime;

		if (unlikely(gettimeofday(&postTime, NULL) < 0)) {
			vlog.error("sendPostPing(): gettimeofday(&postTime) failed");
		} else {
			pingTime = postTime.tv_sec * 1000000 + postTime.tv_usec;
			if (vlog.getLevel() >= vlog.LEVEL_DEBUG)
				sendSignal(HIGHPING_SIGNAL_ID, &pingTime, 8, storedKey, QPRIOHIGH);
			sendSignal(PING_SIGNAL_ID, &pingTime, 8, storedKey, prio);
		}
	}
}

void MultiOutStream::writeOpaque16(U16 u, int key, QPrio prio) {
//	if(!check_key(key, __func__))
//		return;
	if(!check_prio(prio, __func__))
		return;

	struct queueBuffer * buffer = out->getQueueBuffer(MULTIHEADERSIZE + 2, prio);
	if (likely(buffer)) {
		copyHeader(buffer, 2);
		*(buffer->data + MULTIHEADERSIZE) = ((U8*)&u)[0];
		*(buffer->data + MULTIHEADERSIZE + 1) = ((U8*)&u)[1];
		buffer->used = MULTIHEADERSIZE + 2;
		if (unlikely(!out->submitQueueBuffer(buffer, buffer, MULTIKEY, prio))) {
			vlog.error("writeOpaque16(): failed to submit buffer %u for prio %u", buffer->number, prio);
			out->returnQueueBuffer(buffer, prio);
		}
	} else {
		vlog.error("writeOpaque16(): failed to get buffer for prio %u, size %u", prio, MULTIHEADERSIZE + 2);
	}
}

void MultiOutStream::writeOpaque32(U32 u, int key, QPrio prio) {
//	if(!check_key(key, __func__))
//		return;
	if(!check_prio(prio, __func__))
		return;

	struct queueBuffer * buffer = out->getQueueBuffer(MULTIHEADERSIZE + 4, prio);
	if (likely(buffer)) {
		copyHeader(buffer, 4);
		*(buffer->data + MULTIHEADERSIZE) = ((U8*)&u)[0];
		*(buffer->data + MULTIHEADERSIZE + 1) = ((U8*)&u)[1];
		*(buffer->data + MULTIHEADERSIZE + 2) = ((U8*)&u)[2];
		*(buffer->data + MULTIHEADERSIZE + 3) = ((U8*)&u)[3];
		buffer->used = MULTIHEADERSIZE + 4;
		if (unlikely(!out->submitQueueBuffer(buffer, buffer, MULTIKEY, prio))) {
			vlog.error("writeOpaque32(): failed to submit buffer %u for prio %u", buffer->number, prio);
			out->returnQueueBuffer(buffer, prio);
		}
	} else {
		vlog.error("writeOpaque32(): failed to get buffer for prio %u, size %u", prio, MULTIHEADERSIZE + 4);
	}
}

bool MultiOutStream::sendSignal(U16 sigId, const void* data, U16 dataLength, int key, QPrio prio) {
	if (unlikely(dataLength > MAXSIGNAL)) {
		vlog.error("Signal %u dataLength %u too large!", sigId, dataLength);
		return false;
	}
	if (unlikely(dataLength > 0 && !data)) {
		vlog.error("Signal %u dataLength %u, but no data!", sigId, dataLength);
		return false;
	}
//	vlog.debug("sendSignal(): signal %u, data length %u, prio %u", sigId, dataLength, prio);
//	if(!check_key(key, __func__))
//		return false;

	struct queueBuffer * buffer = out->getQueueBuffer(MULTIHEADERSIZE + 4 + dataLength, prio);
	if (likely(buffer)) {
		copyHeader(buffer, dataLength + 4, SIGNAL_STREAM_ID);
		copyU16(buffer->data + MULTIHEADERSIZE, sigId);
		copyU16(buffer->data + MULTIHEADERSIZE + 2, dataLength);
		if (dataLength > 0)
			memcpy(buffer->data + MULTIHEADERSIZE + 4, data, dataLength);
		buffer->used = MULTIHEADERSIZE + 4 + dataLength;
		if (likely(out->submitQueueBuffer(buffer, buffer, MULTIKEY, prio)))
			out->flush(MULTIKEY, prio);
		else {
			vlog.error("sendSignal(): failed to submit buffer %u for prio %u", buffer->number, prio);
			out->returnQueueBuffer(buffer, prio);
		}
	} else {
		vlog.error("sendSignal(): failed to get buffer for prio %u, size %u", prio, MULTIHEADERSIZE + 4 + dataLength);
	}
	return true;
}

void MultiOutStream::setBigMulti() {
	maxchunk = maxBufferSize;
	vlog.debug("setBigMulti: set multi maxchunk to %u", maxchunk);
}

void MultiOutStream::setMultiMaxVNCChunk(bool small) {
	if (small) {
		if (multiMaxVNCChunkInt != multiMaxVNCChunkSmall) {
			vlog.debug("change multiMaxVNCChunkInt to %u", (int) multiMaxVNCChunkSmall);
			multiMaxVNCChunkInt = multiMaxVNCChunkSmall;
		}
	} else {
		if (multiMaxVNCChunkInt != multiMaxVNCChunk) {
			vlog.debug("change multiMaxVNCChunkInt to %u", (int) multiMaxVNCChunk);
			multiMaxVNCChunkInt = multiMaxVNCChunk;
		}
	}
}

void MultiOutStream::printBufferUsage() {
	out->printBufferUsage();
}
