class_name Palette
extends Resource

const DEFAULT_WIDTH := 8
const DEFAULT_HEIGHT := 8

# Metadata
@export var name := "Custom Palette"
@export var comment := ""

# Grid size
@export var width := DEFAULT_WIDTH
@export var height := DEFAULT_HEIGHT

## Sparse colors dictionary - actual color position in the palette is determined by its index
@export var colors := {}

## How many colors fit in palette grid
@export var colors_max := 0


func _init(
	init_name: String = "Custom Palette",
	init_width: int = DEFAULT_WIDTH,
	init_height: int = DEFAULT_HEIGHT,
	init_comment: String = ""
) -> void:
	name = init_name
	comment = init_comment
	width = init_width
	height = init_height
	colors_max = init_width * init_height
	colors = {}


func edit(new_name: String, new_width: int, new_height: int, new_comment: String) -> void:
	var old_width = width
	width = new_width
	height = new_height
	name = new_name
	comment = new_comment

	var old_colors_max = colors_max
	colors_max = width * height

	if colors_max < old_colors_max:
		# If size was reduced colors must be reindexed to fit into new smaller size
		reindex_colors_on_size_reduce(true)

	if old_width < new_width and colors_max > old_colors_max:
		# If width increases colors have to be reindexed so they keep same grid positions
		# unless the height has become smaller and we have to re-position the colors
		# so that they won't get erased
		reindex_colors_on_width_increase(old_width)


## Iterates all colors from lowest index and reindexes them so they start at zero index
## Remove trailing removes all colors that are over colors_max limit and thus don't fit into grid
func reindex_colors_on_size_reduce(remove_trailing: bool) -> void:
	var sorted_colors_indexes = colors.keys()
	sorted_colors_indexes.sort()

	var new_index = 0
	for old_index in sorted_colors_indexes:
		# Color cannot fit into grid anymore - erase it
		if remove_trailing and new_index >= colors_max:
			colors.erase(old_index)

		# Move color to new lower index - erase it from its original index
		elif new_index < old_index:
			colors[new_index] = colors[old_index]
			colors[new_index].index = new_index
			colors.erase(old_index)

		new_index += 1


## Adds difference of old and new width to color indexes
## so they remain on the same position as before resize
func reindex_colors_on_width_increase(old_width: int) -> void:
	var sorted_colors_indexes = colors.keys()
	sorted_colors_indexes.sort()

	var new_colors = {}
	for old_index in sorted_colors_indexes:
		var new_index: int = old_index + (width - old_width) * (old_index / old_width)
		new_colors[new_index] = colors[old_index]

	colors = new_colors


## Adds new color to the first empty swatch
func add_color(new_color: Color, start_index: int = 0) -> void:
	if start_index >= colors_max:
		return

	# If palette is full automatically increase the palette height
	if is_full():
		height += 1

	# Find the first empty index since start index and insert a new color
	for i in range(start_index, colors_max):
		if not colors.has(i):
			colors[i] = PaletteColor.new(new_color, i)
			break


## Returns color at index or null if no color exists
func get_color(index: int):
	var palette_color = colors.get(index)
	if palette_color != null:
		return palette_color.color
	return null


## Changes color data
func set_color(index: int, new_color: Color) -> void:
	if self.colors.has(index):
		self.colors[index].color = new_color


## Removes a color at the specified index
func remove_color(index: int) -> void:
	colors.erase(index)


## Inserts a color to the specified index
## If index is already occupied move the original color to right
func insert_color(index: int, new_color: Color) -> void:
	var c := PaletteColor.new(new_color, index)
	# If insert happens on non empty swatch recursively move the original color
	# and every other color to its right one swatch to right
	if colors.get(index) != null:
		move_right(index)
	colors[index] = c


## Recursive function that moves every color to right until one of them is moved to empty swatch
func move_right(index: int) -> void:
	# Moving colors to right would overflow the size of the palette
	# so increase its height automatically
	if index + 1 == colors_max:
		height += 1
		colors_max = width * height

	# If swatch to right to this color is not empty move that color right too
	if colors.get(index + 1) != null:
		move_right(index + 1)

	colors[index + 1] = colors.get(index)


## Swaps two colors
func swap_colors(from_index: int, to_index: int) -> void:
	var from_color = colors.get(from_index)
	var to_color = colors.get(to_index)

	if not from_color and to_color:
		colors[from_index] = to_color
		colors[from_index].index = from_index
		colors.erase(to_index)
	elif from_color and not to_color:
		colors[to_index] = from_color
		colors[to_index].index = to_index
		colors.erase(from_index)
	elif from_color and to_color:
		colors[to_index] = from_color
		colors[to_index].index = to_index
		colors[from_index] = to_color
		colors[from_index].index = from_index


## Copies color
func copy_colors(from_index: int, to_index: int) -> void:
	# Only allow copy of existing colors
	if colors.has(from_index):
		colors[to_index] = colors[from_index].duplicate()
		colors[to_index].index = to_index


## True if all swatches are occupied
func is_full() -> bool:
	return self.colors.size() >= self.colors_max


## True if palette has no colors
func is_empty() -> bool:
	return self.colors.size() == 0


func has_theme_color(color: Color) -> bool:
	for palette_color in colors.values():
		if palette_color.color == color:
			return true
	return false


## Sets name that is used to save the palette to disk
func set_resource_name(new_resource_name: String) -> void:
	# Store palette path name only with valid path characters
	resource_name = Palette.strip_unvalid_characters(new_resource_name)


static func strip_unvalid_characters(string_to_strip: String) -> String:
	var regex := RegEx.new()
	regex.compile("[^a-zA-Z0-9_]+")
	return regex.sub(string_to_strip, "", true)
