/**
 * FreeRDP: A Remote Desktop Protocol Implementation
 * GDI Bitmap Functions
 *
 * Copyright 2010-2011 Marc-Andre Moreau <marcandre.moreau@gmail.com>
 * Copyright 2016 Armin Novak <armin.novak@thincast.com>
 * Copyright 2016 Thincast Technologies GmbH
 *
 * 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 <freerdp/config.h>

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <freerdp/api.h>
#include <freerdp/freerdp.h>
#include <freerdp/gdi/gdi.h>
#include <freerdp/codec/color.h>

#include <freerdp/gdi/region.h>
#include <freerdp/gdi/bitmap.h>
#include <freerdp/log.h>
#include <freerdp/gdi/shape.h>

#include "brush.h"
#include "clipping.h"
#include "../gdi/gdi.h"

#define TAG FREERDP_TAG("gdi.bitmap")

/**
 * Get pixel at the given coordinates. msdn{dd144909}
 * @param hdc device context
 * @param nXPos pixel x position
 * @param nYPos pixel y position
 * @return pixel color
 */

UINT32 gdi_GetPixel(HGDI_DC hdc, UINT32 nXPos, UINT32 nYPos)
{
	HGDI_BITMAP hBmp = (HGDI_BITMAP)hdc->selectedObject;
	BYTE* data =
	    &(hBmp->data[(nYPos * hBmp->scanline) + nXPos * FreeRDPGetBytesPerPixel(hBmp->format)]);
	return FreeRDPReadColor(data, hBmp->format);
}

BYTE* gdi_GetPointer(HGDI_BITMAP hBmp, UINT32 X, UINT32 Y)
{
	UINT32 bpp = FreeRDPGetBytesPerPixel(hBmp->format);
	return &hBmp->data[(Y * WINPR_ASSERTING_INT_CAST(uint32_t, hBmp->width) * bpp) + X * bpp];
}

/**
 * Set pixel at the given coordinates. msdn{dd145078}
 *
 * @param hBmp device context
 * @param X pixel x position
 * @param Y pixel y position
 * @param crColor new pixel color
 * @return the color written
 */

static inline UINT32 gdi_SetPixelBmp(HGDI_BITMAP hBmp, UINT32 X, UINT32 Y, UINT32 crColor)
{
	BYTE* p = &hBmp->data[(Y * hBmp->scanline) + X * FreeRDPGetBytesPerPixel(hBmp->format)];
	FreeRDPWriteColor(p, hBmp->format, crColor);
	return crColor;
}

UINT32 gdi_SetPixel(HGDI_DC hdc, UINT32 X, UINT32 Y, UINT32 crColor)
{
	HGDI_BITMAP hBmp = (HGDI_BITMAP)hdc->selectedObject;
	return gdi_SetPixelBmp(hBmp, X, Y, crColor);
}

/**
 * Create a new bitmap with the given width, height, color format and pixel buffer. msdn{dd183485}
 *
 * @param nWidth width
 * @param nHeight height
 * @param format the color format used
 * @param data pixel buffer
 * @return new bitmap
 */

HGDI_BITMAP gdi_CreateBitmap(UINT32 nWidth, UINT32 nHeight, UINT32 format, BYTE* data)
{
	return gdi_CreateBitmapEx(nWidth, nHeight, format, 0, data, winpr_aligned_free);
}

/**
 * Create a new bitmap with the given width, height, color format and pixel buffer. msdn{dd183485}
 *
 * @param nWidth width
 * @param nHeight height
 * @param format the color format used
 * @param data pixel buffer
 * @param fkt_free The function used for deallocation of the buffer, nullptr for none.
 * @return new bitmap
 */

HGDI_BITMAP gdi_CreateBitmapEx(UINT32 nWidth, UINT32 nHeight, UINT32 format, UINT32 stride,
                               BYTE* data, void (*fkt_free)(void*))
{
	HGDI_BITMAP hBitmap = (HGDI_BITMAP)calloc(1, sizeof(GDI_BITMAP));

	if (!hBitmap)
		return nullptr;

	hBitmap->objectType = GDIOBJECT_BITMAP;
	hBitmap->format = format;

	if (stride > 0)
		hBitmap->scanline = stride;
	else
		hBitmap->scanline = nWidth * FreeRDPGetBytesPerPixel(hBitmap->format);

	hBitmap->width = WINPR_ASSERTING_INT_CAST(int, nWidth);
	hBitmap->height = WINPR_ASSERTING_INT_CAST(int, nHeight);
	hBitmap->data = data;
	hBitmap->free = fkt_free;
	return hBitmap;
}

