/*
  +----------------------------------------------------------------------+
  | parallel                                                             |
  +----------------------------------------------------------------------+
  | Copyright (c) Joe Watkins 2019-2024                                  |
  +----------------------------------------------------------------------+
  | This source file is subject to version 3.01 of the PHP license,      |
  | that is bundled with this package in the file LICENSE, and is        |
  | available through the world-wide-web at the following url:           |
  | http://www.php.net/license/3_01.txt                                  |
  | If you did not receive a copy of the PHP license and are unable to   |
  | obtain it through the world-wide-web, please send a note to          |
  | license@php.net so we can mail you a copy immediately.               |
  +----------------------------------------------------------------------+
  | Author: krakjoe                                                      |
  +----------------------------------------------------------------------+
 */
#ifndef HAVE_PARALLEL_SYNC
#define HAVE_PARALLEL_SYNC

#include "parallel.h"

zend_class_entry    *php_parallel_sync_ce;
zend_object_handlers php_parallel_sync_handlers;

static zend_string  *php_parallel_sync_string_value;

#define PARALLEL_SYNC_IS_SCALAR(zv)                                                                                    \
	((Z_TYPE_P(zv) != IS_OBJECT) && (Z_TYPE_P(zv) != IS_ARRAY) && (Z_TYPE_P(zv) != IS_RESOURCE))

static zend_always_inline php_parallel_sync_t *php_parallel_sync_fetch(zend_object *o)
{
	php_parallel_sync_object_t *object = php_parallel_sync_object_fetch(o);

	return object->sync;
}

static zend_always_inline php_parallel_sync_t *php_parallel_sync_from(zval *z)
{
	php_parallel_sync_object_t *object = php_parallel_sync_object_from(z);

	return object->sync;
}

static zend_always_inline php_parallel_sync_t *php_parallel_sync_create(zval *zv)
{
	php_parallel_sync_t *sync = (php_parallel_sync_t *)pecalloc(1, sizeof(php_parallel_sync_t), 1);

	php_parallel_mutex_init(&sync->mutex, 1);
	php_parallel_cond_init(&sync->condition);

	if (zv) {
		PARALLEL_ZVAL_COPY(&sync->value, zv, 1);
	}

	sync->refcount = 1;

	return sync;
}

php_parallel_sync_t *php_parallel_sync_copy(php_parallel_sync_t *sync)
{
	php_parallel_atomic_addref(&sync->refcount);

	return sync;
}

void php_parallel_sync_release(php_parallel_sync_t *sync)
{
	if (!sync) {
		return;
	}

	if (php_parallel_atomic_delref(&sync->refcount) == 0) {
		if (Z_TYPE(sync->value) == IS_STRING) {
			PARALLEL_ZVAL_DTOR(&sync->value);
		}

		php_parallel_mutex_destroy(&sync->mutex);
		php_parallel_cond_destroy(&sync->condition);
		pefree(sync, 1);
	}
}

static zend_object *php_parallel_sync_object_create(zend_class_entry *ce)
{
	php_parallel_sync_object_t *object =
	    (php_parallel_sync_object_t *)ecalloc(1, sizeof(php_parallel_sync_object_t) + zend_object_properties_size(ce));

	zend_object_std_init(&object->std, ce);

	object->std.handlers = &php_parallel_sync_handlers;

	return &object->std;
}

static void php_parallel_sync_object_destroy(zend_object *o)
{
	php_parallel_sync_object_t *object = php_parallel_sync_object_fetch(o);

	php_parallel_sync_release(object->sync);

	zend_object_std_dtor(o);
}

static HashTable *php_parallel_sync_object_debug(zend_object *o, int *temp)
{
	php_parallel_sync_object_t *object = php_parallel_sync_object_fetch(o);
	HashTable                  *debug;
	zval                        zv;

	*temp = 1;

	pthread_mutex_lock(&object->sync->mutex);

	ALLOC_HASHTABLE(debug);
	zend_hash_init(debug, 1, NULL, ZVAL_PTR_DTOR, 0);

	if (!Z_ISUNDEF(object->sync->value)) {
		PARALLEL_ZVAL_COPY(&zv, &object->sync->value, 0);

		zend_hash_add(debug, php_parallel_sync_string_value, &zv);
	}

	pthread_mutex_unlock(&object->sync->mutex);

	return debug;
}

