extends Node2D

var type = "sprite"

#Passed Variables
var imageData = null
var tex = null
@export var path = ""

var loadedImageData = null

var id = 0
var parentId = null
var parentSprite = null

var imageSize = Vector2.ZERO

#Node Reference
@onready var sprite = $WobbleOrigin/DragOrigin/Sprite

@onready var grabArea = $WobbleOrigin/DragOrigin/Grab

@onready var dragOrigin = $WobbleOrigin/DragOrigin
@onready var dragger = $WobbleOrigin/Dragger

@onready var originSprite = $WobbleOrigin/DragOrigin/Sprite/Origin

@onready var wob = $WobbleOrigin

@onready var outlineScene = preload("res://ui_scenes/selectedSprite/outline.tscn")
@onready var opacityShader = preload("res://ui_scenes/selectedSprite/opacity.gdshader")

var opacityMaterial = null

#Visuals
var mouseOffset = Vector2.ZERO
var grabDelay = 0
var size = Vector2(1,1)

var showOnTalk = 0
var showOnBlink = 0

var z = 0

#Movement
var heldTicks = 0
var dragSpeed = 0


#Origin
var origTick = 0
var offset = Vector2.ZERO

#Wobble
var xFrq = 0.0
var xAmp = 0.0

var yFrq = 0.0
var yAmp = 0.0

#Rotation Wobble
var rFrq = 0.0
var rAmp = 0.0

#Rotational Drag
var rdragStr = 0
var rLimitMax = 180
var rLimitMin = -180

#Layer
var costumeLayers = [1,1,1,1,1,1,1,1,1,1]

#Stretch
var stretchAmount = 0.0

#Ignore Bounce
var ignoreBounce = false

#Animation
var frames = 1
var animSpeed = 0
var animTime = 0.0  # Time accumulator for animations

var remadePolygon = false

var clipped = false

var time = 0.0  # Time accumulator for physics calculations

#Opacity
var spriteOpacity = 1.0
var affectChildrenOpacity = false

#Vis toggle
var toggle = "null"

#Wobble Sync Group
var wobbleSyncGroup = ""

func _ready():
	
	Global.main.spriteVisToggles.connect(visToggle)
	
	var img = Image.new()
	var err = img.load(path)
	if err != OK:
		#Runs if image import fails. Needs error dialog box at some point
		if loadedImageData == null:
			Global.epicFail(err)
			print_debug("Failed to load image.")
			queue_free()
			return
		else:
			var data = Marshalls.base64_to_raw(loadedImageData)
			var errr = img.load_png_from_buffer(data)
			if errr != OK:
				Global.epicFail(err)
				print_debug("Failed to load image.")
				queue_free()
				return
		
	var texture = ImageTexture.new()
	texture = ImageTexture.create_from_image(img)
	
	
	tex = texture
	imageData = img
	
	imageSize = img.get_size()
	
	sprite.texture = tex
	
	var bitmap = BitMap.new()
	bitmap.create_from_image_alpha(imageData)
	
	var polygons = bitmap.opaque_to_polygons(Rect2(Vector2(0, 0), bitmap.get_size()),4.0) #bitmap.get_size()

	var b = false
	for polygon in polygons:
		b = true
		var collider = CollisionPolygon2D.new()
		collider.polygon = polygon
		grabArea.add_child(collider)
		
		var outline = outlineScene.instantiate()
		outline.points = polygon
		outline.add_point(outline.points[0])
		grabArea.add_child(outline)
	
	size = imageData.get_size()
	grabArea.position = size*-0.5
	
	sprite.offset = offset
	
	grabArea.position = (size*-0.5) + offset
	
	changeFrames()
	setZIndex()
	
	if frames > 1:
		remakePolygon()
	if !b:
		remakePolygon()
	
	
	add_to_group(str(id))
	await get_tree().create_timer(0.1).timeout
	if parentId != null:
		var nodes = get_tree().get_nodes_in_group(str(parentId))
		get_parent().remove_child(self)
		nodes[0].sprite.add_child(self)
		parentSprite = nodes[0]
		set_owner(nodes[0].sprite)
	
	setClip(clipped)
	
	# Always initialize opacity shader
	updateShaderOpacity()
	
	if Global.filtering:
		sprite.texture_filter = 2
	