/**
 * Create a new bitmap of the given width and height compatible with the current device context.
 * msdn{dd183488}
 *
 * @param hdc device context
 * @param nWidth width
 * @param nHeight height
 *
 * @return new bitmap
 */

HGDI_BITMAP gdi_CreateCompatibleBitmap(HGDI_DC hdc, UINT32 nWidth, UINT32 nHeight)
{
	HGDI_BITMAP hBitmap = (HGDI_BITMAP)calloc(1, sizeof(GDI_BITMAP));

	if (!hBitmap)
		return nullptr;

	hBitmap->objectType = GDIOBJECT_BITMAP;
	hBitmap->format = hdc->format;
	WINPR_ASSERT(nWidth <= INT32_MAX);
	hBitmap->width = (INT32)nWidth;

	WINPR_ASSERT(nHeight <= INT32_MAX);
	hBitmap->height = (INT32)nHeight;

	size_t size = 1ull * nWidth * nHeight * FreeRDPGetBytesPerPixel(hBitmap->format);
	hBitmap->data = winpr_aligned_malloc(size, 16);
	hBitmap->free = winpr_aligned_free;

	if (!hBitmap->data)
	{
		free(hBitmap);
		return nullptr;
	}

	/* Initialize with 0xff */
	memset(hBitmap->data, 0xff, size);
	hBitmap->scanline = nWidth * FreeRDPGetBytesPerPixel(hBitmap->format);
	return hBitmap;
}

static BOOL op_not(UINT32* stack, const UINT32* stackp)
{
	if (!stack || !stackp)
		return FALSE;

	if (*stackp < 1)
		return FALSE;

	stack[(*stackp) - 1] = ~stack[(*stackp) - 1];
	return TRUE;
}

static BOOL op_and(UINT32* stack, UINT32* stackp)
{
	if (!stack || !stackp)
		return FALSE;

	if (*stackp < 2)
		return FALSE;

	(*stackp)--;
	stack[(*stackp) - 1] &= stack[(*stackp)];
	return TRUE;
}

static BOOL op_or(UINT32* stack, UINT32* stackp)
{
	if (!stack || !stackp)
		return FALSE;

	if (*stackp < 2)
		return FALSE;

	(*stackp)--;
	stack[(*stackp) - 1] |= stack[(*stackp)];
	return TRUE;
}

static BOOL op_xor(UINT32* stack, UINT32* stackp)
{
	if (!stack || !stackp)
		return FALSE;

	if (*stackp < 2)
		return FALSE;

	(*stackp)--;
	stack[(*stackp) - 1] ^= stack[(*stackp)];
	return TRUE;
}

static UINT32 process_rop(UINT32 src, UINT32 dst, UINT32 pat, const char* rop, UINT32 format)
{
	UINT32 stack[10] = WINPR_C_ARRAY_INIT;
	UINT32 stackp = 0;

	while (*rop != '\0')
	{
		char op = *rop++;

		switch (op)
		{
			case '0':
				stack[stackp++] = FreeRDPGetColor(format, 0, 0, 0, 0xFF);
				break;

			case '1':
				stack[stackp++] = FreeRDPGetColor(format, 0xFF, 0xFF, 0xFF, 0xFF);
				break;

			case 'D':
				stack[stackp++] = dst;
				break;

			case 'S':
				stack[stackp++] = src;
				break;

			case 'P':
				stack[stackp++] = pat;
				break;

			case 'x':
				op_xor(stack, &stackp);
				break;

			case 'a':
				op_and(stack, &stackp);
				break;

			case 'o':
				op_or(stack, &stackp);
				break;

			case 'n':
				op_not(stack, &stackp);
				break;

			default:
				break;
		}
	}

	return stack[0];
}

