class_name NebulaBackdrop
extends Node2D
## Draws original layers in viewport coordinates; put it in a CanvasLayer behind gameplay.
## Horizontal repeat only. Units for speed and camera position are source-image pixels.

enum MotionMode { AUTOSCROLL, CAMERA_INPUT }

@export var preset: NebulaBackdropPreset:
	set(value):
		preset = value
		queue_redraw()
@export var motion_mode: MotionMode = MotionMode.AUTOSCROLL:
	set(value):
		if value == MotionMode.AUTOSCROLL or value == MotionMode.CAMERA_INPUT:
			motion_mode = value
@export_range(-1000.0, 1000.0, 1.0) var speed: float = 44.0:
	set(value):
		if is_finite(value):
			speed = value
@export var layer_depths: Vector3 = Vector3(0.2, 0.5, 1.0):
	set(value):
		if not value.is_finite():
			return
		layer_depths = value.clamp(Vector3.ZERO, Vector3(4.0, 4.0, 4.0))
		queue_redraw()
## RGB multiplication only: alpha is always 1, preserving each source layer's alpha.
@export_color_no_alpha var tint: Color = Color.WHITE:
	set(value):
		if not is_finite(value.r) or not is_finite(value.g) or not is_finite(value.b):
			return
		tint = Color(clampf(value.r, 0.0, 1.0), clampf(value.g, 0.0, 1.0), clampf(value.b, 0.0, 1.0), 1.0)
		queue_redraw()
@export_range(0.0, 1.0, 0.01) var brightness: float = 0.75:
	set(value):
		if not is_finite(value):
			return
		brightness = clampf(value, 0.0, 1.0)
		queue_redraw()
@export var paused: bool = false
@export var reduced_motion: bool = false
## Optional draw area for embedding or deterministic testing; zero follows viewport.
@export var view_size_override: Vector2 = Vector2.ZERO:
	set(value):
		view_size_override = value
		queue_redraw()

var _travel: float = 0.0
var _camera_position: Vector2 = Vector2.ZERO
var _visible_camera_x: float = 0.0

func _ready() -> void:
	get_viewport().size_changed.connect(queue_redraw)
	texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
	queue_redraw()

func _process(delta: float) -> void:
	if paused or reduced_motion:
		return
	if motion_mode == MotionMode.AUTOSCROLL:
		_travel += speed * maxf(delta, 0.0)
	else:
		_visible_camera_x = _camera_position.x
	queue_redraw()

func set_camera_position(position_in_source_pixels: Vector2) -> void:
	if not is_finite(position_in_source_pixels.x):
		return
	_camera_position = Vector2(position_in_source_pixels.x, 0.0)
	if not paused and not reduced_motion:
		_visible_camera_x = _camera_position.x
		queue_redraw()

func reset_scroll() -> void:
	_travel = 0.0
	_camera_position = Vector2.ZERO
	_visible_camera_x = 0.0
	queue_redraw()

func set_preset_resource(value: NebulaBackdropPreset) -> void:
	preset = value
	reset_scroll()

## Shared by rendering and tests. Brightness dims RGB without changing texture alpha.
func layer_modulate() -> Color:
	return Color(tint.r * brightness, tint.g * brightness, tint.b * brightness, 1.0)

## Copy into a new scene script, or merge _ready() with your existing script.
## Transfers configuration and camera input, not elapsed animation/held frame state.
## Empty means the preset must first be saved as a standalone res:// resource.
func settings_snippet() -> String:
	if preset == null or not preset.is_valid():
		return ""
	var path := preset.resource_path
	if not path.begins_with("res://") or path.contains("::"):
		return ""
	var lines := PackedStringArray([
		"extends Node",
		"",
		"# Adjust this node path to match your scene.",
		"@onready var space: NebulaBackdrop = $CanvasLayer/NebulaBackdrop",
		"",
		"func _ready() -> void:",
		"\tspace.set_preset_resource(load(%s) as NebulaBackdropPreset)" % var_to_str(path),
		"\tspace.motion_mode = NebulaBackdrop.MotionMode.%s" % ("AUTOSCROLL" if motion_mode == MotionMode.AUTOSCROLL else "CAMERA_INPUT"),
		"\tspace.speed = %s" % _number_literal(speed),
		"\tspace.layer_depths = Vector3(%s, %s, %s)" % [_number_literal(layer_depths.x), _number_literal(layer_depths.y), _number_literal(layer_depths.z)],
		"\tspace.tint = Color(%s, %s, %s, 1.0)" % [_number_literal(tint.r), _number_literal(tint.g), _number_literal(tint.b)],
		"\tspace.brightness = %s" % _number_literal(brightness),
		# Set the position before the holds so a static camera preview is reproduced.
		"\tspace.paused = false",
		"\tspace.reduced_motion = false",
		"\tspace.set_camera_position(Vector2(%s, 0.0))" % _number_literal(_camera_position.x),
		"\tspace.paused = %s" % ("true" if paused else "false"),
		"\tspace.reduced_motion = %s" % ("true" if reduced_motion else "false"),
	])
	return "\n".join(lines) + "\n"

static func _number_literal(value: float) -> String:
	# Locale-independent decimals, retaining precision for editable colors and speeds.
	return String.num(value, 14)

func view_size() -> Vector2:
	if view_size_override.x > 0.0 and view_size_override.y > 0.0:
		return view_size_override
	return get_viewport_rect().size

## Actual draw geometry, also useful when positioning another layer against this view.
func layout() -> Dictionary:
	if preset == null or not preset.is_valid():
		return {}
	var source := preset.far_texture.get_size()
	var target := view_size()
	var cover_scale := maxf(target.x / source.x, target.y / source.y)
	var tile := source * cover_scale
	return {
		"source_size": source,
		"view_size": target,
		"scale": cover_scale,
		"tile_size": tile,
		"vertical_offset": (target.y - tile.y) * 0.5,
	}

## Positive offset in source pixels. Depth 0 is stationary; negative scroll wraps safely.
func layer_phase(layer_index: int) -> float:
	if preset == null or not preset.is_valid() or layer_index < 0 or layer_index > 2:
		return 0.0
	var position_x := _travel if motion_mode == MotionMode.AUTOSCROLL else _visible_camera_x
	return fposmod(position_x * layer_depths[layer_index], float(preset.far_texture.get_width()))

func layer_rects(layer_index: int) -> Array[Rect2]:
	var geometry := layout()
	var result: Array[Rect2] = []
	if geometry.is_empty():
		return result
	var tile: Vector2 = geometry["tile_size"]
	var target: Vector2 = geometry["view_size"]
	var x: float = -layer_phase(layer_index) * float(geometry["scale"])
	var y: float = geometry["vertical_offset"]
	# The first copy starts at or left of the view; repeat until the right edge is covered.
	while x < target.x:
		result.append(Rect2(Vector2(x, y), tile))
		x += tile.x
	return result

func _draw() -> void:
	draw_rect(Rect2(Vector2.ZERO, view_size()), Color(0.01, 0.018, 0.038))
	if preset == null or not preset.is_valid():
		return
	var layers := preset.textures()
	var color_multiplier := layer_modulate()
	for layer_index in range(3):
		for rect in layer_rects(layer_index):
			draw_texture_rect(layers[layer_index], rect, false, color_multiplier)