ZEND_BEGIN_ARG_INFO_EX(php_parallel_sync_construct_arginfo, 0, 0, 0)
ZEND_ARG_INFO(0, value)
ZEND_END_ARG_INFO()

PHP_METHOD(Parallel_Sync, __construct)
{
	php_parallel_sync_object_t *object = php_parallel_sync_object_from(getThis());
	zval                       *value = NULL;

	if (ZEND_NUM_ARGS()) {
		ZEND_PARSE_PARAMETERS_START(0, 1)
		Z_PARAM_OPTIONAL
		Z_PARAM_ZVAL(value)
		ZEND_PARSE_PARAMETERS_END();
	}

	if (value) {
		if (!PARALLEL_SYNC_IS_SCALAR(value)) {
			php_parallel_exception_ex(php_parallel_sync_error_illegal_value_ce, "sync cannot contain non-scalar %s",
			                          Z_TYPE_P(value) == IS_OBJECT ? ZSTR_VAL(Z_OBJCE_P(value)->name)
			                                                       : zend_get_type_by_const(Z_TYPE_P(value)));
			return;
		}
	}

	object->sync = php_parallel_sync_create(value);
}

ZEND_BEGIN_ARG_INFO_EX(php_parallel_sync_set_arginfo, 0, 0, 1)
ZEND_ARG_INFO(0, value)
ZEND_END_ARG_INFO()

PHP_METHOD(Parallel_Sync, set)
{
	php_parallel_sync_object_t *object = php_parallel_sync_object_from(getThis());
	zval                       *value;

	ZEND_PARSE_PARAMETERS_START(1, 1)
	Z_PARAM_ZVAL(value)
	ZEND_PARSE_PARAMETERS_END();

	if (!PARALLEL_SYNC_IS_SCALAR(value)) {
		php_parallel_exception_ex(php_parallel_sync_error_illegal_value_ce, "sync cannot contain non-scalar %s",
		                          Z_TYPE_P(value) == IS_OBJECT ? ZSTR_VAL(Z_OBJCE_P(value)->name)
		                                                       : zend_get_type_by_const(Z_TYPE_P(value)));
		return;
	}

	pthread_mutex_lock(&object->sync->mutex);

	if (Z_TYPE(object->sync->value) == IS_STRING) {
		PARALLEL_ZVAL_DTOR(&object->sync->value);
	}

	PARALLEL_ZVAL_COPY(&object->sync->value, value, 1);

	pthread_mutex_unlock(&object->sync->mutex);
}

ZEND_BEGIN_ARG_INFO_EX(php_parallel_sync_get_arginfo, 0, 0, 0)
ZEND_END_ARG_INFO()

PHP_METHOD(Parallel_Sync, get)
{
	php_parallel_sync_object_t *object = php_parallel_sync_object_from(getThis());

	pthread_mutex_lock(&object->sync->mutex);

	if (Z_TYPE(object->sync->value) != IS_UNDEF) {
		PARALLEL_ZVAL_COPY(return_value, &object->sync->value, 0);
	}

	pthread_mutex_unlock(&object->sync->mutex);
}

ZEND_BEGIN_ARG_INFO_EX(php_parallel_sync_invoke_arginfo, 0, 0, 1)
ZEND_ARG_TYPE_INFO(0, block, IS_CALLABLE, 0)
ZEND_END_ARG_INFO()

