/******************************************************************************
    Copyright (C) 2023 by Lain Bailey <lain@obsproject.com>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/

#include <obs.h>
#include <util/dstr.h>
#include <util/platform.h>
#include <util/threading.h>
#include <util/deque.h>

#include "obs-scripting-internal.h"
#include "obs-scripting-callback.h"

static obs_module_t *obs_module_pointer;

void obs_scripting_set_module(obs_module_t *module)
{
	obs_module_pointer = module;
}

obs_module_t *obs_current_module(void)
{
	return obs_module_pointer;
}

#if defined(LUAJIT_FOUND)
extern obs_script_t *obs_lua_script_create(const char *path, obs_data_t *settings);
extern bool obs_lua_script_load(obs_script_t *s);
extern void obs_lua_script_unload(obs_script_t *s);
extern void obs_lua_script_destroy(obs_script_t *s);
extern void obs_lua_load(void);
extern void obs_lua_unload(void);

extern obs_properties_t *obs_lua_script_get_properties(obs_script_t *script);
extern void obs_lua_script_update(obs_script_t *script, obs_data_t *settings);
extern void obs_lua_script_save(obs_script_t *script);
#endif

#if defined(Python_FOUND)
extern obs_script_t *obs_python_script_create(const char *path, obs_data_t *settings);
extern bool obs_python_script_load(obs_script_t *s);
extern void obs_python_script_unload(obs_script_t *s);
extern void obs_python_script_destroy(obs_script_t *s);
extern void obs_python_load(void);
extern void obs_python_unload(void);

extern obs_properties_t *obs_python_script_get_properties(obs_script_t *script);
extern void obs_python_script_update(obs_script_t *script, obs_data_t *settings);
extern void obs_python_script_save(obs_script_t *script);
#endif

pthread_mutex_t detach_mutex;
struct script_callback *detached_callbacks;

static struct dstr file_filter = {0};
static bool scripting_loaded = false;

static const char *supported_formats[] = {
#if defined(LUAJIT_FOUND)
	"lua",
#endif
#if defined(Python_FOUND)
	"py",
#endif
	NULL};

/* -------------------------------------------- */

static pthread_mutex_t defer_call_mutex;
static struct deque defer_call_queue;
static bool defer_call_exit = false;
static os_sem_t *defer_call_semaphore;
static pthread_t defer_call_thread;

struct defer_call {
	defer_call_cb call;
	void *cb;
};

static void *defer_thread(void *unused)
{
	UNUSED_PARAMETER(unused);
	os_set_thread_name("scripting: defer");

	while (os_sem_wait(defer_call_semaphore) == 0) {
		struct defer_call info;

		pthread_mutex_lock(&defer_call_mutex);
		if (defer_call_exit) {
			pthread_mutex_unlock(&defer_call_mutex);
			return NULL;
		}

		deque_pop_front(&defer_call_queue, &info, sizeof(info));
		pthread_mutex_unlock(&defer_call_mutex);

		info.call(info.cb);
	}

	return NULL;
}

void defer_call_post(defer_call_cb call, void *cb)
{
	struct defer_call info;
	info.call = call;
	info.cb = cb;

	pthread_mutex_lock(&defer_call_mutex);
	if (!defer_call_exit)
		deque_push_back(&defer_call_queue, &info, sizeof(info));
	pthread_mutex_unlock(&defer_call_mutex);

	os_sem_post(defer_call_semaphore);
}

/* -------------------------------------------- */

