/*
 *   Copyright (c) 2008-2021 SLIBIO <https://github.com/SLIBIO>
 *
 *   Permission is hereby granted, free of charge, to any person obtaining a copy
 *   of this software and associated documentation files (the "Software"), to deal
 *   in the Software without restriction, including without limitation the rights
 *   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *   copies of the Software, and to permit persons to whom the Software is
 *   furnished to do so, subject to the following conditions:
 *
 *   The above copyright notice and this permission notice shall be included in
 *   all copies or substantial portions of the Software.
 *
 *   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *   THE SOFTWARE.
 */

#include "slib/core/definition.h"

#if defined(SLIB_PLATFORM_IS_WINDOWS)

#define PRIV_SLIB_IMPLEMENT_WIN32_COM

namespace slib
{
	namespace priv
	{
		namespace win32_com
		{
			class GenericDataElement;
		}
	}
}

#include "slib/core/win32/com.h"

namespace slib
{

	namespace priv
	{
		namespace win32_com
		{

			class GenericDataElement : public Referable
			{
			public:
				FORMATETC format;
				STGMEDIUM medium;

			public:
				GenericDataElement(const FORMATETC& _format, const STGMEDIUM& _medium) : format(_format), medium(_medium)
				{
				}

				~GenericDataElement()
				{
					ReleaseStgMedium(&medium);
				}

			};

			class CEnumFormatEtc : public IEnumFORMATETC
			{
			private:
				ULONG m_nRef;

				List< Ref<GenericDataElement> > m_list;
				ULONG m_index;

			public:
				CEnumFormatEtc(const List< Ref<GenericDataElement> >& list) : m_list(list)
				{
					m_nRef = 0;
					m_index = 0;
				}

			public:
				ULONG STDMETHODCALLTYPE AddRef() override
				{
					return ++m_nRef;
				}

				ULONG STDMETHODCALLTYPE Release() override
				{
					ULONG nRef = --m_nRef;
					if (!nRef) {
						delete this;
					}
					return nRef;
				}

				HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) override
				{
					if (!ppvObject) {
						return E_POINTER;
					}
					if (riid == __uuidof(IEnumFORMATETC) || riid == __uuidof(IUnknown)) {
						*ppvObject = this;
						AddRef();
						return S_OK;
					}
					return E_NOINTERFACE;
				}

				HRESULT STDMETHODCALLTYPE Next(ULONG celt, FORMATETC *lpFormatEtc, ULONG *pCeltFetched) override
				{
					if (pCeltFetched) {
						*pCeltFetched = 0;
					}

					if (!celt || !lpFormatEtc || m_index + celt >= m_list.getCount()) {
						return S_FALSE;
					}
					if (!pCeltFetched && celt != 1) {
						// pCeltFetched can be NULL only for 1 item request
						return S_FALSE;
					}

					for (ULONG i = 0; i < celt; i++) {
						*lpFormatEtc = m_list[m_index]->format;
						m_index++;
						lpFormatEtc++;
					}
					if (pCeltFetched) {
						*pCeltFetched = celt;
					}

					return S_OK;
				}

				HRESULT STDMETHODCALLTYPE Skip(ULONG celt) override
				{
					if (m_index + celt >= m_list.getCount()) {
						return S_FALSE;
					}
					m_index += celt;
					return S_OK;
				}

				HRESULT STDMETHODCALLTYPE Reset() override
				{
					m_index = 0;
					return S_OK;
				}

				HRESULT STDMETHODCALLTYPE Clone(IEnumFORMATETC **ppEnum) override
				{
					if (!ppEnum) {
						return E_POINTER;
					}
					CEnumFormatEtc* newEnum = new CEnumFormatEtc(m_list);
					if (!newEnum) {
						return E_OUTOFMEMORY;
					}
					newEnum->AddRef();
					newEnum->m_index = m_index;
					*ppEnum = newEnum;
					return S_OK;
				}

			};

