# Copyright (c) 2023-2024 Mansur Isaev and contributors - MIT License
# See `LICENSE.md` included in the source distribution for details.

extends MarginContainer


signal library_changed

signal library_unsaved
signal library_saved

signal collection_changed

signal open_asset_request(path: String)
signal show_in_file_system_request(path: String)
signal show_in_file_manager_request(path: String)
signal asset_display_mode_changed(display_mode: DisplayMode)


enum CollectionTabMenu {
	NEW,
	RENAME,
	DELETE,
}
enum LibraryMenu {
	NEW,
	OPEN,
	SAVE,
	SAVE_AS,
}
enum DisplayMode{
	THUMBNAILS,
	LIST,
}
enum SortMode {
	NAME,
	NAME_REVERSE,
}
enum AssetContextMenu {
	OPEN_ASSET,
	COPY_PATH,
	COPY_UID,
	DELETE_ASSET,
	SHOW_IN_FILE_SYSTEM,
	SHOW_IN_FILE_MANAGER,
	REFRESH,
	MAX,
}


const NULL_LIBRARY: Array[Dictionary] = []
const NULL_COLLECTION: Dictionary = {}

const THUMB_GRID_SIZE: int = 192
const THUMB_LIST_SIZE: int = 48


var _main_vbox: VBoxContainer = null

var _collec_hbox: HBoxContainer = null
var _collec_tab_bar: TabBar = null
var _collec_tab_add: Button = null
var _all_tabs_list: MenuButton = null
var _collec_option: MenuButton = null

var _main_container: PanelContainer = null
var _content_vbox: VBoxContainer = null

var _top_hbox: HBoxContainer = null
var _asset_filter_line: LineEdit = null
var _asset_sort_mode_btn: Button = null

var _mode_thumb_btn: Button = null
var _mode_list_btn: Button = null

var _item_list: ItemList = null

var _open_dialog: ConfirmationDialog = null
var _save_dialog: ConfirmationDialog = null

var _save_timer: Timer = null

var _thumb_grid_icon_size: int = 64
var _thumb_list_icon_size: int = 16

# INFO: May be required for debugging.
var _cache_enabled: bool = true
var _cache_path: String = "res://.godot/thumb_cache"

# Create thumbnail scene:
var _viewport: SubViewport = null

var _camera_2d: Camera2D = null

var _camera_3d: Camera3D = null
var _light_3d: DirectionalLight3D = null

var _asset_display_mode: DisplayMode = DisplayMode.THUMBNAILS
var _sort_mode: SortMode = SortMode.NAME

var _thumbnails: Dictionary = {}

var _mutex: Mutex = null
var _thread: Thread = null
var _thread_queue: Array[Dictionary] = []
var _thread_sem: Semaphore = null
var _thread_work: bool = true

var _saved: bool = true
# INFO: Use key-value pairs to store collections.
var _curr_lib: Array[Dictionary] = NULL_LIBRARY # Array[Dictionary[StringName, ImageTexture]]
var _curr_lib_path: String = ""

var _curr_collec: Dictionary = NULL_COLLECTION


func _update_position_new_collection_btn() -> void:
	var tab_bar_total_width := float(_collec_tab_bar.get_theme_constant(&"h_separation"))
	for i: int in _collec_tab_bar.get_tab_count():
		tab_bar_total_width += _collec_tab_bar.get_tab_rect(i).size.x

	_collec_tab_bar.size = Vector2(minf(_collec_tab_bar.size.x, tab_bar_total_width), 0.0)
	_collec_tab_add.position.x = _collec_tab_bar.size.x

	_all_tabs_list.set_visible(_collec_tab_bar.get_offset_buttons_visible())


static func _def_setting(name: String, value: Variant) -> Variant:
	if not ProjectSettings.has_setting(name):
		ProjectSettings.set_setting(name, value)

	ProjectSettings.set_initial_value(name, value)
	return ProjectSettings.get_setting_with_override(name)

