extends Reference


var little_endian = preload('./little_endian.gd').new()
var lzw = preload('./gif-lzw/lzw.gd').new()
var used_proc_count: int = 4


class GraphicControlExtension:
	var extension_introducer: int = 0x21
	var graphic_control_label: int = 0xf9

	var block_size: int = 4
	var packed_fields: int = 0b00001000
	var delay_time: int = 0
	var transparent_color_index: int = 0

	func _init(_delay_time: int,
			use_transparency: bool = false,
			_transparent_color_index: int = 0):
		delay_time = _delay_time
		transparent_color_index = _transparent_color_index
		if use_transparency:
			packed_fields = 0b00001001

	func to_bytes() -> PoolByteArray:
		var little_endian = preload('./little_endian.gd').new()
		var result: PoolByteArray = PoolByteArray([])

		result.append(extension_introducer)
		result.append(graphic_control_label)

		result.append(block_size)
		result.append(packed_fields)
		result += little_endian.int_to_2bytes(delay_time)
		result.append(transparent_color_index)

		result.append(0)

		return result

class ImageDescriptor:
	var image_separator: int = 0x2c
	var image_left_position: int = 0
	var image_top_position: int = 0
	var image_width: int
	var image_height: int
	var packed_fields: int = 0b10000000

	func _init(_image_left_position: int,
			_image_top_position: int,
			_image_width: int,
			_image_height: int,
			size_of_local_color_table: int):
		image_left_position = _image_left_position
		image_top_position = _image_top_position
		image_width = _image_width
		image_height = _image_height
		packed_fields = packed_fields | (0b111 & size_of_local_color_table)

	func to_bytes() -> PoolByteArray:
		var little_endian = preload('./little_endian.gd').new()
		var result: PoolByteArray = PoolByteArray([])

		result.append(image_separator)
		result += little_endian.int_to_2bytes(image_left_position)
		result += little_endian.int_to_2bytes(image_top_position)
		result += little_endian.int_to_2bytes(image_width)
		result += little_endian.int_to_2bytes(image_height)
		result.append(packed_fields)

		return result

class LocalColorTable:
	var colors: Array = []

	func log2(value: float) -> float:
		return log(value) / log(2.0)

	func get_size() -> int:
		if colors.size() <= 1:
			return 0
		return int(ceil(log2(colors.size()) - 1))

	func to_bytes() -> PoolByteArray:
		var result: PoolByteArray = PoolByteArray([])

		for v in colors:
			result.append(v[0])
			result.append(v[1])
			result.append(v[2])

		if colors.size() != int(pow(2, get_size() + 1)):
			for i in range(int(pow(2, get_size() + 1)) - colors.size()):
				result += PoolByteArray([0, 0, 0])

		return result

class ApplicationExtension:
	var extension_introducer: int = 0x21
	var extension_label: int = 0xff

	var block_size: int = 11
	var application_identifier: PoolByteArray
	var appl_authentication_code: PoolByteArray

	var application_data: PoolByteArray

	func _init(_application_identifier: String,
			_appl_authentication_code: String):
		application_identifier = _application_identifier.to_ascii()
		appl_authentication_code = _appl_authentication_code.to_ascii()

	func to_bytes() -> PoolByteArray:
		var result: PoolByteArray = PoolByteArray([])

		result.append(extension_introducer)
		result.append(extension_label)
		result.append(block_size)
		result += application_identifier
		result += appl_authentication_code

		result.append(application_data.size())
		result += application_data

		result.append(0)

		return result

class ImageData:
	var lzw_minimum_code_size: int
	var image_data: PoolByteArray

	func to_bytes() -> PoolByteArray:
		var result: PoolByteArray = PoolByteArray([])
		result.append(lzw_minimum_code_size)

		var block_size_index: int = 0
		var i: int = 0
		var data_index: int = 0
		while data_index < image_data.size():
			if i == 0:
				result.append(0)
				block_size_index = result.size() - 1
			result.append(image_data[data_index])
			result[block_size_index] += 1
			data_index += 1
			i += 1
			if i == 254:
				i = 0

		if not image_data.empty():
			result.append(0)

		return result