bool obs_scripting_load(void)
{
	deque_init(&defer_call_queue);

	if (pthread_mutex_init(&detach_mutex, NULL) != 0) {
		return false;
	}
	if (pthread_mutex_init(&defer_call_mutex, NULL) != 0) {
		pthread_mutex_destroy(&detach_mutex);
		return false;
	}
	if (os_sem_init(&defer_call_semaphore, 0) != 0) {
		pthread_mutex_destroy(&defer_call_mutex);
		pthread_mutex_destroy(&detach_mutex);
		return false;
	}

	if (pthread_create(&defer_call_thread, NULL, defer_thread, NULL) != 0) {
		os_sem_destroy(defer_call_semaphore);
		pthread_mutex_destroy(&defer_call_mutex);
		pthread_mutex_destroy(&detach_mutex);
		return false;
	}

#if defined(LUAJIT_FOUND)
	obs_lua_load();
#endif

#if defined(Python_FOUND)
	obs_python_load();
#if !defined(_WIN32) && !defined(__APPLE__) /* Win32 and macOS need user-provided Python library paths */
	obs_scripting_load_python(NULL);
#endif
#endif

	scripting_loaded = true;
	return true;
}

void obs_scripting_unload(void)
{
	if (!scripting_loaded)
		return;

	/* ---------------------- */

#if defined(LUAJIT_FOUND)
	obs_lua_unload();
#endif

#if defined(Python_FOUND)
	obs_python_unload();
#endif

	dstr_free(&file_filter);

	/* ---------------------- */

	int total_detached = 0;

	pthread_mutex_lock(&detach_mutex);

	struct script_callback *cur = detached_callbacks;
	while (cur) {
		struct script_callback *next = cur->next;
		just_free_script_callback(cur);
		cur = next;

		++total_detached;
	}

	pthread_mutex_unlock(&detach_mutex);
	pthread_mutex_destroy(&detach_mutex);

	blog(LOG_INFO, "[Scripting] Total detached callbacks: %d", total_detached);

	/* ---------------------- */

	pthread_mutex_lock(&defer_call_mutex);

	/* TODO */

	defer_call_exit = true;
	deque_free(&defer_call_queue);

	pthread_mutex_unlock(&defer_call_mutex);

	os_sem_post(defer_call_semaphore);
	pthread_join(defer_call_thread, NULL);

	pthread_mutex_destroy(&defer_call_mutex);
	os_sem_destroy(defer_call_semaphore);

	scripting_loaded = false;
}

const char **obs_scripting_supported_formats(void)
{
	return supported_formats;
}

static inline bool pointer_valid(const void *x, const char *name, const char *func)
{
	if (!x) {
		blog(LOG_WARNING, "obs-scripting: [%s] %s is null", func, name);
		return false;
	}

	return true;
}

#define ptr_valid(x) pointer_valid(x, #x, __FUNCTION__)

obs_script_t *obs_script_create(const char *path, obs_data_t *settings)
{
	obs_script_t *script = NULL;
	const char *ext;

	if (!scripting_loaded)
		return NULL;
	if (!ptr_valid(path))
		return NULL;

	ext = strrchr(path, '.');
	if (!ext)
		return NULL;

#if defined(LUAJIT_FOUND)
	if (strcmp(ext, ".lua") == 0) {
		script = obs_lua_script_create(path, settings);
	} else
#endif
#if defined(Python_FOUND)
		if (strcmp(ext, ".py") == 0) {
		script = obs_python_script_create(path, settings);
	} else
#endif
	{
		blog(LOG_WARNING, "Unsupported/unknown script type: %s", path);
	}

	return script;
}

const char *obs_script_get_description(const obs_script_t *script)
{
	return ptr_valid(script) ? script->desc.array : NULL;
}

const char *obs_script_get_path(const obs_script_t *script)
{
	const char *path = ptr_valid(script) ? script->path.array : "";
	return path ? path : "";
}

const char *obs_script_get_file(const obs_script_t *script)
{
	const char *file = ptr_valid(script) ? script->file.array : "";
	return file ? file : "";
}

enum obs_script_lang obs_script_get_lang(const obs_script_t *script)
{
	return ptr_valid(script) ? script->type : OBS_SCRIPT_LANG_UNKNOWN;
}

obs_data_t *obs_script_get_settings(obs_script_t *script)
{
	obs_data_t *settings;

	if (!ptr_valid(script))
		return NULL;

	settings = script->settings;
	obs_data_addref(settings);
	return settings;
}