func replaceSprite(pathNew):
	var img = Image.new()
	var err = img.load(pathNew)
	if err != OK:
		#Runs if image import fails. 
		Global.epicFail(err)
		print_debug("Failed to load image.")
		return
	
	path = pathNew
	
	var texture = ImageTexture.new()
	texture = ImageTexture.create_from_image(img)
	
	
	tex = texture
	imageData = img
	
	
	sprite.texture = tex
	
	var bitmap = BitMap.new()
	bitmap.create_from_image_alpha(imageData)
	
	var polygons = bitmap.opaque_to_polygons(Rect2(Vector2(0, 0), bitmap.get_size()))
	
	for i in grabArea.get_children():
		i.queue_free()
	
	var b = false
	for polygon in polygons:
		b = true
		var collider = CollisionPolygon2D.new()
		collider.polygon = polygon
		grabArea.add_child(collider)
	
		var outline = outlineScene.instantiate()
		outline.points = polygon
		outline.add_point(outline.points[0])
		grabArea.add_child(outline)
	size = imageData.get_size()

	sprite.offset = offset
	
	grabArea.position = (size*-0.5) + offset
	
	if !b:
		remakePolygon()

func _process(delta):
	time += delta
	if Global.heldSprite == self:
		
		grabArea.visible = true
		originSprite.visible = true
		
	else:
		grabArea.visible = false
		originSprite.visible = false
	
	# Disable grabArea input and detection when dialogs are open
	if Global.main and Global.main.fileSystemOpen:
		grabArea.input_pickable = false
		grabArea.monitoring = false
		grabArea.monitorable = false
	else:
		grabArea.input_pickable = true
		grabArea.monitoring = true
		grabArea.monitorable = true
	
	var glob = dragger.global_position
	if ignoreBounce:
		glob.y -= Global.main.bounceChange
	
	drag(delta)
	wobble()
	
	var length = (glob.y - dragger.global_position.y)
	
	rotationalDrag(length,delta)
	stretch(length,delta)
	
	if grabDelay > 0:
		grabDelay -= 1
	
	talkBlink()
	
	animation(delta)

func animation(delta):
	if animSpeed > 0 and frames > 1:
		animTime += delta
		# Convert animSpeed to frames per second - no additional scaling needed since this controls frame rate
		var frameRate = animSpeed / 60.0  # Normalize from old frame-based system
		var frameInterval = 1.0 / frameRate
		
		if animTime >= frameInterval:
			animTime -= frameInterval
			if sprite.frame == frames - 1:
				sprite.frame = 0
			else:
				sprite.frame += 1
	if frames > 1:
		remakePolygon()

func setZIndex():
	sprite.z_index = z

func talkBlink():
	var faded = 0.2 * int(Global.main and Global.main.editMode)
	var value = (showOnTalk + (showOnBlink*3)) + (int(Global.speaking)*10) + (int(Global.blink)*20)
	var yes = [0,10,20,30,1,21,12,32,3,13,4,15,26,36,27,38].has(int(value))
	var baseAlpha = max(int(yes), faded)
	
	# Only handle talk/blink/edit visibility with modulate
	# Opacity slider effects are handled by shader
	sprite.self_modulate.a = 1.0  # Always keep original texture alpha for clipping
	var previousAlpha = sprite.modulate.a
	sprite.modulate.a = baseAlpha  # Only apply talk/blink/edit visibility
	
	# Update shader if modulate alpha changed (for editor dimming)
	if previousAlpha != baseAlpha:
		updateShaderOpacity()

func calculateParentOpacityMultiplier():
	var multiplier = 1.0
	var currentParent = parentSprite
	
	# Traverse up the entire parent hierarchy
	while currentParent != null:
		if currentParent.affectChildrenOpacity:
			multiplier *= currentParent.spriteOpacity
		currentParent = currentParent.parentSprite
	
	return multiplier

