@tool
extends RefCounted

const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")

## Handles input action listing, creation, removal, and event binding.
## Actions are persisted via ProjectSettings so they survive editor restarts.


func list_actions(params: Dictionary) -> Dictionary:
	var include_builtin: bool = params.get("include_builtin", false)
	## Authoritative source for user-authored actions is the ``[input]``
	## section of ``project.godot``. ``ProjectSettings.has_setting`` is not
	## reliable here because Godot registers ``ui_*`` defaults via
	## ``GLOBAL_DEF_BASIC``, which makes ``has_setting`` return true for
	## them. Reading the file via ``ConfigFile`` distinguishes the user's
	## entries from engine-registered defaults regardless of namespace.
	## See #213.
	var user_authored := _read_user_authored_actions()
	var actions: Array[Dictionary] = []
	for action_name in InputMap.get_actions():
		var name_str := str(action_name)
		var is_user_action := user_authored.has(name_str)
		if not include_builtin and not is_user_action:
			continue
		var events: Array[Dictionary] = []
		for event in InputMap.action_get_events(action_name):
			events.append(_serialize_event(event))
		actions.append({
			"name": name_str,
			"events": events,
			"event_count": events.size(),
			"is_builtin": not is_user_action,
		})
	return {"data": {"actions": actions, "count": actions.size()}}


func _read_user_authored_actions() -> Dictionary:
	var cfg := ConfigFile.new()
	if cfg.load("res://project.godot") != OK:
		return {}
	if not cfg.has_section("input"):
		return {}
	var result: Dictionary = {}
	for key in cfg.get_section_keys("input"):
		result[key] = true
	return result


func add_action(params: Dictionary) -> Dictionary:
	var action: String = params.get("action", "")
	var deadzone: float = params.get("deadzone", 0.5)

	if action.is_empty():
		return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: action")

	if deadzone < 0.0 or deadzone > 1.0:
		return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
			"deadzone must be in [0.0, 1.0] (got %s). Typical values are 0.2-0.5; default is 0.5." % deadzone)

	if InputMap.has_action(action):
		return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "Action '%s' already exists" % action)

	InputMap.add_action(action, deadzone)

	var key := "input/%s" % action
	ProjectSettings.set_setting(key, {
		"deadzone": deadzone,
		"events": [],
	})
	var err := ProjectSettings.save()
	if err != OK:
		InputMap.erase_action(action)
		ProjectSettings.clear(key)
		return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
			"Failed to save project settings while adding action '%s': %s (error %d)" % [action, error_string(err), err])

	return {
		"data": {
			"action": action,
			"deadzone": deadzone,
			"undoable": false,
			"reason": "Input actions are saved to project.godot",
		}
	}


func remove_action(params: Dictionary) -> Dictionary:
	var action: String = params.get("action", "")
	if action.is_empty():
		return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: action")

	if not InputMap.has_action(action):
		return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Action '%s' not found" % action)

	var key := "input/%s" % action
	var old_setting = ProjectSettings.get_setting(key) if ProjectSettings.has_setting(key) else null
	InputMap.erase_action(action)

	if old_setting != null:
		ProjectSettings.clear(key)
		var err := ProjectSettings.save()
		if err != OK:
			var dz: float = old_setting.get("deadzone", 0.5) if old_setting is Dictionary else 0.5
			InputMap.add_action(action, dz)
			if old_setting is Dictionary:
				for ev in old_setting.get("events", []):
					if ev is InputEvent:
						InputMap.action_add_event(action, ev)
			ProjectSettings.set_setting(key, old_setting)
			return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
				"Failed to save project settings while removing action '%s': %s (error %d)" % [action, error_string(err), err])

	return {
		"data": {
			"action": action,
			"removed": true,
			"undoable": false,
			"reason": "Input actions are saved to project.godot",
		}
	}


