/*
  +----------------------------------------------------------------------+
  | 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_EVENTS_POLL
#define HAVE_PARALLEL_EVENTS_POLL

#include "parallel.h"

#if PHP_VERSION_ID >= 80400
#include "ext/random/php_random.h"
#else
#include "ext/standard/php_mt_rand.h"
#endif

typedef struct _php_parallel_events_poll_t {
	uint32_t       try;
	struct timeval stop;
	struct {
		zend_fcall_info       fci;
		zend_fcall_info_cache fcc;
		zval                  ival;
	} block;
	php_parallel_events_state_t state;
} php_parallel_events_poll_t;

static zend_always_inline php_parallel_events_poll_t *php_parallel_events_poll_init(php_parallel_events_t *events)
{
	if (events->targets.nNumUsed == 0) {
		return NULL;
	}

	php_parallel_events_poll_t *poll = (php_parallel_events_poll_t *)pecalloc(1, sizeof(php_parallel_events_poll_t), 1);

	if (events->timeout > -1) {
		if (gettimeofday(&poll->stop, NULL) == SUCCESS) {
			poll->stop.tv_sec += (events->timeout / 1000000L);
			poll->stop.tv_sec += (poll->stop.tv_usec + (events->timeout % 1000000L)) / 1000000L;
			poll->stop.tv_usec = (poll->stop.tv_usec + (events->timeout % 1000000L)) % 1000000L;
		}
		/* return 0 ? */
	}

	if (!Z_ISUNDEF(events->blocker)) {
		zend_fcall_info_init(&events->blocker, 0, &poll->block.fci, &poll->block.fcc, NULL, NULL);
		poll->block.fci.retval = &poll->block.ival;
	} else {
		memset(&poll->block, 0, sizeof(poll->block));
	}

	return poll;
}

static zend_always_inline void php_parallel_events_poll_free(php_parallel_events_poll_t *poll) { pefree(poll, 1); }

static zend_always_inline void php_parallel_events_poll_unlock(php_parallel_events_poll_t *poll)
{
	if (poll->state.type == PHP_PARALLEL_EVENTS_LINK) {
		php_parallel_channel_t *channel = php_parallel_channel_fetch(poll->state.object);

		php_parallel_link_unlock(channel->link);
	} else {
		php_parallel_future_t *future = php_parallel_future_fetch(poll->state.object);

		php_parallel_future_unlock(future);
	}
}

static zend_always_inline void php_parallel_events_poll_end(php_parallel_events_poll_t *poll)
{
	php_parallel_events_poll_unlock(poll);
	php_parallel_events_poll_free(poll);
}

static zend_always_inline bool php_parallel_events_poll_timeout(php_parallel_events_poll_t *poll,
                                                                php_parallel_events_t      *events)
{
	struct timeval now;

	if (events->timeout > -1 && gettimeofday(&now, NULL) == SUCCESS) {
		if (now.tv_sec >= poll->stop.tv_sec && now.tv_usec >= poll->stop.tv_usec) {
			php_parallel_exception_ex(php_parallel_events_error_timeout_ce, "timeout occured");
			return 1;
		}
	}

	return 0;
}

static zend_always_inline bool php_parallel_events_poll_random(php_parallel_events_t *events, zend_string **name,
                                                               zend_object **object)
{
	uint32_t  size = events->targets.nNumUsed;
	zend_long random = php_mt_rand_range(0, (zend_long)size - 1);

	do {
		Bucket *bucket = &events->targets.arData[random];

		if (!Z_ISUNDEF(bucket->val)) {
			*name = bucket->key;
			*object = Z_OBJ(bucket->val);

			return 1;
		}

		random = php_mt_rand_range(0, (zend_long)size - 1);
	} while (1);

	return 0;
}

static zend_always_inline bool php_parallel_events_poll_begin_link(php_parallel_events_t       *events,
                                                                   php_parallel_events_state_t *state,
                                                                   zend_string *name, zend_object *object)
{
	php_parallel_channel_t *channel = php_parallel_channel_fetch(object);

	php_parallel_link_lock(channel->link);

	if (php_parallel_link_closed(channel->link)) {
		state->closed = 1;
	} else {
		if (php_parallel_events_input_exists(&events->input, name)) {
			state->writable = php_parallel_link_writable(channel->link);
		} else {
			state->readable = php_parallel_link_readable(channel->link);
		}
	}

	if (state->readable || state->writable || state->closed) {
		state->type = PHP_PARALLEL_EVENTS_LINK;
		state->name = name;
		state->object = object;

		return 1;
	}

	php_parallel_link_unlock(channel->link);
	return 0;
}

