/*
 * Copyright (c) 2023, Meta
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <zephyr/logging/log.h>
#include <zephyr/spinlock.h>

#define TRACK_ALLOC (IS_ENABLED(CONFIG_POSIX_ENV_LOG_LEVEL_DBG) || IS_ENABLED(CONFIG_ZTEST))

LOG_MODULE_REGISTER(posix_env, CONFIG_POSIX_ENV_LOG_LEVEL);

static struct k_spinlock environ_lock;
static size_t allocated;
char **environ;

#ifdef CONFIG_ZTEST
size_t posix_env_get_allocated_space(void)
{
	return allocated;
}
#endif

static size_t environ_size(void)
{
	size_t ret;

	if (environ == NULL) {
		return 0;
	}

	for (ret = 0; environ[ret] != NULL; ++ret) {
	}

	return ret;
}

static int findenv(const char *name, size_t namelen)
{
	const char *env;

	if (name == NULL || namelen == 0 || strchr(name, '=') != NULL) {
		/* Note: '=' is not a valid name character */
		return -EINVAL;
	}

	if (environ == NULL) {
		return -ENOENT;
	}

	for (char **envp = &environ[0]; *envp != NULL; ++envp) {
		env = *envp;
		if (strncmp(env, name, namelen) == 0 && env[namelen] == '=') {
			return envp - environ;
		}
	}

	return -ENOENT;
}

char *getenv(const char *name)
{
	int ret;
	size_t nsize;
	char *val = NULL;

	nsize = (name == NULL) ? 0 : strlen(name);
	K_SPINLOCK(&environ_lock)
	{
		ret = findenv(name, nsize);
		if (ret < 0) {
			K_SPINLOCK_BREAK;
		}

		val = environ[ret] + nsize + 1;
	}

	return val;
}

int getenv_r(const char *name, char *buf, size_t len)
{
	int ret = 0;
	size_t vsize;
	size_t nsize;
	char *val = NULL;

	nsize = (name == NULL) ? 0 : strlen(name);
	K_SPINLOCK(&environ_lock)
	{
		ret = findenv(name, nsize);
		if (ret < 0) {
			LOG_DBG("No entry for name '%s'", name);
			K_SPINLOCK_BREAK;
		}

		val = environ[ret] + nsize + 1;
		vsize = strlen(val) + 1;
		if (vsize > len) {
			ret = -ERANGE;
			K_SPINLOCK_BREAK;
		}
		strncpy(buf, val, vsize);
		LOG_DBG("Found entry %s", environ[ret]);
	}

	if (ret < 0) {
		errno = -ret;
		ret = -1;
	}

	return ret;
}

int setenv(const char *name, const char *val, int overwrite)
{
	int ret = 0;
	char *env;
	char **envp;
	size_t esize;
	const size_t vsize = (val == NULL) ? 0 : strlen(val);
	const size_t nsize = (name == NULL) ? 0 : strlen(name);
	/* total size of name + '=' + val + '\0' */
	const size_t tsize = nsize + 1 /* '=' */ + vsize + 1 /* '\0' */;

	if (name == NULL || val == NULL) {
		LOG_DBG("Invalid name '%s' or value '%s'", name, val);
		errno = EINVAL;
		return -1;
	}

	K_SPINLOCK(&environ_lock)
	{
		ret = findenv(name, nsize);
		if (ret == -EINVAL) {
			LOG_DBG("Invalid name '%s'", name);
			K_SPINLOCK_BREAK;
		}
		if (ret >= 0) {
			/* name was found in environ */
			esize = strlen(environ[ret]) + 1;
			if (overwrite == 0) {
				LOG_DBG("Found entry %s", environ[ret]);
				ret = 0;
				K_SPINLOCK_BREAK;
			}
		} else {
			/* name was not found in environ -> add new entry */
			esize = environ_size();
			envp = realloc(environ, sizeof(char **) *
							(esize + 1 /* new entry */ + 1 /* NULL */));
			if (envp == NULL) {
				ret = -ENOMEM;
				K_SPINLOCK_BREAK;
			}

			if (TRACK_ALLOC) {
				allocated += sizeof(char **) * (esize + 2);
				LOG_DBG("realloc %zu bytes (allocated: %zu)",
					sizeof(char **) * (esize + 2), allocated);
			}

			environ = envp;
			ret = esize;
			environ[ret] = NULL;
			environ[ret + 1] = NULL;
			esize = 0;
		}

		if (esize < tsize) {
			/* need to malloc or realloc space for new environ entry */
			env = realloc(environ[ret], tsize);
			if (env == NULL) {
				ret = -ENOMEM;
				K_SPINLOCK_BREAK;
			}
			if (TRACK_ALLOC) {
				allocated += tsize - esize;
				LOG_DBG("realloc %zu bytes (allocated: %zu)", tsize - esize,
					allocated);
			}
			environ[ret] = env;
		}

		strncpy(environ[ret], name, nsize);
		environ[ret][nsize] = '=';
		strncpy(environ[ret] + nsize + 1, val, vsize + 1);
		LOG_DBG("Added entry %s", environ[ret]);

		ret = 0;
	}

	if (ret < 0) {
		errno = -ret;
		ret = -1;
	}

	return ret;
}

int unsetenv(const char *name)
{
	int ret = 0;
	char **envp;
	size_t esize;
	size_t nsize;

	nsize = (name == NULL) ? 0 : strlen(name);
	K_SPINLOCK(&environ_lock)
	{
		ret = findenv(name, nsize);
		if (ret < 0) {
			ret = (ret == -EINVAL) ? -EINVAL : 0;
			K_SPINLOCK_BREAK;
		}

		esize = environ_size();
		if (TRACK_ALLOC) {
			allocated -= strlen(environ[ret]) + 1;
			LOG_DBG("free %zu bytes (allocated: %zu)", strlen(environ[ret]) + 1,
				allocated);
		}
		free(environ[ret]);

		/* shuffle remaining environment variable pointers forward */
		for (; ret < esize; ++ret) {
			environ[ret] = environ[ret + 1];
		}
		/* environ must be terminated with a NULL pointer */
		environ[ret] = NULL;

		/* reduce environ size and update allocation */
		--esize;
		if (esize == 0) {
			free(environ);
			environ = NULL;
		} else {
			envp = realloc(environ, (esize + 1 /* NULL */) * sizeof(char **));
			if (envp != NULL) {
				environ = envp;
			}
		}
		__ASSERT_NO_MSG((esize >= 1 && environ != NULL) || environ == NULL);

		if (TRACK_ALLOC) {
			/* recycle nsize here */
			nsize = ((esize == 0) ? 2 : 1) * sizeof(char **);
			allocated -= nsize;
			LOG_DBG("free %zu bytes (allocated: %zu)", nsize, allocated);
		}

		ret = 0;
	}

	if (ret < 0) {
		errno = -ret;
		ret = -1;
	}

	return ret;
}