@warning_ignore("narrowing_conversion", "unsafe_method_access")
func _enter_tree() -> void:
	_cache_enabled = _def_setting("addons/scene_library/cache/enabled", true)
	_cache_path = _def_setting("addons/scene_library/cache/path", "res://.godot/thumb_cache")

	_thumb_grid_icon_size = _def_setting("addons/scene_library/thumbnail/grid_size", 64)
	_thumb_list_icon_size = _def_setting("addons/scene_library/thumbnail/list_size", 16)

	self.add_theme_constant_override(&"margin_left", -get_theme_stylebox(&"BottomPanel", &"EditorStyles").get_margin(SIDE_LEFT))
	self.add_theme_constant_override(&"margin_right", -get_theme_stylebox(&"BottomPanel", &"EditorStyles").get_margin(SIDE_RIGHT))
	self.add_theme_constant_override(&"margin_top", -get_theme_stylebox(&"BottomPanel", &"EditorStyles").get_margin(SIDE_TOP))

	self.set_custom_minimum_size(Vector2(0.0, 180.0))

	# INFO: Required to create a tab pseudo-container background.
	var tabbar_background := Panel.new()
	tabbar_background.add_theme_stylebox_override(&"panel", get_theme_stylebox(&"tabbar_background", &"TabContainer"))
	self.add_child(tabbar_background)

	_main_vbox = VBoxContainer.new()
	_main_vbox.add_theme_constant_override(&"separation", 0)
	self.add_child(_main_vbox)

	_collec_hbox = HBoxContainer.new()
	_collec_hbox.add_theme_constant_override(&"separation", 0)
	# INFO: Required to calculate the position of the "new" button.
	_collec_hbox.sort_children.connect(_update_position_new_collection_btn)
	_main_vbox.add_child(_collec_hbox)

	_collec_tab_bar = TabBar.new()
	_collec_tab_bar.set_auto_translate(false)
	_collec_tab_bar.set_drag_to_rearrange_enabled(true)
	_collec_tab_bar.set_h_size_flags(Control.SIZE_EXPAND_FILL)
	_collec_tab_bar.set_max_tab_width(256) # TODO: Make this parameter receive global editor settings.
	_collec_tab_bar.set_theme_type_variation(&"TabContainer")
	_collec_tab_bar.add_theme_stylebox_override(&"panel", get_theme_stylebox(&"DebuggerPanel", &"EditorStyles"))
	_collec_tab_bar.set_select_with_rmb(true)
	_collec_tab_bar.add_tab("[null]")
	_collec_tab_bar.set_tab_disabled(0, true)
	_collec_tab_bar.set_tab_close_display_policy(TabBar.CLOSE_BUTTON_SHOW_NEVER)
	_collec_tab_bar.tab_selected.connect(_on_collection_tab_changed)
	_collec_tab_bar.tab_close_pressed.connect(_on_collection_tab_close_pressed)
	_collec_tab_bar.tab_rmb_clicked.connect(_on_collection_tab_rmb_clicked)
	_collec_tab_bar.active_tab_rearranged.connect(_on_collection_tab_rearranged)
	_collec_hbox.add_child(_collec_tab_bar)

	_collec_tab_add = Button.new()
	_collec_tab_add.set_flat(true)
	_collec_tab_add.set_disabled(true)
	_collec_tab_add.set_tooltip_text("Add a new Collection.")
	_collec_tab_add.set_button_icon(get_theme_icon(&"Add", &"EditorIcons"))
	_collec_tab_add.add_theme_color_override(&"icon_normal_color", Color(0.6, 0.6, 0.6, 0.8))
	_collec_tab_add.set_h_size_flags(Control.SIZE_SHRINK_END)
	_collec_tab_add.pressed.connect(show_create_collection_dialog)
	_collec_hbox.add_child(_collec_tab_add)

	_all_tabs_list = MenuButton.new()
	_all_tabs_list.hide()
	_all_tabs_list.set_tooltip_text("List all tabs.")
	_all_tabs_list.set_button_icon(get_theme_icon(&"GuiOptionArrow", &"EditorIcons"))
	_all_tabs_list.add_theme_color_override(&"icon_normal_color", Color(0.6, 0.6, 0.6, 0.8))
	_collec_hbox.add_child(_all_tabs_list)

	var popup: PopupMenu = _all_tabs_list.get_popup()
	popup.index_pressed.connect(_collec_tab_bar.set_current_tab)

	_collec_option = MenuButton.new()
	_collec_option.set_flat(true)
	_collec_option.set_button_icon(get_theme_icon(&"GuiTabMenuHl", &"EditorIcons"))
	_collec_option.add_theme_color_override(&"icon_normal_color", Color(0.6, 0.6, 0.6, 0.8))
	_collec_hbox.add_child(_collec_option)

	popup = _collec_option.get_popup()
	popup.add_item("New Library", LibraryMenu.NEW)
	popup.add_item("Open Library", LibraryMenu.OPEN)
	popup.add_separator()
	popup.add_item("Save Library", LibraryMenu.SAVE)
	popup.add_item("Save Library As...", LibraryMenu.SAVE_AS)
	popup.id_pressed.connect(_on_collection_option_id_pressed)

	_main_container = PanelContainer.new()
	_main_container.set_mouse_filter(Control.MOUSE_FILTER_IGNORE)
	_main_container.set_v_size_flags(Control.SIZE_EXPAND_FILL)
	_main_container.add_theme_stylebox_override(&"panel", get_theme_stylebox(&"DebuggerPanel", &"EditorStyles"))
	_main_vbox.add_child(_main_container)

	_content_vbox = VBoxContainer.new()
	_main_container.add_child(_content_vbox)

	_top_hbox = HBoxContainer.new()
	_content_vbox.add_child(_top_hbox)

	_asset_filter_line = LineEdit.new()
	_asset_filter_line.set_placeholder("Filter assets")
	_asset_filter_line.set_clear_button_enabled(true)
	_asset_filter_line.set_right_icon(get_theme_icon(&"Search", &"EditorIcons"))
	_asset_filter_line.set_editable(false) # The value will be changed when the collection is changed.
	_asset_filter_line.set_h_size_flags(Control.SIZE_EXPAND_FILL)
	_asset_filter_line.text_changed.connect(_on_filter_assets_text_changed)
	_top_hbox.add_child(_asset_filter_line)

	_asset_sort_mode_btn = Button.new()
	_asset_sort_mode_btn.set_disabled(true)
	_asset_sort_mode_btn.set_tooltip_text("Toggle alphabetical sorting of assets")
	_asset_sort_mode_btn.set_flat(true)
	_asset_sort_mode_btn.set_toggle_mode(true)
	_asset_sort_mode_btn.set_button_icon(get_theme_icon(&"Sort", &"EditorIcons"))
	_asset_sort_mode_btn.toggled.connect(_sort_assets_button_toggled)
	_top_hbox.add_child(_asset_sort_mode_btn)

	_top_hbox.add_child(VSeparator.new())

	var button_group := ButtonGroup.new()

	_mode_thumb_btn = Button.new()
	_mode_thumb_btn.set_flat(true)
	_mode_thumb_btn.set_disabled(true)
	_mode_thumb_btn.set_tooltip_text("View items as a grid of thumbnails.")
	_mode_thumb_btn.set_toggle_mode(true)
	_mode_thumb_btn.set_button_icon(get_theme_icon(&"FileThumbnail", &"EditorIcons"))
	_mode_thumb_btn.set_button_group(button_group)
	_mode_thumb_btn.pressed.connect(set_asset_display_mode.bind(DisplayMode.THUMBNAILS))
	_top_hbox.add_child(_mode_thumb_btn)

	_mode_list_btn = Button.new()
	_mode_list_btn.set_flat(true)
	_mode_list_btn.set_disabled(true)
	_mode_list_btn.set_tooltip_text("View items as a list.")
	_mode_list_btn.set_toggle_mode(true)
	_mode_list_btn.set_button_icon(get_theme_icon(&"FileList", &"EditorIcons"))
	_mode_list_btn.set_button_group(button_group)
	_mode_list_btn.pressed.connect(set_asset_display_mode.bind(DisplayMode.LIST))
	_top_hbox.add_child(_mode_list_btn)

	_item_list = AssetItemList.new()
	_item_list.set_focus_mode(Control.FOCUS_CLICK)
	_item_list.set_max_columns(0)
	_item_list.set_mouse_filter(Control.MOUSE_FILTER_PASS)
	_item_list.set_same_column_width(true)
	_item_list.set_select_mode(ItemList.SELECT_MULTI)
	_item_list.set_texture_filter(CanvasItem.TEXTURE_FILTER_LINEAR)
	_item_list.set_v_size_flags(Control.SIZE_EXPAND_FILL)
	_item_list.gui_input.connect(_on_item_list_gui_input)
	_item_list.item_clicked.connect(_on_item_list_item_clicked)
	_item_list.item_activated.connect(_on_item_list_item_activated)
	_content_vbox.add_child(_item_list)

	_asset_display_mode = _def_setting("addons/scene_library/thumbnail/mode", DisplayMode.THUMBNAILS)
	_update_asset_display_mode(_asset_display_mode)

	_open_dialog = _create_file_dialog(true)
	_open_dialog.set_title("Open Asset Library")
	_open_dialog.connect(&"file_selected", load_library)
	self.add_child(_open_dialog)

	_save_dialog = _create_file_dialog(false)
	_save_dialog.set_title("Save Asset Library As...")
	_save_dialog.connect(&"file_selected", save_library)
	self.add_child(_save_dialog)

	_save_timer = Timer.new()
	_save_timer.set_one_shot(true)
	_save_timer.set_wait_time(10.0) # Save unsaved data every 10 seconds.
	_save_timer.timeout.connect(_on_save_timer_timeout)
	library_unsaved.connect(_save_timer.start)
	self.add_child(_save_timer)

	var world_2d := World2D.new()

	var world_3d := World3D.new()
	# TODO: Add a feature to change Environment.
	world_3d.set_environment(get_viewport().get_world_3d().get_environment())

	_viewport = SubViewport.new()
	_viewport.set_world_2d(world_2d)
	_viewport.set_world_3d(world_3d)
	_viewport.set_update_mode(SubViewport.UPDATE_DISABLED) # We'll update the frame manually.
	_viewport.set_debug_draw(Viewport.DEBUG_DRAW_DISABLE_LOD) # This is necessary to avoid visual glitches.
	_viewport.set_process_mode(Node.PROCESS_MODE_DISABLED) # Needs to disable animations.
	_viewport.set_size(Vector2i(THUMB_GRID_SIZE, THUMB_GRID_SIZE))
	_viewport.set_disable_input(true)
	_viewport.set_transparent_background(true)
	_viewport.set_physics_object_picking(false)
	_viewport.set_default_canvas_item_texture_filter(ProjectSettings.get_setting("rendering/textures/canvas_textures/default_texture_filter"))
	_viewport.set_default_canvas_item_texture_repeat(ProjectSettings.get_setting("rendering/textures/canvas_textures/default_texture_repeat"))
	_viewport.set_fsr_sharpness(ProjectSettings.get_setting("rendering/scaling_3d/fsr_sharpness"))
	_viewport.set_msaa_2d(ProjectSettings.get_setting("rendering/anti_aliasing/quality/msaa_2d"))
	_viewport.set_msaa_3d(ProjectSettings.get_setting("rendering/anti_aliasing/quality/msaa_3d"))
	_viewport.set_positional_shadow_atlas_16_bits(ProjectSettings.get_setting("rendering/lights_and_shadows/positional_shadow/atlas_16_bits"))
	_viewport.set_positional_shadow_atlas_quadrant_subdiv(0, ProjectSettings.get_setting("rendering/lights_and_shadows/positional_shadow/atlas_quadrant_0_subdiv"))
	_viewport.set_positional_shadow_atlas_quadrant_subdiv(1, ProjectSettings.get_setting("rendering/lights_and_shadows/positional_shadow/atlas_quadrant_1_subdiv"))
	_viewport.set_positional_shadow_atlas_quadrant_subdiv(2, ProjectSettings.get_setting("rendering/lights_and_shadows/positional_shadow/atlas_quadrant_2_subdiv"))
	_viewport.set_positional_shadow_atlas_quadrant_subdiv(3, ProjectSettings.get_setting("rendering/lights_and_shadows/positional_shadow/atlas_quadrant_3_subdiv"))
	_viewport.set_positional_shadow_atlas_size(ProjectSettings.get_setting("rendering/lights_and_shadows/positional_shadow/atlas_size"))
	_viewport.set_scaling_3d_mode(ProjectSettings.get_setting("rendering/scaling_3d/mode"))
	_viewport.set_scaling_3d_scale(ProjectSettings.get_setting("rendering/scaling_3d/scale"))
	_viewport.set_screen_space_aa(ProjectSettings.get_setting("rendering/anti_aliasing/quality/screen_space_aa"))
	_viewport.set_texture_mipmap_bias(ProjectSettings.get_setting("rendering/textures/default_filters/texture_mipmap_bias"))
	self.add_child(_viewport)

	_camera_2d = Camera2D.new()
	_camera_2d.set_enabled(false)
	_viewport.add_child(_camera_2d)

	# TODO: Add a feature to set lighting.
	_light_3d = DirectionalLight3D.new()
	_light_3d.set_shadow_mode(DirectionalLight3D.SHADOW_PARALLEL_4_SPLITS)
	_light_3d.set_bake_mode(Light3D.BAKE_STATIC)
	_light_3d.set_shadow(true)
	_light_3d.basis *= Basis(Vector3.UP, deg_to_rad(45.0))
	_light_3d.basis *= Basis(Vector3.LEFT, deg_to_rad(65.0))
	_viewport.add_child(_light_3d)

	_camera_3d = Camera3D.new()
	_camera_3d.set_current(false)
	_camera_3d.set_fov(22.5)
	_viewport.add_child(_camera_3d)

	# Multithreading starts here.
	_mutex = Mutex.new()
	_thread_sem = Semaphore.new()
	_thread = Thread.new()
	_thread.start(_thread_process)

	library_changed.connect(update_tabs)

	collection_changed.connect(update_item_list)
	asset_display_mode_changed.connect(_update_asset_display_mode)

	_curr_lib_path = _def_setting("addons/scene_library/library/current_library_path", "res://addons/scene-library/scene_library.cfg")
	load_library(_curr_lib_path)

	collection_changed.connect(_collec_tab_bar.size_flags_changed.emit)