PHP_METHOD(Parallel_Sync, __invoke)
{
	php_parallel_sync_object_t *object = php_parallel_sync_object_from(getThis());
	zend_fcall_info             fci;
	zend_fcall_info_cache       fcc;

	ZEND_PARSE_PARAMETERS_START(1, 1)
	Z_PARAM_FUNC(fci, fcc)
	ZEND_PARSE_PARAMETERS_END();

	pthread_mutex_lock(&object->sync->mutex);

	fci.retval = return_value;

	zend_try { zend_call_function(&fci, &fcc); }
	zend_end_try();

	pthread_mutex_unlock(&object->sync->mutex);
}

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(php_parallel_sync_wait_arginfo, 0, 0, _IS_BOOL, 0)
ZEND_END_ARG_INFO()

PHP_METHOD(Parallel_Sync, wait)
{
	php_parallel_sync_object_t *object = php_parallel_sync_object_from(getThis());

	switch (pthread_cond_wait(&object->sync->condition, &object->sync->mutex)) {
	case SUCCESS:
		RETURN_TRUE;

	/* LCOV_EXCL_START */
	default:
		RETURN_FALSE;
		/* LCOV_EXCL_STOP */
	}
}

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(php_parallel_sync_notify_arginfo, 0, 0, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, all, _IS_BOOL, 0)
ZEND_END_ARG_INFO()

PHP_METHOD(Parallel_Sync, notify)
{
	php_parallel_sync_object_t *object = php_parallel_sync_object_from(getThis());
	bool                        all = 0;

	ZEND_PARSE_PARAMETERS_START(0, 1)
	Z_PARAM_OPTIONAL
	Z_PARAM_BOOL(all)
	ZEND_PARSE_PARAMETERS_END();

	switch (all ? pthread_cond_broadcast(&object->sync->condition) : pthread_cond_signal(&object->sync->condition)) {
	case SUCCESS:
		RETURN_TRUE;

	/* LCOV_EXCL_START */
	default:
		RETURN_FALSE;
		/* LCOV_EXCL_STOP */
	}
}

static zend_function_entry php_parallel_sync_methods[] = {
    PHP_ME(Parallel_Sync, __construct, php_parallel_sync_construct_arginfo, ZEND_ACC_PUBLIC)
        PHP_ME(Parallel_Sync, set, php_parallel_sync_set_arginfo, ZEND_ACC_PUBLIC)
            PHP_ME(Parallel_Sync, get, php_parallel_sync_get_arginfo, ZEND_ACC_PUBLIC)
                PHP_ME(Parallel_Sync, wait, php_parallel_sync_wait_arginfo, ZEND_ACC_PUBLIC)
                    PHP_ME(Parallel_Sync, notify, php_parallel_sync_notify_arginfo, ZEND_ACC_PUBLIC)
                        PHP_ME(Parallel_Sync, __invoke, php_parallel_sync_invoke_arginfo, ZEND_ACC_PUBLIC) PHP_FE_END};

PHP_MINIT_FUNCTION(PARALLEL_SYNC)
{
	zend_class_entry ce;

	memcpy(&php_parallel_sync_handlers, php_parallel_standard_handlers(), sizeof(zend_object_handlers));

	php_parallel_sync_handlers.offset = offsetof(php_parallel_sync_object_t, std);
	php_parallel_sync_handlers.free_obj = php_parallel_sync_object_destroy;
	php_parallel_sync_handlers.get_debug_info = php_parallel_sync_object_debug;

	INIT_NS_CLASS_ENTRY(ce, "parallel", "Sync", php_parallel_sync_methods);

	php_parallel_sync_ce = zend_register_internal_class(&ce);
	php_parallel_sync_ce->create_object = php_parallel_sync_object_create;
	php_parallel_sync_ce->ce_flags |= ZEND_ACC_FINAL;

#ifdef ZEND_ACC_NOT_SERIALIZABLE
	php_parallel_sync_ce->ce_flags |= ZEND_ACC_NOT_SERIALIZABLE;
#else
	php_parallel_sync_ce->serialize = zend_class_serialize_deny;
	php_parallel_sync_ce->unserialize = zend_class_unserialize_deny;
#endif

	php_parallel_sync_string_value = zend_string_init_interned(ZEND_STRL("value"), 1);

	return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(PARALLEL_SYNC) { return SUCCESS; }
#endif