			static void CopyMedium(STGMEDIUM* pMedDest, STGMEDIUM* pMedSrc, FORMATETC* pFmtSrc)
			{
				switch (pMedSrc->tymed) {
				case TYMED_HGLOBAL:
					pMedDest->hGlobal = (HGLOBAL)(OleDuplicateData(pMedSrc->hGlobal, pFmtSrc->cfFormat, 0));
					break;
				case TYMED_GDI:
					pMedDest->hBitmap = (HBITMAP)(OleDuplicateData(pMedSrc->hBitmap, pFmtSrc->cfFormat, 0));
					break;
				case TYMED_MFPICT:
					pMedDest->hMetaFilePict = (HMETAFILEPICT)(OleDuplicateData(pMedSrc->hMetaFilePict, pFmtSrc->cfFormat, 0));
					break;
				case TYMED_ENHMF:
					pMedDest->hEnhMetaFile = (HENHMETAFILE)(OleDuplicateData(pMedSrc->hEnhMetaFile, pFmtSrc->cfFormat, 0));
					break;
				case TYMED_FILE:
					pMedSrc->lpszFileName = (LPOLESTR)(OleDuplicateData(pMedSrc->lpszFileName, pFmtSrc->cfFormat, 0));
					break;
				case TYMED_ISTREAM:
					pMedDest->pstm = pMedSrc->pstm;
					pMedSrc->pstm->AddRef();
					break;
				case TYMED_ISTORAGE:
					pMedDest->pstg = pMedSrc->pstg;
					pMedSrc->pstg->AddRef();
					break;
				case TYMED_NULL:
				default:
					break;
				}
				pMedDest->tymed = pMedSrc->tymed;
				pMedDest->pUnkForRelease = NULL;
				if (pMedSrc->pUnkForRelease != NULL) {
					pMedDest->pUnkForRelease = pMedSrc->pUnkForRelease;
					pMedSrc->pUnkForRelease->AddRef();
				}
			}

		}
	}

	using namespace priv::win32_com;

	namespace win32
	{

		Memory COM::readAllBytesFromStream(IStream* stream)
		{
			Memory ret;
			HRESULT hr;

			STATSTG stat;
			Base::zeroMemory(&stat, sizeof(stat));
			hr = stream->Stat(&stat, STATFLAG_NONAME);
			if (hr == S_OK) {
				LARGE_INTEGER n;
				n.QuadPart = 0;
				stream->Seek(n, STREAM_SEEK_SET, NULL);
				ULONG size = (ULONG)(stat.cbSize.QuadPart);
				if (size > 0) {
					ret = Memory::create(size);
					if (ret.isNotNull()) {
						ULONG nRead = 0;
						hr = stream->Read(ret.getData(), size, &nRead);
						if (hr == S_OK && nRead == size) {
							return ret;
						}
					}
					ret.setNull();
				}
			}
			return ret;
		}


		GenericDataObject::GenericDataObject()
		{
			m_nRef = 0;
		}

		GenericDataObject::~GenericDataObject()
		{
		}

		ULONG STDMETHODCALLTYPE GenericDataObject::AddRef()
		{
			return ++m_nRef;
		}

		ULONG STDMETHODCALLTYPE GenericDataObject::Release()
		{
			ULONG nRef = --m_nRef;
			if (!nRef) {
				delete this;
			}
			return nRef;
		}

		HRESULT STDMETHODCALLTYPE GenericDataObject::QueryInterface(REFIID riid, void **ppvObject)
		{
			if (!ppvObject) {
				return E_POINTER;
			}
			if (riid == __uuidof(IDataObject) || riid == __uuidof(IUnknown)) {
				*ppvObject = this;
				AddRef();
				return S_OK;
			}
			return E_NOINTERFACE;
		}

		HRESULT STDMETHODCALLTYPE GenericDataObject::GetData(FORMATETC *pFormatEtcIn, STGMEDIUM *pMedium)
		{
			if (!pFormatEtcIn || !pMedium) {
				return E_INVALIDARG;
			}
			pMedium->hGlobal = NULL;
			ListElements< Ref<GenericDataElement> > list(m_mediums);
			for (sl_size i = 0; i < list.count; i++) {
				if ((pFormatEtcIn->tymed & list[i]->format.tymed) &&
					pFormatEtcIn->dwAspect == list[i]->format.dwAspect &&
					pFormatEtcIn->cfFormat == list[i]->format.cfFormat
					) {
					CopyMedium(pMedium, &(list[i]->medium), &(list[i]->format));
					return S_OK;
				}
			}
			return DV_E_FORMATETC;
		}

		HRESULT STDMETHODCALLTYPE GenericDataObject::GetDataHere(FORMATETC *pFormatEtc, STGMEDIUM *pMedium)
		{
			return E_NOTIMPL;
		}

		HRESULT STDMETHODCALLTYPE GenericDataObject::QueryGetData(FORMATETC *pFormatEtc)
		{
			if (!pFormatEtc) {
				return E_INVALIDARG;
			}
			ListElements< Ref<GenericDataElement> > list(m_mediums);
			HRESULT hr = E_UNEXPECTED;
			for (sl_size i = 0; i < list.count; i++) {
				if (pFormatEtc->dwAspect & list[i]->format.dwAspect) {
					if (pFormatEtc->tymed & list[i]->format.tymed) {
						if (pFormatEtc->cfFormat == list[i]->format.cfFormat) {
							return S_OK;
						} else {
							hr = DV_E_CLIPFORMAT;
						}
					} else {
						hr = DV_E_TYMED;
					}
				} else {
					hr = DV_E_DVASPECT;
				}
			}
			return hr;
		}

		HRESULT STDMETHODCALLTYPE GenericDataObject::GetCanonicalFormatEtc(FORMATETC *pFormatEctIn, FORMATETC *pFormatEtcOut)
		{
			return DATA_S_SAMEFORMATETC;
		}

		HRESULT STDMETHODCALLTYPE GenericDataObject::SetData(FORMATETC *pFormatEtc, STGMEDIUM *pMedium, BOOL fRelease)
		{
			return E_NOTIMPL;
		}

		HRESULT STDMETHODCALLTYPE GenericDataObject::EnumFormatEtc(DWORD dwDirection, IEnumFORMATETC **ppEnumFormatEtc)
		{
			if (!ppEnumFormatEtc) {
				return E_POINTER;
			}

			*ppEnumFormatEtc = NULL;
			switch (dwDirection) {
			case DATADIR_GET:
				*ppEnumFormatEtc = new CEnumFormatEtc(m_mediums);
				if (!(*ppEnumFormatEtc)) {
					return E_OUTOFMEMORY;
				}
				(*ppEnumFormatEtc)->AddRef();
				break;
			case DATADIR_SET:
			default:
				return E_NOTIMPL;
			}
			return S_OK;
		}

		HRESULT STDMETHODCALLTYPE GenericDataObject::DAdvise(FORMATETC *pformatetc, DWORD advf, IAdviseSink *pAdvSink, DWORD *pdwConnection)
		{
			return OLE_E_ADVISENOTSUPPORTED;
		}

		HRESULT STDMETHODCALLTYPE GenericDataObject::DUnadvise(DWORD dwConnection)
		{
			return E_NOTIMPL;
		}

		HRESULT STDMETHODCALLTYPE GenericDataObject::EnumDAdvise(IEnumSTATDATA **ppenumAdvise)
		{
			return OLE_E_ADVISENOTSUPPORTED;
		}

		void GenericDataObject::setText(const StringParam& _text)
		{
			StringData16 text(_text);

			FORMATETC fmt = { 0 };
			fmt.cfFormat = CF_UNICODETEXT;
			fmt.dwAspect = DVASPECT_CONTENT;
			fmt.lindex = -1;
			fmt.tymed = TYMED_HGLOBAL;

			STGMEDIUM medium = { 0 };
			medium.tymed = TYMED_HGLOBAL;

			sl_size len = text.getLength();
			sl_size size = (len + 1) << 1;
			medium.hGlobal = GlobalAlloc(GMEM_MOVEABLE, (SIZE_T)size);
			sl_char16* pMem = (sl_char16*)GlobalLock(medium.hGlobal);
			Base::copyMemory(pMem, text.getData(), size);
			pMem[len] = 0;
			GlobalUnlock(medium.hGlobal);

			Ref<GenericDataElement> element = new GenericDataElement(fmt, medium);
			if (element.isNotNull()) {
				m_mediums.add_NoLock(Move(element));
			}
		}

	}

}

#endif