func bind_event(params: Dictionary) -> Dictionary:
	var action: String = params.get("action", "")
	var event_type: String = params.get("event_type", "")

	if action.is_empty():
		return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: action")
	if event_type.is_empty():
		return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: event_type")

	if not InputMap.has_action(action):
		return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
			"Action '%s' not found. Call input_map_manage(op='add_action', params={action: '%s'}) first." % [action, action])

	var event_or_error = _create_event(event_type, params)
	if event_or_error is Dictionary:
		return event_or_error
	var event: InputEvent = event_or_error

	InputMap.action_add_event(action, event)

	var err := _save_action_events(action)
	if err != OK:
		InputMap.action_erase_event(action, event)
		return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
			"Failed to save project settings while binding event to action '%s': %s (error %d)" % [action, error_string(err), err])

	return {
		"data": {
			"action": action,
			"event": _serialize_event(event),
			"undoable": false,
			"reason": "Input bindings are saved to project.godot",
		}
	}


## Returns an InputEvent on success, or a Dictionary error on failure.
## Caller must check ``result is Dictionary`` before treating it as an event.
func _create_event(event_type: String, params: Dictionary):
	match event_type:
		"key":
			var ev := InputEventKey.new()
			var keycode_str: String = params.get("keycode", "")
			if keycode_str.is_empty():
				return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM,
					"event_type='key' requires keycode (e.g. 'Space', 'A', 'Enter', 'Escape', 'F1').")
			ev.keycode = OS.find_keycode_from_string(keycode_str)
			if ev.keycode == KEY_NONE:
				return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
					"Invalid keycode '%s'. Use Godot keycode names like 'A', 'Space', 'Enter', 'Escape', 'F1', 'Left', 'Right'." % keycode_str)
			ev.ctrl_pressed = params.get("ctrl", false)
			ev.alt_pressed = params.get("alt", false)
			ev.shift_pressed = params.get("shift", false)
			ev.meta_pressed = params.get("meta", false)
			return ev
		"mouse_button":
			if not params.has("button"):
				return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM,
					"event_type='mouse_button' requires button (1=left, 2=right, 3=middle, 4=wheel up, 5=wheel down).")
			var button: int = int(params.get("button", 0))
			if button <= 0:
				return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
					"mouse_button button must be > 0 (got %d). Use 1=left, 2=right, 3=middle, 4=wheel up, 5=wheel down." % button)
			var ev := InputEventMouseButton.new()
			ev.button_index = button
			return ev
		"joy_button":
			if not params.has("button"):
				return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM,
					"event_type='joy_button' requires button (JoyButton index, e.g. 0=A/Cross, 1=B/Circle).")
			var ev := InputEventJoypadButton.new()
			ev.button_index = int(params.get("button", 0))
			return ev
	return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
		"Unsupported event_type: '%s'. Use 'key', 'mouse_button', or 'joy_button'." % event_type)


func _serialize_event(event: InputEvent) -> Dictionary:
	if event is InputEventKey:
		return {
			"type": "key",
			"keycode": OS.get_keycode_string(event.keycode),
			"physical_keycode": OS.get_keycode_string(event.physical_keycode),
			"ctrl": event.ctrl_pressed,
			"alt": event.alt_pressed,
			"shift": event.shift_pressed,
			"meta": event.meta_pressed,
		}
	if event is InputEventMouseButton:
		return {
			"type": "mouse_button",
			"button": event.button_index,
		}
	if event is InputEventJoypadButton:
		return {
			"type": "joy_button",
			"button": event.button_index,
		}
	if event is InputEventJoypadMotion:
		return {
			"type": "joy_axis",
			"axis": event.axis,
			"axis_value": event.axis_value,
		}
	return {"type": event.get_class(), "string": str(event)}


func _save_action_events(action: String) -> int:
	var events: Array = []
	for event in InputMap.action_get_events(action):
		events.append(event)
	var key := "input/%s" % action
	var had_setting := ProjectSettings.has_setting(key)
	var old_setting = ProjectSettings.get_setting(key) if had_setting else null
	var deadzone: float = 0.5
	if old_setting is Dictionary:
		deadzone = old_setting.get("deadzone", 0.5)
	ProjectSettings.set_setting(key, {
		"deadzone": deadzone,
		"events": events,
	})
	var err := ProjectSettings.save()
	if err != OK:
		if had_setting:
			ProjectSettings.set_setting(key, old_setting)
		else:
			ProjectSettings.clear(key)
	return err
