/*!
 * @file clipboard_image.cpp
 * @brief Definitions for clipboard image handling functionality
 * @remark This is a C++ file because it uses GDI+ behind the scenes. This is because it's super
 *         easy to do image encoding and prevents us from having to include the massive JPG lib.
 *         It's not late-bound using LoadLibrary due to the fact that doing that with C++ stuff
 *         is nothing short of painful.
 */
extern "C" {
#include "extapi.h"
#include "clipboard_image.h"
}
#include <gdiplus.h>

#ifndef max
#define max(x,y) ((x)>(y)?(x):(y))
#endif

/*!
 * @brief Get the Class ID of an encoder which supports encoding to the specified MIME type.
 * @param mimeType The wide-string formatting MIME type identifier.
 * @param pClsId Pointer to the \c CLSID structure that will receive the Class ID.
 * @returns Indication of success or failure.
 * @retval ERROR_SUCCESS The Class ID was extracted successfully.
 * @retval ERROR_NOT_FOUND The Class ID was not found.
 * @retval Otherwise The relevant error code.
 */
DWORD get_encoder_clsid(WCHAR *mimeType, CLSID * pClsId)
{
	using namespace Gdiplus;

	DWORD dwResult = ERROR_NOT_FOUND;
	ImageCodecInfo* pImageCodecInfo = NULL;

	do
	{
		UINT numEncoders;
		UINT size;
		if (GetImageEncodersSize(&numEncoders, &size) != Ok) {
			BREAK_WITH_ERROR("[EXTAPI CLIPIMG] Unable to get encoders array size.", ERROR_FUNCTION_FAILED);
		}

		if (size == 0) {
			BREAK_WITH_ERROR("[EXTAPI CLIPIMG] No encoders found.", ERROR_FUNCTION_FAILED);
		}

		if ((pImageCodecInfo = (ImageCodecInfo*)malloc(size)) == NULL) {
			BREAK_WITH_ERROR("[EXTAPI CLIPIMG] Couldn't allocate memory for ImageCodeInfo", ERROR_OUTOFMEMORY);
		}

		if (GetImageEncoders(numEncoders, size, pImageCodecInfo) != Ok) {
			BREAK_WITH_ERROR("[EXTAPI CLIPIMG] Unable to get encoders.", ERROR_FUNCTION_FAILED);
		}

		for (UINT i = 0; i < numEncoders; ++i) {
			if (wcscmp(pImageCodecInfo[i].MimeType, mimeType) == 0) {
				// Image encoder for the MIME type found, so copy the Class ID...
				memcpy_s(pClsId, sizeof(CLSID), &pImageCodecInfo[i].Clsid, sizeof(CLSID));

				// .. and finish up.
				dwResult = ERROR_SUCCESS;
				break;
			}
		}
	} while (0);

	if (pImageCodecInfo != NULL) {
		free(pImageCodecInfo);
	}

	return dwResult;
}

