/*
   +----------------------------------------------------------------------+
   | Copyright © The PHP Group and Contributors.                          |
   +----------------------------------------------------------------------+
   | This source file is subject to the Modified BSD License that is      |
   | bundled with this package in the file LICENSE, and is available      |
   | through the World Wide Web at <https://www.php.net/license/>.        |
   |                                                                      |
   | SPDX-License-Identifier: BSD-3-Clause                                |
   +----------------------------------------------------------------------+
   | Author: Wez Furlong  <wez@thebrainroom.com>                          |
   +----------------------------------------------------------------------+
 */

/* Infrastructure for working with persistent COM objects.
 * Implements: IStream* wrapper for PHP streams.
 * TODO:
 *  - Magic __wakeup and __sleep handlers for serialization.
 *  - Track the stream and dispatch instances in a global list to make sure
 *    they are destroyed when a fatal error occurs.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include "php.h"
#include "php_com_dotnet.h"
#include "php_com_dotnet_internal.h"
#include "Zend/zend_exceptions.h"
#include "com_persist_arginfo.h"

/* {{{ expose php_stream as a COM IStream */

typedef struct {
	CONST_VTBL struct IStreamVtbl *lpVtbl;
	DWORD engine_thread;
	LONG refcount;
	php_stream *stream;
} php_istream;

static void istream_destructor(php_istream *stm);

#define FETCH_STM()	\
	php_istream *stm = (php_istream*)This; \
	if (GetCurrentThreadId() != stm->engine_thread) \
		return RPC_E_WRONG_THREAD;

#define FETCH_STM_EX()	\
	php_istream *stm = (php_istream*)This;	\
	if (GetCurrentThreadId() != stm->engine_thread)	\
		return RPC_E_WRONG_THREAD;

static HRESULT STDMETHODCALLTYPE stm_queryinterface(
	IStream *This,
	/* [in] */ REFIID riid,
	/* [iid_is][out] */ void **ppvObject)
{
	FETCH_STM_EX();

	if (IsEqualGUID(&IID_IUnknown, riid) ||
			IsEqualGUID(&IID_IStream, riid)) {
		*ppvObject = This;
		InterlockedIncrement(&stm->refcount);
		return S_OK;
	}

	*ppvObject = NULL;
	return E_NOINTERFACE;
}

static ULONG STDMETHODCALLTYPE stm_addref(IStream *This)
{
	FETCH_STM_EX();

	return InterlockedIncrement(&stm->refcount);
}

static ULONG STDMETHODCALLTYPE stm_release(IStream *This)
{
	ULONG ret;
	FETCH_STM();

	ret = InterlockedDecrement(&stm->refcount);
	if (ret == 0) {
		/* destroy it */
		istream_destructor(stm);
	}
	return ret;
}

static HRESULT STDMETHODCALLTYPE stm_read(IStream *This, void *pv, ULONG cb, ULONG *pcbRead)
{
	ULONG nread;
	FETCH_STM();

	nread = (ULONG)php_stream_read(stm->stream, pv, cb);

	if (pcbRead) {
		*pcbRead = nread > 0 ? nread : 0;
	}
	if (nread > 0) {
		return S_OK;
	}
	return S_FALSE;
}

static HRESULT STDMETHODCALLTYPE stm_write(IStream *This, void const *pv, ULONG cb, ULONG *pcbWritten)
{
	ssize_t nwrote;
	FETCH_STM();

	nwrote = php_stream_write(stm->stream, pv, cb);

	if (pcbWritten) {
		*pcbWritten = nwrote > 0 ? (ULONG)nwrote : 0;
	}
	if (nwrote > 0) {
		return S_OK;
	}
	return S_FALSE;
}

