#!/usr/bin/env python3

import sys
import time
from collections.abc import Generator

from coldata import *


def tab(string: str, tabs: int = 1) -> str:
	return '\t' * tabs + string


def codepoints_to_str(codepoints: Codepoints) -> str:
	'''transforms codepoints ["0001", "0002"] into comparable string "0001,0002"
	this string will later be used in a comment as well, but the point is to
	potentially find nested contraction in a string with longer contraction'''
	return ','.join(f'{int(codepoint, base=16):06X}' for codepoint in codepoints)


def is_nested_contraction(contraction: Codepoints, contractions: Collection) -> str | None:
	'''test if contraction is valid, but actually can be considered as a part of longer contraction
	the example of this is [16D63, 16D67] and [16D63, 16D67, 16D67]'''

	contraction_str = codepoints_to_str(contraction)
	for cps, _ in contractions:
		cps_str = codepoints_to_str(cps)
		if cps_str != contraction_str and cps_str.find(contraction_str) == 0:
			return cps_str


def point2bin2point(point: str, offset: int) -> str:
	'''basically apply integer offset to a number in string type'''
	c = int(point, base=16) + offset
	return f'{c:06X}'


def gen_header(tag: str, contractions: Collection):
	print(f'''/* Automatically generated file (contractions-totests), {int(time.time())}
 *
 * Tag          : {tag}
 * Contractions : {len(contractions)}
 */''')
	print('')


def gen_includes():
	print('#include <assert.h>')
	print('#include <stddef.h>')
	print('#include <stdint.h>')
	print('')
	print('#include "switch_test_base.h"')
	print('')


def gen_globals(tag: str):
	'''produce global definitions'''
	print(f'extern int32_t {tag}_weight_switch(uint32_t u, int32_t *w, void *context);')
	print('')
	print(f'static const nu_codepoint_weight_t weight = {tag}_weight_switch;')
	print('')


def gen_run_suite():
	'''produce code for running test suite'''
	print(tab('size_t i = 0; for (; i < contractions_num; ++i) {'))
	print(tab('int32_t r = 0;', 2))
	print(tab('int32_t w = _nu_test_contraction_weight(weight, contractions[i].seq, contractions[i].len, &r);', 2))
	print(tab('assert(w == contractions[i].weight);', 2))
	print(tab('assert(r == contractions[i].rollback);', 2))
	print(tab('}'))


def gen_weights_test(tag: str, contractions: Collection):
	'''check that encoded contractions produce expected weights'''

	def expand_contraction(contraction: Codepoints, weight: Weight):
		'''produce a single record for test suite'''
		assert len(weight) == 1
		nested = is_nested_contraction(contraction, contractions)
		comment_text = f' /* nested contraction under "{nested}", there will a rollback value due to trailing 0 */' if nested else ''
		rollback = 1 if nested else 0
		joined = ', '.join(f'0x{int(point, base=16):06X}' for point in contraction)
		trailing_zero = '0x00, ' if nested else ''
		array_len = len(contraction) + rollback
		formatted = f'{{ {weight[0]}, {rollback}, {array_len}, (uint32_t[{array_len}]){{ {joined}, {trailing_zero}}},  }},{comment_text}'
		print(tab(formatted, 2))

	print('/* test all contractions with assigned weight */')
	print(f'void test_{tag}_weight_switch() {{')
	print(tab('/* clang-format off */'))
	print(tab('const _nu_contraction_test_t contractions[] = {'))

	for contraction, weight in contractions:
		expand_contraction(contraction, weight)

	print(tab('};'))
	print(tab('/* clang-format on */'))
	print(tab('const size_t contractions_num = sizeof(contractions) / sizeof(*contractions);'))
	print('')

	gen_run_suite()

	print('}')
	print('')