static zend_always_inline bool php_parallel_events_poll_begin_future(php_parallel_events_t       *events,
                                                                     php_parallel_events_state_t *state,
                                                                     zend_string *name, zend_object *object)
{
	php_parallel_future_t *future = php_parallel_future_fetch(object);

	php_parallel_future_lock(future);

	state->readable = php_parallel_future_readable(future);

	if (state->readable) {
		state->type = PHP_PARALLEL_EVENTS_FUTURE;
		state->name = name;
		state->object = object;

		return 1;
	}

	php_parallel_future_unlock(future);
	return 0;
}

static zend_always_inline bool php_parallel_events_poll_begin(php_parallel_events_t       *events,
                                                              php_parallel_events_state_t *state)
{
	zend_string *name;
	zend_object *object;

	if (!php_parallel_events_poll_random(events, &name, &object)) {
		return 0;
	}

	memset(state, 0, sizeof(php_parallel_events_state_t));

	if (instanceof_function(object->ce, php_parallel_channel_ce)) {
		return php_parallel_events_poll_begin_link(events, state, name, object);
	} else {
		return php_parallel_events_poll_begin_future(events, state, name, object);
	}

	return 0;
}

static zend_always_inline bool php_parallel_events_poll_link(php_parallel_events_t       *events,
                                                             php_parallel_events_state_t *state, zval *retval)
{
	php_parallel_channel_t *channel = php_parallel_channel_fetch(state->object);
	zval                   *input;

	if (state->closed) {
		php_parallel_events_event_construct(events, PHP_PARALLEL_EVENTS_EVENT_CLOSE, state->name, state->object, NULL,
		                                    retval);

		return 1;
	} else {
		if ((input = php_parallel_events_input_find(&events->input, state->name))) {

			if (state->writable) {

				if (php_parallel_link_send(channel->link, input)) {

					php_parallel_events_event_construct(events, PHP_PARALLEL_EVENTS_EVENT_WRITE, state->name,
					                                    state->object, NULL, retval);

					return 1;
				}
			}
		} else {
			if (state->readable) {
				zval read;

				if (php_parallel_link_recv(channel->link, &read)) {

					php_parallel_events_event_construct(events, PHP_PARALLEL_EVENTS_EVENT_READ, state->name,
					                                    state->object, &read, retval);

					return 1;
				}
			}
		}
	}

	return 0;
}

static zend_always_inline bool php_parallel_events_poll_future(php_parallel_events_t       *events,
                                                               php_parallel_events_state_t *state, zval *retval)
{
	if (state->readable) {
		zval                             read;
		php_parallel_events_event_type_t type = PHP_PARALLEL_EVENTS_EVENT_READ;
		php_parallel_future_t           *future = php_parallel_future_fetch(state->object);

		ZVAL_NULL(&read);

		if (php_parallel_monitor_check(future->monitor, PHP_PARALLEL_KILLED)) {
			type = PHP_PARALLEL_EVENTS_EVENT_KILL;
		} else if (php_parallel_monitor_check(future->monitor, PHP_PARALLEL_CANCELLED)) {
			type = PHP_PARALLEL_EVENTS_EVENT_CANCEL;
		} else {
			if (php_parallel_monitor_check(future->monitor, PHP_PARALLEL_ERROR)) {
				type = PHP_PARALLEL_EVENTS_EVENT_ERROR;
			}

			php_parallel_future_value(future, &read);
		}

		php_parallel_events_event_construct(events, type, state->name, state->object, &read, retval);

		return 1;
	}

	return 0;
}

void php_parallel_events_poll(php_parallel_events_t *events, zval *retval)
{
	php_parallel_events_poll_t *poll;

	if (!(poll = php_parallel_events_poll_init(events))) {
		ZVAL_NULL(retval);
		return;
	}

	do {
		if (!php_parallel_events_poll_begin(events, &poll->state)) {
			if (!events->blocking) {
				php_parallel_events_poll_free(poll);
				return;
			}

			if (poll->block.fci.size) {
				zend_call_function(&poll->block.fci, &poll->block.fcc);

				if (zend_is_true(&poll->block.ival)) {
					zval_ptr_dtor(&poll->block.ival);
					php_parallel_events_poll_free(poll);
					return;
				}

				zval_ptr_dtor(&poll->block.ival);
			} else {
				if ((poll->try++ % 10) == 0) {
					usleep(1);
				}
			}

			if (php_parallel_events_poll_timeout(poll, events)) {
				php_parallel_events_poll_free(poll);
				return;
			}

			continue;
		}

		if (poll->state.type == PHP_PARALLEL_EVENTS_LINK) {
			if (php_parallel_events_poll_link(events, &poll->state, retval)) {
				break;
			}
		} else {
			if (php_parallel_events_poll_future(events, &poll->state, retval)) {
				break;
			}
		}

		/* Unlock but don't free - we're continuing the loop */
		php_parallel_events_poll_unlock(poll);
	} while (1);

	php_parallel_events_poll_end(poll);
}
#endif