static HRESULT STDMETHODCALLTYPE stm_seek(IStream *This, LARGE_INTEGER dlibMove,
		DWORD dwOrigin, ULARGE_INTEGER *plibNewPosition)
{
	off_t offset;
	int whence;
	int ret;
	FETCH_STM();

	switch (dwOrigin) {
		case STREAM_SEEK_SET:	whence = SEEK_SET;	break;
		case STREAM_SEEK_CUR:	whence = SEEK_CUR;	break;
		case STREAM_SEEK_END:	whence = SEEK_END;	break;
		default:
			return STG_E_INVALIDFUNCTION;
	}

	if (dlibMove.HighPart) {
		/* we don't support 64-bit offsets */
		return STG_E_INVALIDFUNCTION;
	}

	offset = (off_t) dlibMove.QuadPart;

	ret = php_stream_seek(stm->stream, offset, whence);

	if (plibNewPosition) {
		plibNewPosition->QuadPart = (ULONGLONG)(ret >= 0 ? ret : 0);
	}

	return ret >= 0 ? S_OK : STG_E_INVALIDFUNCTION;
}

static HRESULT STDMETHODCALLTYPE stm_set_size(IStream *This, ULARGE_INTEGER libNewSize)
{
	FETCH_STM();

	if (libNewSize.HighPart) {
		return STG_E_INVALIDFUNCTION;
	}

	if (php_stream_truncate_supported(stm->stream)) {
		int ret = php_stream_truncate_set_size(stm->stream, (size_t)libNewSize.QuadPart);

		if (ret == 0) {
			return S_OK;
		}
	}

	return STG_E_INVALIDFUNCTION;
}

static HRESULT STDMETHODCALLTYPE stm_copy_to(IStream *This, IStream *pstm, ULARGE_INTEGER cb,
		ULARGE_INTEGER *pcbRead, ULARGE_INTEGER *pcbWritten)
{
	FETCH_STM_EX();

	return E_NOTIMPL;
}

static HRESULT STDMETHODCALLTYPE stm_commit(IStream *This, DWORD grfCommitFlags)
{
	FETCH_STM();

	php_stream_flush(stm->stream);

	return S_OK;
}

static HRESULT STDMETHODCALLTYPE stm_revert(IStream *This)
{
	/* NOP */
	return S_OK;
}

static HRESULT STDMETHODCALLTYPE stm_lock_region(IStream *This,
   	ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, DWORD lockType)
{
	return STG_E_INVALIDFUNCTION;
}

static HRESULT STDMETHODCALLTYPE stm_unlock_region(IStream *This,
		ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, DWORD lockType)
{
	return STG_E_INVALIDFUNCTION;
}

static HRESULT STDMETHODCALLTYPE stm_stat(IStream *This,
		STATSTG *pstatstg, DWORD grfStatFlag)
{
	return STG_E_INVALIDFUNCTION;
}

static HRESULT STDMETHODCALLTYPE stm_clone(IStream *This, IStream **ppstm)
{
	return STG_E_INVALIDFUNCTION;
}

static struct IStreamVtbl php_istream_vtbl = {
	stm_queryinterface,
	stm_addref,
	stm_release,
	stm_read,
	stm_write,
	stm_seek,
	stm_set_size,
	stm_copy_to,
	stm_commit,
	stm_revert,
	stm_lock_region,
	stm_unlock_region,
	stm_stat,
	stm_clone
};

static void istream_destructor(php_istream *stm)
{
	if (stm->refcount > 0) {
		CoDisconnectObject((IUnknown*)stm, 0);
	}

	zend_list_delete(stm->stream->res);

	CoTaskMemFree(stm);
}
/* }}} */

PHP_COM_DOTNET_API IStream *php_com_wrapper_export_stream(php_stream *stream)
{
	php_istream *stm = (php_istream*)CoTaskMemAlloc(sizeof(*stm));

	if (stm == NULL)
		return NULL;

	memset(stm, 0, sizeof(*stm));
	stm->engine_thread = GetCurrentThreadId();
	stm->lpVtbl = &php_istream_vtbl;
	stm->refcount = 1;
	stm->stream = stream;

	GC_ADDREF(stream->res);

	return (IStream*)stm;
}

#define CPH_METHOD(fname)		PHP_METHOD(COMPersistHelper, fname)

#define CPH_FETCH()				php_com_persist_helper *helper = (php_com_persist_helper*)Z_OBJ_P(ZEND_THIS);

#define CPH_NO_OBJ()			if (helper->unk == NULL) { php_com_throw_exception(E_INVALIDARG, "No COM object is associated with this helper instance"); RETURN_THROWS(); }