static inline BOOL BitBlt_write(HGDI_DC hdcDest, HGDI_DC hdcSrc, INT32 nXDest, INT32 nYDest,
                                INT32 nXSrc, INT32 nYSrc, INT32 x, INT32 y, BOOL useSrc,
                                BOOL usePat, UINT32 style, const char* rop,
                                const gdiPalette* palette)
{
	UINT32 dstColor = 0;
	UINT32 colorA = 0;
	UINT32 colorB = 0;
	UINT32 colorC = 0;
	const INT32 dstX = nXDest + x;
	const INT32 dstY = nYDest + y;
	BYTE* dstp = gdi_get_bitmap_pointer(hdcDest, dstX, dstY);

	if (!dstp)
	{
		WLog_ERR(TAG, "dstp=%p", (const void*)dstp);
		return FALSE;
	}

	colorA = FreeRDPReadColor(dstp, hdcDest->format);

	if (useSrc)
	{
		const BYTE* srcp = gdi_get_bitmap_pointer(hdcSrc, nXSrc + x, nYSrc + y);

		if (!srcp)
		{
			WLog_ERR(TAG, "srcp=%p", (const void*)srcp);
			return FALSE;
		}

		colorC = FreeRDPReadColor(srcp, hdcSrc->format);
		colorC = FreeRDPConvertColor(colorC, hdcSrc->format, hdcDest->format, palette);
	}

	if (usePat)
	{
		switch (style)
		{
			case GDI_BS_SOLID:
				colorB = hdcDest->brush->color;
				break;

			case GDI_BS_HATCHED:
			case GDI_BS_PATTERN:
			{
				const BYTE* patp =
				    gdi_get_brush_pointer(hdcDest, WINPR_ASSERTING_INT_CAST(uint32_t, nXDest + x),
				                          WINPR_ASSERTING_INT_CAST(uint32_t, nYDest + y));

				if (!patp)
				{
					WLog_ERR(TAG, "patp=%p", (const void*)patp);
					return FALSE;
				}

				colorB = FreeRDPReadColor(patp, hdcDest->format);
			}
			break;

			default:
				break;
		}
	}

	dstColor = process_rop(colorC, colorA, colorB, rop, hdcDest->format);
	return FreeRDPWriteColor(dstp, hdcDest->format, dstColor);
}

static BOOL adjust_src_coordinates(HGDI_DC hdcSrc, INT32 nWidth, INT32 nHeight, INT32* px,
                                   INT32* py)
{
	HGDI_BITMAP hSrcBmp = nullptr;
	INT32 nXSrc = 0;
	INT32 nYSrc = 0;

	if (!hdcSrc || (nWidth < 0) || (nHeight < 0) || !px || !py)
		return FALSE;

	hSrcBmp = (HGDI_BITMAP)hdcSrc->selectedObject;
	nXSrc = *px;
	nYSrc = *py;

	if (!hSrcBmp)
		return FALSE;

	if (nYSrc < 0)
	{
		nYSrc = 0;
		nHeight = nHeight + nYSrc;
	}

	if ((nXSrc) < 0)
	{
		nXSrc = 0;
		nWidth = nWidth + nXSrc;
	}

	if (hSrcBmp->width < (nXSrc + nWidth))
		nXSrc = hSrcBmp->width - nWidth;

	if (hSrcBmp->height < (nYSrc + nHeight))
		nYSrc = hSrcBmp->height - nHeight;

	if ((nXSrc < 0) || (nYSrc < 0))
		return FALSE;

	*px = nXSrc;
	*py = nYSrc;
	return TRUE;
}

static BOOL adjust_src_dst_coordinates(HGDI_DC hdcDest, INT32* pnXSrc, INT32* pnYSrc, INT32* pnXDst,
                                       INT32* pnYDst, INT32* pnWidth, INT32* pnHeight)
{
	HGDI_BITMAP hDstBmp = nullptr;
	volatile INT32 diffX = 0;
	volatile INT32 diffY = 0;
	volatile INT32 nXSrc = 0;
	volatile INT32 nYSrc = 0;
	volatile INT32 nXDst = 0;
	volatile INT32 nYDst = 0;
	volatile INT32 nWidth = 0;
	volatile INT32 nHeight = 0;

	if (!hdcDest || !pnXSrc || !pnYSrc || !pnXDst || !pnYDst || !pnWidth || !pnHeight)
		return FALSE;

	hDstBmp = (HGDI_BITMAP)hdcDest->selectedObject;
	nXSrc = *pnXSrc;
	nYSrc = *pnYSrc;
	nXDst = *pnXDst;
	nYDst = *pnYDst;
	nWidth = *pnWidth;
	nHeight = *pnHeight;

	if (!hDstBmp)
		return FALSE;

	if (nXDst < 0)
	{
		nXSrc -= nXDst;
		nWidth += nXDst;
		nXDst = 0;
	}

	if (nYDst < 0)
	{
		nYSrc -= nYDst;
		nHeight += nYDst;
		nYDst = 0;
	}

	diffX = hDstBmp->width - nXDst - nWidth;

	if (diffX < 0)
		nWidth += diffX;

	diffY = hDstBmp->height - nYDst - nHeight;

	if (diffY < 0)
		nHeight += diffY;

	if ((nXDst < 0) || (nYDst < 0) || (nWidth < 0) || (nHeight < 0))
	{
		nXDst = 0;
		nYDst = 0;
		nWidth = 0;
		nHeight = 0;
	}

	*pnXSrc = nXSrc;
	*pnYSrc = nYSrc;
	*pnXDst = nXDst;
	*pnYDst = nYDst;
	*pnWidth = nWidth;
	*pnHeight = nHeight;
	return TRUE;
}