func _exit_tree() -> void:
	_mutex.lock()
	_thread_work = false
	_mutex.unlock()

	_thread_sem.post()
	if _thread.is_started():
		_thread.wait_to_finish()

@warning_ignore("unsafe_method_access")
func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
	if not _item_list.get_rect().has_point(at_position):
		return false

	if not data is Dictionary or data.get("type") != "files":
		return false

	if _curr_lib.is_read_only() or _curr_collec.is_read_only():
		return false

	var files: PackedStringArray = data["files"]
	var rec_ext: PackedStringArray = ResourceLoader.get_recognized_extensions_for_type("PackedScene")

	for file: String in files:
		var extension: String = file.get_extension().to_lower()
		if not rec_ext.has(extension):
			return false

		if has_asset_path(file) or not is_valid_scene_file(file):
			return false

	return true


func _drop_data(_at_position: Vector2, data: Variant) -> void:
	if data is Dictionary:
		var files: PackedStringArray = data["files"]

		for path: String in files:
			create_asset(path)


func mark_saved() -> void:
	library_saved.emit()
	_saved = true

func mark_unsaved() -> void:
	library_unsaved.emit()
	_saved = false

func is_saved() -> bool:
	return _saved


func set_current_library(library: Array[Dictionary]) -> void:
	if is_same(_curr_lib, library):
		return

	_curr_lib = library
	library_changed.emit()
	# Switch to the first tab.
	_collec_tab_bar.set_current_tab(0)

func get_current_library() -> Array[Dictionary]:
	return _curr_lib


func set_current_library_path(path: String) -> void:
	if is_same(_curr_lib_path, path):
		return

	ProjectSettings.set_setting("addons/scene_library/library/current_library_path", path)
	_curr_lib_path = path

func get_current_library_path() -> String:
	return _curr_lib_path


func has_collection(collection_name: String) -> bool:
	for collection: Dictionary in get_current_library():
		if collection.name == collection_name:
			return true

	return false

func create_collection(collection_name: String) -> void:
	assert(not has_collection(collection_name), "Collection with this name already exists.")

	var assets: Array[Dictionary] = []
	var new_collection: Dictionary = {
		&"name": collection_name,
		&"assets": assets,
	}

	_curr_lib.push_back(new_collection)

	library_changed.emit()
	mark_unsaved()

	# Switch to the last tab.
	_collec_tab_bar.set_current_tab(_collec_tab_bar.get_tab_count() - 1)