typedef struct {
	zend_object			std;
	long codepage;
	IUnknown 			*unk;
	IPersistStream 		*ips;
	IPersistStreamInit	*ipsi;
	IPersistFile		*ipf;
} php_com_persist_helper;

static zend_object_handlers helper_handlers;
static zend_class_entry *helper_ce;

static inline HRESULT get_persist_stream(php_com_persist_helper *helper)
{
	if (!helper->ips && helper->unk) {
		return IUnknown_QueryInterface(helper->unk, &IID_IPersistStream, (void **) &helper->ips);
	}
	return helper->ips ? S_OK : E_NOTIMPL;
}

static inline HRESULT get_persist_stream_init(php_com_persist_helper *helper)
{
	if (!helper->ipsi && helper->unk) {
		return IUnknown_QueryInterface(helper->unk, &IID_IPersistStreamInit, (void **) &helper->ipsi);
	}
	return helper->ipsi ? S_OK : E_NOTIMPL;
}

static inline HRESULT get_persist_file(php_com_persist_helper *helper)
{
	if (!helper->ipf && helper->unk) {
		return IUnknown_QueryInterface(helper->unk, &IID_IPersistFile, (void **) &helper->ipf);
	}
	return helper->ipf ? S_OK : E_NOTIMPL;
}


/* {{{ Determines the filename into which an object will be saved, or false if none is set, via IPersistFile::GetCurFile */
CPH_METHOD(GetCurFileName)
{
	HRESULT res;
	OLECHAR *olename = NULL;
	CPH_FETCH();

	ZEND_PARSE_PARAMETERS_NONE();

	CPH_NO_OBJ();

	res = get_persist_file(helper);
	if (helper->ipf) {
		res = IPersistFile_GetCurFile(helper->ipf, &olename);

		if (res == S_OK) {
			zend_string *str = php_com_olestring_to_string(olename, helper->codepage);
			CoTaskMemFree(olename);
			RETURN_STR(str);
		} else if (res == S_FALSE) {
			CoTaskMemFree(olename);
			RETURN_FALSE;
		}
		php_com_throw_exception(res, NULL);
	} else {
		php_com_throw_exception(res, NULL);
	}
}
/* }}} */


/* {{{ Persist object data to file, via IPersistFile::Save */
CPH_METHOD(SaveToFile)
{
	HRESULT res;
	char *filename, *fullpath = NULL;
	size_t filename_len;
	bool remember = TRUE;
	OLECHAR *olefilename = NULL;
	CPH_FETCH();

	if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "p!|b",
				&filename, &filename_len, &remember)) {
		RETURN_THROWS();
	}

	CPH_NO_OBJ();

	res = get_persist_file(helper);
	if (helper->ipf) {
		if (filename) {
			fullpath = expand_filepath(filename, NULL);
			if (!fullpath) {
				RETURN_FALSE;
			}

			if (php_check_open_basedir(fullpath)) {
				efree(fullpath);
				RETURN_FALSE;
			}

			olefilename = php_com_string_to_olestring(fullpath, strlen(fullpath), helper->codepage);
			efree(fullpath);
		}
		res = IPersistFile_Save(helper->ipf, olefilename, remember);
		if (SUCCEEDED(res)) {
			if (!olefilename) {
				res = IPersistFile_GetCurFile(helper->ipf, &olefilename);
				if (S_OK == res) {
					IPersistFile_SaveCompleted(helper->ipf, olefilename);
					CoTaskMemFree(olefilename);
					olefilename = NULL;
				}
			} else if (remember) {
				IPersistFile_SaveCompleted(helper->ipf, olefilename);
			}
		}

		if (olefilename) {
			efree(olefilename);
		}

		if (FAILED(res)) {
			php_com_throw_exception(res, NULL);
		}

	} else {
		php_com_throw_exception(res, NULL);
	}
}
/* }}} */