static BOOL BitBlt_process(HGDI_DC hdcDest, INT32 nXDest, INT32 nYDest, INT32 nWidth, INT32 nHeight,
                           HGDI_DC hdcSrc, INT32 nXSrc, INT32 nYSrc, const char* rop,
                           const gdiPalette* palette)
{
	UINT32 style = 0;
	BOOL useSrc = FALSE;
	BOOL usePat = FALSE;
	const char* iter = rop;

	while (*iter != '\0')
	{
		switch (*iter++)
		{
			case 'P':
				usePat = TRUE;
				break;

			case 'S':
				useSrc = TRUE;
				break;

			default:
				break;
		}
	}

	if (!hdcDest)
		return FALSE;

	if (!adjust_src_dst_coordinates(hdcDest, &nXSrc, &nYSrc, &nXDest, &nYDest, &nWidth, &nHeight))
		return FALSE;

	if (useSrc && !hdcSrc)
		return FALSE;

	if (useSrc)
	{
		if (!adjust_src_coordinates(hdcSrc, nWidth, nHeight, &nXSrc, &nYSrc))
			return FALSE;
	}

	if (usePat)
	{
		style = gdi_GetBrushStyle(hdcDest);

		switch (style)
		{
			case GDI_BS_SOLID:
			case GDI_BS_HATCHED:
			case GDI_BS_PATTERN:
				break;

			default:
				WLog_ERR(TAG, "Invalid brush!!");
				return FALSE;
		}
	}

	if ((nXDest > nXSrc) && (nYDest > nYSrc))
	{
		for (INT32 y = nHeight - 1; y >= 0; y--)
		{
			for (INT32 x = nWidth - 1; x >= 0; x--)
			{
				if (!BitBlt_write(hdcDest, hdcSrc, nXDest, nYDest, nXSrc, nYSrc, x, y, useSrc,
				                  usePat, style, rop, palette))
					return FALSE;
			}
		}
	}
	else if (nXDest > nXSrc)
	{
		for (INT32 y = 0; y < nHeight; y++)
		{
			for (INT32 x = nWidth - 1; x >= 0; x--)
			{
				if (!BitBlt_write(hdcDest, hdcSrc, nXDest, nYDest, nXSrc, nYSrc, x, y, useSrc,
				                  usePat, style, rop, palette))
					return FALSE;
			}
		}
	}
	else if (nYDest > nYSrc)
	{
		for (INT32 y = nHeight - 1; y >= 0; y--)
		{
			for (INT32 x = 0; x < nWidth; x++)
			{
				if (!BitBlt_write(hdcDest, hdcSrc, nXDest, nYDest, nXSrc, nYSrc, x, y, useSrc,
				                  usePat, style, rop, palette))
					return FALSE;
			}
		}
	}
	else
	{
		for (INT32 y = 0; y < nHeight; y++)
		{
			for (INT32 x = 0; x < nWidth; x++)
			{
				if (!BitBlt_write(hdcDest, hdcSrc, nXDest, nYDest, nXSrc, nYSrc, x, y, useSrc,
				                  usePat, style, rop, palette))
					return FALSE;
			}
		}
	}

	return TRUE;
}

