class_name CreateIcon
extends SceneTree

# /home/nordup/projects/godot/godot-the-gates/bin/godot.linuxbsd.editor.x86_64 -s create_icon.gd icon.ico icon_256.png icon_128.png icon_64.png icon_48.png icon_32.png icon_16.png
func _init() -> void:
	var arguments = OS.get_cmdline_args()
	if arguments.size() != 4 and arguments.size() != 9:
		print(
			"Usage:\n  godot -s CreateIcon.gd name <file>...\n",
			"\n",
			"Creates uncompressed windows ico file.\n",
			"Add --headless to hide Godot console.\n",
			"\n",
			"Arguments:\n",
			"  godot  path to Godot 4 beta2+ executable\n",
			"  name   path to created icon\n",
			"  <file> provide one or six files. If one provided it will be scaled for all\n",
			"         icon resolutions. Multiple files should be 16x16, 32x32, 48x48, 64x64,\n",
			"         128x128\n and 256x256 pixels big."
		)
		quit()
		return
	var images := []
	if arguments.size() == 9:
		var names := [arguments[3], arguments[4], arguments[5], arguments[6], arguments[7], arguments[8]]
		var check_names := {}
		for name in names:
			if check_names.has(name):
				printerr("File ", name, " was added more than once")
				return
			check_names[name] = true
		images = load_images(names)
	else:
		images = prepare_images(arguments[3])
	if not images.is_empty():
		save_icon(arguments[2], images)
	quit()


func load_images(paths: Array) -> Array:
	var images := []
	for path in paths:
		var image := Image.new()
		var error = image.load(path)
		if error:
			printerr("Could not load image: ", path)
			return []
		image.convert(Image.FORMAT_RGBA8)
		images.append(image)
	images.sort_custom(sort_images_by_size)
	var index := 0
	for size in [16, 32, 48, 64, 128, 256]:
		var image: Image = images[index]
		if image.get_width() != size:
			printerr("Image has incorrect width: ", image.get_width(), " expected: ", size)
			return []
		if image.get_height() != size:
			printerr("Image has incorrect height: ", image.get_height(), " expected: ", size)
			return []
		index += 1
	return images


func prepare_images(path: String) -> Array:
	var images := []
	for size in [16, 32, 48, 64, 128, 256]:
		var image := Image.new()
		var error = image.load(path)
		if error:
			printerr("Could not load image: ", path)
			return []
		image.convert(Image.FORMAT_RGBA8)
		image.resize(size, size)
		images.append(image)
	return images


func save_icon(destination_path: String, images: Array) -> void:
	var file = FileAccess.open(destination_path, FileAccess.WRITE)
	if not file:
		print("Could not open file for writing!\n", file.get_open_error())
		return
	var icon_creator := IconCreator.new()
	file.store_buffer(icon_creator.generate_icon(images))


static func sort_images_by_size(a: Image, b: Image) -> bool:
	return a.get_width() < b.get_width()


