/*
  +----------------------------------------------------------------------+
  | 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_LINK
#define HAVE_PARALLEL_LINK

#include "link.h"
#include "parallel.h"

#define PHP_PARALLEL_LINK_CLOSURE_BUFFER GC_IMMUTABLE

typedef enum { PHP_PARALLEL_LINK_UNBUFFERED, PHP_PARALLEL_LINK_BUFFERED } php_parallel_link_type_t;

typedef struct {
	pthread_mutex_t m;
	pthread_mutex_t w;
	pthread_mutex_t r;
} php_parallel_link_mutex_t;

typedef struct {
	pthread_cond_t r;
	pthread_cond_t w;
} php_parallel_link_cond_t;

typedef struct {
	uint32_t c;
	uint32_t r;
	uint32_t w;
} php_parallel_link_state_t;

typedef struct _php_parallel_link_queue_t {
	zend_llist l;
	zend_long  c;
} php_parallel_link_queue_t;

struct _php_parallel_link_t {
	php_parallel_link_type_t  type;
	zend_string              *name;

	php_parallel_link_mutex_t m;
	php_parallel_link_cond_t  c;
	php_parallel_link_state_t s;

	union {
		php_parallel_link_queue_t q;
		zval                      z;
	} port;

	uint32_t refcount;
};

static zend_string *php_parallel_link_string_name, *php_parallel_link_string_type, *php_parallel_link_string_unbuffered,
    *php_parallel_link_string_buffered, *php_parallel_link_string_capacity, *php_parallel_link_string_size,
    *php_parallel_link_string_infinite;

static zend_always_inline int32_t php_parallel_link_mutex_init(php_parallel_link_mutex_t *mutex)
{
	if (!php_parallel_mutex_init(&mutex->m, 1)) {
		return FAILURE;
	}

	if (!php_parallel_mutex_init(&mutex->r, 1)) {
		php_parallel_mutex_destroy(&mutex->m);

		return FAILURE;
	}

	if (!php_parallel_mutex_init(&mutex->w, 1)) {
		php_parallel_mutex_destroy(&mutex->m);
		php_parallel_mutex_destroy(&mutex->r);

		return FAILURE;
	}

	return SUCCESS;
}

static zend_always_inline void php_parallel_link_mutex_destroy(php_parallel_link_mutex_t *mutex)
{
	php_parallel_mutex_destroy(&mutex->m);
	php_parallel_mutex_destroy(&mutex->r);
	php_parallel_mutex_destroy(&mutex->w);
}

static zend_always_inline int32_t php_parallel_link_cond_init(php_parallel_link_cond_t *condition)
{
	if (!php_parallel_cond_init(&condition->r)) {
		return FAILURE;
	}

	if (!php_parallel_cond_init(&condition->w)) {
		php_parallel_cond_destroy(&condition->r);

		return FAILURE;
	}

	return SUCCESS;
}

static zend_always_inline void php_parallel_link_cond_destroy(php_parallel_link_cond_t *condition)
{
	pthread_cond_destroy(&condition->r);
	pthread_cond_destroy(&condition->w);
}

php_parallel_link_t *php_parallel_link_init(zend_string *name, bool buffered, zend_long capacity)
{
	php_parallel_link_t *link = pecalloc(1, sizeof(php_parallel_link_t), 1);

	if (php_parallel_link_mutex_init(&link->m) != SUCCESS) {
		pefree(link, 1);
		return NULL;
	}

	if (php_parallel_link_cond_init(&link->c) != SUCCESS) {
		php_parallel_link_mutex_destroy(&link->m);
		pefree(link, 1);
		return NULL;
	}

	if (buffered) {
		link->type = PHP_PARALLEL_LINK_BUFFERED;
		zend_llist_init(&link->port.q.l, sizeof(zval), (llist_dtor_func_t)PARALLEL_ZVAL_DTOR, 1);
		link->port.q.c = capacity;
	} else {
		link->type = PHP_PARALLEL_LINK_UNBUFFERED;
	}
	link->name = php_parallel_copy_string_interned(name);
	link->refcount = 1;

	return link;
}

void php_parallel_link_destroy(php_parallel_link_t *link)
{
	if (php_parallel_atomic_delref(&link->refcount) == 0) {
		php_parallel_link_mutex_destroy(&link->m);
		php_parallel_link_cond_destroy(&link->c);

		if (link->type == PHP_PARALLEL_LINK_BUFFERED) {
			zend_llist_destroy(&link->port.q.l);
		} else {
			if (Z_TYPE_FLAGS(link->port.z) == PHP_PARALLEL_LINK_CLOSURE_BUFFER) {
				PARALLEL_ZVAL_DTOR(&link->port.z);
			}
		}
		pefree(link, 1);
	}
}

php_parallel_link_t *php_parallel_link_copy(php_parallel_link_t *link)
{
	php_parallel_atomic_addref(&link->refcount);

	return link;
}

static zend_always_inline bool php_parallel_link_send_unbuffered(php_parallel_link_t *link, zval *value)
{
	pthread_mutex_lock(&link->m.w);
	pthread_mutex_lock(&link->m.m);

	if (link->s.c) {
		pthread_mutex_unlock(&link->m.m);
		pthread_mutex_unlock(&link->m.w);
		return 0;
	}

	if (PARALLEL_ZVAL_CHECK_CLOSURES(value)) {
		PARALLEL_ZVAL_COPY(&link->port.z, value, 1);
		Z_TYPE_FLAGS(link->port.z) = PHP_PARALLEL_LINK_CLOSURE_BUFFER;
	} else {
		ZVAL_COPY_VALUE(&link->port.z, value);

		ZEND_ASSERT(Z_TYPE_FLAGS(link->port.z) != PHP_PARALLEL_LINK_CLOSURE_BUFFER);
	}
	link->s.w++;

	if (link->s.r) {
		pthread_cond_signal(&link->c.r);
	}

	pthread_cond_wait(&link->c.w, &link->m.m);

	pthread_mutex_unlock(&link->m.m);
	pthread_mutex_unlock(&link->m.w);

	return 1;
}

static zend_always_inline bool php_parallel_link_send_buffered(php_parallel_link_t *link, zval *value)
{
	zval sent;

	pthread_mutex_lock(&link->m.m);

	while (link->port.q.c && zend_llist_count(&link->port.q.l) == link->port.q.c) {
		link->s.w++;
		pthread_cond_wait(&link->c.w, &link->m.m);
		link->s.w--;
	}

	if (link->s.c) {
		pthread_mutex_unlock(&link->m.m);
		return 0;
	}

	PARALLEL_ZVAL_COPY(&sent, value, 1);

	zend_llist_add_element(&link->port.q.l, &sent);

	if (link->s.r) {
		pthread_cond_signal(&link->c.r);
	}

	pthread_mutex_unlock(&link->m.m);
	return 1;
}

bool php_parallel_link_send(php_parallel_link_t *link, zval *value)
{
	if (link->type == PHP_PARALLEL_LINK_UNBUFFERED) {
		return php_parallel_link_send_unbuffered(link, value);
	} else {
		return php_parallel_link_send_buffered(link, value);
	}
}

static zend_always_inline bool php_parallel_link_recv_unbuffered(php_parallel_link_t *link, zval *value)
{
	pthread_mutex_lock(&link->m.r);
	pthread_mutex_lock(&link->m.m);

	while (!link->s.c && !link->s.w) {
		link->s.r++;
		pthread_cond_wait(&link->c.r, &link->m.m);
		link->s.r--;
	}

	if (link->s.c) {
		pthread_mutex_unlock(&link->m.m);
		pthread_mutex_unlock(&link->m.r);
		return 0;
	}

	PARALLEL_ZVAL_COPY(value, &link->port.z, 0);
	if (Z_TYPE_FLAGS(link->port.z) == PHP_PARALLEL_LINK_CLOSURE_BUFFER) {
		PARALLEL_ZVAL_DTOR(&link->port.z);
	}
	ZVAL_UNDEF(&link->port.z);
	link->s.w--;

	pthread_cond_signal(&link->c.w);
	pthread_mutex_unlock(&link->m.m);
	pthread_mutex_unlock(&link->m.r);
	return 1;
}

static zend_always_inline int  php_parallel_link_queue_delete(void *lhs, void *rhs) { return lhs == rhs; }

static zend_always_inline bool php_parallel_link_recv_buffered(php_parallel_link_t *link, zval *value)
{
	zval *head;

	pthread_mutex_lock(&link->m.m);

	while (zend_llist_count(&link->port.q.l) == 0) {
		if (link->s.c) {
			pthread_mutex_unlock(&link->m.m);
			return 0;
		}

		link->s.r++;
		pthread_cond_wait(&link->c.r, &link->m.m);
		link->s.r--;
	}

	head = zend_llist_get_first(&link->port.q.l);

	PARALLEL_ZVAL_COPY(value, head, 0);

	zend_llist_del_element(&link->port.q.l, head, php_parallel_link_queue_delete);

	if (link->s.w) {
		pthread_cond_signal(&link->c.w);
	}

	pthread_mutex_unlock(&link->m.m);

	return 1;
}

bool php_parallel_link_recv(php_parallel_link_t *link, zval *value)
{
	if (link->type == PHP_PARALLEL_LINK_UNBUFFERED) {
		return php_parallel_link_recv_unbuffered(link, value);
	} else {
		return php_parallel_link_recv_buffered(link, value);
	}
}

bool php_parallel_link_close(php_parallel_link_t *link)
{
	pthread_mutex_lock(&link->m.m);

	if (link->s.c) {
		pthread_mutex_unlock(&link->m.m);
		return 0;
	}

	link->s.c = 1;
	pthread_cond_broadcast(&link->c.r);
	pthread_cond_broadcast(&link->c.w);
	pthread_mutex_unlock(&link->m.m);

	return 1;
}

bool         php_parallel_link_closed(php_parallel_link_t *link) { return link->s.c; }

zend_string *php_parallel_link_name(php_parallel_link_t *link) { return link->name; }

bool         php_parallel_link_lock(php_parallel_link_t *link) { return pthread_mutex_lock(&link->m.m) == SUCCESS; }

bool         php_parallel_link_writable(php_parallel_link_t *link)
{
	switch (link->type) {
	case PHP_PARALLEL_LINK_UNBUFFERED:
		return link->s.r > 0;

	case PHP_PARALLEL_LINK_BUFFERED:
		return zend_llist_count(&link->port.q.l) < link->port.q.c;
	}

	return 0;
}

bool php_parallel_link_readable(php_parallel_link_t *link)
{
	switch (link->type) {
	case PHP_PARALLEL_LINK_UNBUFFERED:
		return link->s.w > 0;

	case PHP_PARALLEL_LINK_BUFFERED:
		return zend_llist_count(&link->port.q.l) > 0;
	}

	return 0;
}

void php_parallel_link_debug(php_parallel_link_t *link, HashTable *debug)
{
	zval zdbg;

	ZVAL_STR(&zdbg, link->name);

	zend_hash_add(debug, php_parallel_link_string_name, &zdbg);

	switch (link->type) {
	case PHP_PARALLEL_LINK_UNBUFFERED:
		ZVAL_STR_COPY(&zdbg, php_parallel_link_string_unbuffered);
		zend_hash_add(debug, php_parallel_link_string_type, &zdbg);
		break;

	case PHP_PARALLEL_LINK_BUFFERED:
		ZVAL_STR_COPY(&zdbg, php_parallel_link_string_buffered);
		zend_hash_add(debug, php_parallel_link_string_type, &zdbg);

		if (link->port.q.c == -1) {
			ZVAL_STR_COPY(&zdbg, php_parallel_link_string_infinite);
			zend_hash_add(debug, php_parallel_link_string_capacity, &zdbg);
		} else {
			ZVAL_LONG(&zdbg, link->port.q.c);
			zend_hash_add(debug, php_parallel_link_string_capacity, &zdbg);
			if (link->port.q.l.count) {
				ZVAL_LONG(&zdbg, link->port.q.l.count);
				zend_hash_add(debug, php_parallel_link_string_size, &zdbg);
			}
		}
		break;
	}
}

bool php_parallel_link_unlock(php_parallel_link_t *link) { return pthread_mutex_unlock(&link->m.m) == SUCCESS; }

PHP_MINIT_FUNCTION(PARALLEL_LINK)
{
	php_parallel_link_string_name = zend_string_init_interned(ZEND_STRL("name"), 1);
	php_parallel_link_string_type = zend_string_init_interned(ZEND_STRL("type"), 1);
	php_parallel_link_string_unbuffered = zend_string_init_interned(ZEND_STRL("unbuffered"), 1);
	php_parallel_link_string_buffered = zend_string_init_interned(ZEND_STRL("buffered"), 1);
	php_parallel_link_string_capacity = zend_string_init_interned(ZEND_STRL("capacity"), 1);
	php_parallel_link_string_size = zend_string_init_interned(ZEND_STRL("size"), 1);
	php_parallel_link_string_infinite = zend_string_init_interned(ZEND_STRL("infinite"), 1);

	return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(PARALLEL_LINK)
{
	zend_string_release(php_parallel_link_string_name);
	zend_string_release(php_parallel_link_string_type);
	zend_string_release(php_parallel_link_string_unbuffered);
	zend_string_release(php_parallel_link_string_buffered);
	zend_string_release(php_parallel_link_string_capacity);
	zend_string_release(php_parallel_link_string_size);
	zend_string_release(php_parallel_link_string_infinite);

	return SUCCESS;
}
#endif