obs_properties_t *obs_script_get_properties(obs_script_t *script)
{
	obs_properties_t *props = NULL;

	if (!ptr_valid(script))
		return NULL;
#if defined(LUAJIT_FOUND)
	if (script->type == OBS_SCRIPT_LANG_LUA) {
		props = obs_lua_script_get_properties(script);
		goto out;
	}
#endif
#if defined(Python_FOUND)
	if (script->type == OBS_SCRIPT_LANG_PYTHON) {
		props = obs_python_script_get_properties(script);
		goto out;
	}
#endif

out:
	if (!props)
		props = obs_properties_create();
	return props;
}

obs_data_t *obs_script_save(obs_script_t *script)
{
	obs_data_t *settings;

	if (!ptr_valid(script))
		return NULL;

#if defined(LUAJIT_FOUND)
	if (script->type == OBS_SCRIPT_LANG_LUA) {
		obs_lua_script_save(script);
		goto out;
	}
#endif
#if defined(Python_FOUND)
	if (script->type == OBS_SCRIPT_LANG_PYTHON) {
		obs_python_script_save(script);
		goto out;
	}
#endif

out:
	settings = script->settings;
	obs_data_addref(settings);
	return settings;
}

static void clear_queue_signal(void *p_event)
{
	os_event_t *event = p_event;
	os_event_signal(event);
}

static void clear_call_queue(void)
{
	os_event_t *event;
	if (os_event_init(&event, OS_EVENT_TYPE_AUTO) != 0)
		return;

	defer_call_post(clear_queue_signal, event);

	os_event_wait(event);
	os_event_destroy(event);
}

void obs_script_update(obs_script_t *script, obs_data_t *settings)
{
	if (!ptr_valid(script))
		return;
#if defined(LUAJIT_FOUND)
	if (script->type == OBS_SCRIPT_LANG_LUA) {
		obs_lua_script_update(script, settings);
	}
#endif
#if defined(Python_FOUND)
	if (script->type == OBS_SCRIPT_LANG_PYTHON) {
		obs_python_script_update(script, settings);
	}
#endif
}

bool obs_script_reload(obs_script_t *script)
{
	if (!scripting_loaded)
		return false;
	if (!ptr_valid(script))
		return false;

#if defined(LUAJIT_FOUND)
	if (script->type == OBS_SCRIPT_LANG_LUA) {
		obs_lua_script_unload(script);
		clear_call_queue();
		obs_lua_script_load(script);
		goto out;
	}
#endif
#if defined(Python_FOUND)
	if (script->type == OBS_SCRIPT_LANG_PYTHON) {
		obs_python_script_unload(script);
		clear_call_queue();
		obs_python_script_load(script);
		goto out;
	}
#endif

out:
	return script->loaded;
}

bool obs_script_loaded(const obs_script_t *script)
{
	return ptr_valid(script) ? script->loaded : false;
}

void obs_script_destroy(obs_script_t *script)
{
	if (!script)
		return;

#if defined(LUAJIT_FOUND)
	if (script->type == OBS_SCRIPT_LANG_LUA) {
		obs_lua_script_unload(script);
		obs_lua_script_destroy(script);
		return;
	}
#endif
#if defined(Python_FOUND)
	if (script->type == OBS_SCRIPT_LANG_PYTHON) {
		obs_python_script_unload(script);
		obs_python_script_destroy(script);
		return;
	}
#endif
}

#if !defined(Python_FOUND)
bool obs_scripting_load_python(const char *python_path)
{
	UNUSED_PARAMETER(python_path);
	return false;
}

bool obs_scripting_python_loaded(void)
{
	return false;
}

bool obs_scripting_python_runtime_linked(void)
{
	return (bool)true;
}

void obs_scripting_python_version(char *version, size_t version_length)
{
	UNUSED_PARAMETER(version_length);
	version[0] = 0;
}
#endif
