#pragma once

#include <mfapi.h>
#include <mftransform.h>
#include <dmo.h>
#include <wmcodecdsp.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <shlwapi.h>
#include <mferror.h>
#include <strmif.h>
#include <comdef.h>

#pragma comment (lib, "ole32.lib")
#pragma comment (lib, "mf.lib")
#pragma comment (lib, "mfplat.lib")
#pragma comment (lib, "mfuuid.lib")
#pragma comment (lib, "mfreadwrite.lib")
#pragma comment (lib, "strmiids.lib")
#pragma comment (lib, "wmcodecdspuuid.lib")

#include <grabber/video/mediafoundation/MFGrabber.h>

#define SAFE_RELEASE(x) if(x) { x->Release(); x = nullptr; }

// Need more supported formats? Visit https://docs.microsoft.com/en-us/windows/win32/medfound/colorconverter
static PixelFormat GetPixelFormatForGuid(const GUID guid)
{
	if (IsEqualGUID(guid, MFVideoFormat_RGB32)) return PixelFormat::RGB32;
	if (IsEqualGUID(guid, MFVideoFormat_RGB24)) return PixelFormat::BGR24;
	if (IsEqualGUID(guid, MFVideoFormat_YUY2)) return PixelFormat::YUYV;
	if (IsEqualGUID(guid, MFVideoFormat_UYVY)) return PixelFormat::UYVY;
#ifdef HAVE_TURBO_JPEG
	if (IsEqualGUID(guid, MFVideoFormat_MJPG)) return  PixelFormat::MJPEG;
#endif
	if (IsEqualGUID(guid, MFVideoFormat_NV12)) return  PixelFormat::NV12;
	if (IsEqualGUID(guid, MFVideoFormat_I420)) return  PixelFormat::I420;
	return PixelFormat::NO_CHANGE;
};

class SourceReaderCB : public IMFSourceReaderCallback
{
public:
	SourceReaderCB(MFGrabber* grabber)
		: _nRefCount(1)
		, _grabber(grabber)
		, _bEOS(FALSE)
		, _hrStatus(S_OK)
		, _isBusy(false)
		, _transform(nullptr)
		, _pixelformat(PixelFormat::NO_CHANGE)
	{
		// Initialize critical section.
		InitializeCriticalSection(&_critsec);
	}

	// IUnknown methods
	STDMETHODIMP QueryInterface(REFIID iid, void** ppv)
	{
		static const QITAB qit[] =
		{
			QITABENT(SourceReaderCB, IMFSourceReaderCallback),
			{ 0 },
		};
		return QISearch(this, qit, iid, ppv);
	}

	STDMETHODIMP_(ULONG) AddRef()
	{
		return InterlockedIncrement(&_nRefCount);
	}

	STDMETHODIMP_(ULONG) Release()
	{
		ULONG uCount = InterlockedDecrement(&_nRefCount);
		if (uCount == 0)
		{
			delete this;
		}
		return uCount;
	}

	// IMFSourceReaderCallback methods
	STDMETHODIMP OnReadSample(HRESULT hrStatus, DWORD /*dwStreamIndex*/,
		DWORD dwStreamFlags, LONGLONG llTimestamp, IMFSample* pSample)
	{
		EnterCriticalSection(&_critsec);
		_isBusy = true;

		if (_grabber->_sourceReader == nullptr)
		{
			_isBusy = false;
			LeaveCriticalSection(&_critsec);
			return S_OK;
		}

		if (dwStreamFlags & MF_SOURCE_READERF_STREAMTICK)
		{
			Debug(_grabber->_log, "Skipping stream gap");
			LeaveCriticalSection(&_critsec);
			_grabber->_sourceReader->ReadSample(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, nullptr, nullptr, nullptr, nullptr);
			return S_OK;
		}

		if (dwStreamFlags & MF_SOURCE_READERF_NATIVEMEDIATYPECHANGED)
		{
			IMFMediaType* type = nullptr;
			GUID format;
			_grabber->_sourceReader->GetNativeMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, MF_SOURCE_READER_CURRENT_TYPE_INDEX, &type);
			type->GetGUID(MF_MT_SUBTYPE, &format);
			Debug(_grabber->_log, "Native media type changed");
			InitializeVideoEncoder(type, GetPixelFormatForGuid(format));
			SAFE_RELEASE(type);
		}