func remove_collection(index: int) -> void:
	_curr_lib.remove_at(index)

	library_changed.emit()
	mark_unsaved()

	# Swith to the prev tab.
	_collec_tab_bar.set_current_tab(_collec_tab_bar.get_current_tab())

func show_remove_collection_dialog(index: int) -> void:
	var assets: Array[Dictionary] = _curr_lib[index].assets
	if assets.is_empty():
		return remove_collection(index)

	var window := ConfirmationDialog.new()
	window.set_size(Vector2i.ZERO)
	window.set_flag(Window.FLAG_RESIZE_DISABLED, true)
	window.focus_exited.connect(window.queue_free)
	window.confirmed.connect(remove_collection.bind(index))

	window.set_ok_button_text("Remove")

	var label := Label.new()
	label.set_text("Are you sure you want to delete this collection? (Cannot be undone.)")
	window.add_child(label)

	self.add_child(window)
	window.popup_centered(Vector2i(300, 0))


func _queue_has_id(id: int) -> bool:
	_mutex.lock()

	for item: Dictionary in _thread_queue:
		if item.id == id:
			_mutex.unlock()
			return true

	_mutex.unlock()
	return false

func _queue_update_thumbnail(id: int) -> void:
	if not _thumbnails.has(id) or _queue_has_id(id):
		return

	_mutex.lock()
	var queue_item: Dictionary = {&"id": id, &"thumb": _thumbnails[id]}
	_thread_queue.push_back(queue_item)
	_mutex.unlock()

	_thread_sem.post()

func _get_or_create_thumbnail(id: int, path: String) -> ImageTexture:
	var thumb: ImageTexture = _thumbnails.get(id, null)
	if is_instance_valid(thumb):
		return thumb

	var cache_path: String = _get_thumb_cache_path(path)
	if _cache_enabled and FileAccess.file_exists(cache_path):
		thumb = ImageTexture.create_from_image(Image.load_from_file(cache_path))
		_thumbnails[id] = thumb
	else:
		thumb = ImageTexture.create_from_image(Image.load_from_file(ProjectSettings.globalize_path("res://addons/scene-library/icons/thumb_placeholder.svg")))
		_thumbnails[id] = thumb

		_queue_update_thumbnail(id)

	return thumb

func _create_asset(id: int, uid: String, path: String) -> Dictionary:
	var asset: Dictionary = {
		&"id": id,
		&"uid": uid,
		&"path": path,
		&"thumb": _get_or_create_thumbnail(id, path),
	}
	return asset


static func is_valid_scene_file(path: String) -> bool:
	return ResourceLoader.exists(path, "PackedScene") and ResourceLoader.get_recognized_extensions_for_type("PackedScene").has(path.get_extension().to_lower())


static func get_or_create_valid_uid(path: String) -> int:
	var id: int = ResourceLoader.get_resource_uid(path)
	if id == ResourceUID.INVALID_ID:
		id = ResourceUID.create_id()
		ResourceUID.add_id(id, path)

	return id


func create_asset(path: String) -> void:
	assert(is_valid_scene_file(path), "PackedScene file was not found or has an invalid extension.")

	var id: int = get_or_create_valid_uid(path)
	var new_asset: Dictionary = _create_asset(id, ResourceUID.id_to_text(id), path)

	var assets: Array[Dictionary] = _curr_collec.assets
	assets.push_back(new_asset)

	collection_changed.emit()
	mark_unsaved()


func remove_asset(id: int) -> bool:
	var assets: Array[Dictionary] = _curr_collec.assets

	for i: int in assets.size():
		if assets[i].id != id:
			continue

		assets.remove_at(i)

		collection_changed.emit()
		mark_unsaved()

		return true

	return false


func set_current_collection(collection: Dictionary) -> void:
	if is_same(_curr_collec, collection):
		return

	_curr_collec = collection

	_item_list.deselect_all()
	collection_changed.emit()

func get_current_collection() -> Dictionary:
	return _curr_collec


func has_asset_path(path: String) -> bool:
	for asset: Dictionary in _curr_collec.assets:
		if asset.path == path:
			return true

	return false


func update_tabs() -> void:
	var is_valid: bool = not _curr_lib.is_read_only() and not _curr_lib.is_empty()

	_asset_filter_line.set_editable(is_valid)
	_collec_tab_add.set_disabled(_curr_lib.is_read_only())
	_asset_sort_mode_btn.set_disabled(not is_valid)
	_mode_thumb_btn.set_disabled(not is_valid)
	_mode_list_btn.set_disabled(not is_valid)

	if _curr_lib.size():
		_collec_tab_bar.set_tab_count(_curr_lib.size())
		_collec_tab_bar.set_tab_close_display_policy(TabBar.CLOSE_BUTTON_SHOW_ACTIVE_ONLY)

		var popup: PopupMenu = _all_tabs_list.get_popup()
		popup.set_item_count(_curr_lib.size())

		for i: int in _curr_lib.size():
			_collec_tab_bar.set_tab_title(i, _curr_lib[i].name)
			_collec_tab_bar.set_tab_disabled(i, false)
			_collec_tab_bar.set_tab_metadata(i, _curr_lib[i])

			popup.set_item_text(i, _curr_lib[i].name)
	else:
		_collec_tab_bar.set_tab_count(1)
		_collec_tab_bar.set_tab_close_display_policy(TabBar.CLOSE_BUTTON_SHOW_NEVER)
		_collec_tab_bar.set_tab_title(0, "[null]")
		_collec_tab_bar.set_tab_disabled(0, true)
		_collec_tab_bar.set_tab_metadata(0, NULL_COLLECTION)

	# INFO: Required to recalculate position of the "new collection" button.
	_collec_tab_bar.size_flags_changed.emit()

@warning_ignore("unsafe_call_argument")
func update_item_list() -> void:
	var assets: Array[Dictionary] = _curr_collec.assets
	_item_list.set_item_count(assets.size())

	var is_list_mode: bool = _asset_display_mode == DisplayMode.LIST
	var filter: String = _asset_filter_line.get_text()

	var index: int = 0
	for asset: Dictionary in assets:
		var path: String = asset.path
		if not filter.is_subsequence_ofn(path.get_file()):
			continue

		_item_list.set_item_text(index, path.get_file().get_basename())
		_item_list.set_item_icon(index, asset.thumb)
		# NOTE: This tooltip will be hidden because used the custom tooltip.
		_item_list.set_item_tooltip(index, path)
		_item_list.set_item_metadata(index, asset)

		index += 1

	_item_list.set_item_count(index)


func set_asset_display_mode(display_mode: DisplayMode) -> void:
	if is_same(_asset_display_mode, display_mode):
		return

	ProjectSettings.set_setting("addons/scene_library/thumbnail/mode", display_mode)
	_asset_display_mode = display_mode

	asset_display_mode_changed.emit(display_mode)

func get_asset_display_mode() -> DisplayMode:
	return _asset_display_mode

static func sort_asset_ascending(a: Dictionary, b: Dictionary) -> bool:
	@warning_ignore("unsafe_method_access")
	return a.path.get_file() < b.path.get_file()
