# based on: 
# - https://www.youtube.com/watch?v=5CKvGYqagyI
# - https://pastebin.com/wXCqk1nQ

class_name PixelCountComputeShader extends Node

@export var paint_viewport_path: NodePath = NodePath()

var red_score := 0
var blue_score := 0
var pink_score := 0

var thread: Thread
var semaphore: Semaphore
var mutex: Mutex
var exit := false

var paint_texture: ViewportTexture
var paint_viewport: Viewport

var rd: RenderingDevice
var shader
var pipeline

var pixel_count_result_buffer
var pixel_count_uniform

var v_tex
var samp
var tex_uniform

var uniform_set 

func _ready():
	paint_viewport = get_node(paint_viewport_path)
	paint_texture = paint_viewport.get_texture()
	
	mutex = Mutex.new()
	semaphore = Semaphore.new()
	thread = Thread.new()
	thread.start(_thread_calculate_score)

	# We will be using our own RenderingDevice to handle the compute commands
	rd = RenderingServer.create_local_rendering_device()
	
	# Create shader and pipeline
	var shader_file = load("res://materials/shader/PixelCountCompute.glsl")
	var shader_spirv = shader_file.get_spirv()
	shader = rd.shader_create_from_spirv(shader_spirv)
	pipeline = rd.compute_pipeline_create(shader)
	
	var pb = PackedInt32Array([0,0,0])
	var pbb = pb.to_byte_array()
	
	pixel_count_result_buffer = rd.storage_buffer_create(pbb.size(), pbb)
	
	pixel_count_uniform = RDUniform.new()
	pixel_count_uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER
	pixel_count_uniform.binding = 0
	pixel_count_uniform.add_id(pixel_count_result_buffer)
	
	var img = paint_texture.get_image()
	var ta = img.get_data()
	
	var fmt = RDTextureFormat.new()
	fmt.width = paint_texture.get_width()
	fmt.height = paint_texture.get_height()
	fmt.usage_bits = RenderingDevice.TEXTURE_USAGE_CAN_UPDATE_BIT | RenderingDevice.TEXTURE_USAGE_SAMPLING_BIT
	fmt.format = RenderingDevice.DATA_FORMAT_R8G8B8A8_SRGB
	
	v_tex = rd.texture_create(fmt, RDTextureView.new(), [ta])
	var samp_state = RDSamplerState.new()
	samp_state.unnormalized_uvw = true
	samp = rd.sampler_create(samp_state)
	
	tex_uniform = RDUniform.new()
	tex_uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_SAMPLER_WITH_TEXTURE
	tex_uniform.binding = 1
	tex_uniform.add_id(samp)
	tex_uniform.add_id(v_tex)
	
	uniform_set = rd.uniform_set_create([pixel_count_uniform, tex_uniform], shader, 0)

func recalculate_score():
	await RenderingServer.frame_post_draw
	semaphore.post()
	
func get_blue_score() -> int:
	var result := blue_score
	
	return result
	
func get_red_score() -> int:
	var result := red_score
	
	return result

func _thread_calculate_score():
	while true:
		semaphore.wait()
		
		mutex.lock()
		var should_exit := exit
		mutex.unlock()
		
		if should_exit:
			break
		
		mutex.lock()
		var image := paint_texture.get_image()
		#var image := paint_viewport.get_texture()
		mutex.unlock()
		
		var size = image.get_size()
		var ta = image.get_data()
		
		rd.texture_update(v_tex, 0, ta)
	
		# Start compute list to start recording our compute commands
		var compute_list = rd.compute_list_begin()
		# Bind the pipeline, this tells the GPU what shader to use
		rd.compute_list_bind_compute_pipeline(compute_list, pipeline)
		# Binds the uniform set with the data we want to give our shader
		rd.compute_list_bind_uniform_set(compute_list, uniform_set, 0)
		# Dispatch 1x1x1 (XxYxZ) work groups
		rd.compute_list_dispatch(compute_list, 1024/4, 1024/4, 1)
		#rd.compute_list_add_barrier(compute_list)
		# Tell the GPU we are done with this compute task
		rd.compute_list_end()

		# Force the GPU to start our commands
		rd.submit()
		# Force the CPU to wait for the GPU to finish with the recorded commands
		rd.sync()

		mutex.lock()
		# Now we can grab our data from the storage buffer
		var byte_data = rd.buffer_get_data(pixel_count_result_buffer)
		var output := byte_data.to_int32_array()
		
		red_score = floor(output[0]/100)
		blue_score = floor(output[1]/100)
		pink_score = floor(output[2]/100)
		
		var pb = PackedInt32Array([0,0,0])
		var pbb = pb.to_byte_array()
		rd.buffer_update(pixel_count_result_buffer, 0, pbb.size(), pbb)
		mutex.unlock()

func _exit_tree():
	mutex.lock()
	exit = true
	mutex.unlock()
	
	semaphore.post()
	thread.wait_to_finish()