		if (dwStreamFlags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED)
		{
			IMFMediaType* type = nullptr;
			GUID format;
			_grabber->_sourceReader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, &type);
			type->GetGUID(MF_MT_SUBTYPE, &format);
			Debug(_grabber->_log, "Current media type changed");
			InitializeVideoEncoder(type, GetPixelFormatForGuid(format));
			SAFE_RELEASE(type);
		}

		// Variables declaration
		IMFMediaBuffer* buffer = nullptr;
		BYTE* data = nullptr;
		DWORD maxLength = 0, currentLength = 0;



		if (FAILED(hrStatus))
		{
			_hrStatus = hrStatus;
			Error(_grabber->_log, "0x%08x: %s", _hrStatus, std::system_category().message(_hrStatus).c_str());
			goto done;
		}

		if (!pSample)
		{
			Error(_grabber->_log, "Media sample is empty");
			goto done;
		}

#ifdef HAVE_TURBO_JPEG
		if (_pixelformat != PixelFormat::MJPEG && _pixelformat != PixelFormat::BGR24 && _pixelformat != PixelFormat::NO_CHANGE)
#else
		if (_pixelformat != PixelFormat::BGR24 && _pixelformat != PixelFormat::NO_CHANGE)
#endif
			pSample = TransformSample(_transform, pSample);

		_hrStatus = pSample->ConvertToContiguousBuffer(&buffer);
		if (FAILED(_hrStatus))
		{
			Error(_grabber->_log, "Buffer conversion failed => 0x%08x: %s", _hrStatus, std::system_category().message(_hrStatus).c_str());
			goto done;
		}


		_hrStatus = buffer->Lock(&data, &maxLength, &currentLength);
		if (FAILED(_hrStatus))
		{
			Error(_grabber->_log, "Access to the buffer memory failed => 0x%08x: %s", _hrStatus, std::system_category().message(_hrStatus).c_str());
			goto done;
		}

		_grabber->receive_image(data, currentLength);

		_hrStatus = buffer->Unlock();
		if (FAILED(_hrStatus))
		{
			Error(_grabber->_log, "Unlocking the buffer memory failed => 0x%08x: %s", _hrStatus, std::system_category().message(_hrStatus).c_str());
		}

	done:
		SAFE_RELEASE(buffer);

		if (MF_SOURCE_READERF_ENDOFSTREAM & dwStreamFlags)
			_bEOS = TRUE; // Reached the end of the stream.

#ifdef HAVE_TURBO_JPEG
		if (_pixelformat != PixelFormat::MJPEG && _pixelformat != PixelFormat::BGR24 && _pixelformat != PixelFormat::NO_CHANGE)
#else
		if (_pixelformat != PixelFormat::BGR24 && _pixelformat != PixelFormat::NO_CHANGE)