static func sort_asset_descending(a: Dictionary, b: Dictionary) -> bool:
	@warning_ignore("unsafe_method_access")
	return a.path.get_file() > b.path.get_file()
static func sort_assets(assets: Array[Dictionary], sort_mode: SortMode) -> void:
	if sort_mode == SortMode.NAME:
		assets.sort_custom(sort_asset_ascending)
	else:
		assets.sort_custom(sort_asset_descending)

func set_sort_mode(sort_mode: SortMode) -> void:
	if is_same(_sort_mode, sort_mode):
		return

	_sort_mode = sort_mode
	sort_assets(_curr_collec.assets, sort_mode)

	collection_changed.emit()

func get_sort_mode() -> SortMode:
	return _sort_mode


func show_create_collection_dialog() -> AcceptDialog:
	var window := AcceptDialog.new()
	window.set_size(Vector2i.ZERO)
	window.set_title("Create New Collection")
	window.add_cancel_button("Cancel")
	window.set_flag(Window.FLAG_RESIZE_DISABLED, true)
	window.focus_exited.connect(window.queue_free)
	self.add_child(window)

	var ok_button: Button = window.get_ok_button()
	ok_button.set_text("Create")
	ok_button.set_disabled(true)

	var vbox := VBoxContainer.new()
	window.add_child(vbox)

	var label := Label.new()
	label.set_text("New Collection Name:")
	vbox.add_child(label)

	var line_edit := LineEdit.new()
	window.register_text_enter(line_edit)
	line_edit.set_text("new_collection")
	line_edit.select_all()

	# INFO: Disables the ability to create a collection and set a tooltip.
	line_edit.text_changed.connect(func(c_name: String) -> void:
		if c_name.is_empty():
			line_edit.set_tooltip_text("Collection name is empty.")
		elif has_collection(c_name):
			line_edit.set_tooltip_text("Collection with this name already exists.")
		else:
			line_edit.set_tooltip_text("")

		ok_button.set_disabled(c_name.is_empty() or has_collection(c_name))
		line_edit.set_right_icon(get_theme_icon(&"StatusError", &"EditorIcons") if ok_button.is_disabled() else null)
	)
	line_edit.text_changed.emit(line_edit.get_text()) # Required for status updates.
	vbox.add_child(line_edit)

	window.confirmed.connect(func() -> void:
		var new_collec_name: String = line_edit.get_text()
		create_collection(new_collec_name)
	)
	window.popup_centered(Vector2i(300, 0))
	line_edit.grab_focus()

	return window



func _serialize_asset(asset: Dictionary) -> Dictionary:
	return {"uid": asset.uid, "path": asset.path}

func _serialize_assets(assets: Array[Dictionary]) -> Array[Dictionary]:
	var serialized: Array[Dictionary] = []
	serialized.resize(assets.size())

	for i: int in assets.size():
		serialized[i] = _serialize_asset(assets[i])

	return serialized

func _serialize_collection(collection: Dictionary) -> Dictionary:
	return {
		"name": collection.name,
		"assets": _serialize_assets(collection.assets),
	}

func _serialize_library(library: Array[Dictionary]) -> Array[Dictionary]:
	var serialized: Array[Dictionary] = []
	serialized.resize(library.size())

	for i: int in library.size():
		serialized[i] = _serialize_collection(library[i])

	return serialized


func _cfg_save_library(library: Array[Dictionary], path: String) -> void:
	var serialized: Array[Dictionary] = _serialize_library(library)

	var config := ConfigFile.new()
	config.set_value("", "library", serialized)

	var error := config.save(path)
	assert(error == OK, error_string(error))

func _json_save_library(library: Array[Dictionary], path: String) -> void:
	var serialized: Array[Dictionary] = _serialize_library(library)

	var file := FileAccess.open(path, FileAccess.WRITE)
	assert(FileAccess.get_open_error() == OK, error_string(FileAccess.get_open_error()))

	file.store_string(JSON.stringify(serialized, "\t"))
	file.close()

func save_library(path: String) -> void:
	var extension: String = path.get_extension()
	assert(extension == "cfg" or extension == "json", "Invalid extension.")

	if extension == "cfg":
		_cfg_save_library(_curr_lib, path)
	elif extension == "json":
		_json_save_library(_curr_lib, path)
	else:
		return

	mark_saved()


func _deserialize_asset(asset: Dictionary) -> Dictionary:
	var uid: String = asset.get("uid", "")
	var path: String = asset.get("path", "")

	var id: int = ResourceUID.text_to_id(uid)

	# TODO: Add error handling.
	if id != ResourceUID.INVALID_ID and ResourceUID.has_id(id): # If the UID is valid.
		path = ResourceUID.get_id_path(id)
	# If the UID is wrong, try to load the asset by the path.
	# It also checks whether the file extension is valid.
	elif is_valid_scene_file(path):
		id = ResourceLoader.get_resource_uid(path)
		uid = ResourceUID.id_to_text(id)

		if not ResourceUID.has_id(id):
			ResourceUID.add_id(id, path)

	# Invalid assset.
	else:
		return {}

	return _create_asset(id, uid, path)

func _deserialize_assets(assets: Array) -> Array[Dictionary]:
	var deserialized: Array[Dictionary] = []

	for asset: Dictionary in assets:
		asset = _deserialize_asset(asset)
		if asset.is_empty():
			continue

		deserialized.push_back(asset)

	return deserialized

func _deserialize_collection(collection: Dictionary) -> Dictionary:
	var deserialized: Dictionary = {
		&"name": collection[&"name"],
		&"assets": _deserialize_assets(collection["assets"])
	}

	return deserialized

func _deserialize_library(library: Array) -> Array[Dictionary]:
	var deserialized: Array[Dictionary] = []
	deserialized.resize(library.size())

	for i: int in library.size():
		deserialized[i] = _deserialize_collection(library[i])

	return deserialized


func _load_cfg(path: String) -> Array[Dictionary]:
	var config := ConfigFile.new()

	var error := config.load(path)
	assert(error == OK, error_string(error))

	var data: Variant = config.get_value("", "library")
	if data is Array:
		return _deserialize_library(data)

	return NULL_LIBRARY

func _load_json(path: String) -> Array[Dictionary]:
	var json := JSON.new()

	var error := json.parse(FileAccess.get_file_as_string(path))
	assert(error == OK, error_string(error))

	var data: Variant = json.get_data()
	if data is Array:
		return _deserialize_library(data)

	return NULL_LIBRARY

func load_library(path: String) -> void:
	var library: Array[Dictionary] = []

	if FileAccess.file_exists(path):
		var extension: String = path.get_extension()
		assert(extension == "cfg" or extension == "json", "Invalid extension.")

		if extension == "cfg":
			library = _load_cfg(path)
		elif extension == "json":
			library = _load_json(path)

	# Check for “null” value.
	if library.is_read_only():
		return

	set_current_library(library)
	set_current_library_path(path)


