// SPDX-License-Identifier: LGPL-2.1-or-later
/*
 * Copyright (C) 2019-2021 Paul Cercueil <paul@crapouillou.net>
 */

#include "debug.h"
#include "interpreter.h"
#include "lightrec-private.h"
#include "memmanager.h"
#include "slist.h"

#include <errno.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdlib.h>
#include <pthread.h>

struct block_rec {
	struct block *block;
	struct slist_elm slist;
};

struct recompiler {
	struct lightrec_state *state;
	pthread_t thd;
	pthread_cond_t cond;
	pthread_mutex_t mutex;
	bool stop;
	struct block *current_block;
	struct slist_elm slist;
};

static void lightrec_compile_list(struct recompiler *rec)
{
	struct block_rec *block_rec;
	struct slist_elm *next;
	struct block *block;
	int ret;

	while (!!(next = slist_first(&rec->slist))) {
		block_rec = container_of(next, struct block_rec, slist);
		block = block_rec->block;
		rec->current_block = block;

		pthread_mutex_unlock(&rec->mutex);

		ret = lightrec_compile_block(rec->state, block);
		if (ret) {
			pr_err("Unable to compile block at PC 0x%x: %d\n",
			       block->pc, ret);
		}

		pthread_mutex_lock(&rec->mutex);

		slist_remove(&rec->slist, next);
		lightrec_free(rec->state, MEM_FOR_LIGHTREC,
			      sizeof(*block_rec), block_rec);
		pthread_cond_signal(&rec->cond);
	}

	rec->current_block = NULL;
}

static void * lightrec_recompiler_thd(void *d)
{
	struct recompiler *rec = d;

	pthread_mutex_lock(&rec->mutex);

	while (!rec->stop) {
		do {
			pthread_cond_wait(&rec->cond, &rec->mutex);

			if (rec->stop)
				goto out_unlock;

		} while (slist_empty(&rec->slist));

		lightrec_compile_list(rec);
	}

out_unlock:
	pthread_mutex_unlock(&rec->mutex);
	return NULL;
}

struct recompiler *lightrec_recompiler_init(struct lightrec_state *state)
{
	struct recompiler *rec;
	int ret;

	rec = lightrec_malloc(state, MEM_FOR_LIGHTREC, sizeof(*rec));
	if (!rec) {
		pr_err("Cannot create recompiler: Out of memory\n");
		return NULL;
	}

	rec->state = state;
	rec->stop = false;
	rec->current_block = NULL;
	slist_init(&rec->slist);

	ret = pthread_cond_init(&rec->cond, NULL);
	if (ret) {
		pr_err("Cannot init cond variable: %d\n", ret);
		goto err_free_rec;
	}

	ret = pthread_mutex_init(&rec->mutex, NULL);
	if (ret) {
		pr_err("Cannot init mutex variable: %d\n", ret);
		goto err_cnd_destroy;
	}

	ret = pthread_create(&rec->thd, NULL, lightrec_recompiler_thd, rec);
	if (ret) {
		pr_err("Cannot create recompiler thread: %d\n", ret);
		goto err_mtx_destroy;
	}

	pr_info("Threaded recompiler started\n");

	return rec;

err_mtx_destroy:
	pthread_mutex_destroy(&rec->mutex);
err_cnd_destroy:
	pthread_cond_destroy(&rec->cond);
err_free_rec:
	lightrec_free(state, MEM_FOR_LIGHTREC, sizeof(*rec), rec);
	return NULL;
}

void lightrec_free_recompiler(struct recompiler *rec)
{
	rec->stop = true;

	/* Stop the thread */
	pthread_mutex_lock(&rec->mutex);
	pthread_cond_signal(&rec->cond);
	pthread_mutex_unlock(&rec->mutex);
	pthread_join(rec->thd, NULL);

	pthread_mutex_destroy(&rec->mutex);
	pthread_cond_destroy(&rec->cond);
	lightrec_free(rec->state, MEM_FOR_LIGHTREC, sizeof(*rec), rec);
}