class IconCreator:
	const ADLER_MOD := 65521
	const ZLIB_BLOCK_SIZE := 16384
	const CRC_TABLE_SIZE := 256
	const ICON_ENTRY_SIZE := 16
	var PNG_SIGNATURE := PackedByteArray([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0a])
	var IHDR_SIGNATURE := PackedByteArray([0x49, 0x48, 0x44, 0x52])
	var IDAT_SIGNATURE := PackedByteArray([0x49, 0x44, 0x41, 0x54])
	var IEND_SIGNATURE := PackedByteArray([0x49, 0x45, 0x4e, 0x44])

	var crc_table: Array


	func _init() -> void:
		crc_table = generate_crc_table()


	func generate_icon(images: Array) -> PackedByteArray:
		var result := PackedByteArray()
		result.append_array(generate_icon_header(images.size()))
		var offset := result.size() + images.size() * ICON_ENTRY_SIZE
		var pngs := []
		for image in images:
			assert(image.get_format() == Image.FORMAT_RGBA8)
			var png := generate_png(image)
			pngs.append(png)
			var icon_entry := generate_icon_entry(image, png.size(), offset)
			result.append_array(icon_entry)
			offset += png.size()
		for png in pngs:
			result.append_array(png)
		return result


	func generate_icon_header(size: int) -> PackedByteArray:
		var result := PackedByteArray()
		result.append_array(lsb_first(0x0, 2)) # reserved
		result.append_array(lsb_first(0x1, 2)) # icon type
		result.append_array(lsb_first(size, 2)) # image count
		return result


	func generate_icon_entry(image: Image, size: int, offset: int) -> PackedByteArray:
		var result := PackedByteArray()
		result.append(image.get_width()) # width
		result.append(image.get_height()) # height
		result.append(0x0) # size of color palette
		result.append(0x0) # reserved
		result.append_array(lsb_first(0, 2)) # no color planes
		result.append_array(lsb_first(32, 2)) # bits per pixel
		result.append_array(lsb_first(size)) # size of embedded png 
		result.append_array(lsb_first(offset))
		return result


	func generate_png(image: Image) -> PackedByteArray:
		var result := PackedByteArray()
		var header_chunk := generate_header_chunk(image.get_width(), image.get_height())
		var data_chunk := generate_data_chunk(image)
		var end_chunk := generate_end_chunk()
		result.append_array(PNG_SIGNATURE)
		result.append_array(generate_chunk(header_chunk))
		result.append_array(generate_chunk(data_chunk))
		result.append_array(generate_chunk(end_chunk))
		return result


	func generate_chunk(chunk: PackedByteArray) -> PackedByteArray:
		var result := PackedByteArray()
		result.append_array(msb_first(chunk.size() - 4))
		result.append_array(chunk)
		result.append_array(msb_first(crc(chunk)))
		return result


	func generate_header_chunk(width: int, height: int) -> PackedByteArray:
		var result = PackedByteArray()
		result.append_array(IHDR_SIGNATURE)
		result.append_array(msb_first(width))
		result.append_array(msb_first(height))
		result.append(0x8) # bit depth
		result.append(0x6) # color type 32bit RGBA
		result.append(0x0) # compression method
		result.append(0x0) # filter method
		result.append(0x0) # interlace method
		return result


	func generate_data_chunk(image: Image) -> PackedByteArray:
		var filtered_pixels := filtered_pixels(image.get_width(), image.get_height(), image.get_data())
		var zlib_block_count := filtered_pixels.size() / ZLIB_BLOCK_SIZE + (1 if filtered_pixels.size() % ZLIB_BLOCK_SIZE else 0)
		var result := PackedByteArray()
		result.append_array(IDAT_SIGNATURE)
		result.append(0x78) # CMF
		result.append(0x1) # FLG
		for i in range(zlib_block_count):
			var last_block := i == zlib_block_count - 1
			result.append(0x1 if last_block else 0x0)
			var block_size := filtered_pixels.size() % ZLIB_BLOCK_SIZE if last_block else ZLIB_BLOCK_SIZE
			result.append_array(block_size(block_size))
			for b in range(block_size):
				result.append(filtered_pixels[i * ZLIB_BLOCK_SIZE + b])
		result.append_array(msb_first(adler(filtered_pixels)))
		return result


	func generate_end_chunk() -> PackedByteArray:
		return IEND_SIGNATURE


	func filtered_pixels(width: int, height: int, pixels: PackedByteArray) -> PackedByteArray:
		var result = PackedByteArray()
		for row in range(height):
			result.append(0x0)
			for column in range(width * 4):
				result.append(pixels[row * width * 4 + column])
		return result


	func generate_crc_table() -> Array:
		var result = []
		var c: int
		for n in range(CRC_TABLE_SIZE):
			c = n
			for _i in range(8):
				if (c & 1) != 0:
					c = 0xedb88320 ^ (c >> 1)
				else:
					c = c >> 1
			result.append(c)
		return result


	func crc(bytes: PackedByteArray) -> int:
		var c := 0xffffffff
		for i in range(bytes.size()):
			c = crc_table[(c ^ bytes[i]) & 0xff] ^ (c >> 8)
		return c ^ 0xffffffff


	func adler(bytes: PackedByteArray) -> int:
		var a := 1
		var b := 0
		for byte in bytes:
			a = (a + byte) % ADLER_MOD
			b = (a + b) % ADLER_MOD
		return b << 16 | a


	func msb_first(i: int) -> PackedByteArray:
		var result := PackedByteArray()
		result.append((i >> 24) & 0xff)
		result.append((i >> 16) & 0xff)
		result.append((i >> 8) & 0xff)
		result.append(i & 0xff)
		return result


	func lsb_first(i: int, size = 4) -> PackedByteArray:
		var result := PackedByteArray()
		for _s in range(size):
			result.append(i & 0xff)
			i = i >> 8
		return result


	func block_size(i: int) -> PackedByteArray:
		var result := PackedByteArray()
		result.append(i & 0xff)
		result.append((i >> 8) & 0xff)
		result.append((i & 0xff) ^ 0xff)
		result.append(((i >> 8) & 0xff) ^ 0xff)
		return result