@warning_ignore("unsafe_method_access")
func _calculate_node_rect(node: Node) -> Rect2:
	var rect := Rect2()
	if node is Node2D and node.is_visible():
		# HACK: This works only in editor.
		rect = node.get_global_transform() * node.call(&"_edit_get_rect")

	for i: int in node.get_child_count():
		rect = rect.merge(_calculate_node_rect(node.get_child(i)))

	return rect

@warning_ignore("unsafe_method_access")
func _calculate_node_aabb(node: Node) -> AABB:
	var aabb := AABB()

	if node is Node3D and not node.is_visible():
		return aabb
	# NOTE: If the node is not MeshInstance3D, the AABB is not calculated correctly.
	# The camera may have incorrect distances to objects in the scene.
	elif node is MeshInstance3D:
		aabb = node.get_global_transform() * node.get_aabb()

	for i: int in node.get_child_count():
		aabb = aabb.merge(_calculate_node_aabb(node.get_child(i)))

	return aabb


func _focus_camera_on_node_2d(node: Node) -> void:
	var rect: Rect2 = _calculate_node_rect(node)
	_camera_2d.set_position(rect.get_center())

	var zoom_ratio: float = THUMB_GRID_SIZE / maxf(rect.size.x, rect.size.y)
	_camera_2d.set_zoom(Vector2(zoom_ratio, zoom_ratio))

func _focus_camera_on_node_3d(node: Node) -> void:
	var transform := Transform3D.IDENTITY
	# TODO: Add a feature to configure the rotation of the camera.
	transform.basis *= Basis(Vector3.UP, deg_to_rad(40.0))
	transform.basis *= Basis(Vector3.LEFT, deg_to_rad(22.5))

	var aabb: AABB = _calculate_node_aabb(node)
	var distance: float = aabb.get_longest_axis_size() / tan(deg_to_rad(_camera_3d.get_fov()) * 0.5)

	transform.origin = transform * (Vector3.BACK * distance) + aabb.get_center()

	_camera_3d.set_global_transform(transform.orthonormalized())


func _get_thumb_cache_dir() -> String:
	return ProjectSettings.globalize_path(_cache_path)

func _get_thumb_cache_path(path: String) -> String:
	return _get_thumb_cache_dir().path_join(path.md5_text()) + ".png"

func _save_thumb_to_disk(id: int, image: Image) -> void:
	if not DirAccess.dir_exists_absolute(_get_thumb_cache_dir()):
		var error := DirAccess.make_dir_absolute(_get_thumb_cache_dir())
		assert(error == OK, error_string(error))

	var error := image.save_png(_get_thumb_cache_path(ResourceUID.get_id_path(id)))
	assert(error == OK, error_string(error))

func _create_thumb(item: Dictionary, callback: Callable) -> void:
	var path: String = ResourceUID.get_id_path(item.id)
	if not is_valid_scene_file(path):
		return callback.call()

	var packed_scene := ResourceLoader.load(path, "PackedScene") as PackedScene
	# INFO: Could be null if, for example, the dependencies are broken.
	if not is_instance_valid(packed_scene) or not packed_scene.can_instantiate():
		return callback.call()

	var instance: Node = packed_scene.instantiate()

	_viewport.call_deferred(&"add_child", instance)
	await instance.ready

	if instance is Node2D:
		_camera_3d.set_current(false)
		_camera_2d.set_enabled(true)
		_focus_camera_on_node_2d(instance)
	else:
		_camera_2d.set_enabled(false)
		_camera_3d.set_current(true)
		_focus_camera_on_node_3d(instance)

	await RenderingServer.frame_pre_draw
	_viewport.set_update_mode(SubViewport.UPDATE_ONCE)

	await RenderingServer.frame_post_draw

	var image: Image = _viewport.get_texture().get_image()
	image.resize(THUMB_GRID_SIZE, THUMB_GRID_SIZE, Image.INTERPOLATE_LANCZOS)

	var thumb: ImageTexture = item.thumb
	thumb.update(image)

	if _cache_enabled:
		_save_thumb_to_disk(item.id, image)

	instance.call_deferred(&"free")
	await instance.tree_exited

	callback.call()

func _thread_process() -> void:
	var semaphore := Semaphore.new()

	while _thread_work:
		if _thread_queue.is_empty():
			_thread_sem.wait()
		else:
			_mutex.lock()
			var item: Dictionary = _thread_queue.pop_front()
			_mutex.unlock()

			# This ensures that this method will be executed in the main thread.
			call_deferred_thread_group(&"_create_thumb", item, semaphore.post)
			semaphore.wait()




func handle_scene_saved(path: String) -> void:
	# INFO: When we save a scene, we try to update the asset thumbnail.
	# The "_queue_update_thumbnail" method will not create new thumbnails if they have not been previously created.
	_queue_update_thumbnail(ResourceLoader.get_resource_uid(path))


func handle_file_moved(old_file: String, new_file: String) -> void:
	if not _thumbnails.has(ResourceLoader.get_resource_uid(new_file)):
		return

	for collection: Dictionary in _curr_lib:
		for asset: Dictionary in collection.assets:
			if asset.path == old_file:
				asset.path = new_file
				break

	collection_changed.emit()


func handle_file_removed(file: String) -> void:
	# TODO: Need to add Dictionary for asset path.
	# Because we can't use UID for deleted files.
	# And we have to go through all collections and assets.
	var removed: int = 0
	for collection: Dictionary in _curr_lib:
		var assets: Array[Dictionary] = collection.assets

		for i: int in assets.size():
			if assets[i].path != file:
				continue

			assets.remove_at(i)
			removed += 1
			break

	if removed:
		collection_changed.emit()




func _on_collection_tab_changed(tab: int) -> void:
	set_current_collection(_collec_tab_bar.get_tab_metadata(tab))


func _on_collection_tab_close_pressed(tab: int) -> void:
	show_remove_collection_dialog(tab)


