/*
 * license.c
 * Evaluate SPDX license expressions
 *
 * SPDX-License-Identifier: pkgconf
 *
 * Copyright (c) 2026 pkgconf authors (see AUTHORS).
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * This software is provided 'as is' and without any warranty, express or
 * implied.  In no event shall the authors be liable for any damages arising
 * from the use of this software.
 */

#include <libpkgconf/stdinc.h>
#include <libpkgconf/libpkgconf.h>

static bool license_sanitize_string(const char *s, pkgconf_buffer_t *buf)
{
	unsigned int i = 0;
	/*
	 * Allowed chars are:
	 * - a-z
	 * - 0-9
	 * - chars '-', '+', '.', '(' and ')'
	 */
	for (i = 0; i < strlen(s); i ++)
	{
		if (isalnum((unsigned char) s[i]) || s[i] == '-' || s[i] == '+' || s[i] == '(' || s[i] == ')' || s[i] == '.' || s[i] == ':')
		{
			if (!pkgconf_buffer_push_byte(buf, s[i]))
				return false;
		}
	}

	return true;
}

/*
 * !doc
 *
 * .. c:function:: bool pkgconf_license_copy_list(const pkgconf_client_t *client, pkgconf_list_t *list, const pkgconf_list_t *base)
 *
 *    Copies a `license list` to another `license list`
 *
 *    :param pkgconf_client_t* client: The pkgconf client being accessed.
 *    :param pkgconf_list_t* list: The list the fragments are being added to.
 *    :param pkgconf_list_t* base: The list the fragments are being copied from.
 *    :return: true on success, false on allocation failure
 */
bool
pkgconf_license_copy_list(const pkgconf_client_t *client, pkgconf_list_t *list, const pkgconf_list_t *base)
{
	pkgconf_node_t *node;
	(void) client;

	PKGCONF_FOREACH_LIST_ENTRY(base->head, node)
	{
		pkgconf_license_t *license = node->data;
		pkgconf_license_t *cpy_license = calloc(1, sizeof(pkgconf_license_t));

		if (cpy_license == NULL)
			return false;

		cpy_license->type = license->type;

		if (license->data != NULL)
		{
			cpy_license->data = strdup(license->data);
			if (cpy_license->data == NULL)
			{
				free(cpy_license);
				return false;
			}
		}

		pkgconf_node_insert_tail(&cpy_license->iter, cpy_license, list);
	}

	return true;
}

/*
 * !doc
 *
 * .. c:function:: void pkgconf_license_free(pkgconf_list_t *list)
 *
 *    Delete an entire `license list`.
 *
 *    :param pkgconf_list_t* list: The `license list` to delete.
 *    :return: nothing
 */
void
pkgconf_license_free(pkgconf_list_t *list)
{
	if (!list)
		return;

	pkgconf_node_t *node, *next;

	PKGCONF_FOREACH_LIST_ENTRY_SAFE(list->head, next, node)
	{
		pkgconf_license_t *license = node->data;
		free(license->data);
		free(license);
	}

	pkgconf_list_zero(list);
}

/*
 * !doc
 *
 * .. c:function:: bool pkgconf_license_insert(pkgconf_client_t *client, pkgconf_list_t *list, unsigned char type, const char *data)
 *
 *    Adds a `license` of text to a `license list` directly without interpreting it.
 *
 *    :param pkgconf_client_t* client: The pkgconf client being accessed.
 *    :param pkgconf_list_t* list: The fragment list.
 *    :param char type: The type of the license.
 *    :param char* data: The data of the license
 *    :return: true on success, false on allocation failure
 */
bool
pkgconf_license_insert(pkgconf_client_t *client, pkgconf_list_t *list, unsigned char type, const char *data)
{
	pkgconf_license_t *license;

	if (data == NULL)
		return false;

	license = calloc(1, sizeof(pkgconf_license_t));
	if (!license)
	{
		pkgconf_error(client, "pkgconf_license_insert: out of memory");
		return false;
	}
	license->type = type;
	license->data = strdup(data);
	if (license->data == NULL)
	{
		pkgconf_error(client, "pkgconf_license_insert: out of memory");
		free(license);
		return false;
	}

	pkgconf_node_insert_tail(&license->iter, license, list);
	return true;
}