/* {{{ Load object data from file, via IPersistFile::Load */
CPH_METHOD(LoadFromFile)
{
	HRESULT res;
	char *filename, *fullpath;
	size_t filename_len;
	zend_long flags = 0;
	OLECHAR *olefilename;
	CPH_FETCH();

	if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "p|l",
				&filename, &filename_len, &flags)) {
		RETURN_THROWS();
	}

	CPH_NO_OBJ();

	res = get_persist_file(helper);
	if (helper->ipf) {
		if (!(fullpath = expand_filepath(filename, NULL))) {
			RETURN_FALSE;
		}

		if (php_check_open_basedir(fullpath)) {
			efree(fullpath);
			RETURN_FALSE;
		}

		olefilename = php_com_string_to_olestring(fullpath, strlen(fullpath), helper->codepage);
		efree(fullpath);

		res = IPersistFile_Load(helper->ipf, olefilename, (DWORD)flags);
		efree(olefilename);

		if (FAILED(res)) {
			php_com_throw_exception(res, NULL);
		}

	} else {
		php_com_throw_exception(res, NULL);
	}
}
/* }}} */

/* {{{ Gets maximum stream size required to store the object data, via IPersistStream::GetSizeMax (or IPersistStreamInit::GetSizeMax) */
CPH_METHOD(GetMaxStreamSize)
{
	HRESULT res;
	ULARGE_INTEGER size;
	CPH_FETCH();

	ZEND_PARSE_PARAMETERS_NONE();

	CPH_NO_OBJ();

	res = get_persist_stream_init(helper);
	if (helper->ipsi) {
		res = IPersistStreamInit_GetSizeMax(helper->ipsi, &size);
	} else {
		res = get_persist_stream(helper);
		if (helper->ips) {
			res = IPersistStream_GetSizeMax(helper->ips, &size);
		} else {
			php_com_throw_exception(res, NULL);
			RETURN_THROWS();
		}
	}

	if (res != S_OK) {
		php_com_throw_exception(res, NULL);
	} else {
		/* TODO: handle 64 bit properly */
		RETURN_LONG((zend_long)size.QuadPart);
	}
}
/* }}} */

/* {{{ Initializes the object to a default state, via IPersistStreamInit::InitNew */
CPH_METHOD(InitNew)
{
	HRESULT res;
	CPH_FETCH();

	ZEND_PARSE_PARAMETERS_NONE();

	CPH_NO_OBJ();

	res = get_persist_stream_init(helper);
	if (helper->ipsi) {
		res = IPersistStreamInit_InitNew(helper->ipsi);

		if (res != S_OK) {
			php_com_throw_exception(res, NULL);
		} else {
			RETURN_TRUE;
		}
	} else {
		php_com_throw_exception(res, NULL);
	}
}
/* }}} */

/* {{{ Initializes an object from the stream where it was previously saved, via IPersistStream::Load or OleLoadFromStream */
CPH_METHOD(LoadFromStream)
{
	zval *zstm;
	php_stream *stream;
	IStream *stm = NULL;
	HRESULT res;
	CPH_FETCH();

	if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "r", &zstm)) {
		RETURN_THROWS();
	}

	php_stream_from_zval_no_verify(stream, zstm);

	if (stream == NULL) {
		php_com_throw_exception(E_INVALIDARG, "expected a stream");
		RETURN_THROWS();
	}

	stm = php_com_wrapper_export_stream(stream);
	if (stm == NULL) {
		php_com_throw_exception(E_UNEXPECTED, "failed to wrap stream");
		RETURN_THROWS();
	}

	res = S_OK;
	RETVAL_TRUE;

	if (helper->unk == NULL) {
		IDispatch *disp = NULL;

		/* we need to create an object and load using OleLoadFromStream */
		res = OleLoadFromStream(stm, &IID_IDispatch, (void **) &disp);

		if (SUCCEEDED(res)) {
			php_com_wrap_dispatch(return_value, disp, COMG(code_page));
		}
	} else {
		res = get_persist_stream_init(helper);
		if (helper->ipsi) {
			res = IPersistStreamInit_Load(helper->ipsi, stm);
		} else {
			res = get_persist_stream(helper);
			if (helper->ips) {
				res = IPersistStream_Load(helper->ips, stm);
			}
		}
	}
	IStream_Release(stm);

	if (FAILED(res)) {
		php_com_throw_exception(res, NULL);
		RETURN_THROWS();
	}
}
/* }}} */