## Update wobble parameters and sync to group if needed
func updateWobbleParameter(parameter: String, value: float):
	print("DEBUG: updateWobbleParameter called - parameter: ", parameter, ", value: ", value, ", sprite ID: ", id)
	print("DEBUG: Current wobbleSyncGroup: '", wobbleSyncGroup, "'")
	
	# Update this sprite's parameter first
	match parameter:
		"xFrq": xFrq = value
		"xAmp": xAmp = value
		"yFrq": yFrq = value
		"yAmp": yAmp = value
		"rFrq": rFrq = value
		"rAmp": rAmp = value
	
	print("DEBUG: Parameter updated on sprite. New values - xFrq:", xFrq, " xAmp:", xAmp, " yFrq:", yFrq, " yAmp:", yAmp, " rFrq:", rFrq, " rAmp:", rAmp)
	
	# If this sprite is in a sync group, update the entire group
	if wobbleSyncGroup != "" and WobbleSyncManager:
		print("DEBUG: Sprite is in sync group '", wobbleSyncGroup, "' - updating group wobble parameters")
		WobbleSyncManager.updateGroupWobble(wobbleSyncGroup, xFrq, xAmp, yFrq, yAmp, rFrq, rAmp)
		
		# Don't refresh UI immediately - let the sync process complete first
		# The wobble sync control will handle UI updates when needed
	else:
		print("DEBUG: Sprite not in sync group or WobbleSyncManager not available")

## Check if this sprite is synced to a group
func isSynced() -> bool:
	return wobbleSyncGroup != ""

func delete():
	# Clean up from wobble sync groups
	if wobbleSyncGroup != "" and WobbleSyncManager:
		WobbleSyncManager.onSpriteDeleted(id)
	queue_free()

func _physics_process(delta):
	if Global.heldSprite == self:
		var dir = pressingDirection()
		if Input.is_action_pressed("origin"):
			moveOrigin(dir)
		else:
			moveSprite(dir)
	else:
		set_physics_process(false)

func pressingDirection():
	var dir = Vector2.ZERO
	
	dir.x = Input.get_action_strength("move_left") - Input.get_action_strength("move_right")
	dir.y = Input.get_action_strength("move_up") - Input.get_action_strength("move_down")
	return dir
	
func moveSprite(dir):
	if dir != Vector2.ZERO:
		heldTicks += 1
	else:
		heldTicks = 0
	
	if heldTicks > 30 or heldTicks == 1:
		var multiplier = 2
		if heldTicks == 1:
			multiplier = 1
		position -= dir * multiplier
	
	position = Vector2(int(position.x),int(position.y))

func moveOrigin(dir):
	if dir != Vector2.ZERO:
		origTick += 1
	else:
		origTick = 0
	
	if origTick > 30 or origTick == 1:
		var multiplier = 2
		if origTick == 1:
			multiplier = 1

		offset += dir * multiplier
		position -= dir * multiplier
		
	offset = Vector2(int(offset.x),int(offset.y))
	
	sprite.offset = offset
	grabArea.position = (size*-0.5) + offset

func drag(delta):
	if dragSpeed == 0:
		dragger.global_position = wob.global_position
	else:
		# Convert dragSpeed to a proper delta-time lerp factor
		# Higher dragSpeed = slower movement, so we invert it
		# Use Global.main.physicsTimeMultiplier for consistent physics timing
		var physicsMultiplier = Global.main.physicsTimeMultiplier if Global.main else 10.0
		var lerpFactor = clamp(delta * (physicsMultiplier * 10.0 / max(dragSpeed, 0.1)), 0.0, 1.0)
		dragger.global_position = lerp(dragger.global_position, wob.global_position, lerpFactor)
		dragOrigin.global_position = dragger.global_position

func wobble():
	# Use Global.main.physicsTimeMultiplier for consistent physics timing
	var physicsMultiplier = Global.main.physicsTimeMultiplier if Global.main else 10.0
	var scaledTime = time * physicsMultiplier
	wob.position.x = sin(scaledTime * xFrq) * xAmp
	wob.position.y = sin(scaledTime * yFrq) * yAmp

