#!/usr/bin/env python3

from glob import glob
import csv
import html
import io
import re
import sys

SUPPORTED_COMMAND_FLAGS = [
	"CFGFLAG_SAVE",
	"CFGFLAG_CLIENT",
	"CFGFLAG_DEBUG_CLIENT",
	"CFGFLAG_SERVER",
	"CFGFLAG_DEBUG_SERVER",
	"CFGFLAG_STORE",
	"CFGFLAG_ECON",
	"CMDFLAG_TEST",
	"CFGFLAG_CHAT",
	"CFGFLAG_GAME",
	"CFGFLAG_NONTEEHISTORIC",
	"CFGFLAG_COLLIGHT",
	"CFGFLAG_COLLIGHT7",
	"CFGFLAG_COLALPHA",
	"CFGFLAG_INSENSITIVE",
	"CMDFLAG_PRACTICE",
]


def parse_arguments(arguments_line, num, name):
	try:
		arguments_line = arguments_line.replace("\\n", "\\\\n")
		parsed = next(csv.reader(io.StringIO(arguments_line), skipinitialspace=True, escapechar="\\"))
		if len(parsed) != num:
			raise RuntimeError(f"Failed to parse {name} arguments: {arguments_line}, got {parsed}")
		return parsed
	except Exception as e:
		raise RuntimeError(f"Failed to parse {name} arguments: {arguments_line}") from e


def parse_flags(flags_str, name):
	flags = flags_str.split(" | ")
	for flag in flags:
		if flag not in SUPPORTED_COMMAND_FLAGS:
			raise RuntimeError(f"Failed to parse {name} with unknown flag: {flag}")
	return flags


def parse_commands(lines):
	parsed_commands = []
	for line in lines:
		args = re.match(r".*Register\((.*)\);", line)
		if args is None or "CFGFLAG_" not in args[1]:
			continue
		parsed = parse_arguments(args[1], num=6, name="command")
		flags = parse_flags(parsed[2], f"command '{parsed[0]}'")
		parsed_commands.append({"name": parsed[0], "flags": flags, "arguments": parsed[1], "description": parsed[5]})
	parsed_commands.sort(key=lambda command: command["name"])
	return parsed_commands


def parse_settings(lines):
	def parse_value(value):
		value = value.lstrip()
		value = value.replace("SERVERINFO_LEVEL_MIN", "0")
		value = value.replace("SERVERINFO_LEVEL_MAX", "2")
		value = value.replace("SERVER_MAX_CLIENTS", "64")
		value = value.replace("MAX_CLIENTS", "128")
		value = value.replace("CountryCode::DEFAULT", "-1")
		value = value.replace("CountryCode::MINIMUM", "-1")
		value = value.replace("CountryCode::MAXIMUM", "999")
		try:
			return int(value, base=16) if value.startswith(("0x", "0X")) else int(value)
		except Exception as e:
			raise RuntimeError("Failed to parse setting value: {value}") from e

	names = {}
	parsed_settings = []
	for line in lines:
		args = re.match(r"^MACRO_CONFIG_(.*?)\((.*)\)", line)
		if args is None:
			continue

		if args[1] == "STR":
			parsed = parse_arguments(args[2], num=6, name="string setting")
			flags = parse_flags(parsed[4], f"string setting '{parsed[1]}'")
			setting = {"type": "string", "flags": flags, "name": parsed[1], "description": parsed[5], "default": f'"{parsed[3]}"'}
		elif args[1] == "INT":
			parsed = parse_arguments(args[2], num=7, name="int setting")
			flags = parse_flags(parsed[5], f"int setting '{parsed[1]}'")
			setting = {"type": "int", "flags": flags, "name": parsed[1], "description": parsed[6], "default": parse_value(parsed[2]), "min": parse_value(parsed[3]), "max": parse_value(parsed[4])}
		elif args[1] == "COL":
			parsed = parse_arguments(args[2], num=5, name="color setting")
			flags = parse_flags(parsed[3], f"color setting '{parsed[1]}'")
			setting = {"type": "color", "flags": flags, "name": parsed[1], "description": parsed[4], "default": parse_value(parsed[2])}
		else:
			raise RuntimeError(f"Failed to parse settings type: {args[1]}")

		# Filter duplicates because config_variables.h contains some definitions multiple times with #ifdef
		if setting["name"] in names:
			parsed_settings[names[setting["name"]]] = setting
		else:
			names[setting["name"]] = len(parsed_settings)
			parsed_settings.append(setting)

	parsed_settings.sort(key=lambda setting: setting["name"])
	return parsed_settings


def parse_tunings(lines):
	def parse_value(value):
		value = value.strip()
		try:
			if value.endswith("f / (float)SERVER_TICK_SPEED"):
				return float(value.removesuffix("f / (float)SERVER_TICK_SPEED")) / 50.0
			return float(value.strip("f"))
		except Exception as e:
			raise RuntimeError("Failed to parse tuning value: {value}") from e

	parsed_tunings = []
	for line in lines:
		args = re.match(r"^MACRO_TUNING_PARAM\((.*)\)", line)
		if args is None:
			continue
		parsed = parse_arguments(args[1], num=4, name="tuning")
		parsed_tunings.append({"name": parsed[1], "description": parsed[3], "default": parse_value(parsed[2])})
	parsed_tunings.sort(key=lambda tuning: tuning["name"])
	return parsed_tunings