/*
 * !doc
 *
 * .. c:function:: bool pkgconf_license_evaluate_str(pkgconf_client_t *client, pkgconf_list_t *license_list, const char *expression, unsigned int flags)
 *
 * Evaluates SPDX expression strings like:
 * - BSD-3-Clause
 * - LGPL-2.1-only OR MIT
 * - LGPL-2.1-only OR MIT OR BSD-3-Clause
 * - ISC AND (BSD-3-Clause AND BSD-2-Clause)
 *
 * Function parses and sanitizes license strings. Also adding multiple
 * 'License:'-keys is supported like:
 * License: BSD-3-Clause
 * License: BSD-2-Clause
 *
 * Which will evaluate to: BSD-3-Clause AND BSD-2-Clause
 *
 * :param pkgconf_client_t* client: The client object that owns the package this dependency list belongs to.
 * :param pkgconf_list_t* license_list: The dependency list to populate with dependency nodes.
 * :param char* expression: The dependency data to parse.
 * :param uint flags: Any flags to attach to the dependency nodes.
 * :return: true on success, false on allocation failure
 */
bool
pkgconf_license_evaluate_str(pkgconf_client_t *client, pkgconf_list_t *license_list, const char *expression, unsigned int flags)
{
	pkgconf_buffer_t out_buffer = PKGCONF_BUFFER_INITIALIZER;
	size_t buf_size = 0;
	char *cur_word = NULL;
	int i, ret, argc;
	char **argv;
	size_t string_len = 0;

	(void)flags;

	if (expression == NULL)
		return true;

	buf_size = strlen(expression) + 1;
	ret = pkgconf_argv_split(expression, &argc, &argv);
	if (ret)
		return true;

	/* This is not the first License:
	 * so add AND
	 */
	if (license_list->head)
	{
		if (!pkgconf_license_insert(client, license_list, PKGCONF_LICENSE_AND, "AND"))
			goto fail;
	}

	i = 0;
	while (i < argc)
	{
		string_len = strnlen(argv[i], buf_size);
		cur_word = argv[i];
		if (string_len >= 1)
		{
			if (!license_sanitize_string(cur_word, &out_buffer))
				goto fail;

			if (!pkgconf_buffer_len(&out_buffer))
			{
				i ++;
				pkgconf_buffer_finalize(&out_buffer);
				continue;
			}

			cur_word = (char *)pkgconf_buffer_str(&out_buffer);
			size_t cur_len = strnlen(cur_word, buf_size);
			if (!cur_len)
			{
				i ++;
				pkgconf_buffer_finalize(&out_buffer);
				continue;
			}

			if (cur_word[0] == '(')
			{
				if (!pkgconf_license_insert(client, license_list, PKGCONF_LICENSE_BRACKET_OPEN, "("))
					goto fail;
				/* If there is more after '(' like '(BSD-2-Clause'
				 * Then append rest to fragments as license.
				 * This is expression like GPL-2.0-only OR (BSD-2-Clause AND ISC)
				 */
				if (cur_len >= 2)
				{
					cur_word ++;
					if (!pkgconf_license_insert(client, license_list, PKGCONF_LICENSE_EXPRESSION, cur_word))
						goto fail;
				}
			}
			else if (cur_word[cur_len - 1] == ')')
			{
				if (cur_len >= 2)
				{
					cur_word[cur_len - 1] = 0x00;
					if (!pkgconf_license_insert(client, license_list, PKGCONF_LICENSE_EXPRESSION, cur_word))
						goto fail;
				}
				if (!pkgconf_license_insert(client, license_list, PKGCONF_LICENSE_BRACKET_CLOSE, ")"))
					goto fail;
			}
			else if (!strcasecmp(cur_word, "and"))
			{
				if (!pkgconf_license_insert(client, license_list, PKGCONF_LICENSE_AND, "AND"))
					goto fail;
			}
			else if (!strcasecmp(cur_word, "or"))
			{
				if (!pkgconf_license_insert(client, license_list, PKGCONF_LICENSE_OR, "OR"))
					goto fail;
			}
			else if (!strcasecmp(cur_word, "with"))
			{
				if (!pkgconf_license_insert(client, license_list, PKGCONF_LICENSE_WITH, "WITH"))
					goto fail;
			}
			else
			{
				if (!pkgconf_license_insert(client, license_list, PKGCONF_LICENSE_EXPRESSION, cur_word))
					goto fail;
			}
			pkgconf_buffer_finalize(&out_buffer);
		}
		i++;
	}

	pkgconf_argv_free(argv);
	return true;

fail:
	pkgconf_buffer_finalize(&out_buffer);
	pkgconf_argv_free(argv);
	return false;
}