class ConvertedImage:
	var image_converted_to_codes: PoolByteArray
	var color_table: Array
	var transparency_color_index: int
	var width: int
	var height: int

class ConvertionResult:
	var converted_image: ConvertedImage = ConvertedImage.new()
	var error: int = Error.OK

	func with_error_code(_error: int) -> ConvertionResult:
		error = _error
		return self

class ThreadWriteFrameResult:
	var frame_data: PoolByteArray = PoolByteArray([])
	var error: int = Error.OK

	func with_error_code(_error: int) -> ThreadWriteFrameResult:
		error = _error
		return self

enum Error {
	OK = 0,
	EMPTY_IMAGE = 1,
	BAD_IMAGE_FORMAT = 2
}

# File data and Header
var data: PoolByteArray = 'GIF'.to_ascii() + '89a'.to_ascii()

func _init(_width: int, _height: int):
	# Logical Screen Descriptor
	var width: int = _width
	var height: int = _height
	# not Global Color Table Flag
	# Color Resolution = 8 bits
	# Sort Flag = 0, not sorted.
	# Size of Global Color Table set to 0
	# because we'll use only Local Tables
	var packed_fields: int = 0b01110000
	var background_color_index: int = 0
	var pixel_aspect_ratio: int = 0

	data += little_endian.int_to_2bytes(width)
	data += little_endian.int_to_2bytes(height)
	data.append(packed_fields)
	data.append(background_color_index)
	data.append(pixel_aspect_ratio)

	var application_extension: ApplicationExtension = ApplicationExtension.new(
			"NETSCAPE",
			"2.0")
	application_extension.application_data = PoolByteArray([1, 0, 0])
	data += application_extension.to_bytes()

func calc_delay_time(frame_delay: float) -> int:
	return int(ceil(frame_delay / 0.01))

func color_table_to_indexes(colors: Array) -> PoolByteArray:
	var result: PoolByteArray = PoolByteArray([])
	for i in range(colors.size()):
		result.append(i)
	return result

func find_color_table_if_has_less_than_256_colors(image: Image) -> Dictionary:
	image.lock()
	var result: Dictionary = {}
	var image_data: PoolByteArray = image.get_data()

	for i in range(0, image_data.size(), 4):
		var color: Array = [int(image_data[i]), int(image_data[i + 1]), int(image_data[i + 2]), int(image_data[i + 3])]
		if not color in result:
			result[color] = result.size()
		if result.size() > 256:
			break

	image.unlock()
	return result

func change_colors_to_codes(image: Image,
		color_palette: Dictionary,
		transparency_color_index: int) -> PoolByteArray:
	image.lock()
	var image_data: PoolByteArray = image.get_data()
	var result: PoolByteArray = PoolByteArray([])

	for i in range(0, image_data.size(), 4):
		var color: Array = [int(image_data[i]), int(image_data[i + 1]), int(image_data[i + 2]), int(image_data[i + 3])]
		if color in color_palette:
			if color[3] == 0 and transparency_color_index != -1:
				result.append(transparency_color_index)
			else:
				result.append(color_palette[color])
		else:
			result.append(0)
			push_warning('change_colors_to_codes: color not found! [%d, %d, %d, %d]' % color)

	image.unlock()
	return result

func sum_color(color: Array) -> int:
	return color[0] + color[1] + color[2] + color[3]

func find_transparency_color_index(color_table: Dictionary) -> int:
	for color in color_table:
		if sum_color(color) == 0:
			return color_table[color]
	return -1

func find_transparency_color_index_for_quantized_image(color_table: Array) -> int:
	for i in range(color_table.size()):
		if sum_color(color_table[i]) == 0:
			return i
	return -1

func make_sure_color_table_is_at_least_size_4(color_table: Array) -> Array:
	var result := [] + color_table
	if color_table.size() < 4:
		for i in range(4 - color_table.size()):
			result.append([0, 0, 0, 0])
	return result