func _on_collection_tab_rmb_clicked(tab: int) -> void:
	var collection: Dictionary = _collec_tab_bar.get_tab_metadata(tab)

	var popup := PopupMenu.new()
	popup.id_pressed.connect(func(option: CollectionTabMenu) -> void:
		match option:
			CollectionTabMenu.NEW:
				show_create_collection_dialog()

			CollectionTabMenu.RENAME:
				var old_name: String = collection.name

				var rename_collec_window := AcceptDialog.new()
				rename_collec_window.set_size(Vector2i.ZERO)
				rename_collec_window.set_title("Rename Collection")
				rename_collec_window.add_cancel_button("Cancel")
				rename_collec_window.set_flag(Window.FLAG_RESIZE_DISABLED, true)
				rename_collec_window.focus_exited.connect(rename_collec_window.queue_free)

				var ok_button: Button = rename_collec_window.get_ok_button()
				ok_button.set_text("OK")
				ok_button.set_disabled(true)

				var vbox := VBoxContainer.new()
				rename_collec_window.add_child(vbox)

				var label := Label.new()
				label.set_text("Change Collection Name:")
				vbox.add_child(label)

				var line_edit := LineEdit.new()
				line_edit.set_select_all_on_focus(true)
				line_edit.set_text(old_name)
				rename_collec_window.register_text_enter(line_edit)

				# INFO: Disables the ability to create a collection and set a tooltip.
				line_edit.text_changed.connect(func(new_name: String) -> void:
					var is_valid := false

					if new_name.is_empty():
						line_edit.set_tooltip_text("Collection name is empty.")
					elif has_collection(new_name):
						line_edit.set_tooltip_text("Collection with this name already exists.")
					else:
						line_edit.set_tooltip_text("")
						is_valid = true

					ok_button.set_disabled(not is_valid)
					line_edit.set_right_icon(null if is_valid else get_theme_icon(&"StatusError", &"EditorIcons"))
				)

				line_edit.text_changed.emit(line_edit.get_text()) # Required for update status.
				vbox.add_child(line_edit)

				rename_collec_window.confirmed.connect(func() -> void:
					collection.name = line_edit.get_text()
					_collec_tab_bar.set_tab_title(tab, line_edit.get_text())
					mark_unsaved()
				)

				self.add_child(rename_collec_window)
				rename_collec_window.popup_centered(Vector2i(300, 0))
				line_edit.grab_focus()

			CollectionTabMenu.DELETE:
				show_remove_collection_dialog(tab)
		)
	popup.focus_exited.connect(popup.queue_free)
	self.add_child(popup)

	if collection.is_read_only(): # If "null" collection.
		# BUG: You can't see it because the tab is disabled.
		popup.add_item("New Collection", CollectionTabMenu.NEW)
	else:
		popup.add_item("New Collection", CollectionTabMenu.NEW)
		popup.add_separator()
		popup.add_item("Rename Collection", CollectionTabMenu.RENAME)
		popup.add_item("Delete Collection", CollectionTabMenu.DELETE)

	popup.popup(Rect2i(get_screen_position() + get_local_mouse_position(), Vector2i.ZERO))


func _on_collection_tab_rearranged(_to_idx: int) -> void:
	for i: int in _collec_tab_bar.get_tab_count():
		_curr_lib[i] = _collec_tab_bar.get_tab_metadata(i)


func _create_file_dialog(open: bool) -> ConfirmationDialog:
	var dialog: ConfirmationDialog = null

	if Engine.is_editor_hint(): # Works only in the editor.
		var editor_file_dialog: EditorFileDialog = ClassDB.instantiate(&"EditorFileDialog")
		editor_file_dialog.set_access(EditorFileDialog.ACCESS_FILESYSTEM)
		editor_file_dialog.set_file_mode(EditorFileDialog.FILE_MODE_OPEN_FILE if open else EditorFileDialog.FILE_MODE_SAVE_FILE)
		editor_file_dialog.add_filter("*.cfg", "Config File")
		editor_file_dialog.add_filter("*.json", "JSON File")
		dialog = editor_file_dialog
	else:
		var file_dialog := FileDialog.new()
		file_dialog.set_access(FileDialog.ACCESS_FILESYSTEM)
		file_dialog.set_file_mode(FileDialog.FILE_MODE_OPEN_FILE if open else FileDialog.FILE_MODE_SAVE_FILE)
		file_dialog.add_filter("*.cfg", "Config File")
		file_dialog.add_filter("*.json", "JSON File")
		dialog = file_dialog

	dialog.set_exclusive(true)

	return dialog

func _popup_file_dialog(window: Window) -> void:
	window.popup_centered_clamped(Vector2(1050, 700) * DisplayServer.screen_get_scale(), 0.8)

func _on_collection_option_id_pressed(option: LibraryMenu) -> void:
	match option:
		# TODO: Add a feature to check if the current library is saved.
		LibraryMenu.NEW:
			var new_library: Array[Dictionary] = []
			set_current_library(new_library)

			_curr_lib_path = ""

		LibraryMenu.OPEN:
			_popup_file_dialog(_open_dialog)

		LibraryMenu.SAVE when not _curr_lib_path.is_empty():
			save_library(_curr_lib_path)

		LibraryMenu.SAVE, LibraryMenu.SAVE_AS:
			_popup_file_dialog(_save_dialog)


func _on_filter_assets_text_changed(_filter: String) -> void:
	update_item_list()


func _sort_assets_button_toggled(reverse: bool) -> void:
	set_sort_mode(SortMode.NAME_REVERSE if reverse else SortMode.NAME)


func _update_thumb_icon_size(display_mode: DisplayMode) -> void:
	if display_mode == DisplayMode.THUMBNAILS:
		_item_list.set_fixed_column_width(_thumb_grid_icon_size * 1.5)
		_item_list.set_fixed_icon_size(Vector2i(_thumb_grid_icon_size, _thumb_grid_icon_size))
	else:
		_item_list.set_fixed_column_width(0)
		_item_list.set_fixed_icon_size(Vector2i(_thumb_list_icon_size, _thumb_list_icon_size))

func _update_asset_display_mode(display_mode: DisplayMode) -> void:
	if display_mode == DisplayMode.THUMBNAILS:
		_item_list.set_max_columns(0)
		_item_list.set_icon_mode(ItemList.ICON_MODE_TOP)
		_item_list.set_max_text_lines(2)

		_mode_thumb_btn.set_pressed_no_signal(true)
	else:
		_item_list.set_max_columns(0)
		_item_list.set_icon_mode(ItemList.ICON_MODE_LEFT)
		_item_list.set_max_text_lines(1)

		_mode_list_btn.set_pressed_no_signal(true)

	for i: int in _item_list.get_item_count():
		var asset: Dictionary = _item_list.get_item_metadata(i)
		_item_list.set_item_icon(i, asset.thumb)

	_update_thumb_icon_size(display_mode)


func _set_thumb_grid_icon_size(icon_size: int) -> void:
	icon_size = clampi(icon_size, THUMB_LIST_SIZE, THUMB_GRID_SIZE)
	if _thumb_grid_icon_size == icon_size:
		return

	ProjectSettings.set_setting("addons/scene_library/thumbnail/grid_size", icon_size)
	_thumb_grid_icon_size = icon_size

	_update_thumb_icon_size(_asset_display_mode)

func _set_thumb_list_icon_size(icon_size: int) -> void:
	const ICON_MIN_SIZE = 16

	icon_size = clampi(icon_size, ICON_MIN_SIZE, THUMB_LIST_SIZE)
	if _thumb_list_icon_size == icon_size:
		return

	ProjectSettings.set_setting("addons/scene_library/thumbnail/list_size", icon_size)
	_thumb_list_icon_size = icon_size

	_update_thumb_icon_size(_asset_display_mode)


