/*
   +----------------------------------------------------------------------+
   | 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>                           |
   +----------------------------------------------------------------------+
 */

#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"

struct php_com_iterator {
	zend_object_iterator iter;
	IEnumVARIANT *ev;
	zend_ulong key;
	VARIANT v; /* cached element */
	int code_page;
	VARIANT safe_array;
	VARTYPE sa_type;
	LONG sa_max;
	zval zdata;
};

static void com_iter_dtor(zend_object_iterator *iter)
{
	struct php_com_iterator *I = (struct php_com_iterator*)Z_PTR(iter->data);

	if (I->ev) {
		IEnumVARIANT_Release(I->ev);
	}
	VariantClear(&I->v);
	VariantClear(&I->safe_array);
	zval_ptr_dtor(&I->zdata);
}

static zend_result com_iter_valid(zend_object_iterator *iter)
{
	struct php_com_iterator *I = (struct php_com_iterator*)Z_PTR(iter->data);

	if (Z_TYPE(I->zdata) != IS_UNDEF) {
		return SUCCESS;
	}

	return FAILURE;
}

static zval* com_iter_get_data(zend_object_iterator *iter)
{
	struct php_com_iterator *I = (struct php_com_iterator*)Z_PTR(iter->data);

	return &I->zdata;
}

static void com_iter_get_key(zend_object_iterator *iter, zval *key)
{
	struct php_com_iterator *I = (struct php_com_iterator*)Z_PTR(iter->data);

	if (I->key == (zend_ulong)-1) {
		ZVAL_NULL(key);
	} else {
		ZVAL_LONG(key, I->key);
	}
}

static void com_iter_move_forwards(zend_object_iterator *iter)
{
	struct php_com_iterator *I = (struct php_com_iterator*)Z_PTR(iter->data);
	unsigned long n_fetched;
	zval ptr;

	/* release current cached element */
	VariantClear(&I->v);

	if (Z_TYPE(I->zdata) != IS_UNDEF) {
		zval_ptr_dtor(&I->zdata);
		ZVAL_UNDEF(&I->zdata);
	}

	if (I->ev) {
		/* Get the next element */
		if (SUCCEEDED(IEnumVARIANT_Next(I->ev, 1, &I->v, &n_fetched)) && n_fetched > 0) {
			I->key++;
		} else {
			/* indicate that there are no more items */
			I->key = (zend_ulong)-1;
			return;
		}
	} else {
		/* safe array */
		if (I->key >= (ULONG) I->sa_max) {
			I->key = (zend_ulong)-1;
			return;
		}
		I->key++;
		if (!php_com_safearray_get_elem(&I->safe_array, &I->v, (LONG)I->key)) {
			I->key = (zend_ulong)-1;
			return;
		}
	}

	ZVAL_NULL(&ptr);
	php_com_zval_from_variant(&ptr, &I->v, I->code_page);
	/* php_com_wrap_variant(ptr, &I->v, I->code_page); */
	ZVAL_COPY_VALUE(&I->zdata, &ptr);
}


static const zend_object_iterator_funcs com_iter_funcs = {
	com_iter_dtor,
	com_iter_valid,
	com_iter_get_data,
	com_iter_get_key,
	com_iter_move_forwards,
	NULL,
	NULL, /* get_gc */
};

zend_object_iterator *php_com_iter_get(zend_class_entry *ce, zval *object, int by_ref)
{
	php_com_dotnet_object *obj;
	struct php_com_iterator *I;
	IEnumVARIANT *iev = NULL;
	DISPPARAMS dp;
	VARIANT v;
	unsigned long n_fetched;
	zval ptr;

	if (by_ref) {
		zend_throw_error(NULL, "An iterator cannot be used with foreach by reference");
		return NULL;
	}

	obj = CDNO_FETCH(object);

	if (V_VT(&obj->v) != VT_DISPATCH && !V_ISARRAY(&obj->v)) {
		/* TODO Promote to TypeError? */
		php_error_docref(NULL, E_WARNING, "Variant is not an object or array VT=%d", V_VT(&obj->v));
		return NULL;
	}

	memset(&dp, 0, sizeof(dp));
	VariantInit(&v);

	I = (struct php_com_iterator*)ecalloc(1, sizeof(*I));
	zend_iterator_init(&I->iter);
	I->iter.funcs = &com_iter_funcs;
	Z_PTR(I->iter.data) = I;
	I->code_page = obj->code_page;
	ZVAL_UNDEF(&I->zdata);
	VariantInit(&I->safe_array);
	VariantInit(&I->v);

	if (V_ISARRAY(&obj->v)) {
		LONG bound;
		UINT dims;

		dims = SafeArrayGetDim(V_ARRAY(&obj->v));

		if (dims != 1) {
			/* TODO Promote to ValueError? */
			php_error_docref(NULL, E_WARNING,
				   "Can only handle single dimension variant arrays (this array has %d)", dims);
			goto fail;
		}

		/* same semantics as foreach on a PHP array;
		 * make a copy and enumerate that copy */
		VariantCopy(&I->safe_array, &obj->v);

		/* determine the key value for the array */
		SafeArrayGetLBound(V_ARRAY(&I->safe_array), 1, &bound);
		SafeArrayGetUBound(V_ARRAY(&I->safe_array), 1, &I->sa_max);

		/* pre-fetch the element */
		if (I->sa_max >= bound && php_com_safearray_get_elem(&I->safe_array, &I->v, bound)) {
			I->key = bound;
			ZVAL_NULL(&ptr);
			php_com_zval_from_variant(&ptr, &I->v, I->code_page);
			ZVAL_COPY_VALUE(&I->zdata, &ptr);
		} else {
			I->key = (zend_ulong)-1;
		}

	} else {
		/* can we enumerate it? */
		if (FAILED(IDispatch_Invoke(V_DISPATCH(&obj->v), DISPID_NEWENUM,
						&IID_NULL, LOCALE_NEUTRAL, DISPATCH_METHOD|DISPATCH_PROPERTYGET,
						&dp, &v, NULL, NULL))) {
			goto fail;
		}

		/* get something useful out of it */
		if (V_VT(&v) == VT_UNKNOWN) {
			IUnknown_QueryInterface(V_UNKNOWN(&v), &IID_IEnumVARIANT, (void**)&iev);
		} else if (V_VT(&v) == VT_DISPATCH) {
			IDispatch_QueryInterface(V_DISPATCH(&v), &IID_IEnumVARIANT, (void**)&iev);
		}

		VariantClear(&v);

		if (iev == NULL) {
			goto fail;
		}

		I->ev = iev;

		/* Get the first element now */
		if (SUCCEEDED(IEnumVARIANT_Next(I->ev, 1, &I->v, &n_fetched)) && n_fetched > 0) {
			/* indicate that we have element 0 */
			I->key = 0;
			ZVAL_NULL(&ptr);
			php_com_zval_from_variant(&ptr, &I->v, I->code_page);
			ZVAL_COPY_VALUE(&I->zdata, &ptr);
		} else {
			/* indicate that there are no more items */
			I->key = (zend_ulong)-1;
		}
	}

	return &I->iter;

fail:
	if (I) {
		VariantClear(&I->safe_array);
		VariantClear(&I->v);
		efree(I);
	}
	return NULL;
}