func convert_image(image: Image, quantizator) -> ConvertionResult:
	var result := ConvertionResult.new()

	# check if image is of good format
	if image.get_format() != Image.FORMAT_RGBA8:
		return result.with_error_code(Error.BAD_IMAGE_FORMAT)

	# check if image isn't empty
	if image.is_empty():
		return result.with_error_code(Error.EMPTY_IMAGE)

	var found_color_table: Dictionary = find_color_table_if_has_less_than_256_colors(
			image)

	var image_converted_to_codes: PoolByteArray
	var transparency_color_index: int = -1
	var color_table: Array
	if found_color_table.size() <= 256: # we don't need to quantize the image.
		# exporter images always try to include transparency because I'm lazy.
		transparency_color_index = find_transparency_color_index(found_color_table)
		if transparency_color_index == -1 and found_color_table.size() <= 255:
			found_color_table[[0, 0, 0, 0]] = found_color_table.size()
			transparency_color_index = found_color_table.size() - 1
		image_converted_to_codes = change_colors_to_codes(
				image, found_color_table, transparency_color_index)
		color_table = make_sure_color_table_is_at_least_size_4(found_color_table.keys())
	else: # we have to quantize the image.
		var quantization_result: Array = quantizator.quantize_and_convert_to_codes(image)
		image_converted_to_codes = quantization_result[0]
		color_table = quantization_result[1]
		# don't find transparency index if the quantization algorithm
		# provides it as third return value
		if quantization_result.size() == 3:
			transparency_color_index = 0 if quantization_result[2] else -1
		else:
			transparency_color_index = find_transparency_color_index_for_quantized_image(quantization_result[1])

	result.converted_image.image_converted_to_codes = image_converted_to_codes
	result.converted_image.color_table = color_table
	result.converted_image.transparency_color_index = transparency_color_index
	result.converted_image.width = image.get_width()
	result.converted_image.height = image.get_height()

	return result.with_error_code(Error.OK)

func write_frame(image: Image, frame_delay: float, quantizator) -> int:
	var converted_image_result := convert_image(image, quantizator)
	if converted_image_result.error != Error.OK:
		return converted_image_result.error

	var converted_image := converted_image_result.converted_image
	return write_frame_from_conv_image(converted_image, frame_delay)

func write_frame_from_conv_image(converted_image: ConvertedImage,
		frame_delay: float) -> int:
	var delay_time := calc_delay_time(frame_delay)

	var color_table_indexes = color_table_to_indexes(converted_image.color_table)
	var compressed_image_result: Array = lzw.compress_lzw(
		converted_image.image_converted_to_codes, color_table_indexes)
	var compressed_image_data: PoolByteArray = compressed_image_result[0]
	var lzw_min_code_size: int = compressed_image_result[1]

	var table_image_data_block: ImageData = ImageData.new()
	table_image_data_block.lzw_minimum_code_size = lzw_min_code_size
	table_image_data_block.image_data = compressed_image_data

	var local_color_table: LocalColorTable = LocalColorTable.new()
	local_color_table.colors = converted_image.color_table

	var image_descriptor: ImageDescriptor = ImageDescriptor.new(0, 0,
			converted_image.width,
			converted_image.height,
			local_color_table.get_size())

	var graphic_control_extension: GraphicControlExtension
	if converted_image.transparency_color_index != -1:
		graphic_control_extension = GraphicControlExtension.new(
				delay_time, true, converted_image.transparency_color_index)
	else:
		graphic_control_extension = GraphicControlExtension.new(
				delay_time, false, 0)

	data += graphic_control_extension.to_bytes()
	data += image_descriptor.to_bytes()
	data += local_color_table.to_bytes()
	data += table_image_data_block.to_bytes()

	return Error.OK

func scale_conv_image(converted_image: ConvertedImage, scale_factor: int) -> ConvertedImage:
	var result = ConvertedImage.new()

	result.image_converted_to_codes = PoolByteArray([])
	result.color_table = converted_image.color_table.duplicate()
	result.transparency_color_index = converted_image.transparency_color_index
	result.width = converted_image.width * scale_factor
	result.height = converted_image.height * scale_factor

	for y in range(converted_image.height):
		var row := PoolByteArray([])
		for x in range(converted_image.width):
			for i in range(scale_factor):
				row.append(converted_image.image_converted_to_codes[(y * converted_image.width) + x])
		for i in range(scale_factor):
			result.image_converted_to_codes += row
		row = PoolByteArray([])

	return result

func export_file_data() -> PoolByteArray:
	return data + PoolByteArray([0x3b])