int lightrec_recompiler_add(struct recompiler *rec, struct block *block)
{
	struct slist_elm *elm, *prev;
	struct block_rec *block_rec;
	int ret = 0;

	pthread_mutex_lock(&rec->mutex);

	/* If the block is marked as dead, don't compile it, it will be removed
	 * as soon as it's safe. */
	if (block->flags & BLOCK_IS_DEAD)
		goto out_unlock;

	for (elm = slist_first(&rec->slist), prev = NULL; elm;
	     prev = elm, elm = elm->next) {
		block_rec = container_of(elm, struct block_rec, slist);

		if (block_rec->block == block) {
			/* The block to compile is already in the queue - bump
			 * it to the top of the list, unless the block is being
			 * recompiled. */
			if (prev && !(block->flags & BLOCK_SHOULD_RECOMPILE)) {
				slist_remove_next(prev);
				slist_append(&rec->slist, elm);
			}

			goto out_unlock;
		}
	}

	/* By the time this function was called, the block has been recompiled
	 * and ins't in the wait list anymore. Just return here. */
	if (block->function && !(block->flags & BLOCK_SHOULD_RECOMPILE))
		goto out_unlock;

	block_rec = lightrec_malloc(rec->state, MEM_FOR_LIGHTREC,
				    sizeof(*block_rec));
	if (!block_rec) {
		ret = -ENOMEM;
		goto out_unlock;
	}

	pr_debug("Adding block PC 0x%x to recompiler\n", block->pc);

	block_rec->block = block;

	elm = &rec->slist;

	/* If the block is being recompiled, push it to the end of the queue;
	 * otherwise push it to the front of the queue. */
	if (block->flags & BLOCK_SHOULD_RECOMPILE)
		for (; elm->next; elm = elm->next);

	slist_append(elm, &block_rec->slist);

	/* Signal the thread */
	pthread_cond_signal(&rec->cond);

out_unlock:
	pthread_mutex_unlock(&rec->mutex);
	return ret;
}

void lightrec_recompiler_remove(struct recompiler *rec, struct block *block)
{
	struct block_rec *block_rec;
	struct slist_elm *elm;

	pthread_mutex_lock(&rec->mutex);

	for (elm = slist_first(&rec->slist); elm; elm = elm->next) {
		block_rec = container_of(elm, struct block_rec, slist);

		if (block_rec->block == block) {
			if (block == rec->current_block) {
				/* Block is being recompiled - wait for
				 * completion */
				do {
					pthread_cond_wait(&rec->cond,
							  &rec->mutex);
				} while (block == rec->current_block);
			} else {
				/* Block is not yet being processed - remove it
				 * from the list */
				slist_remove(&rec->slist, elm);
				lightrec_free(rec->state, MEM_FOR_LIGHTREC,
					      sizeof(*block_rec), block_rec);
			}

			break;
		}
	}

	pthread_mutex_unlock(&rec->mutex);
}

void * lightrec_recompiler_run_first_pass(struct lightrec_state *state,
					  struct block *block, u32 *pc)
{
	bool freed;

	if (likely(block->function)) {
		if (block->flags & BLOCK_FULLY_TAGGED) {
			freed = atomic_flag_test_and_set(&block->op_list_freed);

			if (!freed) {
				pr_debug("Block PC 0x%08x is fully tagged"
					 " - free opcode list\n", block->pc);

				/* The block was already compiled but the opcode list
				 * didn't get freed yet - do it now */
				lightrec_free_opcode_list(state, block);
				block->opcode_list = NULL;
			}
		}

		return block->function;
	}

	/* Mark the opcode list as freed, so that the threaded compiler won't
	 * free it while we're using it in the interpreter. */
	freed = atomic_flag_test_and_set(&block->op_list_freed);

	/* Block wasn't compiled yet - run the interpreter */
	*pc = lightrec_emulate_block(state, block, *pc);

	if (!freed)
		atomic_flag_clear(&block->op_list_freed);

	/* The block got compiled while the interpreter was running.
	 * We can free the opcode list now. */
	if (block->function && (block->flags & BLOCK_FULLY_TAGGED) &&
	    !atomic_flag_test_and_set(&block->op_list_freed)) {
		pr_debug("Block PC 0x%08x is fully tagged"
			 " - free opcode list\n", block->pc);

		lightrec_free_opcode_list(state, block);
		block->opcode_list = NULL;
	}

	return NULL;
}
