/* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only */
/* Copyright (c) 2020-2023 Brett Sheffield <bacs@librecast.net> */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "job.h"
#include "log.h"

#define aload(x) __atomic_load_n((x), __ATOMIC_ACQUIRE)
#define astor(x, y) __atomic_store_n((x),(y), __ATOMIC_RELEASE)

job_t *job_shift(job_queue_t *q)
{
	job_t *job;
	if (sem_wait(&q->lock) == -1) return NULL;
	if ((job = aload(&q->next))) {
		job_t *next = aload(&job->next);
		astor(&q->next, next);
		if (!next) astor(&q->last, NULL);
	}
	sem_post(&q->lock);
	return job;
}

static job_t *job_shiftlock(job_queue_t *q, int(*lockf)(sem_t *sem))
{
	job_t *job = NULL;
	if (!lockf(&q->jobs)) {
		job = job_shift(q);
	}
	return job;
}

job_t *job_trywait(job_queue_t *q)
{
	return job_shiftlock(q, &sem_trywait);
}

job_t *job_wait(job_queue_t *q)
{
	return job_shiftlock(q, &sem_wait);
}

job_t *job_new(void *(*f)(void *), void *arg, size_t len, void (*callback)(void *), int flags)
{
	job_t *job = calloc(1, sizeof(job_t));
	if (!job) return NULL;
	job->f = f;
	if ((flags & JOB_COPY) == JOB_COPY) {
		job->arg = malloc(len);
		if (!job->arg) goto exit_0;
		memcpy(job->arg, arg, len);
	}
	else {
		job->arg = arg;
	}
	job->len = len;
	job->flags = flags;
	job->callback = callback;
	return job;
exit_0:
	free(job);
	return NULL;
}

job_t *job_push(job_queue_t *q, job_t *job)
{
	if (!job || sem_wait(&q->lock) == -1) return NULL;
	if (!q->next) q->next = job;
	if (q->last) q->last->next = job;
	q->last = job;
	sem_post(&q->jobs);
	sem_post(&q->lock);
	return job;
}

job_t *job_push_new(job_queue_t *q, void *(*f)(void *), void *arg, size_t len, void (*callback)(void *), int flags)
{
	job_t *job = job_new(f, arg, len, callback, flags);
	if (!job) return NULL;
	return job_push(q, job);
}

static void job_free(job_t *job)
{
	if (job->flags & JOB_FREE) free(job->arg);
	if (job->flags & JOB_RET) free(job->ret);
	free(job);
}

static void *job_seek(void *arg)
{
	job_thread_t *jt = (job_thread_t *)arg;
	job_t *job;
	while((job = job_wait(jt->q))) {
		pthread_cleanup_push((void (*)(void *))job_free, job);
		jt->q->cur = job;
		if (job->f) job->ret = job->f(job->arg);
		if (job->callback) job->callback(job->arg);
		pthread_cleanup_pop(1); /* job_free(job) */
	}
	/* never reached */
	return jt;
}

void job_queue_destroy(job_queue_t *q)
{
	job_t *job;
	while ((job = job_shift(q))) {
		job_free(job);
	}
	for (size_t z = 0; z < q->nthreads; z++) {
		pthread_cancel(q->thread[z].thread);
	}
	for (size_t z = 0; z < q->nthreads; z++) {
		pthread_join(q->thread[z].thread, NULL);
	}
	sem_destroy(&q->lock);
	sem_destroy(&q->jobs);
	sem_destroy(&q->done);
	free(q->thread);
	free(q);
}

#define qthread &q->thread[q->nthreads]
job_queue_t *job_queue_create(size_t nthreads)
{
	job_queue_t *q = calloc(1, sizeof (job_queue_t));
	if (!q) return NULL;
	q->thread = calloc(nthreads, sizeof (job_thread_t));
	if (!q->thread) goto err_free_q;
	if (sem_init(&q->done, 0, 0)) goto err_free_qthread;
	if (sem_init(&q->jobs, 0, 0)) goto err_sem_destroy_done;
	if (sem_init(&q->lock, 0, 1)) goto err_sem_destroy_jobs;
	while (q->nthreads < nthreads) {
		q->thread[q->nthreads].id = q->nthreads;
		q->thread[q->nthreads].q = q;
		if (pthread_create(qthread.thread, NULL, &job_seek, qthread)) {
			goto err_job_queue_destroy;
		}
		q->nthreads++;
	}
	return q;
err_job_queue_destroy:
	job_queue_destroy(q);
	return NULL;
err_sem_destroy_jobs:
	sem_destroy(&q->jobs);
err_sem_destroy_done:
	sem_destroy(&q->done);
err_free_qthread:
	free(q->thread);
err_free_q:
	free(q);
	return NULL;
}
