# Hurry Curry! - a game about cooking
# Copyright (C) 2026 Hurry Curry! Contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, version 3 of the License only.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
extends Node
class_name Settings

static func get_root():
	return SettingsRoot.new([
		SettingsCategory.new("gameplay", [
			ToggleSetting.new("usernames", true),
			ToggleSetting.new("latch_boost", true),
			ToggleSetting.new("allow_focus_players", true),
			ToggleSetting.new("vibration", true),
			ToggleSetting.new("invert_camera", false),
			ToggleSetting.new("interpolate_camera_rotation", false),
			ButtonSetting.new("setup_completed", false, launch_setup),
			ToggleSetting.new("tutorial_disabled", false),
			ToggleSetting.new("first_person", false),
			PathSetting.new("screenshot_path", "", FileDialog.FileMode.FILE_MODE_OPEN_DIR),
		]),
		SettingsCategory.new("graphics", [
			PresetRow.new("preset", {
				"low": {"ui_blur": true, "aa": "disabled", "ssao": false, "taa": false, "shadows": false, "glow": false, "grass_amount": 0, "lq_trees": true},
				"medium": {"ui_blur": true, "aa": "fx", "ssao": false, "taa": false, "shadows": true, "glow": false, "grass_amount": 16, "lq_trees": false},
				"high": {"ui_blur": true, "aa": "ms2x", "ssao": true, "taa": false, "shadows": true, "glow": true, "grass_amount": 24, "lq_trees": false}
			}),
			DropdownSetting.new("fullscreen", "keep", ["keep", "always", "never"]),
			DropdownSetting.new("aa", "ms2x" if Global.on_high_end() else "disabled", ["disabled", "fx", "ms2x", "ms4x"]),
			ToggleSetting.new("ssao", true if Global.on_high_end() else false),
			ToggleSetting.new("taa", false),
			DropdownSetting.new("gi", "disabled", ["disabled", "sdfgi", "voxelgi"]),
			ToggleSetting.new("shadows", true if Global.on_high_end() else false),
			ToggleSetting.new("glow", true if Global.on_high_end() else false),
			RangeSetting.new("grass_amount", 24 if Global.on_high_end() else 0, 0, 32, false),
			ToggleSetting.new("lq_trees", false if Global.on_high_end() else true),
			ToggleSetting.new("debug_info", false),
			ToggleSetting.new("ui_blur", true)
		]),
		SettingsCategory.new("audio", [
			RangeSetting.new("master_volume", 0, -30, 0),
			RangeSetting.new("music_volume", 0, -30, 0),
			RangeSetting.new("sfx_volume", 0, -30, 0),
		]),
		SettingsCategory.new("ui", [
			DropdownSetting.new("touch_controls", "automatic", ["automatic", "enabled", "disabled"]),
			DropdownSetting.new("language", "system", Global.language_list(), Global.language_tooltip_list()),
			ToggleSetting.new("hide_overlays", false),
			DropdownSetting.new("scale_mode", "resize", ["resize", "disabled"]),
			RangeSetting.new("scale_factor", 1. if not Global.on_mobile() else 1.5, 0.5, 1.5, 3),
			RangeSetting.new("transition_speed", 4., 1., 8.),
		]),
		SettingsCategory.new("input", InputManager.settings([
			RangeSetting.new("fps_mouse_sensitivity", 0.001, 0.0001, 0.003)
		])),
		SettingsCategory.new("server", [
			PathSetting.new("binary_path", "", FileDialog.FileMode.FILE_MODE_OPEN_FILE),
			PathSetting.new("editor_binary_path", "", FileDialog.FileMode.FILE_MODE_OPEN_FILE),
			PathSetting.new("data_path", "", FileDialog.FileMode.FILE_MODE_OPEN_DIR),
			TextSetting.new("name", "A Hurry Curry! Server"),
			ToggleSetting.new("allow_external_connections", true),
			ToggleSetting.new("enable_ipv6", true),
			NumberSetting.new("bind_port", 27032, 1, 65535),
			ToggleSetting.new("upnp", false),
			ToggleSetting.new("mdns", true),
			ToggleSetting.new("register", false),
		]),
		SettingsCategory.new("online", [
			ToggleSetting.new("use_registry", false),
			TextSetting.new("registry_url", "https://registry.hurrycurry.org/"),
			ToggleSetting.new("use_discover", true),
			PathSetting.new("discover_binary", "", FileDialog.FileMode.FILE_MODE_OPEN_FILE),
		])
	])

static var tree: GameSetting
static var values: Dictionary = {}
static var loaded_path: String