/* {{{ Saves the object to a stream, via IPersistStream::Save */
CPH_METHOD(SaveToStream)
{
	zval *zstm;
	php_stream *stream;
	IStream *stm = NULL;
	HRESULT res;
	CPH_FETCH();

	CPH_NO_OBJ();

	if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "r", &zstm)) {
		RETURN_THROWS();
	}

	php_stream_from_zval_no_verify(stream, zstm);

	if (stream == NULL) {
		php_com_throw_exception(E_INVALIDARG, "expected a stream");
		RETURN_THROWS();
	}

	stm = php_com_wrapper_export_stream(stream);
	if (stm == NULL) {
		php_com_throw_exception(E_UNEXPECTED, "failed to wrap stream");
		RETURN_THROWS();
	}

	res = get_persist_stream_init(helper);
	if (helper->ipsi) {
		res = IPersistStreamInit_Save(helper->ipsi, stm, TRUE);
	} else {
		res = get_persist_stream(helper);
		if (helper->ips) {
			res = IPersistStream_Save(helper->ips, stm, TRUE);
		}
	}

	IStream_Release(stm);

	if (FAILED(res)) {
		php_com_throw_exception(res, NULL);
		RETURN_THROWS();
	}

	RETURN_TRUE;
}
/* }}} */

/* {{{ Creates a persistence helper object, usually associated with a com_object */
CPH_METHOD(__construct)
{
	php_com_dotnet_object *obj = NULL;
	zval *zobj = NULL;
	CPH_FETCH();

	if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "|O!",
				&zobj, php_com_variant_class_entry)) {
		RETURN_THROWS();
	}

	if (!zobj) {
		return;
	}

	obj = CDNO_FETCH(zobj);

	if (V_VT(&obj->v) != VT_DISPATCH || V_DISPATCH(&obj->v) == NULL) {
		php_com_throw_exception(E_INVALIDARG, "parameter must represent an IDispatch COM object");
		RETURN_THROWS();
	}

	/* it is always safe to cast an interface to IUnknown */
	helper->unk = (IUnknown*)V_DISPATCH(&obj->v);
	IUnknown_AddRef(helper->unk);
	helper->codepage = obj->code_page;
}
/* }}} */


static void helper_free_storage(zend_object *obj)
{
	php_com_persist_helper *object = (php_com_persist_helper*)obj;

	if (object->ipf) {
		IPersistFile_Release(object->ipf);
	}
	if (object->ips) {
		IPersistStream_Release(object->ips);
	}
	if (object->ipsi) {
		IPersistStreamInit_Release(object->ipsi);
	}
	if (object->unk) {
		IUnknown_Release(object->unk);
	}
	zend_object_std_dtor(&object->std);
}


static zend_object* helper_clone(zend_object *obj)
{
	php_com_persist_helper *clone, *object = (php_com_persist_helper*) obj;

	clone = emalloc(sizeof(*object));
	memcpy(clone, object, sizeof(*object));

	zend_object_std_init(&clone->std, object->std.ce);

	if (clone->ipf) {
		IPersistFile_AddRef(clone->ipf);
	}
	if (clone->ips) {
		IPersistStream_AddRef(clone->ips);
	}
	if (clone->ipsi) {
		IPersistStreamInit_AddRef(clone->ipsi);
	}
	if (clone->unk) {
		IUnknown_AddRef(clone->unk);
	}
	return (zend_object*)clone;
}

static zend_object* helper_new(zend_class_entry *ce)
{
	php_com_persist_helper *helper;

	helper = emalloc(sizeof(*helper));
	memset(helper, 0, sizeof(*helper));

	zend_object_std_init(&helper->std, helper_ce);

	return &helper->std;
}

void php_com_persist_minit(INIT_FUNC_ARGS)
{
	memcpy(&helper_handlers, &std_object_handlers, sizeof(helper_handlers));
	helper_handlers.free_obj = helper_free_storage;
	helper_handlers.clone_obj = helper_clone;

	helper_ce = register_class_COMPersistHelper();
	helper_ce->create_object = helper_new;
	helper_ce->default_object_handlers = &helper_handlers;
}
