/**
 * FreeRDP: A Remote Desktop Protocol Implementation
 * Bulk Compression
 *
 * Copyright 2014 Marc-Andre Moreau <marcandre.moreau@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <math.h>
#include <winpr/assert.h>

#include <freerdp/config.h>

#include "../core/settings.h"
#include "bulk.h"
#include "../codec/mppc.h"
#include "../codec/ncrush.h"
#include "../codec/xcrush.h"

#include <freerdp/log.h>
#define TAG FREERDP_TAG("core")

//#define WITH_BULK_DEBUG 1

struct rdp_bulk
{
	ALIGN64 rdpContext* context;
	ALIGN64 UINT32 CompressionLevel;
	ALIGN64 UINT16 CompressionMaxSize;
	ALIGN64 MPPC_CONTEXT* mppcSend;
	ALIGN64 MPPC_CONTEXT* mppcRecv;
	ALIGN64 NCRUSH_CONTEXT* ncrushRecv;
	ALIGN64 NCRUSH_CONTEXT* ncrushSend;
	ALIGN64 XCRUSH_CONTEXT* xcrushRecv;
	ALIGN64 XCRUSH_CONTEXT* xcrushSend;
	ALIGN64 BYTE OutputBuffer[65536];
};

#if defined(WITH_BULK_DEBUG)
static inline const char* bulk_get_compression_flags_string(UINT32 flags)
{
	flags &= BULK_COMPRESSION_FLAGS_MASK;

	if (flags == 0)
		return "PACKET_UNCOMPRESSED";
	else if (flags == PACKET_COMPRESSED)
		return "PACKET_COMPRESSED";
	else if (flags == PACKET_AT_FRONT)
		return "PACKET_AT_FRONT";
	else if (flags == PACKET_FLUSHED)
		return "PACKET_FLUSHED";
	else if (flags == (PACKET_COMPRESSED | PACKET_AT_FRONT))
		return "PACKET_COMPRESSED | PACKET_AT_FRONT";
	else if (flags == (PACKET_COMPRESSED | PACKET_FLUSHED))
		return "PACKET_COMPRESSED | PACKET_FLUSHED";
	else if (flags == (PACKET_AT_FRONT | PACKET_FLUSHED))
		return "PACKET_AT_FRONT | PACKET_FLUSHED";
	else if (flags == (PACKET_COMPRESSED | PACKET_AT_FRONT | PACKET_FLUSHED))
		return "PACKET_COMPRESSED | PACKET_AT_FRONT | PACKET_FLUSHED";

	return "PACKET_UNKNOWN";
}
#endif

WINPR_ATTR_NODISCARD
static UINT32 bulk_compression_level(rdpBulk* WINPR_RESTRICT bulk)
{
	WINPR_ASSERT(bulk);
	WINPR_ASSERT(bulk->context);
	const rdpSettings* settings = bulk->context->settings;
	WINPR_ASSERT(settings);

	const UINT32 CompressionLevel = freerdp_settings_get_uint32(settings, FreeRDP_CompressionLevel);
	bulk->CompressionLevel =
	    (CompressionLevel >= PACKET_COMPR_TYPE_RDP61) ? PACKET_COMPR_TYPE_RDP61 : CompressionLevel;
	WINPR_ASSERT(bulk->CompressionLevel <= UINT16_MAX);
	return bulk->CompressionLevel;
}

static void bulk_update_compression_max_size(rdpBulk* WINPR_RESTRICT bulk)
{
	WINPR_ASSERT(bulk);
	const UINT32 CompressionLevel = bulk_compression_level(bulk);
	bulk->CompressionMaxSize = (CompressionLevel < PACKET_COMPR_TYPE_64K) ? 8192 : UINT16_MAX;
}
UINT16 bulk_compression_max_size(rdpBulk* WINPR_RESTRICT bulk)
{
	bulk_update_compression_max_size(bulk);
	return bulk->CompressionMaxSize;
}

#if defined(WITH_BULK_DEBUG)
static inline int bulk_compress_validate(rdpBulk* bulk, const BYTE* pSrcData, UINT32 SrcSize,
                                         const BYTE* pDstData, UINT32 DstSize, UINT32 Flags)
{
	int status;
	const BYTE* v_pSrcData = nullptr;
	const BYTE* v_pDstData = nullptr;
	UINT32 v_SrcSize = 0;
	UINT32 v_DstSize = 0;
	UINT32 v_Flags = 0;

	WINPR_ASSERT(bulk);
	WINPR_ASSERT(pSrcData);
	WINPR_ASSERT(pDstData);

	v_pSrcData = pDstData;
	v_SrcSize = DstSize;
	v_Flags = Flags | bulk->CompressionLevel;
	status = bulk_decompress(bulk, v_pSrcData, v_SrcSize, &v_pDstData, &v_DstSize, v_Flags);

	if (status < 0)
	{
		WLog_DBG(TAG, "compression/decompression failure");
		return status;
	}

	if (v_DstSize != SrcSize)
	{
		WLog_DBG(TAG,
		         "compression/decompression size mismatch: Actual: %" PRIu32 ", Expected: %" PRIu32
		         "",
		         v_DstSize, SrcSize);
		return -1;
	}

	if (memcmp(v_pDstData, pSrcData, SrcSize) != 0)
	{
		WLog_DBG(TAG, "compression/decompression input/output mismatch! flags: 0x%08" PRIX32 "",
		         v_Flags);
#if 1
		WLog_DBG(TAG, "Actual:");
		winpr_HexDump(TAG, WLOG_DEBUG, v_pDstData, SrcSize);
		WLog_DBG(TAG, "Expected:");
		winpr_HexDump(TAG, WLOG_DEBUG, pSrcData, SrcSize);
#endif
		return -1;
	}

	return status;
}
#endif

int bulk_decompress(rdpBulk* WINPR_RESTRICT bulk, const BYTE* WINPR_RESTRICT pSrcData,
                    UINT32 SrcSize, const BYTE** WINPR_RESTRICT ppDstData,
                    UINT32* WINPR_RESTRICT pDstSize, UINT32 flags)
{
	int status = -1;

	WINPR_ASSERT(bulk);
	WINPR_ASSERT(bulk->context);
	WINPR_ASSERT(pSrcData);
	WINPR_ASSERT(ppDstData);
	WINPR_ASSERT(pDstSize);

	rdpMetrics* metrics = bulk->context->metrics;
	WINPR_ASSERT(metrics);

	bulk_update_compression_max_size(bulk);
	const UINT32 type = flags & BULK_COMPRESSION_TYPE_MASK;

	if (flags & BULK_COMPRESSION_FLAGS_MASK)
	{
		switch (type)
		{
			case PACKET_COMPR_TYPE_8K:
				mppc_set_compression_level(bulk->mppcRecv, 0);
				status =
				    mppc_decompress(bulk->mppcRecv, pSrcData, SrcSize, ppDstData, pDstSize, flags);
				break;

			case PACKET_COMPR_TYPE_64K:
				mppc_set_compression_level(bulk->mppcRecv, 1);
				status =
				    mppc_decompress(bulk->mppcRecv, pSrcData, SrcSize, ppDstData, pDstSize, flags);
				break;

			case PACKET_COMPR_TYPE_RDP6:
				status = ncrush_decompress(bulk->ncrushRecv, pSrcData, SrcSize, ppDstData, pDstSize,
				                           flags);
				break;

			case PACKET_COMPR_TYPE_RDP61:
				status = xcrush_decompress(bulk->xcrushRecv, pSrcData, SrcSize, ppDstData, pDstSize,
				                           flags);
				break;

			case PACKET_COMPR_TYPE_RDP8:
				WLog_ERR(TAG, "Unsupported bulk compression type %08" PRIx32,
				         bulk->CompressionLevel);
				status = -1;
				break;
			default:
				WLog_ERR(TAG, "Unknown bulk compression type %08" PRIx32, bulk->CompressionLevel);
				status = -1;
				break;
		}
	}
	else
	{
		*ppDstData = pSrcData;
		*pDstSize = SrcSize;
		status = 0;
	}

	if (status >= 0)
	{
		const UINT32 CompressedBytes = SrcSize;
		const UINT32 UncompressedBytes = *pDstSize;
		const double CompressionRatio =
		    metrics_write_bytes(metrics, UncompressedBytes, CompressedBytes);
#ifdef WITH_BULK_DEBUG
		{
			WLog_DBG(TAG,
			         "Decompress Type: %" PRIu32 " Flags: %s (0x%08" PRIX32
			         ") Compression Ratio: %f (%" PRIu32 " / %" PRIu32 "), Total: %f (%" PRIu64
			         " / %" PRIu64 ")",
			         type, bulk_get_compression_flags_string(flags), flags, CompressionRatio,
			         CompressedBytes, UncompressedBytes, metrics->TotalCompressionRatio,
			         metrics->TotalCompressedBytes, metrics->TotalUncompressedBytes);
		}
#else
		WINPR_UNUSED(CompressionRatio);
#endif
	}
	else
	{
		WLog_ERR(TAG, "Decompression failure!");
	}

	return status;
}

int bulk_compress(rdpBulk* WINPR_RESTRICT bulk, const BYTE* WINPR_RESTRICT pSrcData, UINT32 SrcSize,
                  const BYTE** WINPR_RESTRICT ppDstData, UINT32* WINPR_RESTRICT pDstSize,
                  UINT32* WINPR_RESTRICT pFlags)
{
	int status = -1;

	WINPR_ASSERT(bulk);
	WINPR_ASSERT(bulk->context);
	WINPR_ASSERT(pSrcData);
	WINPR_ASSERT(ppDstData);
	WINPR_ASSERT(pDstSize);

	rdpMetrics* metrics = bulk->context->metrics;
	WINPR_ASSERT(metrics);

	if ((SrcSize <= 50) || (SrcSize >= 16384))
	{
		*ppDstData = pSrcData;
		*pDstSize = SrcSize;
		return 0;
	}

	*pDstSize = sizeof(bulk->OutputBuffer);
	const UINT32 CompressionLevel = bulk_compression_level(bulk);
	bulk_update_compression_max_size(bulk);

	switch (CompressionLevel)
	{
		case PACKET_COMPR_TYPE_8K:
		case PACKET_COMPR_TYPE_64K:
			mppc_set_compression_level(bulk->mppcSend, CompressionLevel);
			status = mppc_compress(bulk->mppcSend, pSrcData, SrcSize, bulk->OutputBuffer, ppDstData,
			                       pDstSize, pFlags);
			break;
		case PACKET_COMPR_TYPE_RDP6:
			status = ncrush_compress(bulk->ncrushSend, pSrcData, SrcSize, bulk->OutputBuffer,
			                         ppDstData, pDstSize, pFlags);
			break;
		case PACKET_COMPR_TYPE_RDP61:
			status = xcrush_compress(bulk->xcrushSend, pSrcData, SrcSize, bulk->OutputBuffer,
			                         ppDstData, pDstSize, pFlags);
			break;
		case PACKET_COMPR_TYPE_RDP8:
			WLog_ERR(TAG, "Unsupported bulk compression type %08" PRIx32, CompressionLevel);
			status = -1;
			break;
		default:
			WLog_ERR(TAG, "Unknown bulk compression type %08" PRIx32, CompressionLevel);
			status = -1;
			break;
	}

	if (status >= 0)
	{
		const UINT32 CompressedBytes = *pDstSize;
		const UINT32 UncompressedBytes = SrcSize;
		const double CompressionRatio =
		    metrics_write_bytes(metrics, UncompressedBytes, CompressedBytes);
#ifdef WITH_BULK_DEBUG
		{
			WLog_DBG(TAG,
			         "Compress Type: %" PRIu32 " Flags: %s (0x%08" PRIX32
			         ") Compression Ratio: %f (%" PRIu32 " / %" PRIu32 "), Total: %f (%" PRIu64
			         " / %" PRIu64 ")",
			         bulk->CompressionLevel, bulk_get_compression_flags_string(*pFlags), *pFlags,
			         CompressionRatio, CompressedBytes, UncompressedBytes,
			         metrics->TotalCompressionRatio, metrics->TotalCompressedBytes,
			         metrics->TotalUncompressedBytes);
		}
#else
		WINPR_UNUSED(CompressionRatio);
#endif
	}

#if defined(WITH_BULK_DEBUG)

	if (bulk_compress_validate(bulk, pSrcData, SrcSize, *ppDstData, *pDstSize, *pFlags) < 0)
		status = -1;

#endif
	return status;
}

void bulk_reset(rdpBulk* WINPR_RESTRICT bulk)
{
	WINPR_ASSERT(bulk);

	mppc_context_reset(bulk->mppcSend, FALSE);
	mppc_context_reset(bulk->mppcRecv, FALSE);
	ncrush_context_reset(bulk->ncrushRecv, FALSE);
	ncrush_context_reset(bulk->ncrushSend, FALSE);
	xcrush_context_reset(bulk->xcrushRecv, FALSE);
	xcrush_context_reset(bulk->xcrushSend, FALSE);
}

rdpBulk* bulk_new(rdpContext* context)
{
	rdpBulk* bulk = nullptr;
	WINPR_ASSERT(context);

	bulk = (rdpBulk*)calloc(1, sizeof(rdpBulk));

	if (!bulk)
		goto fail;

	bulk->context = context;
	bulk->mppcSend = mppc_context_new(1, TRUE);
	if (!bulk->mppcSend)
		goto fail;
	bulk->mppcRecv = mppc_context_new(1, FALSE);
	if (!bulk->mppcRecv)
		goto fail;
	bulk->ncrushRecv = ncrush_context_new(FALSE);
	if (!bulk->ncrushRecv)
		goto fail;
	bulk->ncrushSend = ncrush_context_new(TRUE);
	if (!bulk->ncrushSend)
		goto fail;
	bulk->xcrushRecv = xcrush_context_new(FALSE);
	if (!bulk->xcrushRecv)
		goto fail;
	bulk->xcrushSend = xcrush_context_new(TRUE);
	if (!bulk->xcrushSend)
		goto fail;
	bulk->CompressionLevel =
	    freerdp_settings_get_uint32(context->settings, FreeRDP_CompressionLevel);

	return bulk;
fail:
	WINPR_PRAGMA_DIAG_PUSH
	WINPR_PRAGMA_DIAG_IGNORED_MISMATCHED_DEALLOC
	bulk_free(bulk);
	WINPR_PRAGMA_DIAG_POP
	return nullptr;
}

void bulk_free(rdpBulk* bulk)
{
	if (!bulk)
		return;

	mppc_context_free(bulk->mppcSend);
	mppc_context_free(bulk->mppcRecv);
	ncrush_context_free(bulk->ncrushRecv);
	ncrush_context_free(bulk->ncrushSend);
	xcrush_context_free(bulk->xcrushRecv);
	xcrush_context_free(bulk->xcrushSend);
	free(bulk);
}