/**
 * Perform a bit blit operation on the given pixel buffers.
 * msdn{dd183370}
 *
 * @param hdcDest destination device context
 * @param nXDest destination x1
 * @param nYDest destination y1
 * @param nWidth width
 * @param nHeight height
 * @param hdcSrc source device context
 * @param nXSrc source x1
 * @param nYSrc source y1
 * @param rop raster operation code
 * @return 0 on failure, non-zero otherwise
 */
BOOL gdi_BitBlt(HGDI_DC hdcDest, INT32 nXDest, INT32 nYDest, INT32 nWidth, INT32 nHeight,
                HGDI_DC hdcSrc, INT32 nXSrc, INT32 nYSrc, DWORD rop, const gdiPalette* palette)
{
	HGDI_BITMAP hSrcBmp = nullptr;
	HGDI_BITMAP hDstBmp = nullptr;

	if (!hdcDest)
		return FALSE;

	if (!gdi_ClipCoords(hdcDest, &nXDest, &nYDest, &nWidth, &nHeight, &nXSrc, &nYSrc))
		return TRUE;

	/* Check which ROP should be performed.
	 * Some specific ROP are used heavily and are resource intensive,
	 * add optimized versions for these here.
	 *
	 * For all others fall back to the generic implementation.
	 */
	switch (rop)
	{
		case GDI_SRCCOPY:
			if (!hdcSrc)
				return FALSE;

			if (!adjust_src_dst_coordinates(hdcDest, &nXSrc, &nYSrc, &nXDest, &nYDest, &nWidth,
			                                &nHeight))
				return FALSE;

			if (!adjust_src_coordinates(hdcSrc, nWidth, nHeight, &nXSrc, &nYSrc))
				return FALSE;

			hSrcBmp = (HGDI_BITMAP)hdcSrc->selectedObject;
			hDstBmp = (HGDI_BITMAP)hdcDest->selectedObject;

			if (!hSrcBmp || !hDstBmp)
				return FALSE;

			if (!freerdp_image_copy(
			        hDstBmp->data, hDstBmp->format, hDstBmp->scanline,
			        WINPR_ASSERTING_INT_CAST(UINT32, nXDest),
			        WINPR_ASSERTING_INT_CAST(UINT32, nYDest),
			        WINPR_ASSERTING_INT_CAST(UINT32, nWidth),
			        WINPR_ASSERTING_INT_CAST(UINT32, nHeight), hSrcBmp->data, hSrcBmp->format,
			        hSrcBmp->scanline, WINPR_ASSERTING_INT_CAST(UINT32, nXSrc),
			        WINPR_ASSERTING_INT_CAST(UINT32, nYSrc), palette, FREERDP_FLIP_NONE))
				return FALSE;

			break;

		case GDI_DSTCOPY:
			hSrcBmp = (HGDI_BITMAP)hdcDest->selectedObject;
			hDstBmp = (HGDI_BITMAP)hdcDest->selectedObject;

			if (!adjust_src_dst_coordinates(hdcDest, &nXSrc, &nYSrc, &nXDest, &nYDest, &nWidth,
			                                &nHeight))
				return FALSE;

			if (!adjust_src_coordinates(hdcDest, nWidth, nHeight, &nXSrc, &nYSrc))
				return FALSE;

			if (!hSrcBmp || !hDstBmp)
				return FALSE;

			if (!freerdp_image_copy(
			        hDstBmp->data, hDstBmp->format, hDstBmp->scanline,
			        WINPR_ASSERTING_INT_CAST(UINT32, nXDest),
			        WINPR_ASSERTING_INT_CAST(UINT32, nYDest),
			        WINPR_ASSERTING_INT_CAST(UINT32, nWidth),
			        WINPR_ASSERTING_INT_CAST(UINT32, nHeight), hSrcBmp->data, hSrcBmp->format,
			        hSrcBmp->scanline, WINPR_ASSERTING_INT_CAST(UINT32, nXSrc),
			        WINPR_ASSERTING_INT_CAST(UINT32, nYSrc), palette, FREERDP_FLIP_NONE))
				return FALSE;

			break;

		default:
			if (!BitBlt_process(hdcDest, nXDest, nYDest, nWidth, nHeight, hdcSrc, nXSrc, nYSrc,
			                    gdi_rop_to_string(rop), palette))
				return FALSE;

			break;
	}

	return gdi_InvalidateRegion(hdcDest, nXDest, nYDest, nWidth, nHeight);
}