static func read(key: String):
	if !values.has(key):
		push_error("Tried to access setting \"%s\", which does not exist (missing key)" % key)
		return null
	return values[key]

static func write_unchecked(key: String, value):
	value = value.duplicate(true) if value is Array else value
	if key in values and typeof(values[key]) == typeof(value) and not value is Array and values[key] == value: return
	values[key] = value
	trigger_hook(key, value)

static func write(key: String, value):
	if !values.has(key):
		push_error("Tried to set setting \"%s\", which does not yet exist (missing key)" % key)
		return
	else: write_unchecked(key, value)

static func load(path: String):
	print("Loading settings from %s" % path)
	tree = get_root()
	loaded_path = path
	var changed = {}
	if FileAccess.file_exists(path):
		var f = FileAccess.open(path, FileAccess.READ)
		changed = JSON.parse_string(f.get_as_text())
	tree.load(changed)
	setup_static_hooks()

static func save():
	var changed = {}
	tree.save(changed)
	DirAccess.make_dir_recursive_absolute(loaded_path.rsplit("/", true, 1)[0])
	var f = FileAccess.open(loaded_path, FileAccess.WRITE)
	f.store_string(JSON.stringify(changed))

static func trigger_hook(key: String, value):
	for slot in change_hooks.get(key, {}):
		change_hooks[key][slot].callv([value] if value != null else [])
	if key.find(".") != -1: trigger_hook(key.rsplit(".", false, 1)[0], key.rsplit(".", false, 1)[1])

# slot: String | Node
static func hook_changed(key: String, slot, callable: Callable):
	var slot_id = 0
	if slot is Node:
		slot_id = slot.get_instance_id()
		if not slot.tree_exiting.is_connected(_unhook_changed):
			slot.tree_exiting.connect(_unhook_changed.bind(slot_id), CONNECT_ONE_SHOT)
	elif slot is String:
		slot_id = slot.hash()
	else:
		push_error("hook_changed called with invalid slot")
		return
	if not change_hooks.has(key): change_hooks[key] = {}
	change_hooks[key][slot_id] = callable

static func _unhook_changed(slot_id: int):
	for key in change_hooks:
		change_hooks[key].erase(slot_id)

# slot: String | Node
static func hook_changed_init(key: String, slot, callable: Callable):
	hook_changed(key, slot, callable)
	callable.call(read(key))

static func get_category_dict(prefix: String):
	var map = {}
	for k in values.keys():
		var kn = k.trim_prefix(prefix + ".")
		if kn == k: continue
		map[kn] = read(k)
	return map

static func launch_setup():
	Global.focused_menu.submenu("res://gui/menus/setup/setup.tscn")

static var change_hooks = {}

static func setup_static_hooks():
	for action in InputManager.action_list():
		hook_changed_init("input.%s" % action, "static", InputManager.update_input_map.bind(action))
	hook_changed_init("graphics.aa", "static", h_aa)
	hook_changed_init("graphics.taa", "static", h_taa)
	hook_changed_init("graphics.fullscreen", "static", h_fullscreen)
	hook_changed_init("ui.scale_mode", "static", h_scale_mode)
	hook_changed_init("ui.scale_factor", "static", h_scale_factor)
	hook_changed_init("ui.language", "static", h_language)
	hook_changed_init("audio.master_volume", "static", h_volume_master)
	hook_changed_init("audio.music_volume", "static", h_volume_music)
	hook_changed_init("audio.sfx_volume", "static", h_volume_sfx)

static func h_aa(_mode):
	Global.configure_viewport_aa(Global.get_viewport())

static func h_taa(enabled):
	Global.get_viewport().use_taa = enabled

static func h_scale_mode(mode: String):
	var root = Global.get_tree().root
	if OS.has_feature("movie"):
		root.content_scale_mode = Window.CONTENT_SCALE_MODE_VIEWPORT
		root.content_scale_aspect = Window.CONTENT_SCALE_ASPECT_KEEP
	else: match mode:
		"resize": root.content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS
		"disabled": root.content_scale_mode = Window.CONTENT_SCALE_MODE_DISABLED

static func h_scale_factor(value: float):
	Global.get_tree().root.content_scale_factor = value

static func h_volume_master(value: float): Sound.set_volume(0, value)
static func h_volume_music(value: float): Sound.set_volume(1, value)
static func h_volume_sfx(value: float): Sound.set_volume(2, value)

static func h_language(language: String):
	if language == "system": language = OS.get_locale_language()
	TranslationServer.set_locale(language)

static func h_fullscreen(mode: String):
	match mode:
		"keep": pass
		"always": DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
		"never": if DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_FULLSCREEN:
			DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