#endif
			SAFE_RELEASE(pSample);

		_isBusy = false;
		LeaveCriticalSection(&_critsec);
		return _hrStatus;
	}

	HRESULT InitializeVideoEncoder(IMFMediaType* type, PixelFormat format)
	{
		_pixelformat = format;
#ifdef HAVE_TURBO_JPEG
		if (format == PixelFormat::MJPEG || format == PixelFormat::BGR24 || format == PixelFormat::NO_CHANGE)
#else
		if (format == PixelFormat::BGR24 || format == PixelFormat::NO_CHANGE)
#endif
			return S_OK;

		// Variable declaration
		IMFMediaType* output = nullptr;
		DWORD mftStatus = 0;
		QString error = "";

		// Create instance of IMFTransform interface pointer as CColorConvertDMO
		_hrStatus = CoCreateInstance(CLSID_CColorConvertDMO, nullptr, CLSCTX_INPROC_SERVER, IID_IMFTransform, (void**)&_transform);
		if (FAILED(_hrStatus))
		{
			Error(_grabber->_log, "Creation of the Color Converter failed => 0x%08x: %s", _hrStatus, std::system_category().message(_hrStatus).c_str());
			goto done;
		}

		// Set input type as media type of our input stream
		_hrStatus = _transform->SetInputType(0, type, 0);
		if (FAILED(_hrStatus))
		{
			Error(_grabber->_log, "Setting the input media type failed => 0x%08x: %s", _hrStatus, std::system_category().message(_hrStatus).c_str());
			goto done;
		}

		// Create new media type
		_hrStatus = MFCreateMediaType(&output);
		if (FAILED(_hrStatus))
		{
			Error(_grabber->_log, "Creating a new media type failed => 0x%08x: %s", _hrStatus, std::system_category().message(_hrStatus).c_str());
			goto done;
		}

		// Copy all attributes from input type to output media type
		_hrStatus = type->CopyAllItems(output);
		if (FAILED(_hrStatus))
		{
			Error(_grabber->_log, "Copying of all attributes from input to output media type failed => 0x%08x: %s", _hrStatus, std::system_category().message(_hrStatus).c_str());
			goto done;
		}

		UINT32 width, height;
		UINT32 numerator, denominator;

		// Fill the missing attributes

		if (FAILED(output->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video)) ||
			FAILED(output->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB24)) ||
			FAILED(output->SetUINT32(MF_MT_FIXED_SIZE_SAMPLES, TRUE)) ||
			FAILED(output->SetUINT32(MF_MT_ALL_SAMPLES_INDEPENDENT, TRUE)) ||
			FAILED(output->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive)) ||
			FAILED(MFGetAttributeSize(type, MF_MT_FRAME_SIZE, &width, &height)) ||
			FAILED(MFSetAttributeSize(output, MF_MT_FRAME_SIZE, width, height)) ||
			FAILED(MFGetAttributeRatio(type, MF_MT_FRAME_RATE, &numerator, &denominator)) ||
			FAILED(MFSetAttributeRatio(output, MF_MT_PIXEL_ASPECT_RATIO, 1, 1)))
		{
			Error(_grabber->_log, "Setting output media type attributes failed");
			goto done;
		}

		// Set transform output type
		_hrStatus = _transform->SetOutputType(0, output, 0);
		if (FAILED(_hrStatus))
		{
			Error(_grabber->_log, "Setting the output media type failed => 0x%08x: %s", _hrStatus, std::system_category().message(_hrStatus).c_str());
			goto done;
		}

		// Check if encoder parameters set properly
		_hrStatus = _transform->GetInputStatus(0, &mftStatus);
		if (FAILED(_hrStatus))
		{
			Error(_grabber->_log, "Failed to query the input stream for more data => 0x%08x: %s", _hrStatus, std::system_category().message(_hrStatus).c_str());
			goto done;
		}

		if (MFT_INPUT_STATUS_ACCEPT_DATA == mftStatus)
		{
			// Notify the transform we are about to begin streaming data
			if (FAILED(_transform->ProcessMessage(MFT_MESSAGE_COMMAND_FLUSH, 0)) ||
				FAILED(_transform->ProcessMessage(MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, 0)) ||
				FAILED(_transform->ProcessMessage(MFT_MESSAGE_NOTIFY_START_OF_STREAM, 0)))
			{
				Error(_grabber->_log, "Failed to begin streaming data");
			}
		}

	done:
		SAFE_RELEASE(output);
		return _hrStatus;
	}

	BOOL isBusy()
	{
		EnterCriticalSection(&_critsec);
		BOOL result = _isBusy;
		LeaveCriticalSection(&_critsec);

		return result;
	}

	STDMETHODIMP OnEvent(DWORD, IMFMediaEvent*) { return S_OK; }
	STDMETHODIMP OnFlush(DWORD) { return S_OK; }