def gen_rollback_test(tag: str, contractions: Collection, codepoints: Collection):
	'''check that state machine rolls back correctly from the middle of contraction'''

	def check_contraction(contraction: Codepoints, contractions: Collection) -> bool:
		for c, _ in contractions:
			if c == contraction:
				return True
		return False

	def find_closest_parent(contraction: Codepoints, contractions: Collection,
		codepoints: Collection) -> tuple[Codepoints | None, Weight | None]:
		'''closest parent with weight'''
		closest_parent = None
		weight = None

		for i in range(1, len(contraction)):
			assert len(contraction) > 1

			c = contraction[:-i]
			w = find_weight(c, contractions)
			if w is None:
				w = find_weight(c, codepoints)

			if w is None:
				continue

			if closest_parent is None or len(c) > len(closest_parent):
				closest_parent = c
				weight = w

		return (closest_parent, weight)  # return None instead of empty list

	def expand_contraction_with_rollback(contraction: Codepoints, original_contraction: Codepoints):
		'''produce one record for test suite'''
		closest_parent, weight = find_closest_parent(contraction, contractions, codepoints)
		assert closest_parent is not None
		assert weight is not None and len(weight) == 1

		rollback = len(contraction) - len(closest_parent)
		joined = ', '.join(f'0x{int(point, base=16):06X}' for point in contraction)
		codepoints_str = codepoints_to_str(closest_parent)
		contraction_str = codepoints_to_str(original_contraction)
		comment_text = f' /* parent: {codepoints_str}, contraction was: {contraction_str} */'
		array_len = len(contraction)
		formatted = f'{{ {weight[0]}, {rollback}, {array_len}, (uint32_t[{array_len}]){{ {joined}, }},  }},{comment_text}'
		print(tab(formatted, 2))

	def expand_contraction(contraction: Codepoints):
		'''find out contraction variants before and after original contraction.
		if before or after is a valid contraction - ignore it, otherwise
		use it as a test'''
		top = contraction[:-1] + [point2bin2point(contraction[-1], -1)]
		bottom = contraction[:-1] + [point2bin2point(contraction[-1], +1)]

		if not check_contraction(top, contractions):
			expand_contraction_with_rollback(top, contraction)
		if not check_contraction(bottom, contractions):
			expand_contraction_with_rollback(bottom, contraction)

		# test parent contraction recursively
		if len(contraction) > 2:
			expand_contraction(contraction[:-1])

	print('/* test unweighted contractions using weighted contractions as a base:')
	print(' * base contraction: U+006C, U+00B7; contractions around it:')
	print(' *  a) U+006C, U+00B6')
	print(' *  b) U+006C, U+00B8 */')
	print(f'void test_{tag}_weight_switch_rollbacks() {{')
	print(tab('/* clang-format off */'))
	print(tab('const _nu_contraction_test_t contractions[] = {'))

	for contraction, _ in contractions:
		expand_contraction(contraction)

	print(tab('};'))
	print(tab('/* clang-format on */'))
	print(tab('const size_t contractions_num = sizeof(contractions) / sizeof(*contractions);'))
	print('')

	gen_run_suite()

	print('}')
	print('')


def gen_unknowns_test(tag: str, contractions: Collection):
	'''produce test that codepoints not included into weighted list
	are weighted correctly (weight == 0 == unknown weight)'''

	def check_codepoint(point: str, contractions: Collection) -> bool:
		'''return True if point is a root of any of known contractions'''
		for c, _ in contractions:
			if c[0] == point:
				return True
		return False

	def expand_contraction_wo_weight(codepoints: Codepoints):
		'''procude single record for contraction w/o weight'''
		joined = ', '.join(f'0x{int(point, base=16):06X}' for point in codepoints)
		array_len = len(codepoints)
		formatted = f'{{ 0, 0, {array_len}, (uint32_t[{array_len}]){{ {joined}, }},  }},'
		print(tab(formatted, 2))

	def neighboring_codepoints(contraction: Codepoints, contractions: Collection) -> Generator[str]:
		'''return codepoints around contraction (before root codepoint
		of contraction and after)'''
		top = [point2bin2point(contraction[0], -1)]
		bottom = [point2bin2point(contraction[0], +1)]

		assert len(top) == 1
		assert len(bottom) == 1

		if not check_codepoint(top[0], contractions):
			yield top[0]
		if not check_codepoint(bottom[0], contractions):
			yield bottom[0]

	def expand_codepoints(codepoints: Codepoints):
		'''just iteration over set'''
		for c in codepoints:
			expand_contraction_wo_weight([c])

	print('/* test that switch successfully recognize unweighted contractions')
	print(' * (weight == 0) based on weighted contractions. e.g.')
	print(' * base contraction: U+006C, U+00B7; codepoints to check:')
	print(' *  a) U+006B')
	print(' *  b) U+006D */')
	print(f'void test_{tag}_weight_switch_unknowns() {{')
	print(tab('/* clang-format off */'))
	print(tab('const _nu_contraction_test_t contractions[] = {'))

	cps: set[str] = set()  # different contractions might have same root codepoint
	for contraction, _ in contractions:
		cps.update(neighboring_codepoints(contraction, contractions))
	expand_codepoints(sorted(cps))

	print(tab('};'))
	print(tab('/* clang-format on */'))
	print(tab('const size_t contractions_num = sizeof(contractions) / sizeof(*contractions);'))
	print('')

	gen_run_suite()

	print('}')
	print('')


def usage():
	print('usage: ' + sys.argv[0] + ' [CODEPOINTS] [CONTRACTIONS] [TAG]')
	print('')
	print('  [CODEPOINTS]   - filename with list of codepoints')
	print('  [CONTRACTIONS] - filename with list of contractions from the same collation')
	print('  [TAG]          - prefix to weighting switch')


if __name__ == '__main__':
	if len(sys.argv) < 4:
		usage()
		sys.exit(1)

	CODEPOINTS, CONTRACTIONS = sys.argv[1], sys.argv[2]
	TAG = sys.argv[3]

	codepoints, contractions = collect_contractions(CODEPOINTS, CONTRACTIONS)

	gen_header(TAG, contractions)
	gen_includes()
	gen_globals(TAG)
	gen_weights_test(TAG, contractions)
	gen_rollback_test(TAG, contractions, codepoints)
	gen_unknowns_test(TAG, contractions)