func rotationalDrag(length, delta):
	var yvel = (length * rdragStr)
	
	#Calculate Max angle
	yvel = clamp(yvel, rLimitMin, rLimitMax)
	
	# Add rotation wobble as percentage of rotational limits range
	var rotationRange = rLimitMax - rLimitMin
	var physicsMultiplier = Global.main.physicsTimeMultiplier if Global.main else 10.0
	var scaledTime = time * physicsMultiplier
	var rotationWobble = sin(scaledTime * rFrq) * (rotationRange * rAmp * 0.01)
	var totalRotation = yvel + rotationWobble
	
	# Use delta-based lerp for smooth rotation with slower speed
	var lerpFactor = clamp(delta * (physicsMultiplier * 1.0), 0.0, 1.0)  # Reduced from 15.0 to 4.0
	sprite.rotation = lerp_angle(sprite.rotation, deg_to_rad(totalRotation), lerpFactor)

func stretch(length, delta):
	var yvel = (length * stretchAmount * 0.01)
	var target = Vector2(1.0 - yvel, 1.0 + yvel)
	
	# Use delta-based lerp for smooth stretching
	var physicsMultiplier = Global.main.physicsTimeMultiplier if Global.main else 10.0
	var lerpFactor = clamp(delta * (physicsMultiplier * 30.0), 0.0, 1.0)
	sprite.scale = lerp(sprite.scale, target, lerpFactor)

func changeCollision(enable):
	grabArea.monitorable = enable
	grabArea.monitorable = enable

func changeFrames():
	sprite.hframes = frames
	sprite.frame = 0

func remakePolygon():
	if remadePolygon:
		return
	for c in grabArea.get_children():
		c.queue_free()
	var collider = CollisionShape2D.new()
	var shape = RectangleShape2D.new()
	shape.size = Vector2(imageSize.y,imageSize.y)
	collider.shape = shape
	collider.position = Vector2(imageSize.x,imageSize.y) * Vector2(0.5,0.5)
	grabArea.add_child(collider)
	
	var p = imageSize.y * 0.5
	var outline = outlineScene.instantiate()
	outline.add_point(Vector2(-p,-p))
	outline.add_point(Vector2(p,-p))
	outline.add_point(Vector2(p,p))
	outline.add_point(Vector2(-p,p))
	outline.add_point(Vector2(-p,-p))
	outline.position = collider.position
	grabArea.add_child(outline)
	
	remadePolygon = true
	
func setClip(toggle):
	if toggle:
		sprite.clip_children = CLIP_CHILDREN_AND_DRAW
		
		for node in getAllLinkedSprites():
			node.z = z
			node.setZIndex()
		
	else:
		sprite.clip_children = CLIP_CHILDREN_DISABLED
		
	clipped = toggle

func getAllLinkedSprites():
	var nodes = get_tree().get_nodes_in_group("saved")
	var linkedSprites = []
	for node in nodes:
		if node.parentId == id:
			linkedSprites.append(node)
	return linkedSprites

func updateOpacity():
	# Force a refresh of opacity for this sprite and its children
	talkBlink()
	updateShaderOpacity()
	
	# Update all child sprites in the entire hierarchy if this sprite affects children
	if affectChildrenOpacity:
		updateChildrenOpacityRecursively()

func updateShaderOpacity():
	# Calculate total opacity from user settings and parent hierarchy
	var totalOpacity = spriteOpacity * calculateParentOpacityMultiplier()
	
	# Also factor in the modulate alpha for editor dimming/onion skinning
	var editorAlpha = sprite.modulate.a
	var finalOpacity = totalOpacity * editorAlpha
	
	# Always apply the opacity shader to ensure it's loaded
	if opacityMaterial == null:
		opacityMaterial = ShaderMaterial.new()
		opacityMaterial.shader = opacityShader
	
	opacityMaterial.set_shader_parameter("opacity", finalOpacity)
	sprite.material = opacityMaterial

func updateChildrenOpacityRecursively():
	var linkedSprites = getAllLinkedSprites()
	for child in linkedSprites:
		child.talkBlink()
		child.updateShaderOpacity()
		# Always update the entire hierarchy below this child
		child.updateChildrenOpacityRecursively()

func visToggle(keys):
	if keys.has(toggle):
		$WobbleOrigin/DragOrigin.visible = !$WobbleOrigin/DragOrigin.visible

func makeVis():
	$WobbleOrigin/DragOrigin.visible = true
