/* SPDX-License-Identifier: GPL-2.0-only */

#include <stdlib.h>
#include <types.h>
#include <tests/test.h>
#include <acpi/acpigen.h>

#define ACPIGEN_TEST_BUFFER_SZ (16 * KiB)

/* Returns AML package length. Works with normal and extended packages.
   This implementation is independent from acpigen.c implementation of package length. */
static u32 decode_package_length(const char *ptr)
{
	const u8 *aml = (u8 *)ptr;
	const u32 offset = (aml[0] == EXT_OP_PREFIX ? 2 : 1);
	u32 byte_zero_mask = 0x3F; /* Bits [0:5] */
	u32 byte_count = aml[offset] >> 6;
	u32 package_length = 0;

	while (byte_count) {
		package_length |= aml[offset + byte_count] << ((byte_count << 3) - 4);
		byte_zero_mask = 0x0F; /* Use bits [0:3] of byte 0 */
		byte_count--;
	}

	package_length |= (aml[offset] & byte_zero_mask);

	return package_length;
}

static u32 get_current_block_length(const char *base)
{
	const u32 offset = (base[0] == EXT_OP_PREFIX ? 2 : 1);

	return ((uintptr_t)acpigen_get_current() - ((uintptr_t)base + offset));
}

static int setup_acpigen(void **state)
{
	void *buffer = malloc(ACPIGEN_TEST_BUFFER_SZ);

	if (buffer == NULL)
		return -1;

	memset(buffer, 0, ACPIGEN_TEST_BUFFER_SZ);

	*state = buffer;
	return 0;
}

static int teardown_acpigen(void **state)
{
	free(*state);
	return 0;
}

static void test_acpigen_single_if(void **state)
{
	char *acpigen_buf = *state;
	u32 if_package_length = 0;
	u32 block_length = 0;

	acpigen_set_current(acpigen_buf);

	/* Create dummy AML */
	acpigen_write_if_lequal_op_int(LOCAL0_OP, 64);

	for (int i = 0; i < 20; ++i)
		acpigen_write_store_ops(ZERO_OP, LOCAL1_OP);

	/* Close if */
	acpigen_pop_len();

	if_package_length = decode_package_length(acpigen_buf);
	block_length = get_current_block_length(acpigen_buf);
	assert_int_equal(if_package_length, block_length);
}

static void create_nested_ifs_recursive(size_t stack_len[], u32 i, u32 n)
{
	if (i >= n)
		return;

	char *const start = acpigen_get_current();
	acpigen_write_if_and(LOCAL0_OP, ZERO_OP);

	for (int k = 0; k < 3; ++k)
		acpigen_write_store_ops(ZERO_OP, LOCAL1_OP);

	create_nested_ifs_recursive(stack_len, i + 1, n);

	acpigen_pop_len();
	stack_len[i] = acpigen_get_current() - start;
}

static void test_acpigen_nested_ifs(void **state)
{
	char *acpigen_buf = *state;
	const size_t nesting_level = 8;
	size_t block_len[8] = {0};

	acpigen_set_current(acpigen_buf);

	create_nested_ifs_recursive(block_len, 0, nesting_level);

	for (int i = 0, j = 0; i < nesting_level; ++i, ++j) {
		/* Find next if op */
		for (; j < ACPIGEN_TEST_BUFFER_SZ; ++j) {
			if ((u8)acpigen_buf[j] == IF_OP)
				break;
		}
		assert_int_equal(decode_package_length(acpigen_buf + j), block_len[i] - 1);
	}
}

static void test_acpigen_write_package(void **state)
{
	char *acpigen_buf = *state;
	u32 package_length;
	u32 block_length;

	acpigen_set_current(acpigen_buf);
	acpigen_write_package(3);

	acpigen_write_return_singleton_buffer(0xA);
	acpigen_write_return_singleton_buffer(0x7);
	acpigen_write_return_singleton_buffer(0xF);

	acpigen_pop_len();

	package_length = decode_package_length(acpigen_buf);
	block_length = get_current_block_length(acpigen_buf);
	assert_int_equal(package_length, block_length);
}

static void test_acpigen_scope_with_contents(void **state)
{
	char *acpigen_buf = *state;
	char *block_start[8] = {0};
	u32 block_counter = 0;
	u32 package_length;
	u32 block_length;

	acpigen_set_current(acpigen_buf);

	/* Scope("\_SB") { */
	block_start[block_counter++] = acpigen_get_current();
	acpigen_write_scope("\\_SB");

	/* Device("PCI0") { */
	block_start[block_counter++] = acpigen_get_current();
	acpigen_write_device("PCI0");

	/* Name(INT1, 0x1234) */
	acpigen_write_name_integer("INT1", 0x1234);

	/* Name (_HID, EisaId ("PNP0A08")) // PCI Express Bus */
	acpigen_write_name("_HID");
	acpigen_emit_eisaid("PNP0A08");

	/* Method(^BN00, 0, NotSerialized) { */
	block_start[block_counter++] = acpigen_get_current();
	acpigen_write_method("^BN00", 0);

	/* Return( 0x12 + ^PCI0.INT1 ) */
	acpigen_write_return_op(AND_OP);
	acpigen_write_byte(0x12);
	acpigen_emit_namestring("^PCI0.INT1");

	/* } */
	acpigen_pop_len();
	block_counter--;
	package_length = decode_package_length(block_start[block_counter]);
	block_length = get_current_block_length(block_start[block_counter]);
	assert_int_equal(package_length, block_length);

	/* Method (_BBN, 0, NotSerialized) { */
	block_start[block_counter++] = acpigen_get_current();
	acpigen_write_method("_BBN", 0);

	/* Return (BN00 ()) */
	acpigen_write_return_namestr("BN00");
	acpigen_emit_byte(0x0A);

	/* } */
	acpigen_pop_len();
	block_counter--;
	package_length = decode_package_length(block_start[block_counter]);
	block_length = get_current_block_length(block_start[block_counter]);
	assert_int_equal(package_length, block_length);

	/* } */
	acpigen_pop_len();
	block_counter--;
	package_length = decode_package_length(block_start[block_counter]);
	block_length = get_current_block_length(block_start[block_counter]);
	assert_int_equal(package_length, block_length);

	/* } */
	acpigen_pop_len();
	block_counter--;
	package_length = decode_package_length(block_start[block_counter]);
	block_length = get_current_block_length(block_start[block_counter]);
	assert_int_equal(package_length, block_length);
}

int main(void)
{
	const struct CMUnitTest tests[] = {
		cmocka_unit_test_setup_teardown(test_acpigen_single_if, setup_acpigen,
						teardown_acpigen),
		cmocka_unit_test_setup_teardown(test_acpigen_nested_ifs, setup_acpigen,
						teardown_acpigen),
		cmocka_unit_test_setup_teardown(test_acpigen_write_package, setup_acpigen,
						teardown_acpigen),
		cmocka_unit_test_setup_teardown(test_acpigen_scope_with_contents, setup_acpigen,
						teardown_acpigen),
	};

	return cb_run_group_tests(tests, NULL, NULL);
}