func _on_item_list_gui_input(event: InputEvent) -> void:
	if event is InputEventMouseButton and event.is_pressed() and event.is_command_or_control_pressed():
		const ICON_GRID_STEP = 8
		const ICON_LIST_STEP = 4

		match event.get_button_index():
			MOUSE_BUTTON_WHEEL_UP:
				if _asset_display_mode == DisplayMode.THUMBNAILS:
					_set_thumb_grid_icon_size(_thumb_grid_icon_size + ICON_GRID_STEP)
				else:
					_set_thumb_list_icon_size(_thumb_list_icon_size + ICON_LIST_STEP)

			MOUSE_BUTTON_WHEEL_DOWN:
				if _asset_display_mode == DisplayMode.THUMBNAILS:
					_set_thumb_grid_icon_size(_thumb_grid_icon_size - ICON_GRID_STEP)
				else:
					_set_thumb_list_icon_size(_thumb_list_icon_size - ICON_LIST_STEP)

			_:
				return

		accept_event()

func _on_item_list_item_clicked(index: int, at_position: Vector2, mouse_button_index: int) -> void:
	if mouse_button_index != MOUSE_BUTTON_RIGHT:
		return

	_item_list.select(index, false)
	var selected_assets: PackedInt32Array = _item_list.get_selected_items()

	var popup := PopupMenu.new()
	popup.connect(&"focus_exited", popup.queue_free)
	popup.connect(&"id_pressed", func(option: AssetContextMenu) -> void:
		var asset: Dictionary = _item_list.get_item_metadata(selected_assets[0])

		match option:
			AssetContextMenu.OPEN_ASSET:
				open_asset_request.emit(asset.path)

			AssetContextMenu.COPY_PATH:
				DisplayServer.clipboard_set(asset.path)

			AssetContextMenu.COPY_UID:
				DisplayServer.clipboard_set(asset.uid)

			AssetContextMenu.DELETE_ASSET:
				var assets: Array[Dictionary] = _curr_collec.assets

				if selected_assets.size() == 1:
					assets.remove_at(selected_assets[0])
				else:
					selected_assets.reverse()

					for i: int in selected_assets:
						assets.remove_at(i)

				collection_changed.emit()
				mark_unsaved()

			AssetContextMenu.SHOW_IN_FILE_SYSTEM:
				show_in_file_system_request.emit(asset.path)

			AssetContextMenu.SHOW_IN_FILE_MANAGER:
				show_in_file_manager_request.emit(asset.path)

			AssetContextMenu.REFRESH:
				for i: int in selected_assets:
					asset = _item_list.get_item_metadata(i)
					_queue_update_thumbnail(asset.id)
		)
	self.add_child(popup)

	if selected_assets.size() == 1: # If only one asset is selected.
		popup.add_item("Open", AssetContextMenu.OPEN_ASSET)
		popup.set_item_icon(-1, get_theme_icon(&"Load", &"EditorIcons"))
		popup.add_separator()
		popup.add_item("Copy Path", AssetContextMenu.COPY_PATH)
		popup.set_item_icon(-1, get_theme_icon(&"ActionCopy", &"EditorIcons"))
		popup.add_item("Copy UID", AssetContextMenu.COPY_UID)
		popup.set_item_icon(-1, get_theme_icon(&"Instance", &"EditorIcons"))
		popup.add_item("Delete", AssetContextMenu.DELETE_ASSET)
		popup.set_item_icon(-1, get_theme_icon(&"Remove", &"EditorIcons"))
		popup.add_separator()
		popup.add_item("Show in FileSystem", AssetContextMenu.SHOW_IN_FILE_SYSTEM)
		popup.set_item_icon(-1, get_theme_icon(&"Filesystem", &"EditorIcons"))
		popup.add_item("Show in File Manager", AssetContextMenu.SHOW_IN_FILE_MANAGER)
		popup.set_item_icon(-1, get_theme_icon(&"Folder", &"EditorIcons"))
		popup.add_separator()
		popup.add_item("Refresh", AssetContextMenu.REFRESH)
		popup.set_item_icon(-1, get_theme_icon(&"Reload", &"EditorIcons"))
	else: # If many assets are selected.
		popup.add_item("Delete", AssetContextMenu.DELETE_ASSET)
		popup.set_item_icon(popup.get_item_index(AssetContextMenu.DELETE_ASSET), get_theme_icon(&"Remove", &"EditorIcons"))
		popup.add_item("Refresh", AssetContextMenu.REFRESH)
		popup.set_item_icon(-1, get_theme_icon(&"Reload", &"EditorIcons"))

	popup.popup(Rect2i(_item_list.get_screen_position() + at_position, Vector2i.ZERO))


func _on_item_list_item_activated(index: int) -> void:
	var asset: Dictionary = _item_list.get_item_metadata(index)
	open_asset_request.emit(asset.path)


func _on_save_timer_timeout() -> void:
	if _curr_lib_path.is_empty():
		return

	save_library(_curr_lib_path)




class AssetItemList extends ItemList:
	func _gui_input(event: InputEvent) -> void:
		if event.is_action_pressed(&"ui_text_select_all"):
			for i: int in get_item_count():
				select(i, false)

			accept_event()

	func _create_drag_preview(files: PackedStringArray) -> Control:
		const MAX_ROWS = 6

		var vbox := VBoxContainer.new()
		var num_rows := mini(files.size(), MAX_ROWS)

		for i: int in num_rows:
			var hbox := HBoxContainer.new()
			vbox.add_child(hbox)

			var icon := TextureRect.new()
			icon.set_texture(get_theme_icon(&"File", &"EditorIcons"))
			icon.set_stretch_mode(TextureRect.STRETCH_KEEP_CENTERED)
			icon.set_size(Vector2(16.0, 16.0))
			hbox.add_child(icon)

			var label := Label.new()
			label.set_text(files[i].get_file().get_basename())
			hbox.add_child(label)

		if files.size() > num_rows:
			var label := Label.new()
			label.set_text("%d more files" % int(files.size() - num_rows))
			vbox.add_child(label)

		return vbox

	func _get_drag_data(at_position: Vector2) -> Variant:
		var item: int = get_item_at_position(at_position)
		if item < 0:
			return null

		var files := PackedStringArray()
		for i: int in get_selected_items():
			var asset: Dictionary = get_item_metadata(i)
			files.push_back(asset.path)

		set_drag_preview(_create_drag_preview(files))

		return {"type": "files", "files": files}

	func _make_custom_tooltip(_for_text: String) -> Object:
		var item: int = get_item_at_position(get_local_mouse_position())
		if item < 0:
			return null

		var asset: Dictionary = get_item_metadata(item)
		if asset.is_empty():
			return null

		var vbox := VBoxContainer.new()

		var thumb_rect := TextureRect.new()
		thumb_rect.set_expand_mode(TextureRect.EXPAND_IGNORE_SIZE)
		thumb_rect.set_h_size_flags(Control.SIZE_SHRINK_CENTER)
		thumb_rect.set_v_size_flags(Control.SIZE_SHRINK_CENTER)
		thumb_rect.set_custom_minimum_size(Vector2(THUMB_GRID_SIZE, THUMB_GRID_SIZE))
		thumb_rect.set_texture(asset.thumb)
		vbox.add_child(thumb_rect)

		var label := Label.new()
		label.set_text(asset.path)
		vbox.add_child(label)

		return vbox