extern "C" {

/*!
 * @brief Calculate the size of the specified bitmap information header.
 * @param lpBI Pointer to the \cBITMAPINFO structure that contains the detail of the bitmap.
 *             In the case of the clipboard, this is the CF_DIB data.
 * @param bRGB Set to \c TRUE if the colors are in RBG format, \c FALSE otherwise.
 * @remark This function is necessary due to the fact that the information
 *         stored on the clipboard can't be handled properly unless we know
 *         where in memory the DIB bits are.
 * @returns The size of the bitmap information header.
 */
DWORD get_bitmapinfo_size(const LPBITMAPINFO lpBI, BOOL bRGB)
{
	DWORD dwColors, dwSize;

	if (lpBI->bmiHeader.biSize == sizeof(BITMAPCOREHEADER)) {
		const BITMAPCOREHEADER* core = (const BITMAPCOREHEADER*)lpBI;

		dwColors = core->bcBitCount <= 8
			? 1 << core->bcBitCount
			: 0;

		dwSize = sizeof(BITMAPCOREHEADER);
	}
	else {
		dwColors = lpBI->bmiHeader.biClrUsed;

		if (!dwColors && lpBI->bmiHeader.biBitCount <= 8) {
			dwColors = 1 << lpBI->bmiHeader.biBitCount;
		}

		dwSize = max(lpBI->bmiHeader.biSize,
			(lpBI->bmiHeader.biCompression == BI_BITFIELDS ? 3 : 0)
			* sizeof(DWORD)+sizeof(BITMAPINFOHEADER));
	}

	return dwSize + dwColors * (bRGB ? sizeof(RGBTRIPLE) : sizeof(WORD));
}

/*!
 * @brief Convert the given bitmap data into a JPEG image of the specified quality.
 * @param lpBI Pointer to the \cBITMAPINFO structure that contains the detail of the bitmap.
 *             In the case of the clipboard, this is the CF_DIB data.
 * @param lpDIB Pointer to the DIB bytes that make up the image data.
 * @param ulQuality Quality of the resulting JPG image.
 * @param pImage Pointer to the image structure that will receive the image data
 * @retval ERROR_SUCCESS The Class ID was extracted successfully.
 * @retval Otherwise The relevant error code.
 * @remark This functionality uses GDI+ to convert the image to a JPG.
 */
DWORD convert_to_jpg(const LPBITMAPINFO lpBI, const LPVOID lpDIB, ULONG ulQuality, ConvertedImage* pImage)
{
	using namespace Gdiplus;

	HRESULT hRes = S_OK;
	DWORD dwResult = ERROR_SUCCESS;
	ULONG_PTR gdiPlusToken = 0;
	Bitmap* pBitmap = NULL;
	GdiplusStartupInput gdiStartupInput;
	IStream* pStream = NULL;

	// set this to NULL up front so that we can keep track of allocations;
	pImage->pImageBuffer = NULL;
	pImage->dwImageBufferSize = 0;

	do
	{
		if (GdiplusStartup(&gdiPlusToken, &gdiStartupInput, NULL) != Ok) {
			BREAK_WITH_ERROR("[EXTAPI CLIPIMG] Unable to initialize GdiPlus", ERROR_FUNCTION_FAILED);
		}

		CLSID jpegClsid;
		dprintf("[EXTAPI CLIPIMG] Attempting to get the jpg class id");
		if (get_encoder_clsid(L"image/jpeg", &jpegClsid) != ERROR_SUCCESS) {
			BREAK_WITH_ERROR("[EXTAPI CLIPIMG] Unable to find an appropriate image encoder", ERROR_FUNCTION_FAILED);
		}

		if ((pBitmap = new Bitmap(lpBI, lpDIB)) == NULL) {
			BREAK_WITH_ERROR("[EXTAPI CLIPIMG] Failed to create bitmap instance", ERROR_FUNCTION_FAILED);
		}

		EncoderParameters encParams;
		encParams.Count = 1;
		encParams.Parameter[0].NumberOfValues = 1;
		encParams.Parameter[0].Guid = EncoderQuality;
		encParams.Parameter[0].Type = EncoderParameterValueTypeLong;
		encParams.Parameter[0].Value = &ulQuality;

		if (CreateStreamOnHGlobal(NULL, TRUE, &pStream) != S_OK) {
			BREAK_WITH_ERROR("[EXTAPI CLIPIMG] Failed to create stream", ERROR_FUNCTION_FAILED);
		}

		if (pBitmap->Save(pStream, &jpegClsid, &encParams) != Ok) {
			BREAK_WITH_ERROR("[EXTAPI CLIPIMG] Failed to save image to stream", ERROR_FUNCTION_FAILED);
		}

		STATSTG stat;
		if (pStream->Stat(&stat, STATFLAG_NONAME) != S_OK) {
			BREAK_WITH_ERROR("[EXTAPI CLIPIMG] Failed to get image stat", ERROR_FUNCTION_FAILED);
		}

		// if the image requires the quadpart, then we're in trouble anyway!
		pImage->dwImageBufferSize = stat.cbSize.LowPart;
		if ((pImage->pImageBuffer = (LPBYTE)malloc(pImage->dwImageBufferSize)) == NULL) {
			BREAK_WITH_ERROR("[EXTAPI CLIPIMG] Failed to allocate memory for the JPEG", ERROR_OUTOFMEMORY);
		}

		ULARGE_INTEGER pos;
		LARGE_INTEGER zero;
		zero.QuadPart = 0;
		pos.QuadPart = 0;
		if (pStream->Seek(zero, STREAM_SEEK_SET, &pos) != S_OK) {
			BREAK_WITH_ERROR("[EXTAPI CLIPIMG] Failed set stream position", ERROR_FUNCTION_FAILED);
		}

		ULONG bytesRead = 0;
		if ((hRes = pStream->Read(pImage->pImageBuffer, pImage->dwImageBufferSize, &bytesRead) != S_OK)) {
			dprintf("[EXTAPI CLIPIMG] Failed to read image data from stream: %u %x", hRes, hRes);
			dwResult = ERROR_FUNCTION_FAILED;
			break;
		}

		if (bytesRead != pImage->dwImageBufferSize) {
			BREAK_WITH_ERROR("[EXTAPI CLIPIMG] Failed to read image data from stream", ERROR_FUNCTION_FAILED);
		}
	} while (0);

	if (dwResult != ERROR_SUCCESS && pImage->pImageBuffer != NULL) {
		free(pImage->pImageBuffer);
		pImage->pImageBuffer = NULL;
	}

	if (pStream != NULL) {
		pStream->Release();
	}

	if (pBitmap != NULL) {
		delete pBitmap;
	}

	if (gdiPlusToken != 0) {
		GdiplusShutdown(gdiPlusToken);
	}

	return dwResult;
}
}