/*
 * !doc
 *
 * .. c:function:: bool pkgconf_license_evaluate(pkgconf_client_t *client, pkgconf_pkg_t *pkg, pkgconf_list_t *license_list, const char *license_str, unsigned int flags)
 *
 * Evaluates SPDX expression strings like:
 * - BSD-3-Clause
 * - LGPL-2.1-only OR MIT
 * - LGPL-2.1-only OR MIT OR BSD-3-Clause
 * - ISC AND (BSD-3-Clause AND BSD-2-Clause)
 *
 * Function parses and sanitizes license strings. Also adding multiple
 * 'License:'-keys is supported like:
 * License: BSD-3-Clause
 * License: BSD-2-Clause
 *
 * Which will evaluate to: BSD-3-Clause AND BSD-2-Clause
 *
 * :param pkgconf_client_t* client: The client object that owns the package this dependency list belongs to.
 * :param pkgconf_list_t* license_list: The dependency list to populate with dependency nodes.
 * :param char* expression: The dependency data to parse.
 * :param uint flags: Any flags to attach to the dependency nodes.
 * :return: true on success, false on allocation failure
 */
bool
pkgconf_license_evaluate(pkgconf_client_t *client, pkgconf_pkg_t *pkg, pkgconf_list_t *license_list, const char *license_str, unsigned int flags)
{
	char *license_expression = pkgconf_bytecode_eval_str(client, &pkg->vars, license_str, NULL);
	if (license_expression == NULL)
		return false;

	bool ret = pkgconf_license_evaluate_str(client, license_list, license_expression, flags);
	free(license_expression);
	return ret;
}

/*
 * !doc
 *
 * .. c:function:: void pkgconf_dependency_parse_str(pkgconf_list_t *deplist_head, const char *depends)
 *
 * Renders license fragments back to SPDX expression string. Tries
 * to keep output as close as possible to original input
 *
 * :param pkgconf_client_t* client: The client object that owns the package this dependency list belongs to.
 * :param pkgconf_list_t* deplist_head: The dependency list to populate with dependency nodes.
 * :param char* depends: The dependency data to parse.
 * :param uint flags: Any flags to attach to the dependency nodes.
 * :return: nothing
 */
bool
pkgconf_license_render(pkgconf_client_t *client, const pkgconf_list_t *list, pkgconf_buffer_t *buf)
{
	const pkgconf_buffer_t *frag_string = NULL;
	pkgconf_node_t *node;
	bool is_delim = true;

	(void)client;

	PKGCONF_FOREACH_LIST_ENTRY(list->head, node)
	{
		pkgconf_license_t *license = node->data;
		is_delim = true;
		frag_string = PKGCONF_BUFFER_FROM_STR(license->data);
		if (!pkgconf_buffer_append(buf, pkgconf_buffer_str_or_empty(frag_string)))
			return false;

		if (license->type == PKGCONF_LICENSE_BRACKET_OPEN || (node->next != NULL && ((const pkgconf_license_t *)node->next->data)->type == PKGCONF_LICENSE_BRACKET_CLOSE))
		{
			is_delim = false;
		}

		if (node->next != NULL && is_delim)
		{
			if (!pkgconf_buffer_push_byte(buf, ' '))
				return false;
		}
	}

	return true;
}