private:
	virtual ~SourceReaderCB()
	{
		if (_transform)
		{
			_transform->ProcessMessage(MFT_MESSAGE_NOTIFY_END_OF_STREAM, 0);
			_transform->ProcessMessage(MFT_MESSAGE_NOTIFY_END_STREAMING, 0);
		}

		SAFE_RELEASE(_transform);

		// Delete critical section.
		DeleteCriticalSection(&_critsec);
	}

	IMFSample* TransformSample(IMFTransform* transform, IMFSample* in_sample)
	{
		IMFSample* result = nullptr;
		IMFMediaBuffer* out_buffer = nullptr;
		MFT_OUTPUT_DATA_BUFFER outputDataBuffer = { 0 };
		DWORD status = 0;

		// Process the input sample
		_hrStatus = transform->ProcessInput(0, in_sample, 0);
		if (FAILED(_hrStatus))
		{
			Error(_grabber->_log, "Failed to process the input sample => 0x%08x: %s", _hrStatus, std::system_category().message(_hrStatus).c_str());
			goto done;
		}

		// Gets the buffer demand for the output stream
		MFT_OUTPUT_STREAM_INFO streamInfo;
		_hrStatus = transform->GetOutputStreamInfo(0, &streamInfo);
		if (FAILED(_hrStatus))
		{
			Error(_grabber->_log, "Failed to retrieve buffer requirement for output current => 0x%08x: %s", _hrStatus, std::system_category().message(_hrStatus).c_str());
			goto done;
		}

		// Create an output media buffer
		_hrStatus = MFCreateMemoryBuffer(streamInfo.cbSize, &out_buffer);
		if (FAILED(_hrStatus))
		{
			Error(_grabber->_log, "Failed to create an output media buffer => 0x%08x: %s", _hrStatus, std::system_category().message(_hrStatus).c_str());
			goto done;
		}

		// Create an empty media sample
		_hrStatus = MFCreateSample(&result);
		if (FAILED(_hrStatus))
		{
			Error(_grabber->_log, "Failed to create an empty media sampl => 0x%08x: %s", _hrStatus, std::system_category().message(_hrStatus).c_str());
			goto done;
		}

		// Add the output media buffer to the media sample
		_hrStatus = result->AddBuffer(out_buffer);
		if (FAILED(_hrStatus))
		{
			Error(_grabber->_log, "Failed to add the output media buffer to the media sample => 0x%08x: %s", _hrStatus, std::system_category().message(_hrStatus).c_str());
			goto done;
		}

		// Create the output buffer structure
		memset(&outputDataBuffer, 0, sizeof outputDataBuffer);
		outputDataBuffer.dwStreamID = 0;
		outputDataBuffer.dwStatus = 0;
		outputDataBuffer.pEvents = nullptr;
		outputDataBuffer.pSample = result;

		// Generate the output sample
		_hrStatus = transform->ProcessOutput(0, 1, &outputDataBuffer, &status);
		if (FAILED(_hrStatus))
		{
			Error(_grabber->_log, "Failed to generate the output sample => 0x%08x: %s", _hrStatus, std::system_category().message(_hrStatus).c_str());
		}
		else
		{
			SAFE_RELEASE(out_buffer);
			return result;
		}

	done:
		SAFE_RELEASE(out_buffer);
		return nullptr;
	}

private:
	long				_nRefCount;
	CRITICAL_SECTION	_critsec;
	MFGrabber*			_grabber;
	BOOL				_bEOS;
	HRESULT				_hrStatus;
	IMFTransform*		_transform;
	PixelFormat			_pixelformat;
	std::atomic<bool>	_isBusy;
};