def export_commands(group_id, parsed_commands):
	output = ['<div style="overflow-x: auto;"><table class="settingscommands">']
	output.append("  <tr><th>Command</th><th>Arguments</th><th>Description</th></tr>")

	for command in parsed_commands:
		name = html.escape(command["name"])
		output.append(f'  <tr><td><a id="{group_id}-{name}" href="#{group_id}-{name}">{name}</a></td><td>{html.escape(command["arguments"])}</td><td>{html.escape(command["description"])}</td></tr>')

	output.append("</table></div>")
	return "\n".join(output)


def export_settings(group_id, parsed_settings):
	def export_color_value(setting):
		alpha = "CFGFLAG_COLALPHA" in setting["flags"]
		if "CFGFLAG_COLLIGHT7" in setting["flags"]:
			darkest_lgt = 61.0 / 255.0
		elif "CFGFLAG_COLLIGHT" in setting["flags"]:
			darkest_lgt = 0.5
		else:
			darkest_lgt = 0.0
		number = setting["default"]
		a = ((number >> 24) & 0xFF) / 255.0 if alpha else 1.0
		h = ((number >> 16) & 0xFF) / 255.0
		s = ((number >> 8) & 0xFF) / 255.0
		l = (number & 0xFF) / 255.0
		l = darkest_lgt + l * (1.0 - darkest_lgt)
		return f'<span class="colorpreview" style="background: hsla({h * 360.0:.3f}, {s * 100.0:.3f}%, {l * 100.0:.3f}%, {a:.3f})"></span> {number}'

	output = ['<div style="overflow-x: auto;"><table class="settingscommands">']
	output.append("  <tr><th>Setting</th><th>Description</th><th>Default</th><th>Min</th><th>Max</th></tr>")

	for setting in parsed_settings:
		name = html.escape(setting["name"])
		line = f'  <tr><td><a id="{group_id}-{name}" href="#{group_id}-{name}">{name}</a></td><td>{html.escape(setting["description"])}</td>'
		if setting["type"] == "string":
			line = line + f"<td>{html.escape(setting['default'])}</td><td></td><td></td></tr>"
		elif setting["type"] == "int":
			line = line + f"<td>{setting['default']}</td><td>{setting['min']}</td><td>{setting['max']}</td></tr>"
		elif setting["type"] == "color":
			line = line + f"<td>{export_color_value(setting)}</td><td></td><td></td></tr>"
		else:
			raise RuntimeError(f"Unhandled setting type: {setting['type']}")
		output.append(line)

	output.append("</table></div>")
	return "\n".join(output)


def export_tunings(parsed_tunings):
	output = ['<div style="overflow-x: auto;"><table class="settingscommands">']
	output.append("  <tr><th>Tuning</th><th>Description</th><th>Default</th></tr>")

	for tuning in parsed_tunings:
		name = html.escape(tuning["name"])
		output.append(f'  <tr><td><a id="tuning-{name}" href="#tuning-{name}">{name}</a></td><td>{html.escape(tuning["description"])}</td><td>{tuning["default"]}</td></tr>')

	output.append("</table></div>")
	return "\n".join(output)


def export_block(title, content):
	anchor = title.lower().replace(" ", "-")
	print('<div class="block">')
	print(f'<h2 id="{anchor}"><a href="#{anchor}">{title}</a></h2>')
	print(content)
	print("</div>")


def read_files(pattern):
	for file in glob(pattern, recursive=True):
		with open(file, "r", encoding="utf-8", errors="ignore") as f:
			yield from f


def export_settings_commands_table():
	print("<!-- DO NOT EDIT MANUALLY! THIS FILE IS AUTOMATICALLY GENERATED BY https://github.com/ddnet/ddnet/blob/master/scripts/export_settings_commands_table.py -->")

	settings = parse_settings(read_files("src/engine/shared/config_variables.h"))
	commands = parse_commands(read_files("src/**/*.cpp"))

	export_block(
		"Map Settings",
		export_settings("map", [setting for setting in settings if "CFGFLAG_SERVER" in setting["flags"] and "CFGFLAG_GAME" in setting["flags"]]) + "\n" + export_commands("map", [command for command in commands if "CFGFLAG_SERVER" in command["flags"] and "CFGFLAG_GAME" in command["flags"]]),
	)

	export_block(
		"Server Settings",
		export_settings("server", [setting for setting in settings if "CFGFLAG_SERVER" in setting["flags"]]),
	)

	export_block(
		"Econ Settings",
		export_settings("econ", [setting for setting in settings if "CFGFLAG_ECON" in setting["flags"]]),
	)

	export_block(
		"Server Commands",
		export_commands("server", [command for command in commands if "CFGFLAG_SERVER" in command["flags"] and "CFGFLAG_CHAT" not in command["flags"]]),
	)

	export_block(
		"Chat Commands",
		export_commands("chat", [command for command in commands if "CFGFLAG_SERVER" in command["flags"] and "CFGFLAG_CHAT" in command["flags"]]),
	)

	export_block(
		"Client Settings",
		export_settings("client", [setting for setting in settings if "CFGFLAG_CLIENT" in setting["flags"]]),
	)

	export_block(
		"Client Commands",
		export_commands("client", [command for command in commands if "CFGFLAG_CLIENT" in command["flags"]]),
	)

	export_block(
		"Tunings",
		export_tunings(parse_tunings(read_files("src/game/tuning.h"))),
	)


if __name__ == "__main__":
	# Fix UTF-8 encoding of output stream when redirecting to file
	sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
	sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
	export_settings_commands_table()
