write python script that would export images  (png, webp) and audio (ogg, mp3, wav) from ctex/stex for images and from oggstr, mp3str and sample files for audio in its original format without convertion and be able to put it back,  with suppor6 in case if imported file would be smaller than original (for example was compressed) with fix of file size (not just zero bytes fill) based on cpp files code below:
/*************************************************************************/
/*  gdre_editor.cpp                                                      */
/*************************************************************************/
#ifdef TOOLS_ENABLED

#include "exporters/sample_exporter.h"
#include "exporters/texture_exporter.h"
#include "utility/gdre_version.gen.h"
/*************************************************************************/
#include "editor/editor_node.h"
#include "editor/editor_settings.h"
/*************************************************************************/
#include "gdre_editor.h"

#include "bytecode/bytecode_versions.h"
#include "compat/oggstr_loader_compat.h"
#include "compat/resource_loader_compat.h"
#include "exporters/oggstr_exporter.h"
#include "utility/gdre_settings.h"
#include "utility/import_exporter.h"
#include "utility/pck_dumper.h"

#include "core/io/resource_format_binary.h"
#include "modules/svg/image_loader_svg.h"
#include "scene/gui/button.h"
#include "scene/gui/texture_rect.h"
#include "scene/main/canvas_item.h"
#include "scene/resources/image_texture.h"

#include "core/version_generated.gen.h"

#include "gdre_icons.gen.h"

#if GODOT_VERSION_MAJOR < 4
#error Unsupported Godot version
#endif

#ifndef TOOLS_ENABLED
#include "core/object/message_queue.h"
#include "core/os/os.h"
#include "main/main.h"

#endif
#include "compat/file_access_encrypted_v3.h"

#include <utility/pck_creator.h>

/*************************************************************************/

GodotREEditor *GodotREEditor::singleton = NULL;

/*************************************************************************/

ResultDialog::ResultDialog() {
	set_title(RTR("OK"));
	set_flag(Window::Flags::FLAG_RESIZE_DISABLED, false);

	VBoxContainer *script_vb = memnew(VBoxContainer);

	lbl = memnew(Label);
	script_vb->add_child(lbl);

	message = memnew(TextEdit);
	message->set_editable(false);
	message->set_custom_minimum_size(Size2(600, 200) * EDSCALE);
	message->set_h_size_flags(Control::SIZE_EXPAND_FILL);
	message->set_v_size_flags(Control::SIZE_EXPAND_FILL);
	script_vb->add_child(message);

	add_child(script_vb);
}

ResultDialog::~ResultDialog() {
	//NOP
}

void ResultDialog::_notification(int p_notification) {
	//NOP
}

void ResultDialog::_bind_methods() {
	//NOP
}

void ResultDialog::set_message(const String &p_text, const String &p_title) {
	lbl->set_text(p_title);
	message->set_text(p_text);
}

/*************************************************************************/

OverwriteDialog::OverwriteDialog() {
	set_title(RTR("Files already exist!"));
	set_flag(Window::Flags::FLAG_RESIZE_DISABLED, false);

	VBoxContainer *script_vb = memnew(VBoxContainer);

	lbl = memnew(Label);
	lbl->set_text(RTR("Following files already exist and will be overwritten:"));
	script_vb->add_child(lbl);

	message = memnew(TextEdit);
	message->set_editable(false);
	message->set_custom_minimum_size(Size2(600, 100) * EDSCALE);
	message->set_h_size_flags(Control::SIZE_EXPAND_FILL);
	message->set_v_size_flags(Control::SIZE_EXPAND_FILL);
	script_vb->add_child(message);

	add_child(script_vb);

	get_ok_button()->set_text(RTR("Overwrite"));
	add_cancel_button(RTR("Cancel"));
}

OverwriteDialog::~OverwriteDialog() {
	//NOP
}

void OverwriteDialog::_notification(int p_notification) {
	//NOP
}

void OverwriteDialog::_bind_methods() {
	//NOP
}

void OverwriteDialog::set_message(const String &p_text) {
	message->set_text(p_text);
}

/*************************************************************************/

static Ref<ImageTexture> generate_icon(int p_index) {
	Ref<Image> img = memnew(Image);

#ifdef MODULE_SVG_ENABLED
	// Upsample icon generation only if the scale isn't an integer multiplier.
	// Generating upsampled icons is slower, and the benefit is hardly visible
	// with integer scales.
	ImageLoaderSVG img_loader;
	img_loader.create_image_from_string(img, gdre_icons_sources[p_index], 1.0, false, false);
#endif

	return ImageTexture::create_from_image(img);
}

#ifdef TOOLS_ENABLED
GodotREEditor::GodotREEditor(EditorNode *p_editor) {
	singleton = this;
	editor = p_editor;
	ne_parent = editor->get_gui_base();

	init_gui(editor->get_gui_base(), (HBoxContainer *)editor->get_title_bar(), false);
}
#endif

GodotREEditor::GodotREEditor(Control *p_control, HBoxContainer *p_menu) {
	singleton = this;
	ne_parent = p_control;
	pdialog_singleton = memnew(GDREProgressDialog);
	p_control->add_child(pdialog_singleton);

	init_gui(p_control, p_menu, true);
}

void init_icons() {
}

void GodotREEditor::init_gui(Control *p_control, HBoxContainer *p_menu, bool p_long_menu) {
	//Init dialogs

	// Convert the generated icon sources to a dictionary for easier access.
	// Unlike the editor icons, there is no central repository of icons in the Theme resource itself to keep it tidy.
	for (int i = 0; i < gdre_icons_count; i++) {
		icons[gdre_icons_names[i]] = generate_icon(i);
		ne_parent->add_theme_icon_override(gdre_icons_names[i], icons[gdre_icons_names[i]]);
	}

	ovd = memnew(OverwriteDialog);
	p_control->add_child(ovd);

	rdl = memnew(ResultDialog);
	p_control->add_child(rdl);

	key_dialog = memnew(EncKeyDialog);
	p_control->add_child(key_dialog);

	script_dialog_d = memnew(ScriptDecompDialog);
	script_dialog_d->connect("confirmed", callable_mp(this, &GodotREEditor::_decompile_files));
	p_control->add_child(script_dialog_d);

	script_dialog_c = memnew(ScriptCompDialog);
	script_dialog_c->connect("confirmed", callable_mp(this, &GodotREEditor::_compile_files));
	p_control->add_child(script_dialog_c);

	pck_dialog = memnew(PackDialog);
	pck_dialog->connect("confirmed", callable_mp(this, &GodotREEditor::_pck_extract_files));
	pck_dialog->connect("canceled", callable_mp(this, &GodotREEditor::_pck_unload));
	p_control->add_child(pck_dialog);

	auto desktop_dir = GDRESettings::get_singleton()->get_home_dir().path_join("Desktop");
	pck_source_folder = memnew(FileDialog);
	pck_source_folder->set_access(FileDialog::ACCESS_FILESYSTEM);
	pck_source_folder->set_file_mode(FileDialog::FILE_MODE_OPEN_DIR);
	pck_source_folder->connect("dir_selected", callable_mp(this, &GodotREEditor::_pck_create_request));
	pck_source_folder->set_show_hidden_files(true);
	pck_source_folder->set_current_dir(desktop_dir);
	p_control->add_child(pck_source_folder);

	pck_save_dialog = memnew(NewPackDialog);
	pck_save_dialog->connect("confirmed", callable_mp(this, &GodotREEditor::_pck_save_prep));
	p_control->add_child(pck_save_dialog);

	pck_save_file_selection = memnew(FileDialog);
	pck_save_file_selection->set_access(FileDialog::ACCESS_FILESYSTEM);
	pck_save_file_selection->set_file_mode(FileDialog::FILE_MODE_SAVE_FILE);
	pck_save_file_selection->connect("file_selected", callable_mp(this, &GodotREEditor::_pck_save_request));
	pck_save_file_selection->set_show_hidden_files(true);
	pck_save_file_selection->set_current_dir(desktop_dir);
	p_control->add_child(pck_save_file_selection);

	pck_file_selection = memnew(FileDialog);
	pck_file_selection->set_access(FileDialog::ACCESS_FILESYSTEM);
	pck_file_selection->set_file_mode(FileDialog::FILE_MODE_OPEN_FILES);
	pck_file_selection->add_filter("*.pck;PCK archive files");
	pck_file_selection->add_filter("*.exe,*.bin,*.32,*.64;Self contained executable files");
	pck_file_selection->add_filter("*.apk;Android application files");
	pck_file_selection->add_filter("*.zip;Zipped Godot project files");
	pck_file_selection->connect("files_selected", callable_mp(this, &GodotREEditor::_pck_select_request));
	pck_file_selection->set_show_hidden_files(true);
	pck_file_selection->set_current_dir(desktop_dir);
	p_control->add_child(pck_file_selection);

	bin_res_file_selection = memnew(FileDialog);
	bin_res_file_selection->set_access(FileDialog::ACCESS_FILESYSTEM);
	bin_res_file_selection->set_file_mode(FileDialog::FILE_MODE_OPEN_FILES);
	bin_res_file_selection->add_filter("*.scn,*.res;Binary resource files");
	bin_res_file_selection->connect("files_selected", callable_mp(this, &GodotREEditor::_res_bin_2_txt_request));
	bin_res_file_selection->set_show_hidden_files(true);
	bin_res_file_selection->set_current_dir(desktop_dir);
	p_control->add_child(bin_res_file_selection);

	txt_res_file_selection = memnew(FileDialog);
	txt_res_file_selection->set_access(FileDialog::ACCESS_FILESYSTEM);
	txt_res_file_selection->set_file_mode(FileDialog::FILE_MODE_OPEN_FILES);
	txt_res_file_selection->add_filter("*.escn,*.tscn,*.tres;Text resource files");
	txt_res_file_selection->connect("files_selected", callable_mp(this, &GodotREEditor::_res_txt_2_bin_request));
	txt_res_file_selection->set_show_hidden_files(true);
	txt_res_file_selection->set_current_dir(desktop_dir);
	p_control->add_child(txt_res_file_selection);

	stex_file_selection = memnew(FileDialog);
	stex_file_selection->set_access(FileDialog::ACCESS_FILESYSTEM);
	stex_file_selection->set_file_mode(FileDialog::FILE_MODE_OPEN_FILES);
	stex_file_selection->add_filter("*.ctex,*.stex,*.tex;Stream texture files");
	stex_file_selection->connect("files_selected", callable_mp(this, &GodotREEditor::_res_stex_2_png_request));
	stex_file_selection->set_show_hidden_files(true);
	stex_file_selection->set_current_dir(desktop_dir);
	p_control->add_child(stex_file_selection);

	ostr_file_selection = memnew(FileDialog);
	ostr_file_selection->set_access(FileDialog::ACCESS_FILESYSTEM);
	ostr_file_selection->set_file_mode(FileDialog::FILE_MODE_OPEN_FILES);
	ostr_file_selection->add_filter("*.oggstr,*.oggvorbisstr;OGG Sample files");
	ostr_file_selection->connect("files_selected", callable_mp(this, &GodotREEditor::_res_ostr_2_ogg_request));
	ostr_file_selection->set_show_hidden_files(true);
	ostr_file_selection->set_current_dir(desktop_dir);
	p_control->add_child(ostr_file_selection);

	smpl_file_selection = memnew(FileDialog);
	smpl_file_selection->set_access(FileDialog::ACCESS_FILESYSTEM);
	smpl_file_selection->set_file_mode(FileDialog::FILE_MODE_OPEN_FILES);
	smpl_file_selection->add_filter("*.sample;WAV Sample files");
	smpl_file_selection->connect("files_selected", callable_mp(this, &GodotREEditor::_res_smpl_2_wav_request));
	smpl_file_selection->set_show_hidden_files(true);
	smpl_file_selection->set_current_dir(desktop_dir);
	p_control->add_child(smpl_file_selection);

	//Init about/warning dialog
	{
		about_dialog = memnew(AcceptDialog);
		p_control->add_child(about_dialog);
		about_dialog->set_title(RTR("Important: Legal Notice"));

		VBoxContainer *about_vbc = memnew(VBoxContainer);
		about_dialog->add_child(about_vbc);

		HBoxContainer *about_hbc = memnew(HBoxContainer);
		about_vbc->add_child(about_hbc);

		TextureRect *about_icon = memnew(TextureRect);
		about_hbc->add_child(about_icon);
		about_icon->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);
		about_icon->set_texture(icons["RELogoBig"]);

		Label *about_label = memnew(Label);
		about_hbc->add_child(about_label);
		about_label->set_custom_minimum_size(Size2(600, 100) * EDSCALE);
		about_label->set_v_size_flags(Control::SIZE_EXPAND_FILL);
		about_label->set_h_size_flags(Control::SIZE_EXPAND_FILL);
		about_label->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);
		String about_text = GDRESettings::get_singleton()->get_disclaimer_text();
		about_label->set_text(about_text);

#ifdef TOOLS_ENABLED
		if (EditorSettings::get_singleton()) {
			EDITOR_DEF("re/editor/show_info_on_start", true);
		}
#endif
		about_dialog_checkbox = memnew(CheckBox);
		about_vbc->add_child(about_dialog_checkbox);
		about_dialog_checkbox->set_text(RTR("Show this warning when starting the editor"));
		about_dialog_checkbox->connect("toggled", callable_mp(this, &GodotREEditor::_toggle_about_dialog_on_start));
	}

	//Init menu
	if (p_long_menu) {
		p_menu->set_anchor(Side::SIDE_TOP, 0);
		menu_button = memnew(MenuButton);
		menu_button->set_text(RTR("RE Tools"));
		menu_button->set_button_icon(icons["RELogo"]);
		menu_popup = menu_button->get_popup();
		menu_popup->add_icon_item(icons["RELogo"], RTR("Recover project..."), MENU_EXT_PCK);
		menu_popup->add_separator();
		menu_popup->add_icon_item(icons["RELogo"], RTR("Set encryption key..."), MENU_KEY);
		menu_popup->add_separator();
		menu_popup->add_icon_item(icons["RELogo"], RTR("About Godot RE Tools"), MENU_ABOUT_RE);
		menu_popup->add_icon_item(icons["RELogo"], RTR("Report a bug..."), MENU_REPORT_ISSUE);

		menu_popup->add_icon_item(icons["RELogo"], RTR("Quit"), MENU_EXIT_RE);
		menu_popup->connect("id_pressed", callable_mp(this, &GodotREEditor::menu_option_pressed));
		menu_button->set_anchor(Side::SIDE_TOP, 0);
		p_menu->add_child(menu_button);

		menu_button = memnew(MenuButton);
		menu_button->set_text(RTR("PCK"));
		menu_button->set_button_icon(icons["REPack"]);
		menu_popup = menu_button->get_popup();
		menu_popup->add_icon_item(icons["REPack"], RTR("Create PCK archive from folder..."), MENU_CREATE_PCK);
		menu_button->set_anchor(Side::SIDE_TOP, 0);
		menu_popup->connect("id_pressed", callable_mp(this, &GodotREEditor::menu_option_pressed));
		p_menu->add_child(menu_button);

		menu_button = memnew(MenuButton);
		menu_button->set_text(RTR("GDScript"));
		menu_button->set_button_icon(icons["REScript"]);
		menu_popup = menu_button->get_popup();
		menu_popup->add_icon_item(icons["REScript"], RTR("Decompile .GDC/.GDE script files..."), MENU_DECOMP_GDS);
		menu_popup->add_icon_item(icons["REScript"], RTR("Compile .GD script files..."), MENU_COMP_GDS);
		menu_button->set_anchor(Side::SIDE_TOP, 0);
		menu_popup->connect("id_pressed", callable_mp(this, &GodotREEditor::menu_option_pressed));
		p_menu->add_child(menu_button);

		menu_button = memnew(MenuButton);
		menu_button->set_text(RTR("Resources"));
		menu_button->set_button_icon(icons["REResBT"]);
		menu_popup = menu_button->get_popup();
		menu_popup->add_icon_item(icons["REResBT"], RTR("Convert binary resources to text..."), MENU_CONV_TO_TXT);
		menu_popup->add_icon_item(icons["REResTB"], RTR("Convert text resources to binary..."), MENU_CONV_TO_BIN);
		menu_popup->add_separator();
		menu_popup->add_icon_item(icons["REResOther"], RTR("Convert stream textures to PNG..."), MENU_STEX_TO_PNG);
		menu_popup->add_icon_item(icons["REResOther"], RTR("Convert OGG Samples to OGG..."), MENU_OSTR_TO_OGG);
		menu_popup->add_icon_item(icons["REResOther"], RTR("Convert WAV Samples to WAV..."), MENU_SMPL_TO_WAV);
		menu_popup->connect("id_pressed", callable_mp(this, &GodotREEditor::menu_option_pressed));
		menu_button->set_anchor(Side::SIDE_TOP, 0);
		p_menu->add_child(menu_button);
	} else {
		menu_button = memnew(MenuButton);
		menu_button->set_text(RTR("RE Tools"));
		menu_button->set_button_icon(icons["RELogo"]);
		menu_popup = menu_button->get_popup();

		menu_popup->add_icon_item(icons["RELogo"], RTR("Recover project..."), MENU_EXT_PCK);
		menu_popup->add_separator();
		menu_popup->add_icon_item(icons["RELogo"], RTR("Set encryption key..."), MENU_KEY);
		menu_popup->add_separator();
		menu_popup->add_icon_item(icons["RELogo"], RTR("About Godot RE Tools"), MENU_ABOUT_RE);
		menu_popup->add_icon_item(icons["RELogo"], RTR("Report a bug..."), MENU_REPORT_ISSUE);
		menu_popup->add_separator();

		menu_popup->add_icon_item(icons["REPack"], RTR("Create PCK archive from folder..."), MENU_CREATE_PCK);
		menu_popup->add_separator();

		menu_popup->add_icon_item(icons["REScript"], RTR("Decompile .GDC/.GDE script files..."), MENU_DECOMP_GDS);
		menu_popup->add_icon_item(icons["REScript"], RTR("Compile .GD script files..."), MENU_COMP_GDS);
		menu_popup->set_item_disabled(menu_popup->get_item_index(MENU_COMP_GDS), true); //TEMP RE-ENABLE WHEN IMPLEMENTED
		menu_popup->add_separator();
		menu_popup->add_icon_item(icons["REResBT"], RTR("Convert binary resources to text..."), MENU_CONV_TO_TXT);
		menu_popup->add_icon_item(icons["REResTB"], RTR("Convert text resources to binary..."), MENU_CONV_TO_BIN);
		menu_popup->add_separator();
		menu_popup->add_icon_item(icons["REResOther"], RTR("Convert stream textures to PNG..."), MENU_STEX_TO_PNG);
		menu_popup->add_icon_item(icons["REResOther"], RTR("Convert OGG Samples to OGG..."), MENU_OSTR_TO_OGG);
		menu_popup->add_icon_item(icons["REResOther"], RTR("Convert WAV Samples to WAV..."), MENU_SMPL_TO_WAV);
		menu_popup->connect("id_pressed", callable_mp(this, &GodotREEditor::menu_option_pressed));
		menu_button->set_anchor(Side::SIDE_TOP, 0);
		p_menu->add_child(menu_button);
		if (p_menu->get_child_count() >= 2) {
			menu_button->set_anchor(Side::SIDE_TOP, 0);
			p_menu->move_child(menu_button, p_menu->get_child_count() - 2);
		}
	}
}

GodotREEditor::~GodotREEditor() {
	singleton = NULL;
	if (pdialog_singleton) {
		auto parent = pdialog_singleton->get_parent();
		if (parent) {
			parent->remove_child(pdialog_singleton);
		}
		memdelete(pdialog_singleton);
	}
}

void GodotREEditor::show_about_dialog() {
#ifdef TOOLS_ENABLED
	if (EditorSettings::get_singleton()) {
		bool show_on_start = EDITOR_GET("re/editor/show_info_on_start");
		about_dialog_checkbox->set_pressed(show_on_start);
	} else {
#else
	{
#endif
		about_dialog_checkbox->set_visible(false);
	}
	about_dialog->popup_centered();
}

void GodotREEditor::_toggle_about_dialog_on_start(bool p_enabled) {
#ifdef TOOLS_ENABLED
	if (EditorSettings::get_singleton()) {
		bool show_on_start = EDITOR_GET("re/editor/show_info_on_start");
		if (show_on_start != p_enabled) {
			EditorSettings::get_singleton()->set_setting("re/editor/show_info_on_start", p_enabled);
		}
	}
#endif
}

void GodotREEditor::menu_option_pressed(int p_id) {
	switch (p_id) {
		case MENU_ONE_CLICK_UNEXPORT: {
		} break;
		case MENU_KEY: {
			key_dialog->popup_centered(Size2(600, 400));
		} break;
		case MENU_CREATE_PCK: {
			pck_source_folder->popup_centered(Size2(600, 400));
		} break;
		case MENU_EXT_PCK: {
			pck_file_selection->popup_centered(Size2(600, 400));
		} break;
		case MENU_DECOMP_GDS: {
			script_dialog_d->popup_centered(Size2(600, 400));
		} break;
		case MENU_COMP_GDS: {
			script_dialog_c->popup_centered(Size2(600, 400));
		} break;
		case MENU_CONV_TO_TXT: {
			bin_res_file_selection->popup_centered(Size2(600, 400));
		} break;
		case MENU_CONV_TO_BIN: {
			txt_res_file_selection->popup_centered(Size2(600, 400));
		} break;
		case MENU_STEX_TO_PNG: {
			stex_file_selection->popup_centered(Size2(600, 400));
		} break;
		case MENU_OSTR_TO_OGG: {
			ostr_file_selection->popup_centered(Size2(600, 400));
		} break;
		case MENU_SMPL_TO_WAV: {
			smpl_file_selection->popup_centered(Size2(600, 400));
		} break;
		case MENU_ABOUT_RE: {
			show_about_dialog();
		} break;
		case MENU_REPORT_ISSUE: {
			OS::get_singleton()->shell_open("https://github.com/bruvzg/gdsdecomp/issues/new?assignees=&labels=bug&template=bug_report.yml&sys_info=" + GDRESettings::get_singleton()->get_sys_info_string());
		} break;
		case MENU_EXIT_RE: {
			get_tree()->quit();
		} break;
		default:
			ERR_FAIL();
	}
}

#if defined(MINGW_ENABLED) || defined(_MSC_VER)
#define sprintf sprintf_s
#endif
// TODO: More robust logging
void GodotREEditor::print_log(const String &p_text) {
	Vector<String> lines = p_text.split("\n");
	for (int i = 0; i < lines.size(); i++) {
		if (!lines[i].is_empty()) {
			emit_signal("write_log_message", lines[i] + "\n");
		}
	}
}

void GodotREEditor::print_warning(const String &p_text, const String &p_title, const String &p_sub_text) {
	char timestamp[21];
	OS::DateTime date = OS::get_singleton()->get_datetime();
	snprintf(timestamp, sizeof(timestamp), "-%04d-%02d-%02d-%02d-%02d-%02d", (uint16_t)date.year, date.month, date.day, date.hour, date.minute, date.second);

	Vector<String> lines = p_text.split("\n");
	if (lines.size() > 1) {
		emit_signal("write_log_message", String(timestamp) + " [" + p_title + "]: " + "\n");
		for (int i = 0; i < lines.size(); i++) {
			emit_signal("write_log_message", String(timestamp) + "       " + lines[i] + "\n");
		}
	} else {
		emit_signal("write_log_message", String(timestamp) + " [" + p_title + "]: " + p_text + "\n");
	}
	if (p_sub_text != String()) {
		Vector<String> slines = p_sub_text.split("\n");
		if (slines.size() > 1) {
			for (int i = 0; i < slines.size(); i++) {
				emit_signal("write_log_message", String(timestamp) + "       " + slines[i] + "\n");
			}
		} else {
			emit_signal("write_log_message", String(timestamp) + " [" + p_title + "]: " + p_sub_text + "\n");
		}
	}
}

void GodotREEditor::show_warning(const String &p_text, const String &p_title, const String &p_sub_text) {
	print_warning(p_text, p_title, p_sub_text);

	rdl->set_title(p_title);
	rdl->set_message(p_text, p_sub_text);
	rdl->popup_centered();
}

void GodotREEditor::show_report(const String &p_text, const String &p_title, const String &p_sub_text) {
	rdl->set_title(p_title);
	rdl->set_message(p_text, p_sub_text);
	Size2i size = rdl->get_parent_rect().get_size();
	size.height -= (size.height / 3);
	size.width -= (size.width / 2);
	rdl->set_wrap_controls(true);
	rdl->popup_centered(size);
}
/*************************************************************************/
/* Decompile                                                             */
/*************************************************************************/

void GodotREEditor::_decompile_files() {
	Vector<String> files = script_dialog_d->get_file_list();
	String dir = script_dialog_d->get_target_dir();

	Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
	String overwrite_list = String();
	for (int i = 0; i < files.size(); i++) {
		String target_name = dir.path_join(files[i].get_file().get_basename() + ".gd");
		if (da->file_exists(target_name)) {
			overwrite_list += target_name + "\n";
		}
	}

	if (overwrite_list.length() == 0) {
		_decompile_process();
	} else {
		ovd->set_message(overwrite_list);
		ovd->connect("confirmed", callable_mp(this, &GodotREEditor::_decompile_process).bind(Vector<Variant>()), CONNECT_ONE_SHOT);
		ovd->popup_centered();
	}
}

void GodotREEditor::_decompile_process() {
	Vector<String> files = script_dialog_d->get_file_list();
	Vector<uint8_t> key = key_dialog->get_key();
	String dir = script_dialog_d->get_target_dir();

	print_warning("GDScriptDecomp{" + itos(script_dialog_d->get_bytecode_version()) + "}", RTR("Decompile"));

	String failed_files;
	GDScriptDecomp *dce = create_decomp_for_commit(script_dialog_d->get_bytecode_version());

	if (!dce) {
		show_warning(failed_files, RTR("Decompile"), RTR("Invalid bytecode version!"));
		return;
	}

	EditorProgressGDDC *pr = memnew(EditorProgressGDDC(ne_parent, "re_decompile", RTR("Decompiling files..."), files.size(), true));
	for (int i = 0; i < files.size(); i++) {
		print_warning(RTR("decompiling") + " " + files[i].get_file(), RTR("Decompile"));

		String target_name = dir.path_join(files[i].get_file().get_basename() + ".gd");

		bool cancel = pr->step(files[i].get_file(), i, true);
		if (cancel) {
			break;
		}

		Error err;
		if (files[i].ends_with(".gde")) {
			err = dce->decompile_byte_code_encrypted(files[i], key);
		} else {
			err = dce->decompile_byte_code(files[i]);
		}

		if (err == OK) {
			String scr = dce->get_script_text();
			Ref<FileAccess> file = FileAccess::open(target_name, FileAccess::WRITE, &err);
			if (err) {
				failed_files += files[i] + " (FileAccess error)\n";
			}
			file->store_string(scr);
		} else {
			failed_files += files[i] + " (" + dce->get_error_message() + ")\n";
		}
	}

	memdelete(pr);
	memdelete(dce);

	if (failed_files.length() > 0) {
		show_warning(failed_files, RTR("Decompile"), RTR("At least one error was detected!"));
	} else {
		show_warning(RTR("No errors detected."), RTR("Decompile"), RTR("The operation completed successfully!"));
	}
}

/*************************************************************************/
/* Compile                                                               */
/*************************************************************************/

void GodotREEditor::_compile_files() {
	Vector<String> files = script_dialog_c->get_file_list();
	Vector<uint8_t> key = key_dialog->get_key();
	String dir = script_dialog_c->get_target_dir();
	String ext = (key.size() == 32) ? ".gde" : ".gds";

	Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
	String overwrite_list = String();
	for (int i = 0; i < files.size(); i++) {
		String target_name = dir.path_join(files[i].get_file().get_basename() + ext);
		if (da->file_exists(target_name)) {
			overwrite_list += target_name + "\n";
		}
	}

	if (overwrite_list.length() == 0) {
		_compile_process();
	} else {
		ovd->set_message(overwrite_list);
		ovd->connect("confirmed", callable_mp(this, &GodotREEditor::_compile_process).bind(Vector<Variant>()), CONNECT_ONE_SHOT);
		ovd->popup_centered();
	}
}

void GodotREEditor::_compile_process() {
	Vector<String> files = script_dialog_c->get_file_list();
	int rev = script_dialog_c->get_bytecode_version();
	Vector<uint8_t> key = key_dialog->get_key();
	String dir = script_dialog_c->get_target_dir();
	String ext = (key.size() == 32) ? ".gde" : ".gdc";

	String failed_files;
	Ref<GDScriptDecomp> dce = GDScriptDecomp::create_decomp_for_commit(rev);
	EditorProgressGDDC *pr = memnew(EditorProgressGDDC(ne_parent, "re_compile", RTR("Compiling files..."), files.size(), true));

	for (int i = 0; i < files.size(); i++) {
		print_warning(RTR("compiling") + " " + files[i].get_file(), RTR("Compile"));
		String target_name = dir.path_join(files[i].get_file().get_basename() + ext);

		bool cancel = pr->step(files[i].get_file(), i, true);
		if (cancel) {
			break;
		}

		Vector<uint8_t> file = FileAccess::get_file_as_bytes(files[i]);
		if (file.size() > 0) {
			String txt;
			txt.append_utf8((const char *)file.ptr(), file.size());
			file = dce->compile_code_string(txt);
			if (file.is_empty()) {
				failed_files += files[i] + " (" + dce->get_error_message() + ")\n";
				continue;
			}
			Ref<FileAccess> fa = FileAccess::open(target_name, FileAccess::WRITE);
			if (fa.is_valid()) {
				if (key.size() == 32) {
					Ref<FileAccessEncryptedv3> fae;
					fae.instantiate();
					Error err = fae->open_and_parse(fa, key, FileAccessEncryptedv3::MODE_WRITE_AES256);
					if (err == OK) {
						fae->store_buffer(file.ptr(), file.size());
						fae->close();
					} else {
						failed_files += files[i] + " (FileAccessEncrypted error)\n";
					}
				} else {
					fa->store_buffer(file.ptr(), file.size());
					fa->close();
				}
			} else {
				failed_files += files[i] + " (FileAccess error)\n";
			}
		} else {
			failed_files += files[i] + " (Empty file)\n";
		}
	}

	memdelete(pr);

	if (failed_files.length() > 0) {
		show_warning(failed_files, RTR("Compile"), RTR("At least one error was detected!"));
	} else {
		show_warning(RTR("No errors detected."), RTR("Compile"), RTR("The operation completed successfully!"));
	}
}

/*************************************************************************/
/* PCK explorer                                                          */
/*************************************************************************/

void GodotREEditor::_pck_select_request(const Vector<String> &p_paths) {
	pck_dialog->clear();
	pck_files.clear();
	pck_file = p_paths[0];
	if (key_dialog->get_key().size() > 0) {
		GDRESettings::get_singleton()->set_encryption_key(key_dialog->get_key());
	}

	Error err = GDRESettings::get_singleton()->load_project(p_paths);
	if (err) {
		if (err == ERR_PRINTER_ON_FIRE) {
			show_warning(RTR("Error opening encrypted PCK file: ") + pck_file + "\nSet correct encryption key and try again.", RTR("Read PCK"));
		} else {
			show_warning(RTR("Error opening PCK file: ") + pck_file, RTR("Read PCK"));
		}
		return;
	}
	pck_file_selection->set_visible(false);

	Ref<PckDumper> pckdump;
	pckdump.instantiate();
	Vector<String> broken_files;
	int files_checked = 0;
	int files_broken = 0;

	err = pckdump->_check_md5_all_files(broken_files, files_checked);
	// memdelete(pr);
	// canceled
	bool p_check_md5 = true;
	pck_dialog->set_version(GDRESettings::get_singleton()->get_version_string());
	pck_dialog->set_info(String("    ") + RTR("Total files: ") +
			itos(GDRESettings::get_singleton()->get_file_count()) + "; " + RTR("Checked: ") +
			itos(files_checked) + "; " + RTR("Broken: ") + itos(broken_files.size()));
	print_line(RTR("Total files: ") + itos(GDRESettings::get_singleton()->get_file_count()) +
					"; " + RTR("Checked: ") + itos(files_checked) + "; " + RTR("Broken: ") + itos(files_broken),
			RTR("Read PCK"));
	if (err == ERR_PRINTER_ON_FIRE || err == ERR_SKIP) {
		//do things
		p_check_md5 = false;
	}
	auto files = GDRESettings::get_singleton()->get_file_info_list();
	pck_dialog->start_initial_load();
	for (int i = 0; i < files.size(); i++) {
		const auto &file = files[i];
		Ref<Texture2D> icon;
		String error_string = "";
		bool md5_error = !file->is_checksum_validated();
		bool is_malformed = file->is_malformed();
		if (p_check_md5 && md5_error) {
			icon = icons["REFileBroken"];
			error_string += "MD5 mismatch";
		} else if (is_malformed) {
			icon = icons["REFileBroken"];
			error_string += String(error_string.length() > 0 ? ", " : "") + "Malformed_path";
		} else if (!p_check_md5) {
			icon = icons["REFile"];
		} else {
			icon = icons["REFileOk"];
		}
		if (files.size() > 50000 && i % 10000 == 0) {
			print_line("Loading file " + itos(i) + " of " + itos(files.size()));
		}
		pck_dialog->add_file(file->get_path(), file->get_size(), icon, error_string, file->is_malformed(), file->is_encrypted());
	}
	pck_dialog->finish_initial_load();
	String user_desktop = OS::get_singleton()->get_system_dir(OS::SYSTEM_DIR_DESKTOP);
	String proj_name = pck_file.get_file().get_basename();
	pck_dialog->set_target_dir(user_desktop.path_join(proj_name));
	pck_dialog->popup_centered(Size2(600, 400));
}

void GodotREEditor::_pck_unload() {
	GDRESettings::get_singleton()->unload_project();
}

void GodotREEditor::_pck_extract_files() {
	Vector<String> files = pck_dialog->get_selected_files();
	String dir = pck_dialog->get_target_dir();
	Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
	String overwrite_list = String();
	for (int i = 0; i < files.size(); i++) {
		String target_name = dir.path_join(files[i].get_file());
		if (da->file_exists(target_name)) {
			overwrite_list += target_name + "\n";
		}
	}

	if (overwrite_list.length() == 0) {
		_pck_extract_files_process();
	} else {
		ovd->set_message(overwrite_list);
		ovd->connect("confirmed", callable_mp(this, &GodotREEditor::_pck_extract_files_process).bind(), CONNECT_ONE_SHOT);
		ovd->connect("canceled", callable_mp(this, &GodotREEditor::_pck_unload).bind(), CONNECT_ONE_SHOT);

		ovd->popup_centered();
	}
}

void GodotREEditor::_pck_extract_files_process() {
	Vector<String> files = pck_dialog->get_selected_files();
	String dir = pck_dialog->get_target_dir();
	bool is_full_recovery = pck_dialog->get_is_full_recovery();
	GDRESettings::get_singleton()->open_log_file(dir);
	String failed_files;
	pck_dialog->set_visible(false);
	ovd->set_visible(false);
	Ref<PckDumper> pckdumper;
	pckdumper.instantiate();
	Error err = pckdumper->_pck_dump_to_dir(dir, files, failed_files);
	Ref<ImportExporter> ie;

	if (is_full_recovery && !err) {
		ie.instantiate();
		err = ie->export_imports(dir, files);
	}
	pck_file = String();
	String log_path = GDRESettings::get_singleton()->get_log_file_path();
	String report = "Log file written to " + log_path;
	report += "\nPlease include this file when reporting an issue!\n\n";
	if (ie.is_valid()) {
		auto full_report = ie->get_report();
		if (is_full_recovery) {
			report += full_report->get_editor_message_string();
			String notes = full_report->get_session_notes_string();
			if (!notes.is_empty()) {
				report += "\n------------IMPORTANT NOTES-----------\n";
				report += notes;
			}
			report += "\n************EXPORT REPORT************\n";

			report += full_report->get_report_string();
		}
	}
	_pck_unload();
	GDRESettings::get_singleton()->close_log_file();

	if (failed_files.length() > 0) {
		show_warning(report + failed_files, RTR("Read PCK"), RTR("At least one error was detected!") + (is_full_recovery ? "\nSkipping full recovery!" : ""));
	} else if (is_full_recovery && !err) {
		show_report(report, RTR("Recovery report"), RTR("Recovery report"));
	} else if (!is_full_recovery && !err) {
		show_report(RTR("No errors detected.") + String("\n") + report, RTR("Extract report"), RTR("Extract report"));
	} else if (err) {
		show_warning("Resource export failed." + String("\n") + report, RTR("Read PCK"), RTR("At least one error was detected!") + (is_full_recovery ? "\nSkipping full recovery!" : ""));
	}
	print_warning("Log file written to " + log_path, RTR("Read PCK"));
	print_warning("Please include this file when reporting an issue!", RTR("Read PCK"));
}

/*************************************************************************/
/* Res convert                                                           */
/*************************************************************************/

void GodotREEditor::_res_smpl_2_wav_request(const Vector<String> &p_files) {
	Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
	String overwrite_list = String();
	for (int i = 0; i < p_files.size(); i++) {
		if (da->file_exists(p_files[i].get_basename() + ".wav")) {
			overwrite_list += p_files[i].get_basename() + ".wav" + "\n";
		}
	}

	res_files = p_files;

	if (overwrite_list.length() == 0) {
		_res_smpl_2_wav_process();
	} else {
		ovd->set_message(overwrite_list);
		ovd->connect("confirmed", callable_mp(this, &GodotREEditor::_res_smpl_2_wav_process).bind(Vector<Variant>()), CONNECT_ONE_SHOT);
		ovd->popup_centered();
	}
}

void GodotREEditor::_res_smpl_2_wav_process() {
	EditorProgressGDDC *pr = memnew(EditorProgressGDDC(ne_parent, "re_wav2_res", RTR("Converting files..."), res_files.size(), true));

	String failed_files;
	Ref<ResourceFormatLoaderBinary> rl = memnew(ResourceFormatLoaderBinary);
	for (int i = 0; i < res_files.size(); i++) {
		print_warning("converting " + res_files[i], RTR("Convert WAV samples"));
		bool cancel = pr->step(res_files[i], i, true);
		if (cancel) {
			break;
		}
		String dst = res_files[i].get_basename() + ".wav";
		SampleExporter se;
		Error err = se.export_file(dst, res_files[i]);
		if (err) {
			failed_files += res_files[i] + " (load AudioStreamWAV error)\n";
			continue;
		}
	}

	memdelete(pr);
	res_files = Vector<String>();

	if (failed_files.length() > 0) {
		show_warning(failed_files, RTR("Convert WAV samples"), RTR("At least one error was detected!"));
	} else {
		show_warning(RTR("No errors detected."), RTR("Convert WAV samples"), RTR("The operation completed successfully!"));
	}
}

void GodotREEditor::_res_ostr_2_ogg_request(const Vector<String> &p_files) {
	Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
	String overwrite_list = String();
	for (int i = 0; i < p_files.size(); i++) {
		if (da->file_exists(p_files[i].get_basename() + ".ogg")) {
			overwrite_list += p_files[i].get_basename() + ".ogg" + "\n";
		}
	}

	res_files = p_files;

	if (overwrite_list.length() == 0) {
		_res_ostr_2_ogg_process();
	} else {
		ovd->set_message(overwrite_list);
		ovd->connect("confirmed", callable_mp(this, &GodotREEditor::_res_ostr_2_ogg_process).bind(Vector<Variant>()), CONNECT_ONE_SHOT);
		ovd->popup_centered();
	}
}

void GodotREEditor::_res_ostr_2_ogg_process() {
	EditorProgressGDDC *pr = memnew(EditorProgressGDDC(ne_parent, "re_ogg2_res", RTR("Converting files..."), res_files.size(), true));

	String failed_files;
	Ref<ResourceFormatLoaderBinary> rl = memnew(ResourceFormatLoaderBinary);
	for (int i = 0; i < res_files.size(); i++) {
		bool cancel = pr->step(res_files[i], i, true);
		if (cancel) {
			break;
		}

		print_warning("converting " + res_files[i], RTR("Convert OGG samples"));
		Error err;
		OggStrExporter ose;
		String dst = res_files[i].get_basename() + ".ogg";
		err = ose.export_file(dst, res_files[i]);
		if (err) {
			failed_files += res_files[i] + " (load AudioStreamOggVorbis error)\n";
			continue;
		}
	}

	memdelete(pr);
	res_files = Vector<String>();

	if (failed_files.length() > 0) {
		show_warning(failed_files, RTR("Convert OGG samples"), RTR("At least one error was detected!"));
	} else {
		show_warning(RTR("No errors detected."), RTR("Convert OGG samples"), RTR("The operation completed successfully!"));
	}
}

void GodotREEditor::_res_stex_2_png_request(const Vector<String> &p_files) {
	Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
	String overwrite_list = String();
	for (int i = 0; i < p_files.size(); i++) {
		if (da->file_exists(p_files[i].get_basename() + ".png")) {
			overwrite_list += p_files[i].get_basename() + ".png" + "\n";
		}
	}

	res_files = p_files;

	if (overwrite_list.length() == 0) {
		_res_stxt_2_png_process();
	} else {
		ovd->set_message(overwrite_list);
		ovd->connect("confirmed", callable_mp(this, &GodotREEditor::_res_stxt_2_png_process).bind(Vector<Variant>()), CONNECT_ONE_SHOT);
		ovd->popup_centered();
	}
}

void GodotREEditor::_res_stxt_2_png_process() {
	EditorProgressGDDC *pr = memnew(EditorProgressGDDC(ne_parent, "re_st2pnh_res", RTR("Converting files..."), res_files.size(), true));

	String failed_files;
	for (int i = 0; i < res_files.size(); i++) {
		print_warning("converting " + res_files[i], RTR("Convert textures"));
		bool cancel = pr->step(res_files[i], i, true);
		if (cancel) {
			break;
		}

		Error err;
		TextureExporter tlc;
		String dst = res_files[i].get_basename() + ".png";
		err = tlc.export_file(dst, res_files[i]);
		if (err != OK) {
			failed_files += res_files[i] + " (load StreamTexture error)\n";
			continue;
		}
	}

	memdelete(pr);
	res_files = Vector<String>();

	if (failed_files.length() > 0) {
		show_warning(failed_files, RTR("Convert textures"), RTR("At least one error was detected!"));
	} else {
		show_warning(RTR("No errors detected."), RTR("Convert textures"), RTR("The operation completed successfully!"));
	}
}

void GodotREEditor::_res_bin_2_txt_request(const Vector<String> &p_files) {
	Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
	String overwrite_list = String();
	for (int i = 0; i < p_files.size(); i++) {
		String ext = p_files[i].get_extension().to_lower();
		String new_ext = ".txt";
		if (ext == "scn") {
			if (p_files[i].to_lower().find(".escn.") != -1) {
				new_ext = ".escn";
			} else {
				new_ext = ".tscn";
			}
		} else if (ext == "res") {
			new_ext = ".tres";
		}

		if (da->file_exists(p_files[i].get_basename() + new_ext)) {
			overwrite_list += p_files[i].get_basename() + new_ext + "\n";
		}
	}

	res_files = p_files;

	if (overwrite_list.length() == 0) {
		_res_bin_2_txt_process();
	} else {
		ovd->set_message(overwrite_list);
		ovd->connect("confirmed", callable_mp(this, &GodotREEditor::_res_bin_2_txt_process).bind(), CONNECT_ONE_SHOT);
		ovd->popup_centered();
	}
}

void GodotREEditor::_res_bin_2_txt_process() {
	EditorProgressGDDC *pr = memnew(EditorProgressGDDC(ne_parent, "re_b2t_res", RTR("Converting files..."), res_files.size(), true));

	String failed_files;
	for (int i = 0; i < res_files.size(); i++) {
		print_warning("converting " + res_files[i], RTR("Convert resources"));
		bool cancel = pr->step(res_files[i], i, true);
		if (cancel) {
			break;
		}

		String ext = res_files[i].get_extension().to_lower();
		String new_ext = ".txt";
		if (ext == "scn") {
			if (res_files[i].to_lower().find(".escn") != -1) {
				new_ext = ".escn";
			} else {
				new_ext = ".tscn";
			}
		} else if (ext == "res") {
			new_ext = ".tres";
		}

		Error err = convert_file_to_text(res_files[i], res_files[i].get_basename() + new_ext);
		if (err != OK) {
			failed_files += res_files[i] + " (ResourceFormatLoaderText error)\n";
		}
	}

	memdelete(pr);
	res_files = Vector<String>();

	if (failed_files.length() > 0) {
		show_warning(failed_files, RTR("Convert resources"), RTR("At least one error was detected!"));
	} else {
		show_warning(RTR("No errors detected."), RTR("Convert resources"), RTR("The operation completed successfully!"));
	}
}

Error GodotREEditor::convert_file_to_text(const String &p_src_path, const String &p_dst_path) {
	return ResourceCompatLoader::to_text(p_src_path, p_dst_path);
}

void GodotREEditor::_res_txt_2_bin_request(const Vector<String> &p_files) {
	Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
	String overwrite_list = String();
	for (int i = 0; i < p_files.size(); i++) {
		String ext = p_files[i].get_extension().to_lower();
		String new_ext = ".bin";
		if (ext == "tscn" || ext == "escn") {
			new_ext = ".scn";
		} else if (ext == "tres") {
			new_ext = ".res";
		}

		if (da->file_exists(p_files[i].get_basename() + new_ext)) {
			overwrite_list += p_files[i].get_basename() + new_ext + "\n";
		}
	}

	res_files = p_files;

	if (overwrite_list.length() == 0) {
		_res_txt_2_bin_process();
	} else {
		ovd->set_message(overwrite_list);
		ovd->connect("confirmed", callable_mp(this, &GodotREEditor::_res_txt_2_bin_process).bind(Vector<Variant>()), CONNECT_ONE_SHOT);
		ovd->popup_centered();
	}
}

void GodotREEditor::_res_txt_2_bin_process() {
	EditorProgressGDDC *pr = memnew(EditorProgressGDDC(ne_parent, "re_t2b_res", RTR("Converting files..."), res_files.size(), true));

	String failed_files;
	for (int i = 0; i < res_files.size(); i++) {
		print_warning("converting " + res_files[i], RTR("Convert resources"));
		bool cancel = pr->step(res_files[i], i, true);
		if (cancel) {
			break;
		}

		String ext = res_files[i].get_extension().to_lower();
		String new_ext = ".bin";
		if (ext == "tscn" || ext == "escn") {
			new_ext = ".scn";
		} else if (ext == "tres") {
			new_ext = ".res";
		}

		Error err = convert_file_to_binary(res_files[i], res_files[i].get_basename() + new_ext);
		if (err != OK) {
			failed_files += res_files[i] + " (ResourceFormatLoaderText error)\n";
		}
	}

	memdelete(pr);
	res_files = Vector<String>();

	if (failed_files.length() > 0) {
		show_warning(failed_files, RTR("Convert resources"), RTR("At least one error was detected!"));
	} else {
		show_warning(RTR("No errors detected."), RTR("Convert resources"), RTR("The operation completed successfully!"));
	}
}

Error GodotREEditor::convert_file_to_binary(const String &p_src_path, const String &p_dst_path) {
	return ResourceCompatLoader::to_binary(p_src_path, p_dst_path);
}

/*************************************************************************/
/* PCK create                                                            */
/*************************************************************************/

void GodotREEditor::_pck_create_request(const String &p_path) {
	pck_save_files.clear();
	pck_file = p_path;
	if (key_dialog->get_key().size() > 0) {
		GDRESettings::get_singleton()->set_encryption_key(key_dialog->get_key());
	}

	pck_save_dialog->popup_centered(Size2(600, 400));
}

void GodotREEditor::_pck_save_prep() {
	pck_save_file_selection->clear_filters();
	if (pck_save_dialog->get_is_emb()) {
		pck_save_file_selection->add_filter("*.exe,*.bin,*.32,*.64,*.x86_64,*.x86,*.arm64,*.universal;Self contained executable files");
	} else {
		pck_save_file_selection->add_filter("*.pck;PCK archive files");
	}
	pck_save_file_selection->popup_centered(Size2(600, 400));
}

#define PCK_PADDING 16

void GodotREEditor::_pck_save_request(const String &p_path) {
	String error_string;
	Vector<String> include_filters = pck_save_dialog->get_enc_filters_in().split(",", false);
	Vector<String> exclude_filters = pck_save_dialog->get_enc_filters_ex().split(",", false);
	auto file_paths_to_pack = PckCreator::get_files_to_pack(pck_file, include_filters, exclude_filters);
	if (file_paths_to_pack.is_empty()) {
		show_warning(RTR("Error opening folder (or empty folder): ") + pck_file, RTR("New PCK"));
		return;
	}
	PckCreator pck_creator;
	pck_creator.set_embed(pck_save_dialog->get_is_emb());
	pck_creator.set_pack_version(pck_save_dialog->get_version_pack());
	pck_creator.set_ver_major(pck_save_dialog->get_version_major());
	pck_creator.set_ver_minor(pck_save_dialog->get_version_minor());
	pck_creator.set_ver_rev(pck_save_dialog->get_version_rev());
	pck_creator.set_encrypt(pck_save_dialog->get_enc_dir());
	pck_creator.set_exe_to_embed(pck_save_dialog->get_emb_source());
	pck_creator.set_watermark(pck_save_dialog->get_watermark());
	Error err;
	err = pck_creator._process_folder(p_path, pck_file, file_paths_to_pack);
	if (err) {
		show_warning(RTR(pck_creator.get_error_message()) + ":" + pck_file, RTR("New PCK"));
		return;
	}
	err = pck_creator._create_after_process();
	if (err) {
		show_warning(RTR(pck_creator.get_error_message()) + ":" + pck_file, RTR("New PCK"));
	}
}

/*************************************************************************/

void GodotREEditor::_notification(int p_notification) {
	switch (p_notification) {
		case NOTIFICATION_READY: {
#ifdef TOOLS_ENABLED
			if (EditorSettings::get_singleton()) {
				bool show_info_dialog = EDITOR_GET("re/editor/show_info_on_start");
				if (show_info_dialog) {
					about_dialog->set_exclusive(true);
					show_about_dialog();
					about_dialog->set_exclusive(false);
				}
			}
#endif
			emit_signal("write_log_message", String("****\nGodot RE Tools, ") + String(GDRE_VERSION) + String("\n****\n\n"));
		}
	}
}

void GodotREEditor::_bind_methods() {
	ClassDB::bind_method(D_METHOD("_decompile_files"), &GodotREEditor::_decompile_files);
	ClassDB::bind_method(D_METHOD("_decompile_process"), &GodotREEditor::_decompile_process);

	ClassDB::bind_method(D_METHOD("_compile_files"), &GodotREEditor::_compile_files);
	ClassDB::bind_method(D_METHOD("_compile_process"), &GodotREEditor::_compile_process);

	ClassDB::bind_method(D_METHOD("_pck_select_request", "path"), &GodotREEditor::_pck_select_request);
	ClassDB::bind_method(D_METHOD("_pck_extract_files"), &GodotREEditor::_pck_extract_files);
	ClassDB::bind_method(D_METHOD("_pck_extract_files_process"), &GodotREEditor::_pck_extract_files_process);

	ClassDB::bind_method(D_METHOD("_pck_create_request", "path"), &GodotREEditor::_pck_create_request);
	ClassDB::bind_method(D_METHOD("_pck_save_prep"), &GodotREEditor::_pck_save_prep);
	ClassDB::bind_method(D_METHOD("_pck_save_request", "path"), &GodotREEditor::_pck_save_request);

	ClassDB::bind_method(D_METHOD("_toggle_about_dialog_on_start"), &GodotREEditor::_toggle_about_dialog_on_start);

	ClassDB::bind_method(D_METHOD("_res_bin_2_txt_request", "files"), &GodotREEditor::_res_bin_2_txt_request);
	ClassDB::bind_method(D_METHOD("_res_bin_2_txt_process"), &GodotREEditor::_res_bin_2_txt_process);

	ClassDB::bind_method(D_METHOD("_res_txt_2_bin_request", "files"), &GodotREEditor::_res_txt_2_bin_request);
	ClassDB::bind_method(D_METHOD("_res_txt_2_bin_process"), &GodotREEditor::_res_txt_2_bin_process);

	ClassDB::bind_method(D_METHOD("_res_stex_2_png_request", "files"), &GodotREEditor::_res_stex_2_png_request);
	ClassDB::bind_method(D_METHOD("_res_stxt_2_png_process"), &GodotREEditor::_res_stxt_2_png_process);

	ClassDB::bind_method(D_METHOD("_res_ostr_2_ogg_request", "files"), &GodotREEditor::_res_ostr_2_ogg_request);
	ClassDB::bind_method(D_METHOD("_res_ostr_2_ogg_process"), &GodotREEditor::_res_ostr_2_ogg_process);

	ClassDB::bind_method(D_METHOD("_res_smpl_2_wav_request", "files"), &GodotREEditor::_res_smpl_2_wav_request);
	ClassDB::bind_method(D_METHOD("_res_smpl_2_wav_process"), &GodotREEditor::_res_smpl_2_wav_process);

	ClassDB::bind_method(D_METHOD("show_about_dialog"), &GodotREEditor::show_about_dialog);

	ClassDB::bind_method(D_METHOD("menu_option_pressed", "id"), &GodotREEditor::menu_option_pressed);
	ClassDB::bind_method(D_METHOD("convert_file_to_binary", "src_path", "dst_path"), &GodotREEditor::convert_file_to_binary);
	ClassDB::bind_method(D_METHOD("convert_file_to_text", "src_path", "dst_path"), &GodotREEditor::convert_file_to_text);

	ADD_SIGNAL(MethodInfo("write_log_message", PropertyInfo(Variant::STRING, "message")));
}
#endif // TOOLS_ENABLED

/*************************************************************************/
#include "texture_exporter.h"

#include "compat/resource_compat_binary.h"
#include "compat/resource_loader_compat.h"
#include "utility/common.h"

#include "core/error/error_list.h"
#include "core/io/file_access.h"
#include "core/io/image_loader.h"
#include "core/io/resource_loader.h"
#include "scene/resources/atlas_texture.h"
#include "scene/resources/compressed_texture.h"
#include "utility/resource_info.h"
#include <cstdint>
namespace {
bool get_bit(const Vector<uint8_t> &bitmask, int width, int p_x, int p_y) {
	int ofs = width * p_y + p_x;
	int bbyte = ofs / 8;
	int bbit = ofs % 8;

	return (bitmask[bbyte] & (1 << bbit)) != 0;
}
} //namespace

// Format is the same on V2 - V4
Ref<Image> TextureExporter::load_image_from_bitmap(const String p_path, Error *r_err) {
	Error err = OK;
	if (!r_err) {
		r_err = &err;
	}
	Ref<Image> image;
	image.instantiate();
	ResourceFormatLoaderCompatBinary rlcb;
	auto res = ResourceCompatLoader::fake_load(p_path, "", r_err);
	ERR_FAIL_COND_V_MSG(*r_err != OK, Ref<Image>(), "Cannot open resource '" + p_path + "'.");

	String name;
	Vector2 size;
	Dictionary data;
	PackedByteArray bitmask;
	int width;
	int height;

	// Load the main resource, which should be the ImageTexture
	name = ResourceCompatConverter::get_resource_name(res, 0);
	data = res->get("data");
	bitmask = data.get("data", PackedByteArray());
	size = data.get("size", Vector2());
	width = size.width;
	height = size.height;
	image->initialize_data(width, height, false, Image::FORMAT_L8);

	if (!name.is_empty()) {
		image->set_name(name);
	}
	for (int i = 0; i < width; i++) {
		for (int j = 0; j < height; j++) {
			image->set_pixel(i, j, get_bit(bitmask, width, i, j) ? Color(1, 1, 1) : Color(0, 0, 0));
		}
	}
	ERR_FAIL_COND_V_MSG(image.is_null() || image->is_empty(), Ref<Image>(), "Failed to load image from " + p_path);
	*r_err = OK;
	return image;
}

Error TextureExporter::_convert_bitmap(const String &p_path, const String &dest_path, bool lossy, Ref<ExportReport> report) {
	String dst_dir = dest_path.get_base_dir();
	Error err = OK;
	Ref<Image> img = load_image_from_bitmap(p_path, &err);
	// deprecated format
	if (err == ERR_UNAVAILABLE) {
		// TODO: Not reporting here because we can't get the deprecated format type yet,
		// implement functionality to pass it back
		print_line("Did not convert deprecated Bitmap resource " + p_path);
		return err;
	}
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to load bitmap " + p_path);
	err = gdre::ensure_dir(dst_dir);
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to create dirs for " + dest_path);
	err = save_image(dest_path, img, lossy);
	if (err == ERR_UNAVAILABLE) {
		return err;
	}
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to save image " + dest_path + " from texture " + p_path);

	print_verbose("Converted " + p_path + " to " + dest_path);
	return OK;
}

bool dest_format_supports_mipmaps(const String &ext) {
	return ext == "dds" || ext == "exr";
}

Error TextureExporter::save_image(const String &dest_path, const Ref<Image> &img, bool lossy) {
	ERR_FAIL_COND_V_MSG(img->is_empty(), ERR_FILE_EOF, "Image data is empty for texture " + dest_path + ", not saving");
	Error err = gdre::ensure_dir(dest_path.get_base_dir());
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to create dirs for " + dest_path);
	String dest_ext = dest_path.get_extension().to_lower();
	if (!dest_format_supports_mipmaps(dest_ext) && img->is_compressed() && img->has_mipmaps()) {
		img->clear_mipmaps();
	}
	GDRE_ERR_DECOMPRESS_OR_FAIL(img);
	if (dest_ext == "jpg" || dest_ext == "jpeg") {
		err = img->save_jpg(dest_path, 1.0);
	} else if (dest_ext == "webp") {
		err = img->save_webp(dest_path, lossy, 1.0);
	} else if (dest_ext == "png") {
		err = img->save_png(dest_path);
	} else if (dest_ext == "tga") {
		err = gdre::save_image_as_tga(dest_path, img);
	} else if (dest_ext == "svg") {
		err = gdre::save_image_as_svg(dest_path, img);
	} else if (dest_ext == "dds") {
		err = img->save_dds(dest_path);
	} else if (dest_ext == "exr") {
		err = img->save_exr(dest_path);
	} else if (dest_ext == "bmp") {
		err = gdre::save_image_as_bmp(dest_path, img);
	} else {
		ERR_FAIL_V_MSG(ERR_FILE_BAD_PATH, "Invalid file name: " + dest_path);
	}
	return err;
}
enum V4ImporterCompressMode {
	COMPRESS_LOSSLESS,
	COMPRESS_LOSSY,
	COMPRESS_VRAM_COMPRESSED,
	COMPRESS_VRAM_UNCOMPRESSED,
	COMPRESS_BASIS_UNIVERSAL
};

enum ttttype {
	TEXTURE_2D,
	TEXTURE_3D,
	TEXTURE_LAYERED
};

void set_tex_params(Ref<ImportInfo> p_import_info, Ref<Resource> p_tex, Ref<Image> p_img, int ver_major, ttttype p_type) {
	// Import options reference:
	// compress/mode (int): Controls how the texture is compressed
	//   0: Lossless (PNG/WebP)
	//   1: Lossy (WebP)
	//   2: VRAM Compressed (S3TC/BPTC, ETC2/ASTC)
	//   3: VRAM Uncompressed
	//   4: Basis Universal
	//
	// compress/high_quality (bool): When true, uses higher quality compression formats
	//   - BPTC instead of S3TC
	//   - ASTC instead of ETC2
	//
	// compress/lossy_quality (float): Controls quality of lossy compression (0-1)
	//   - Higher values = better quality but larger file size
	//   - Default: 0.7
	//
	// compress/uastc_level (int): Controls Basis Universal UASTC compression quality/speed
	//   0: Fastest
	//   1: Faster
	//   2: Medium
	//   3: Slower
	//   4: Slowest
	//
	// compress/rdo_quality_loss (float): Rate-distortion optimization quality for Basis Universal (0-10)
	//   - Higher values = better compression but more quality loss
	//
	// compress/hdr_compression (int): Controls how HDR textures are handled
	//   0: Disabled
	//   1: Opaque Only
	//   2: Always
	//
	// compress/normal_map (int): Controls normal map compression
	//   0: Detect
	//   1: Enable
	//   2: Disabled
	//
	// compress/channel_pack (int): Controls how color channels are packed
	//   0: sRGB Friendly
	//   1: Optimized
	//
	// mipmaps/generate (bool): Whether to generate mipmaps
	//   - Default: true for 3D textures, false for 2D
	//
	// mipmaps/limit (int): Limits the number of mipmap levels (-1 to 256)
	//   - -1 means no limit
	//
	// roughness/mode (int): Controls how roughness is handled
	//   0: Detect
	//   1: Disabled
	//   2: Red
	//   3: Green
	//   4: Blue
	//   5: Alpha
	//   6: Gray
	//
	// roughness/src_normal (string): Path to normal map used for roughness calculation
	//   - Supports: bmp, dds, exr, jpeg, jpg, hdr, png, svg, tga, webp
	//
	// process/fix_alpha_border (bool): Fixes alpha border artifacts
	//   - Default: true for 2D textures, false for 3D
	//
	// process/premult_alpha (bool): Premultiplies alpha channel
	//
	// process/normal_map_invert_y (bool): Inverts the Y channel of normal maps
	//
	// process/hdr_as_srgb (bool): Treats HDR textures as sRGB
	//
	// process/hdr_clamp_exposure (bool): Clamps HDR exposure to prevent artifacts
	//
	// process/size_limit (int): Maximum texture dimension (0-16383)
	//   - 0 means no limit
	//
	// detect_3d/compress_to (int): Controls how 3D textures are compressed
	//   0: Disabled
	//   1: VRAM Compressed
	//   2: Basis Universal
	//
	// SVG-specific options (only for SVG files):
	// svg/scale (float): Scale factor for SVG import (0.001-100)
	//   - Default: 1.0
	//
	// editor/scale_with_editor_scale (bool): Whether to scale SVG with editor scale
	//
	// editor/convert_colors_with_editor_theme (bool): Whether to convert SVG colors to match editor theme

	//CompressedTexture2D::DATA_FORMAT_WEBP

	if (!(p_import_info.is_valid() && p_tex.is_valid() && ver_major >= 4)) {
		return;
	}

	String ext = p_import_info->get_source_file().get_extension().to_lower();
	Dictionary params;
	Ref<ResourceInfo> info = ResourceInfo::get_info_from_resource(p_tex);
	ERR_FAIL_COND_MSG(!info.is_valid(), "TEXTURE LOADERS SHOULD HAVE SET THE RESOURCE INFO FOR THIS TEXTURE!!!!!!!!");
	// Get the image from the texture
	Ref<Image> img;

	if (p_type == TEXTURE_2D) {
		Ref<Texture2D> tex2d = p_tex;
		img = tex2d->get_image();
	} else if (p_type == TEXTURE_3D) {
		Ref<Texture3D> tex3d = p_tex;
		img = p_img;
	} else if (p_type == TEXTURE_LAYERED) {
		Ref<TextureLayered> texlay = p_tex;
		img = p_img;
	}

	if (!img.is_valid()) {
		return;
	}
	int compress_mode = -1;
	CompressedTexture2D::DataFormat data_format = CompressedTexture2D::DATA_FORMAT_IMAGE;
	ERR_FAIL_COND(info->extra.is_empty());
	Dictionary extra = info->extra;
	int df = extra.get("data_format", -1);
	int tf = extra.get("texture_flags", -1);
	ERR_FAIL_COND(df == -1 || tf == -1);
	data_format = CompressedTexture2D::DataFormat(df);
	int texture_flags = tf;
	switch (data_format) {
		case CompressedTexture2D::DATA_FORMAT_PNG:
		case CompressedTexture2D::DATA_FORMAT_WEBP: // force WEBP to lossless
			compress_mode = COMPRESS_LOSSLESS;
			break;
		case CompressedTexture2D::DATA_FORMAT_BASIS_UNIVERSAL:
			compress_mode = COMPRESS_BASIS_UNIVERSAL;
			break;
		case CompressedTexture2D::DATA_FORMAT_IMAGE:
			if (img->is_compressed()) {
				compress_mode = COMPRESS_VRAM_COMPRESSED;
			} else {
				compress_mode = COMPRESS_VRAM_UNCOMPRESSED;
			}
			break;
		default: //DATA_FORMAT_IMAGE, we need to check the img format
			compress_mode = 0;
			break;
	}
	// Check if the image is compressed
	params["compress/mode"] = compress_mode;
	// Set high quality flag for VRAM compression
	auto format = img->get_format();
	auto decompressed_fmt = format;
	Ref<Image> decompressed_img = img;
	if (img->is_compressed()) {
		decompressed_img = img->duplicate();
		decompressed_img->decompress();
		decompressed_fmt = decompressed_img->get_format();
	}
	bool is_hdr = (decompressed_fmt >= Image::FORMAT_RF && decompressed_fmt <= Image::FORMAT_RGBE9995);

	if (img->is_compressed() && (is_hdr || ((format >= Image::FORMAT_BPTC_RGBA && format <= Image::FORMAT_BPTC_RGBFU)) || (format >= Image::FORMAT_ASTC_4x4 && format <= Image::FORMAT_ASTC_8x8_HDR))) {
		params["compress/high_quality"] = true;
	} else {
		params["compress/high_quality"] = false;
	}

	// Set lossy quality if using lossy compression
	params["compress/lossy_quality"] = 1.0; // prevent generational loss

	// Set Basis Universal parameters if used
	if (p_import_info->get_ver_minor() >= 5) {
		params["compress/uastc_level"] = 2; // Default is Fastest, but force to Medium to prevent generational loss
		params["compress/rdo_quality_loss"] = 0; // Default
	}
	params["compress/hdr_compression"] = is_hdr && img->is_compressed() ? 1 : 0;

	// enum FormatBits {
	// 	FORMAT_BIT_STREAM = 1 << 22,
	// 	FORMAT_BIT_HAS_MIPMAPS = 1 << 23,
	// 	FORMAT_BIT_DETECT_3D = 1 << 24,
	// 	//FORMAT_BIT_DETECT_SRGB = 1 << 25,
	// 	FORMAT_BIT_DETECT_NORMAL = 1 << 26,
	// 	FORMAT_BIT_DETECT_ROUGNESS = 1 << 27,
	// };
	// set it to "Detect" if the flag is set; else use "Disabled"
	if (p_type == TEXTURE_2D) {
		params["compress/normal_map"] = ((texture_flags & CompressedTexture2D::FORMAT_BIT_DETECT_NORMAL) != 0) ? 0 : 2;
	}
	// Set channel packing

	int channel_pack = 0;
	auto used_channels = decompressed_img->detect_used_channels();
	if (used_channels == Image::USED_CHANNELS_R || used_channels == Image::USED_CHANNELS_RG) {
		channel_pack = 1; // not sRGB friendly
	}
	params["compress/channel_pack"] = channel_pack;

	// Set mipmap settings
	params["mipmaps/generate"] = (texture_flags & CompressedTexture2D::FORMAT_BIT_HAS_MIPMAPS) != 0 || (img->has_mipmaps());
	int limit = -1;
	if (img->has_mipmaps()) {
		int mipmap_count = img->get_mipmap_count();
		int min_width, min_height;
		img->get_format_min_pixel_size(img->get_format(), min_width, min_height);
		if (mipmap_count > 1) {
			int64_t offset, size;
			int last_width, last_height;
			img->get_mipmap_offset_size_and_dimensions(mipmap_count - 1, offset, size, last_width, last_height);
			// it may be a non-power of 2 last mipmap, so it's either min height or width
			if (last_width != min_width && last_height != min_height) {
				limit = mipmap_count;
			}
		}
	}

	params["mipmaps/limit"] = limit;

	if (p_type == TEXTURE_2D) {
		bool is_roughness = (texture_flags & CompressedTexture2D::FORMAT_BIT_DETECT_ROUGNESS) != 0;
		if (is_roughness) {
			// TODO: this puts it into "Detect", which will make the editor detect what it is; maybe we can do better??
			params["roughness/mode"] = 0;
		} else {
			params["roughness/mode"] = 1;
		}

		// TODO: This? Probably not though, it is a destructive process to apply the roughness map
		params["roughness/src_normal"] = "";

		// Set processing options
		params["process/fix_alpha_border"] = false; // default true, but forcing to false to prevent re-fixing
		params["process/premult_alpha"] = false; // default false, and forcing cuz destructive
		params["process/normal_map_invert_y"] = false; // default true, but forcing to false to prevent re-inverting

		// Set HDR settings
		params["process/hdr_as_srgb"] = false;
		params["process/hdr_clamp_exposure"] = false;

		// Set size limit
		params["process/size_limit"] = 0; // Default to no limit

		// Set 3D detection
		int detect_3d_compress_to = 0;
		if (texture_flags & CompressedTexture2D::FORMAT_BIT_DETECT_3D) {
			if (compress_mode == COMPRESS_VRAM_COMPRESSED) {
				detect_3d_compress_to = 1;
			} else if (compress_mode == COMPRESS_BASIS_UNIVERSAL) {
				detect_3d_compress_to = 2;
			}
		}
		params["detect_3d/compress_to"] = detect_3d_compress_to;

		if (ext == "svg") {
			params["svg/scale"] = 1.0;
			// No editor scaling; we want it to stay the same size
			params["editor/scale_with_editor_scale"] = false;
			params["editor/convert_colors_with_editor_theme"] = false;
		}
	}

	// Set the parameters in the import info
	p_import_info->set_params(params);
}

Error TextureExporter::_convert_tex(const String &p_path, const String &dest_path, bool lossy, String &image_format, Ref<ExportReport> report) {
	Error err;
	String dst_dir = dest_path.get_base_dir();
	Ref<Texture2D> tex;
	tex = ResourceCompatLoader::non_global_load(p_path, "", &err);

	if (err == ERR_UNAVAILABLE) {
		// TODO: Not reporting here because we can't get the deprecated format type yet,
		// implement functionality to pass it back
		image_format = "Unknown deprecated image format";
		print_line("Did not convert deprecated Texture resource " + p_path);
		return err;
	}
	ERR_FAIL_COND_V_MSG(err != OK || tex.is_null(), err, "Failed to load texture " + p_path);

	Ref<Image> img = tex->get_image();

	ERR_FAIL_COND_V_MSG(img.is_null(), ERR_PARSE_ERROR, "Failed to load image for texture " + p_path);
	if (report.is_valid()) {
		auto iinfo = report->get_import_info();
		if (iinfo.is_valid()) {
			set_tex_params(iinfo, tex, img, iinfo->get_ver_major(), TEXTURE_2D);
		}
	}
	image_format = Image::get_format_name(img->get_format());
	err = save_image(dest_path, img, lossy);
	if (err == ERR_UNAVAILABLE) {
		return err;
	}
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to save image " + dest_path + " from texture " + p_path);
	print_verbose("Converted " + p_path + " to " + dest_path);
	return OK;
}

Error TextureExporter::_convert_atex(const String &p_path, const String &dest_path, bool lossy, String &image_format, Ref<ExportReport> report) {
	Error err;
	String dst_dir = dest_path.get_base_dir();
	Ref<Texture2D> loaded_tex = ResourceCompatLoader::custom_load(p_path, "", ResourceInfo::GLTF_LOAD, &err, false, ResourceFormatLoader::CACHE_MODE_IGNORE_DEEP);
	// deprecated format
	if (err == ERR_UNAVAILABLE) {
		// TODO: Not reporting here because we can't get the deprecated format type yet,
		// implement functionality to pass it back
		image_format = "Unknown deprecated image format";
		return err;
	}
	ERR_FAIL_COND_V_MSG(err != OK || loaded_tex.is_null(), err, "Failed to load texture " + p_path);
	Ref<AtlasTexture> atex = loaded_tex;
	if (atex.is_null()) {
		// this is not an AtlasTexture, return TextureExporter::_convert_tex
		return _convert_tex(p_path, dest_path, lossy, image_format, report);
	}
	Ref<Texture2D> tex = atex->get_atlas();
	ERR_FAIL_COND_V_MSG(tex.is_null(), ERR_PARSE_ERROR, "Failed to load atlas texture " + p_path);
	Ref<Image> img = tex->get_image();

	ERR_FAIL_COND_V_MSG(img.is_null(), ERR_PARSE_ERROR, "Failed to load image for texture " + p_path);
	ERR_FAIL_COND_V_MSG(img->is_empty(), ERR_FILE_EOF, "Image data is empty for texture " + p_path + ", not saving");
	image_format = Image::get_format_name(img->get_format());

	if (img->is_compressed() && img->has_mipmaps()) {
		img->clear_mipmaps();
	}

	// resize it according to the properties of the atlas
	GDRE_ERR_DECOMPRESS_OR_FAIL(img);
	if (img->get_format() != Image::FORMAT_RGBA8) {
		img->convert(Image::FORMAT_RGBA8);
	}

	auto margin = atex->get_margin();
	auto region = atex->get_region();

	// now we have to add the margin padding
	Ref<Image> new_img = Image::create_empty(atex->get_width(), atex->get_height(), false, img->get_format());
	new_img->blit_rect(img, region, Point2i(margin.position.x, margin.position.y));
	err = save_image(dest_path, new_img, lossy);
	if (err == ERR_UNAVAILABLE) {
		return err;
	}
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to save image " + dest_path + " from texture " + p_path);

	print_verbose("Converted " + p_path + " to " + dest_path);
	return OK;
}

Error TextureExporter::export_file(const String &out_path, const String &res_path) {
	Error err;
	auto res_info = ResourceCompatLoader::get_resource_info(res_path, "", &err);
	if (res_info.is_null() || !handles_import("", res_info->type)) {
		return ERR_FILE_UNRECOGNIZED;
	}
	if (res_info->type == "BitMap") {
		return _convert_bitmap(res_path, out_path, false, nullptr);
	}
	String fmt_name;
	if (res_info->type == "AtlasTexture") {
		return _convert_atex(res_path, out_path, false, fmt_name);
	}
	return _convert_tex(res_path, out_path, false, fmt_name);
}

Error preprocess_images(
		String p_path,
		String dest_path,
		int num_images_w,
		int num_images_h,
		bool lossy,
		Vector<Ref<Image>> &images,
		bool &had_mipmaps,
		bool &detected_alpha,
		bool ignore_dimensions = false) {
	ERR_FAIL_COND_V_MSG(images.size() == 0, ERR_PARSE_ERROR, "No images to concat");
	int layer_count = num_images_w * num_images_h;
	int width = images[0]->get_width();
	int height = images[0]->get_height();

	auto format = images[0]->get_format();
	auto decompressed_fmt = format;

	Ref<Image> ref_img;
	if (layer_count > 0) {
		ref_img = images[0];
		if (!ref_img.is_null()) {
			// dupe to avoid modifying the original
			if (ref_img->is_compressed()) {
				Ref<Image> dupe = ref_img->duplicate();
				GDRE_ERR_DECOMPRESS_OR_FAIL(dupe);
				decompressed_fmt = dupe->get_format();
			}
		}
	}
	if (ref_img.is_null()) {
		return ERR_PARSE_ERROR;
	}
	auto new_format = decompressed_fmt;
	had_mipmaps = layer_count != images.size();
	Vector<Vector<uint8_t>> images_data;
	bool is_hdr = decompressed_fmt >= Image::FORMAT_RF && decompressed_fmt <= Image::FORMAT_RGBE9995;

	Vector<Image::Format> formats;
	detected_alpha = false;
	for (int64_t i = 0; i < layer_count; i++) {
		Ref<Image> img = images[i];
		if (img.is_null()) {
			return ERR_PARSE_ERROR;
		}
		if (img->has_mipmaps()) {
			had_mipmaps = true;
			img->clear_mipmaps();
		}
		GDRE_ERR_DECOMPRESS_OR_FAIL(img);
		detected_alpha = detected_alpha || img->detect_alpha();
		if (is_hdr) {
			new_format = img->get_format();
			formats.push_back(img->get_format());
			images_data.push_back(img->get_data());
		}
		if (!ignore_dimensions) {
			ERR_FAIL_COND_V_MSG(img->get_width() != width || img->get_height() != height, ERR_PARSE_ERROR, "Image " + p_path + " has incorrect dimensions");
		}
	}

	if (!is_hdr || gdre::vector_to_hashset(formats).size() > 1) {
		if (!is_hdr) {
			if (!detected_alpha) {
				new_format = Image::FORMAT_RGB8;
			} else if (detected_alpha) {
				new_format = Image::FORMAT_RGBA8;
			}
		} else {
			new_format = gdre::get_most_popular_value(formats);
			// check if we've detected alpha and if this format supports it
			const bool supports_alpha = new_format == Image::FORMAT_RGBA8 || new_format == Image::FORMAT_RGBA4444 || new_format == Image::FORMAT_RGBAH || new_format == Image::FORMAT_RGBAF;
			if (detected_alpha && !supports_alpha) {
				new_format = Image::FORMAT_RGBA8;
			}
		}
		images_data.clear();
		for (int i = 0; i < layer_count; i++) {
			Ref<Image> img = images[i];
			if (img->get_format() != new_format) {
				img->convert(new_format);
			}
			images_data.push_back(img->get_data());
		}
	}
	return OK;
}

Error save_image_with_mipmaps(const String &dest_path, const Vector<Ref<Image>> &images, int num_images_w, int num_images_h, bool lossy, bool had_mipmaps, int override_width = -1, int override_height = -1) {
	auto new_format = images[0]->get_format();
	int pixel_size = Image::get_format_pixel_size(new_format);
	Vector<Vector<uint8_t>> images_data;
	int max_height = 0;
	for (int i = 0; i < images.size(); i++) {
		ERR_FAIL_COND_V_MSG(images[i].is_null(), ERR_PARSE_ERROR, "Image " + dest_path.get_file() + " is null");
		images_data.push_back(images[i]->get_data());
		max_height = MAX(max_height, images[i]->get_height());
	}

	Vector<uint8_t> new_image_data;
	size_t new_width = override_width != -1 ? override_width : images[0]->get_width() * num_images_w;
	size_t new_height = override_height != -1 ? override_height : images[0]->get_height() * num_images_h;
	size_t new_data_size = Image::get_image_data_size(new_width, new_height, new_format, false);
	new_image_data.resize(new_data_size);
	size_t current_offset = 0;
	for (int row_idx = 0; row_idx < num_images_h; row_idx++) {
		for (int i = 0; i < max_height; i++) {
			for (int img_idx = row_idx * num_images_w; img_idx < (row_idx + 1) * num_images_w; img_idx++) {
				if (images[img_idx]->get_height() <= i) {
					continue;
				}
				size_t copy_size = images[img_idx]->get_width() * pixel_size;
				// We're concatenating the images horizontally; so we have to take a width-sized slice of the image
				// and copy it into the new image data
				size_t start_idx = i * copy_size;
				ERR_FAIL_COND_V(images_data[img_idx].size() < start_idx + copy_size, ERR_PARSE_ERROR);
				memcpy(new_image_data.ptrw() + current_offset, images_data[img_idx].ptr() + start_idx, copy_size);
				current_offset += copy_size;
			}
		}
	}
	DEV_ASSERT(Image::get_image_data_size(new_width, new_height, new_format, false) == new_image_data.size());
	Ref<Image> img = Image::create_from_data(new_width, new_height, false, new_format, new_image_data);
	ERR_FAIL_COND_V_MSG(img.is_null(), ERR_PARSE_ERROR, "Failed to create image for texture " + dest_path.get_file());
	if (had_mipmaps && dest_format_supports_mipmaps(dest_path.get_extension().to_lower())) {
		img->generate_mipmaps();
		DEV_ASSERT(Image::get_image_data_size(new_width, new_height, new_format, true) == img->get_data_size());
	}
	Error err = TextureExporter::save_image(dest_path, img, lossy);
	if (err == ERR_UNAVAILABLE) {
		return err;
	}
	return err;
}

Error TextureExporter::_convert_3d(const String &p_path, const String &dest_path, bool lossy, String &image_format, Ref<ExportReport> report) {
	Error err;
	String dst_dir = dest_path.get_base_dir();
	Ref<Texture3D> tex;
	tex = ResourceCompatLoader::non_global_load(p_path, "", &err);
	Ref<ImportInfo> iinfo;
	if (report.is_valid()) {
		iinfo = report->get_import_info();
	}

	// deprecated format
	if (err == ERR_UNAVAILABLE) {
		image_format = "Unknown deprecated image format";
		print_line("Did not convert deprecated Texture resource " + p_path);
		return err;
	}
	ERR_FAIL_COND_V_MSG(err != OK || tex.is_null(), err, "Failed to load texture " + p_path);
	ERR_FAIL_COND_V_MSG(tex->get_depth() <= 0, ERR_PARSE_ERROR, "Texture " + p_path + " has no layers");

	auto layer_count = tex->get_depth();
	Vector<Ref<Image>> images = tex->get_data();
	Ref<Image> ref_img = images[0]->duplicate();
	ERR_FAIL_COND_V_MSG(images.size() == 0, ERR_PARSE_ERROR, "No images to concat");

	int64_t num_images_w = -1;
	int64_t num_images_h = -1;
	Dictionary params;
	if (iinfo.is_valid()) {
		params = iinfo->get_params();
	}
	bool had_valid_params = false;
	if (iinfo.is_valid()) {
		num_images_w = params.get("slices/horizontal", -1);
		num_images_h = params.get("slices/vertical", -1);
		had_valid_params = num_images_w != -1 && num_images_h != -1;
	}
	if (!had_valid_params) {
		if (layer_count == 64) {
			num_images_w = 8;
			num_images_h = 8;
		} else if (layer_count == 16) {
			num_images_w = 4;
			num_images_h = 4;
		} else if (layer_count == 4) {
			num_images_w = 2;
			num_images_h = 2;
		} else {
			num_images_w = layer_count;
			num_images_h = 1;
		}
	}
	bool had_mipmaps = layer_count != images.size();
	bool detected_alpha = false;
	err = preprocess_images(p_path, dest_path, num_images_w, num_images_h, lossy, images, had_mipmaps, detected_alpha);
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to preprocess images for texture " + p_path);
	err = save_image_with_mipmaps(dest_path, images, num_images_w, num_images_h, lossy, had_mipmaps);
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to save image " + dest_path + " from texture " + p_path);
	set_tex_params(iinfo, tex, ref_img, iinfo->get_ver_major(), TEXTURE_3D);

	if (!had_valid_params && iinfo.is_valid()) {
		iinfo->set_param("slices/horizontal", num_images_w);
		iinfo->set_param("slices/vertical", num_images_h);
		iinfo->set_param("mipmaps/generate", had_mipmaps);
	}
	print_verbose("Converted " + p_path + " to " + dest_path);
	return OK;
}

enum CubemapFormat {
	CUBEMAP_FORMAT_1X6,
	CUBEMAP_FORMAT_2X3,
	CUBEMAP_FORMAT_3X2,
	CUBEMAP_FORMAT_6X1,
};

Ref<Image> crop_transparent(const Ref<Image> &img) {
	ERR_FAIL_COND_V(img.is_null(), img);
	int width = img->get_width();
	int height = img->get_height();

	// check if it's width-wise or height-wise based on the ratio of the width and height
	bool is_horizontal = width > height;
	int64_t num_parts = (is_horizontal ? width / height : height / width);

	int new_width = is_horizontal ? width / num_parts : width;
	int new_height = is_horizontal ? height : height / num_parts;

	// now we need to find the first non-transparent pixel in the image
	int min_x = 0;
	int width_region_start = 0;
	int min_y = 0;
	int height_region_start = 0;

	// first, check to see if the image is entirely transparent
	bool is_entirely_transparent = true;
	for (int y = 0; y < height; y++) {
		for (int x = 0; x < width; x++) {
			if (img->get_pixel(x, y).a > 0.0) {
				is_entirely_transparent = false;
			}
		}
	}
	if (is_entirely_transparent) {
		return Ref<Image>();
	}

	if (is_horizontal) {
		// the transparent pixels start at either at 0,0, new_width,0, or width - new_width,0
		if (img->get_pixel(0, 0).a == 0.0) {
			min_x = 0;
			width_region_start = new_width * (num_parts - 1);
		} else if (img->get_pixel(new_width, 0).a == 0.0) {
			min_x = new_width;
			width_region_start = 0;
		} else if (img->get_pixel(width - new_width, 0).a == 0.0) {
			min_x = width - new_width;
			width_region_start = width - (new_width * (num_parts - 1));
		}
		min_y = 0;
		height_region_start = 0;
	} else {
		// the transparent pixels start at either at 0,0, 0,new_height, or 0,height - new_height
		if (img->get_pixel(0, 0).a == 0.0) {
			min_y = 0;
			height_region_start = new_height * (num_parts - 1);
		} else if (img->get_pixel(0, new_height).a == 0.0) {
			min_y = new_height;
			height_region_start = 0;
		} else if (img->get_pixel(0, height - new_height).a == 0.0) {
			min_y = height - new_height;
			height_region_start = height - (new_height * (num_parts - 1));
		}
		min_x = 0;
		width_region_start = 0;
	}

	return img->get_region(Rect2i(width_region_start, height_region_start, new_width, new_height));
	;
}

Vector<Ref<Image>> fix_cross_cubemaps(const Vector<Ref<Image>> &images, int width, int height, int layer_count, bool detected_alpha) {
	// here is where we fix the "cross" style of cubemaps that got imported all funky
	// check if the images have the same width and height
	Vector<Ref<Image>> fixed_images;
	bool is_horizontal = width > height;
	int64_t num_parts = is_horizontal ? width / height : height / width;
	if (width != height) {
		if (detected_alpha) {
			// we need to fix the images
			for (int i = 0; i < layer_count; i++) {
				Ref<Image> img = images[i];
				if (img->detect_alpha()) {
					Ref<Image> cropped = crop_transparent(img);
					size_t new_width;
					size_t new_height;
					if (!cropped.is_null()) {
						new_width = cropped->get_width();
						new_height = cropped->get_height();
						fixed_images.push_back(cropped);
					} else {
						new_width = 0;
						new_height = 0;
					}
				} else {
					// otherwise, divide it into parts based on the ratio of the width and height
					for (int j = 0; j < num_parts; j++) {
						Rect2i rect;
						if (is_horizontal) {
							rect.position.x = j * width / num_parts;
							rect.size.width = width / num_parts;
							rect.position.y = 0;
							rect.size.height = height;
						} else {
							rect.position.x = 0;
							rect.size.width = width;
							rect.position.y = j * height / num_parts;
							rect.size.height = height / num_parts;
						}
						Ref<Image> part = img->get_region(rect);
						fixed_images.push_back(part);
					}
				}
			}
		}
	}
#if 0
	for (int i = 0; i < fixed_images.size(); i++) {
		Ref<Image> img = fixed_images[i];
		if (img.is_null()) {
			continue;
		}
		auto new_dest = dest_path.get_basename() + "_cropped_" + String::num_int64(i) + "." + dest_path.get_extension();
		Error err = TextureExporter::save_image(new_dest, img, lossy);
	}
#endif
	if (fixed_images.size() > 0) {
		Vector<Ref<Image>> images;
		images.resize(6);
		// X+, X-, Y+, Y-, Z+, Z-
		// this is upside down;
		images.write[0] = fixed_images[3];
		images.write[1] = fixed_images[1];
		images.write[2] = fixed_images[0];
		images.write[3] = fixed_images[5];
		images.write[4] = fixed_images[2];
		images.write[5] = fixed_images[4];

		// arrangement = is_horizontal ? CUBEMAP_FORMAT_6X1 : CUBEMAP_FORMAT_1X6;
		// num_images_w = is_horizontal ? 6 : 1;
		// num_images_h = is_horizontal ? 1 : 6;
		return images;
	} else {
		return {};
	}
}

Error TextureExporter::_convert_layered_2d(const String &p_path, const String &dest_path, bool lossy, String &image_format, Ref<ExportReport> report) {
	Error err;
	String dst_dir = dest_path.get_base_dir();
	Ref<TextureLayered> tex;
	tex = ResourceCompatLoader::non_global_load(p_path, "", &err);
	Ref<ImportInfo> iinfo;
	if (report.is_valid()) {
		iinfo = report->get_import_info();
	}

	if (err == ERR_UNAVAILABLE) {
		image_format = "Unknown deprecated image format";
		print_line("Did not convert deprecated Texture resource " + p_path);
		return err;
	}
	ERR_FAIL_COND_V_MSG(err != OK || tex.is_null(), err, "Failed to load texture " + p_path);

	auto layer_count = tex->get_layers();
	if (layer_count == 0) {
		return ERR_PARSE_ERROR;
	}
	Vector<Ref<Image>> images;
	Ref<Image> ref_img = tex->get_layer_data(0)->duplicate();
	for (int i = 0; i < layer_count; i++) {
		images.push_back(tex->get_layer_data(i));
	}
	ERR_FAIL_COND_V_MSG(images.size() == 0, ERR_PARSE_ERROR, "No images to concat");
#if 0
	for (int i = 0; i < layer_count; i++) {
		Ref<Image> img = images[i];
		auto new_dest = dest_path.get_basename() + "_" + String::num_int64(i) + "." + dest_path.get_extension();
		Error err = TextureExporter::save_image(new_dest, img, lossy);
	}
#endif

	int64_t num_images_w = -1;
	int64_t num_images_h = -1;
	int64_t arrangement = -1;
	int64_t layout = -1;
	int64_t amount = -1;
	int64_t override_width = -1;
	int64_t override_height = -1;
	auto mode = tex->get_layered_type();
	Dictionary params;
	if (iinfo.is_valid()) {
		params = iinfo->get_params();
	}
	// get the resource info
	Ref<ResourceInfo> res_info = ResourceInfo::get_info_from_resource(tex);
	bool had_valid_params = false;
	bool ignore_dimensions = res_info.is_valid() && res_info->get_ver_major() <= 2;

	if (mode == TextureLayered::LAYERED_TYPE_2D_ARRAY) {
		// get the square root of the number of layers; if it's a whole number, then we have a square
		if (iinfo.is_valid()) {
			num_images_w = params.get("slices/horizontal", -1);
			num_images_h = params.get("slices/vertical", -1);
			had_valid_params = num_images_w != -1 && num_images_h != -1;
		}
		if (res_info.is_valid() && res_info->get_type() == "LargeTexture") {
			Vector<Vector2> offsets = res_info->get_extra().get("offsets", Vector<Vector2>());
			Vector2 whole_size = res_info->get_extra().get("whole_size", Vector2(-1, -1));
			override_width = whole_size.x;
			override_height = whole_size.y;
			// get the number of unique individual x and y values
			HashSet<int64_t> unique_x;
			HashSet<int64_t> unique_y;
			for (int i = 0; i < offsets.size(); i++) {
				unique_x.insert(offsets[i].x);
				unique_y.insert(offsets[i].y);
			}
			num_images_w = unique_x.size();
			num_images_h = unique_y.size();
			had_valid_params = true;
		} else if (!had_valid_params) {
			if (layer_count == 64) {
				num_images_w = 8;
				num_images_h = 8;
			} else if (layer_count == 16) {
				num_images_w = 4;
				num_images_h = 4;
			} else if (layer_count == 4) {
				num_images_w = 2;
				num_images_h = 2;
			} else {
				num_images_w = tex->get_layers();
				num_images_h = 1;
			}
		}
	} else if (mode == TextureLayered::LAYERED_TYPE_CUBEMAP || mode == TextureLayered::LAYERED_TYPE_CUBEMAP_ARRAY) {
		if (iinfo.is_valid()) {
			arrangement = params.get("slices/arrangement", -1);
			if (arrangement != -1 && mode == TextureLayered::LAYERED_TYPE_CUBEMAP) {
				had_valid_params = true;
			} else if (arrangement != -1 && mode == TextureLayered::LAYERED_TYPE_CUBEMAP_ARRAY) {
				layout = params.get("slices/layout", -1);
				amount = params.get("slices/amount", -1);
				if (layout != -1 && amount != -1) {
					had_valid_params = true;
				}
			}
		}
		if (!had_valid_params) {
			arrangement = 1;
			layout = 1;
			amount = layer_count / 6;
		}
		if (arrangement == CUBEMAP_FORMAT_1X6) {
			num_images_w = 1;
			num_images_h = 6;
		} else if (arrangement == CUBEMAP_FORMAT_2X3) {
			num_images_w = 2;
			num_images_h = 3;
		} else if (arrangement == CUBEMAP_FORMAT_3X2) {
			num_images_w = 3;
			num_images_h = 2;
		} else if (arrangement == CUBEMAP_FORMAT_6X1) {
			num_images_w = 6;
			num_images_h = 1;
		}
		if (mode == TextureLayered::LAYERED_TYPE_CUBEMAP_ARRAY) {
			if (layout == 0) {
				num_images_w *= amount;
			} else if (layout == 1) {
				num_images_h *= amount;
			}
		}
	}
	bool had_mipmaps = layer_count != images.size();
	bool detected_alpha = false;
	err = preprocess_images(p_path, dest_path, num_images_w, num_images_h, lossy, images, had_mipmaps, detected_alpha, ignore_dimensions);
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to preprocess images for texture " + p_path);
	int width = tex->get_width();
	int height = tex->get_height();
#if 0 // This was an attempt at fixing incorrectly imported cubemaps; if it was incorrectly imported by the original author, we should just leave it be.
	if (mode == TextureLayered::LAYERED_TYPE_CUBEMAP || mode == TextureLayered::LAYERED_TYPE_CUBEMAP_ARRAY) {
		Vector<Ref<Image>> fixed_images = fix_cross_cubemaps(images, width, height, layer_count, detected_alpha);
	}
#endif
	err = save_image_with_mipmaps(dest_path, images, num_images_w, num_images_h, lossy, had_mipmaps, override_width, override_height);
	image_format = Image::get_format_name(images[0]->get_format());
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to concat images for texture " + p_path);
	set_tex_params(iinfo, tex, ref_img, iinfo->get_ver_major(), TEXTURE_LAYERED);

	if (!had_valid_params && iinfo.is_valid()) {
		if (mode == TextureLayered::LAYERED_TYPE_2D_ARRAY) {
			iinfo->set_param("slices/horizontal", num_images_w);
			iinfo->set_param("slices/vertical", num_images_h);
		} else if (mode == TextureLayered::LAYERED_TYPE_CUBEMAP) {
			// 1x6
			iinfo->set_param("slices/arrangement", arrangement);
		} else if (mode == TextureLayered::LAYERED_TYPE_CUBEMAP_ARRAY) {
			// 1x6
			iinfo->set_param("slices/arrangement", arrangement);
			iinfo->set_param("slices/layout", layout);
			iinfo->set_param("slices/amount", layer_count / 6);
		}
		iinfo->set_param("mipmaps/generate", had_mipmaps);
	}
	print_verbose("Converted " + p_path + " to " + dest_path);
	return OK;
}

Ref<ExportReport> TextureExporter::export_resource(const String &output_dir, Ref<ImportInfo> iinfo) {
	String path = iinfo->get_path();
	String source = iinfo->get_source_file();
	bool lossy = false;
	int ver_major = iinfo->get_ver_major();
	int ver_minor = iinfo->get_ver_minor();
	Ref<ExportReport> report = memnew(ExportReport(iinfo));

	// Sonic Colors Unlimited specific hack: We don't support atsc and nx-low formats, so we need to set the path to something else
	if (ver_major == 3 && ver_minor == 1) {
		String format_type = path.get_basename().get_extension();
		Vector<String> banned_types = { "atsc", "nx-low", "atsc-low" };
		if (banned_types.has(format_type)) {
			bool found = false;
			Vector<String> dest_files = iinfo->get_dest_files();
			if (dest_files.size() > 0) {
				String new_path = path.get_basename().get_basename() + ".s3tc" + path.get_extension();
				if (FileAccess::exists(new_path)) {
					path = new_path;
					found = true;
				}
				if (!found) {
					for (auto &dest : dest_files) {
						String fmt = dest.get_basename().get_extension();
						if (!banned_types.has(format_type) && FileAccess::exists(new_path)) {
							path = dest;
							found = true;
							break;
						}
					}
				}
			}
			if (!found) {
				report->set_error(ERR_UNAVAILABLE);
				report->set_message("Cannot convert custom SCU texture format");
				report->set_unsupported_format_type(format_type);
			}
		}
	}
	if (!FileAccess::exists(path)) {
		path = "";
		for (auto &dest : iinfo->get_dest_files()) {
			if (FileAccess::exists(dest)) {
				path = dest;
				break;
			}
		}
		if (path.is_empty()) {
			report->set_error(ERR_FILE_NOT_FOUND);
			report->set_message("No existing textures found for this import");
			report->append_message_detail({ "Possibles:" });
			report->append_message_detail(iinfo->get_dest_files());
			return report;
		}
	}
	// for Godot 2.x resources, we can easily rewrite the metadata to point to a renamed file with a different extension,
	// but this isn't the case for 3.x and greater, so we have to save in the original (lossy) format.
	String source_ext = source.get_extension().to_lower();
	if (source_ext != "png" || ver_major == 2) {
		if (ver_major > 2) {
			if ((source_ext == "jpg" || source_ext == "jpeg")) {
				lossy = true;
				report->set_loss_type(ImportInfo::STORED_LOSSY);
			} else if (source_ext == "webp") {
				// if the engine <3.4, it can't handle lossless encoded WEBPs
				if (ver_major < 4 && !(ver_major == 3 && ver_minor >= 4)) {
					lossy = true;
				}
				report->set_loss_type(ImportInfo::STORED_LOSSY);
			} else if (source_ext == "tga") {
				lossy = false;
			} else if (source_ext == "svg") {
				lossy = true;
				report->set_loss_type(ImportInfo::STORED_LOSSY);
			} else if (source_ext == "dds") {
				lossy = false;
			} else if (source_ext == "exr") {
				lossy = false;
			} else if (source_ext == "bmp") {
				lossy = false;
			} else {
				iinfo->set_export_dest(iinfo->get_export_dest().get_basename() + ".png");
				// If this is version 3-4, we need to rewrite the import metadata to point to the new resource name
				// save it under .assets, which won't be picked up for import by the godot editor
				if (false) {
					// disable this for now
					// iinfo->set_source_file(iinfo->get_export_dest());
				} else {
					if (!iinfo->get_export_dest().replace("res://", "").begins_with(".assets")) {
						String prefix = ".assets";
						if (iinfo->get_export_dest().begins_with("res://")) {
							prefix = "res://.assets";
						}
						iinfo->set_export_dest(prefix.path_join(iinfo->get_export_dest().replace("res://", "")));
					}
				}
			}
		} else { //version 2
			if (!iinfo->is_import()) {
				if (source_ext == "webp" || source_ext == "svg" || source_ext == "jpg" || source_ext == "jpeg") {
					lossy = true;
					report->set_loss_type(ImportInfo::STORED_LOSSY);
				}
			} else {
				iinfo->set_export_dest(iinfo->get_export_dest().get_basename() + ".png");
			}
		}
	}
	report->set_new_source_path(iinfo->get_export_dest());

	Error err = OK;
	String img_format = "bitmap";
	String importer = iinfo->get_importer();
	String dest_path = output_dir.path_join(iinfo->get_export_dest().replace("res://", ""));
	if (importer == "image") {
		ResourceFormatLoaderImage rli;
		Ref<Image> img = rli.load(path, "", &err, false, nullptr, ResourceFormatLoader::CACHE_MODE_IGNORE);
		if (!err && !img.is_null()) {
			img_format = Image::get_format_name(img->get_format());
			err = save_image(dest_path, img, lossy);
		}
	} else if (importer == "texture_atlas") {
		if (ver_major <= 2 && (iinfo->get_type() == "ImageTexture" || iinfo->get_additional_sources().size() > 0)) {
			// this is the sprite sheet for the texture atlas; we can't save it to the original sources, so we save it to another file
			auto new_dest = dest_path.get_base_dir().path_join(iinfo->get_path().get_file());
			new_dest = new_dest.get_basename() + ".ATLAS_SHEET." + dest_path.get_extension();
			iinfo->set_export_dest(new_dest);
			dest_path = new_dest;
			err = _convert_tex(path, dest_path, lossy, img_format, report);
			// Don't rewrite the metadata for this
			report->set_rewrote_metadata(ExportReport::NOT_IMPORTABLE);
		} else {
			err = _convert_atex(path, dest_path, lossy, img_format);
		}
	} else if (importer == "bitmap") {
		err = _convert_bitmap(path, dest_path, lossy);
	} else if (importer == "texture_large" || importer == "2d_array_texture" || importer == "cubemap_array_texture" || importer == "cubemap_texture" || importer == "texture_array") {
		err = _convert_layered_2d(path, dest_path, lossy, img_format, report);
	} else if (importer == "3d_texture" || importer == "texture_3d") {
		err = _convert_3d(path, dest_path, lossy, img_format, report);
	} else if (importer == "texture" || importer == "texture_2d") {
		err = _convert_tex(path, dest_path, lossy, img_format, report);
	} else {
		report->set_error(ERR_UNAVAILABLE);
		report->set_message("Unsupported texture importer: " + importer);
		return report;
	}
	report->set_error(err);
	if (err == ERR_UNAVAILABLE) {
		report->set_unsupported_format_type(img_format);
		report->set_message("Decompression not implemented yet for texture format " + img_format);
		// Already reported in export functions above
		return report;
	} else if (err) {
		return report;
	}
	report->set_saved_path(dest_path);
	// If lossy, also convert it as a png
	bool saving_lossless_copy = false; // TODO: add config option
	if (saving_lossless_copy && lossy && importer == "texture") {
		String dest = iinfo->get_export_dest().get_basename() + ".png";
		if (!dest.replace("res://", "").begins_with(".assets")) {
			String prefix = ".assets";
			if (dest.begins_with("res://")) {
				prefix = "res://.assets";
			}
			dest = prefix.path_join(dest.replace("res://", ""));
		}
		iinfo->set_export_lossless_copy(dest);
		dest_path = output_dir.path_join(dest.replace("res://", ""));
		err = _convert_tex(path, dest_path, false, img_format, nullptr);
		ERR_FAIL_COND_V(err != OK, report);
	}

	return report;
}

void TextureExporter::get_handled_types(List<String> *out) const {
	out->push_back("Texture");
	out->push_back("Texture2D");
	out->push_back("ImageTexture");
	out->push_back("StreamTexture");
	out->push_back("CompressedTexture2D");
	out->push_back("BitMap");
	out->push_back("LargeTexture");
	out->push_back("AtlasTexture");
	out->push_back("StreamTexture");
	out->push_back("StreamTexture3D");
	out->push_back("StreamTextureArray");
	out->push_back("CompressedTexture2D");
	out->push_back("CompressedTexture3D");
	out->push_back("CompressedTextureLayered");
	out->push_back("CompressedTexture2DArray");
	out->push_back("CompressedCubemap");
	out->push_back("CompressedCubemapArray");
	out->push_back("TextureArray");
}

void TextureExporter::get_handled_importers(List<String> *out) const {
	out->push_back("texture");
	out->push_back("texture_2d");
	out->push_back("bitmap");
	out->push_back("image");
	out->push_back("texture_atlas");
	out->push_back("texture_large");
	out->push_back("texture_array");
	out->push_back("cubemap_texture");
	out->push_back("2d_array_texture");
	out->push_back("cubemap_array_texture");
	out->push_back("texture_3d");
	out->push_back("3d_texture");
}

String TextureExporter::get_name() const {
	return "Texture";
}

String TextureExporter::get_default_export_extension(const String &res_path) const {
	return "png";
}
#include "sample_exporter.h"

#include "compat/resource_loader_compat.h"
#include "utility/common.h"
#include "utility/import_info.h"

#include "scene/resources/audio_stream_wav.h"

struct IMA_ADPCM_State {
	int16_t step_index = 0;
	int32_t predictor = 0;
	int32_t last_nibble = -1;
};
struct QOA_State {
	qoa_desc desc = {};
	uint32_t data_ofs = 0;
	uint32_t frame_len = 0;
	LocalVector<int16_t> dec;
	uint32_t dec_len = 0;
	int64_t cache_pos = -1;
	int16_t cache[2] = { 0, 0 };
	int16_t cache_r[2] = { 0, 0 };
};

#define DATA_PAD 16

Ref<AudioStreamWAV> SampleExporter::convert_qoa_to_16bit(const Ref<AudioStreamWAV> &p_sample) {
	ERR_FAIL_COND_V_MSG(p_sample->get_format() != AudioStreamWAV::FORMAT_QOA, Ref<AudioStreamWAV>(), "Sample is not QOA.");
	Ref<AudioStreamWAV> new_sample = memnew(AudioStreamWAV);

	new_sample->set_format(AudioStreamWAV::FORMAT_16_BITS);
	new_sample->set_loop_mode(p_sample->get_loop_mode());
	new_sample->set_loop_begin(p_sample->get_loop_begin());
	new_sample->set_loop_end(p_sample->get_loop_end());
	new_sample->set_mix_rate(p_sample->get_mix_rate());
	new_sample->set_stereo(p_sample->is_stereo());

	auto data = p_sample->get_data();
	bool is_stereo = p_sample->is_stereo();
	Vector<uint8_t> dest_data;
	int64_t p_offset = 0;
	int64_t p_increment = 1;
	int32_t final = 0, final_r = 0, next = 0, next_r = 0;
	QOA_State p_qoa;
	int sign = 1;

	qoa_decode_header(data.ptr(), data.size(), &p_qoa.desc);
	int64_t p_amount = p_qoa.desc.samples; // number of samples for EACH channel, not total
	dest_data.resize(p_amount * sizeof(int16_t) * (is_stereo ? 2 : 1)); // number of 16-bit samples * number of channels
	p_qoa.frame_len = qoa_max_frame_size(&p_qoa.desc);
	int samples_len = (p_qoa.desc.samples > QOA_FRAME_LEN ? QOA_FRAME_LEN : p_qoa.desc.samples);
	int dec_len = p_qoa.desc.channels * samples_len;
	p_qoa.dec.resize(dec_len);
	int16_t *dest = (int16_t *)dest_data.ptrw();
	while (p_amount) {
		p_amount--;
		int64_t pos = p_offset;
		if (pos != p_qoa.cache_pos) { // Prevents triple decoding on lower mix rates.
			for (int i = 0; i < 2; i++) {
				// Sign operations prevent triple decoding on backward loops, maxing prevents pop.
				uint32_t interp_pos = MIN(pos + (i * sign) + (sign < 0), p_qoa.desc.samples - 1);
				uint32_t new_data_ofs = 8 + interp_pos / QOA_FRAME_LEN * p_qoa.frame_len;
				const uint8_t *src_ptr = (const uint8_t *)data.ptr();

				if (p_qoa.data_ofs != new_data_ofs) {
					p_qoa.data_ofs = new_data_ofs;
					const uint8_t *ofs_src = (uint8_t *)src_ptr + p_qoa.data_ofs;
					qoa_decode_frame(ofs_src, p_qoa.frame_len, &p_qoa.desc, p_qoa.dec.ptr(), &p_qoa.dec_len);
				}

				uint32_t dec_idx = (interp_pos % QOA_FRAME_LEN) * p_qoa.desc.channels;

				if ((sign > 0 && i == 0) || (sign < 0 && i == 1)) {
					final = p_qoa.dec[dec_idx];
					p_qoa.cache[0] = final;
					if (is_stereo) {
						final_r = p_qoa.dec[dec_idx + 1];
						p_qoa.cache_r[0] = final_r;
					}
				} else {
					next = p_qoa.dec[dec_idx];
					p_qoa.cache[1] = next;
					if (is_stereo) {
						next_r = p_qoa.dec[dec_idx + 1];
						p_qoa.cache_r[1] = next_r;
					}
				}
			}
			p_qoa.cache_pos = pos;
		} else {
			final = p_qoa.cache[0];
			if (is_stereo) {
				final_r = p_qoa.cache_r[0];
			}

			next = p_qoa.cache[1];
			if (is_stereo) {
				next_r = p_qoa.cache_r[1];
			}
		}
		*dest = final;
		dest++;
		if (is_stereo) {
			*dest = final_r;
			dest++;
		}
		p_offset += p_increment;
	}

	new_sample->set_data(dest_data);
	return new_sample;
}

Ref<AudioStreamWAV> SampleExporter::convert_adpcm_to_16bit(const Ref<AudioStreamWAV> &p_sample) {
	ERR_FAIL_COND_V_MSG(p_sample->get_format() != AudioStreamWAV::FORMAT_IMA_ADPCM, Ref<AudioStreamWAV>(), "Sample is not IMA ADPCM.");
	Ref<AudioStreamWAV> new_sample = memnew(AudioStreamWAV);

	new_sample->set_format(AudioStreamWAV::FORMAT_16_BITS);
	new_sample->set_loop_mode(p_sample->get_loop_mode());
	new_sample->set_loop_begin(p_sample->get_loop_begin());
	new_sample->set_loop_end(p_sample->get_loop_end());
	new_sample->set_mix_rate(p_sample->get_mix_rate());
	new_sample->set_stereo(p_sample->is_stereo());

	IMA_ADPCM_State p_ima_adpcm[2];
	int32_t final, final_r;
	int64_t p_offset = 0;
	int64_t p_increment = 1;
	auto data = p_sample->get_data(); // This gets the data past the DATA_PAD, so no need to add it to the offsets.
	bool is_stereo = p_sample->is_stereo();
	int64_t p_amount = data.size() * (is_stereo ? 1 : 2); // number of samples for EACH channel, not total
	Vector<uint8_t> dest_data;
	dest_data.resize(p_amount * sizeof(int16_t) * (is_stereo ? 2 : 1)); // number of 16-bit samples * number of channels
	int16_t *dest = (int16_t *)dest_data.ptrw();
	while (p_amount) {
		p_amount--;
		int64_t pos = p_offset;

		while (pos > p_ima_adpcm[0].last_nibble) {
			static const int16_t _ima_adpcm_step_table[89] = {
				7, 8, 9, 10, 11, 12, 13, 14, 16, 17,
				19, 21, 23, 25, 28, 31, 34, 37, 41, 45,
				50, 55, 60, 66, 73, 80, 88, 97, 107, 118,
				130, 143, 157, 173, 190, 209, 230, 253, 279, 307,
				337, 371, 408, 449, 494, 544, 598, 658, 724, 796,
				876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066,
				2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358,
				5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899,
				15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767
			};

			static const int8_t _ima_adpcm_index_table[16] = {
				-1, -1, -1, -1, 2, 4, 6, 8,
				-1, -1, -1, -1, 2, 4, 6, 8
			};

			for (int i = 0; i < (is_stereo ? 2 : 1); i++) {
				int16_t nibble, diff, step;

				p_ima_adpcm[i].last_nibble++;
				const uint8_t *src_ptr = (const uint8_t *)data.ptr();
				// src_ptr += AudioStreamWAV::DATA_PAD;
				int source_index = (p_ima_adpcm[i].last_nibble >> 1) * (is_stereo ? 2 : 1) + i;
				uint8_t nbb = src_ptr[source_index];
				nibble = (p_ima_adpcm[i].last_nibble & 1) ? (nbb >> 4) : (nbb & 0xF);
				step = _ima_adpcm_step_table[p_ima_adpcm[i].step_index];

				p_ima_adpcm[i].step_index += _ima_adpcm_index_table[nibble];
				if (p_ima_adpcm[i].step_index < 0) {
					p_ima_adpcm[i].step_index = 0;
				}
				if (p_ima_adpcm[i].step_index > 88) {
					p_ima_adpcm[i].step_index = 88;
				}

				diff = step >> 3;
				if (nibble & 1) {
					diff += step >> 2;
				}
				if (nibble & 2) {
					diff += step >> 1;
				}
				if (nibble & 4) {
					diff += step;
				}
				if (nibble & 8) {
					diff = -diff;
				}

				p_ima_adpcm[i].predictor += diff;
				if (p_ima_adpcm[i].predictor < -0x8000) {
					p_ima_adpcm[i].predictor = -0x8000;
				} else if (p_ima_adpcm[i].predictor > 0x7FFF) {
					p_ima_adpcm[i].predictor = 0x7FFF;
				}

				//printf("%i - %i - pred %i\n",int(p_ima_adpcm[i].last_nibble),int(nibble),int(p_ima_adpcm[i].predictor));
			}
		}

		final = p_ima_adpcm[0].predictor;
		if (is_stereo) {
			final_r = p_ima_adpcm[1].predictor;
		}

		*dest = final;
		dest++;
		if (is_stereo) {
			*dest = final_r;
			dest++;
		}
		p_offset += p_increment;
	}
	new_sample->set_data(dest_data);
	return new_sample;
}

Error SampleExporter::_export_file(const String &out_path, const String &res_path, Ref<AudioStreamWAV> &r_sample, int ver_major) {
	// Implement the export logic here
	Error err;
	r_sample = ResourceCompatLoader::non_global_load(res_path, "", &err);
	ERR_FAIL_COND_V_MSG(err != OK, err, "Could not load sample file " + res_path);
	ERR_FAIL_COND_V_MSG(r_sample.is_null(), ERR_FILE_UNRECOGNIZED, "Sample not loaded: " + res_path);
	bool converted = false;
	if (r_sample->get_format() == AudioStreamWAV::FORMAT_IMA_ADPCM) {
		// convert to 16-bit
		r_sample = convert_adpcm_to_16bit(r_sample);
		converted = true;
	} else if (r_sample->get_format() == AudioStreamWAV::FORMAT_QOA) {
		r_sample = convert_qoa_to_16bit(r_sample);
		converted = true;
	}
	err = gdre::ensure_dir(out_path.get_base_dir());
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to create dirs for " + out_path);
	err = r_sample->save_to_wav(out_path);
	ERR_FAIL_COND_V_MSG(err != OK, err, "Could not save " + out_path);

	return converted ? ERR_PRINTER_ON_FIRE : OK;
}

Error SampleExporter::export_file(const String &out_path, const String &res_path) {
	Ref<AudioStreamWAV> sample;
	Error err = _export_file(out_path, res_path, sample, 0);
	if (err == ERR_PRINTER_ON_FIRE) {
		return OK;
	}
	return err;
}

Ref<ExportReport> SampleExporter::export_resource(const String &output_dir, Ref<ImportInfo> import_infos) {
	// Implement the resource export logic here
	Ref<ExportReport> report = memnew(ExportReport(import_infos));
	String src_path = import_infos->get_path();
	String dst_path = output_dir.path_join(import_infos->get_export_dest().replace("res://", ""));
	Ref<AudioStreamWAV> sample;
	Error err = _export_file(dst_path, src_path, sample, import_infos->get_ver_major());

	if (err == ERR_PRINTER_ON_FIRE) {
		report->set_loss_type(ImportInfo::STORED_LOSSY);
	} else if (err != OK) {
		report->set_error(err);
		report->set_message("Failed to export sample: " + src_path);
	}

	if (err == OK || err == ERR_PRINTER_ON_FIRE) {
		report->set_saved_path(dst_path);
		if (import_infos->get_ver_major() >= 4) {
			// 			r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "force/8_bit"), false));
			// r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "force/mono"), false));
			// r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "force/max_rate", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false));
			// r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "force/max_rate_hz", PROPERTY_HINT_RANGE, "11025,192000,1,exp"), 44100));
			// r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "edit/trim"), false));
			// r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "edit/normalize"), false));
			// // Keep the `edit/loop_mode` enum in sync with AudioStreamWAV::LoopMode (note: +1 offset due to "Detect From WAV").
			// r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "edit/loop_mode", PROPERTY_HINT_ENUM, "Detect From WAV,Disabled,Forward,Ping-Pong,Backward", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), 0));
			// r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "edit/loop_begin"), 0));
			// r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "edit/loop_end"), -1));
			// // Quite OK Audio is lightweight enough and supports virtually every significant AudioStreamWAV feature.
			// r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/mode", PROPERTY_HINT_ENUM, "PCM (Uncompressed),IMA ADPCM,Quite OK Audio"), 2));
			import_infos->set_param("force/8_bit", false);
			import_infos->set_param("force/mono", false);
			import_infos->set_param("force/max_rate", false);
			import_infos->set_param("force/max_rate_hz", sample->get_mix_rate());
			import_infos->set_param("edit/trim", false);
			import_infos->set_param("edit/normalize", false);
			import_infos->set_param("edit/loop_mode", sample->get_loop_mode());
			import_infos->set_param("edit/loop_begin", sample->get_loop_begin());
			import_infos->set_param("edit/loop_end", sample->get_loop_end());
			// quote ok was added in 4.3
			import_infos->set_param("compress/mode", 0); // force uncompressed to prevent generational loss
		}
	}
	return report;
}

void SampleExporter::get_handled_types(List<String> *out) const {
	out->push_back("AudioStreamWAV");
	out->push_back("AudioStreamSample");
}

void SampleExporter::get_handled_importers(List<String> *out) const {
	out->push_back("sample");
	out->push_back("wav");
}

String SampleExporter::get_name() const {
	return "WAV";
}

String SampleExporter::get_default_export_extension(const String &res_path) const {
	return "wav";
}
#include "texture_exporter.h"

#include "compat/resource_compat_binary.h"
#include "compat/resource_loader_compat.h"
#include "utility/common.h"

#include "core/error/error_list.h"
#include "core/io/file_access.h"
#include "core/io/image_loader.h"
#include "core/io/resource_loader.h"
#include "scene/resources/atlas_texture.h"
#include "scene/resources/compressed_texture.h"
#include "utility/resource_info.h"
#include <cstdint>
namespace {
bool get_bit(const Vector<uint8_t> &bitmask, int width, int p_x, int p_y) {
	int ofs = width * p_y + p_x;
	int bbyte = ofs / 8;
	int bbit = ofs % 8;

	return (bitmask[bbyte] & (1 << bbit)) != 0;
}
} //namespace

// Format is the same on V2 - V4
Ref<Image> TextureExporter::load_image_from_bitmap(const String p_path, Error *r_err) {
	Error err = OK;
	if (!r_err) {
		r_err = &err;
	}
	Ref<Image> image;
	image.instantiate();
	ResourceFormatLoaderCompatBinary rlcb;
	auto res = ResourceCompatLoader::fake_load(p_path, "", r_err);
	ERR_FAIL_COND_V_MSG(*r_err != OK, Ref<Image>(), "Cannot open resource '" + p_path + "'.");

	String name;
	Vector2 size;
	Dictionary data;
	PackedByteArray bitmask;
	int width;
	int height;

	// Load the main resource, which should be the ImageTexture
	name = ResourceCompatConverter::get_resource_name(res, 0);
	data = res->get("data");
	bitmask = data.get("data", PackedByteArray());
	size = data.get("size", Vector2());
	width = size.width;
	height = size.height;
	image->initialize_data(width, height, false, Image::FORMAT_L8);

	if (!name.is_empty()) {
		image->set_name(name);
	}
	for (int i = 0; i < width; i++) {
		for (int j = 0; j < height; j++) {
			image->set_pixel(i, j, get_bit(bitmask, width, i, j) ? Color(1, 1, 1) : Color(0, 0, 0));
		}
	}
	ERR_FAIL_COND_V_MSG(image.is_null() || image->is_empty(), Ref<Image>(), "Failed to load image from " + p_path);
	*r_err = OK;
	return image;
}

Error TextureExporter::_convert_bitmap(const String &p_path, const String &dest_path, bool lossy, Ref<ExportReport> report) {
	String dst_dir = dest_path.get_base_dir();
	Error err = OK;
	Ref<Image> img = load_image_from_bitmap(p_path, &err);
	// deprecated format
	if (err == ERR_UNAVAILABLE) {
		// TODO: Not reporting here because we can't get the deprecated format type yet,
		// implement functionality to pass it back
		print_line("Did not convert deprecated Bitmap resource " + p_path);
		return err;
	}
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to load bitmap " + p_path);
	err = gdre::ensure_dir(dst_dir);
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to create dirs for " + dest_path);
	err = save_image(dest_path, img, lossy);
	if (err == ERR_UNAVAILABLE) {
		return err;
	}
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to save image " + dest_path + " from texture " + p_path);

	print_verbose("Converted " + p_path + " to " + dest_path);
	return OK;
}

bool dest_format_supports_mipmaps(const String &ext) {
	return ext == "dds" || ext == "exr";
}

Error TextureExporter::save_image(const String &dest_path, const Ref<Image> &img, bool lossy) {
	ERR_FAIL_COND_V_MSG(img->is_empty(), ERR_FILE_EOF, "Image data is empty for texture " + dest_path + ", not saving");
	Error err = gdre::ensure_dir(dest_path.get_base_dir());
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to create dirs for " + dest_path);
	String dest_ext = dest_path.get_extension().to_lower();
	if (!dest_format_supports_mipmaps(dest_ext) && img->is_compressed() && img->has_mipmaps()) {
		img->clear_mipmaps();
	}
	GDRE_ERR_DECOMPRESS_OR_FAIL(img);
	if (dest_ext == "jpg" || dest_ext == "jpeg") {
		err = img->save_jpg(dest_path, 1.0);
	} else if (dest_ext == "webp") {
		err = img->save_webp(dest_path, lossy, 1.0);
	} else if (dest_ext == "png") {
		err = img->save_png(dest_path);
	} else if (dest_ext == "tga") {
		err = gdre::save_image_as_tga(dest_path, img);
	} else if (dest_ext == "svg") {
		err = gdre::save_image_as_svg(dest_path, img);
	} else if (dest_ext == "dds") {
		err = img->save_dds(dest_path);
	} else if (dest_ext == "exr") {
		err = img->save_exr(dest_path);
	} else if (dest_ext == "bmp") {
		err = gdre::save_image_as_bmp(dest_path, img);
	} else {
		ERR_FAIL_V_MSG(ERR_FILE_BAD_PATH, "Invalid file name: " + dest_path);
	}
	return err;
}
enum V4ImporterCompressMode {
	COMPRESS_LOSSLESS,
	COMPRESS_LOSSY,
	COMPRESS_VRAM_COMPRESSED,
	COMPRESS_VRAM_UNCOMPRESSED,
	COMPRESS_BASIS_UNIVERSAL
};

enum ttttype {
	TEXTURE_2D,
	TEXTURE_3D,
	TEXTURE_LAYERED
};

void set_tex_params(Ref<ImportInfo> p_import_info, Ref<Resource> p_tex, Ref<Image> p_img, int ver_major, ttttype p_type) {
	// Import options reference:
	// compress/mode (int): Controls how the texture is compressed
	//   0: Lossless (PNG/WebP)
	//   1: Lossy (WebP)
	//   2: VRAM Compressed (S3TC/BPTC, ETC2/ASTC)
	//   3: VRAM Uncompressed
	//   4: Basis Universal
	//
	// compress/high_quality (bool): When true, uses higher quality compression formats
	//   - BPTC instead of S3TC
	//   - ASTC instead of ETC2
	//
	// compress/lossy_quality (float): Controls quality of lossy compression (0-1)
	//   - Higher values = better quality but larger file size
	//   - Default: 0.7
	//
	// compress/uastc_level (int): Controls Basis Universal UASTC compression quality/speed
	//   0: Fastest
	//   1: Faster
	//   2: Medium
	//   3: Slower
	//   4: Slowest
	//
	// compress/rdo_quality_loss (float): Rate-distortion optimization quality for Basis Universal (0-10)
	//   - Higher values = better compression but more quality loss
	//
	// compress/hdr_compression (int): Controls how HDR textures are handled
	//   0: Disabled
	//   1: Opaque Only
	//   2: Always
	//
	// compress/normal_map (int): Controls normal map compression
	//   0: Detect
	//   1: Enable
	//   2: Disabled
	//
	// compress/channel_pack (int): Controls how color channels are packed
	//   0: sRGB Friendly
	//   1: Optimized
	//
	// mipmaps/generate (bool): Whether to generate mipmaps
	//   - Default: true for 3D textures, false for 2D
	//
	// mipmaps/limit (int): Limits the number of mipmap levels (-1 to 256)
	//   - -1 means no limit
	//
	// roughness/mode (int): Controls how roughness is handled
	//   0: Detect
	//   1: Disabled
	//   2: Red
	//   3: Green
	//   4: Blue
	//   5: Alpha
	//   6: Gray
	//
	// roughness/src_normal (string): Path to normal map used for roughness calculation
	//   - Supports: bmp, dds, exr, jpeg, jpg, hdr, png, svg, tga, webp
	//
	// process/fix_alpha_border (bool): Fixes alpha border artifacts
	//   - Default: true for 2D textures, false for 3D
	//
	// process/premult_alpha (bool): Premultiplies alpha channel
	//
	// process/normal_map_invert_y (bool): Inverts the Y channel of normal maps
	//
	// process/hdr_as_srgb (bool): Treats HDR textures as sRGB
	//
	// process/hdr_clamp_exposure (bool): Clamps HDR exposure to prevent artifacts
	//
	// process/size_limit (int): Maximum texture dimension (0-16383)
	//   - 0 means no limit
	//
	// detect_3d/compress_to (int): Controls how 3D textures are compressed
	//   0: Disabled
	//   1: VRAM Compressed
	//   2: Basis Universal
	//
	// SVG-specific options (only for SVG files):
	// svg/scale (float): Scale factor for SVG import (0.001-100)
	//   - Default: 1.0
	//
	// editor/scale_with_editor_scale (bool): Whether to scale SVG with editor scale
	//
	// editor/convert_colors_with_editor_theme (bool): Whether to convert SVG colors to match editor theme

	//CompressedTexture2D::DATA_FORMAT_WEBP

	if (!(p_import_info.is_valid() && p_tex.is_valid() && ver_major >= 4)) {
		return;
	}

	String ext = p_import_info->get_source_file().get_extension().to_lower();
	Dictionary params;
	Ref<ResourceInfo> info = ResourceInfo::get_info_from_resource(p_tex);
	ERR_FAIL_COND_MSG(!info.is_valid(), "TEXTURE LOADERS SHOULD HAVE SET THE RESOURCE INFO FOR THIS TEXTURE!!!!!!!!");
	// Get the image from the texture
	Ref<Image> img;

	if (p_type == TEXTURE_2D) {
		Ref<Texture2D> tex2d = p_tex;
		img = tex2d->get_image();
	} else if (p_type == TEXTURE_3D) {
		Ref<Texture3D> tex3d = p_tex;
		img = p_img;
	} else if (p_type == TEXTURE_LAYERED) {
		Ref<TextureLayered> texlay = p_tex;
		img = p_img;
	}

	if (!img.is_valid()) {
		return;
	}
	int compress_mode = -1;
	CompressedTexture2D::DataFormat data_format = CompressedTexture2D::DATA_FORMAT_IMAGE;
	ERR_FAIL_COND(info->extra.is_empty());
	Dictionary extra = info->extra;
	int df = extra.get("data_format", -1);
	int tf = extra.get("texture_flags", -1);
	ERR_FAIL_COND(df == -1 || tf == -1);
	data_format = CompressedTexture2D::DataFormat(df);
	int texture_flags = tf;
	switch (data_format) {
		case CompressedTexture2D::DATA_FORMAT_PNG:
		case CompressedTexture2D::DATA_FORMAT_WEBP: // force WEBP to lossless
			compress_mode = COMPRESS_LOSSLESS;
			break;
		case CompressedTexture2D::DATA_FORMAT_BASIS_UNIVERSAL:
			compress_mode = COMPRESS_BASIS_UNIVERSAL;
			break;
		case CompressedTexture2D::DATA_FORMAT_IMAGE:
			if (img->is_compressed()) {
				compress_mode = COMPRESS_VRAM_COMPRESSED;
			} else {
				compress_mode = COMPRESS_VRAM_UNCOMPRESSED;
			}
			break;
		default: //DATA_FORMAT_IMAGE, we need to check the img format
			compress_mode = 0;
			break;
	}
	// Check if the image is compressed
	params["compress/mode"] = compress_mode;
	// Set high quality flag for VRAM compression
	auto format = img->get_format();
	auto decompressed_fmt = format;
	Ref<Image> decompressed_img = img;
	if (img->is_compressed()) {
		decompressed_img = img->duplicate();
		decompressed_img->decompress();
		decompressed_fmt = decompressed_img->get_format();
	}
	bool is_hdr = (decompressed_fmt >= Image::FORMAT_RF && decompressed_fmt <= Image::FORMAT_RGBE9995);

	if (img->is_compressed() && (is_hdr || ((format >= Image::FORMAT_BPTC_RGBA && format <= Image::FORMAT_BPTC_RGBFU)) || (format >= Image::FORMAT_ASTC_4x4 && format <= Image::FORMAT_ASTC_8x8_HDR))) {
		params["compress/high_quality"] = true;
	} else {
		params["compress/high_quality"] = false;
	}

	// Set lossy quality if using lossy compression
	params["compress/lossy_quality"] = 1.0; // prevent generational loss

	// Set Basis Universal parameters if used
	if (p_import_info->get_ver_minor() >= 5) {
		params["compress/uastc_level"] = 2; // Default is Fastest, but force to Medium to prevent generational loss
		params["compress/rdo_quality_loss"] = 0; // Default
	}
	params["compress/hdr_compression"] = is_hdr && img->is_compressed() ? 1 : 0;

	// enum FormatBits {
	// 	FORMAT_BIT_STREAM = 1 << 22,
	// 	FORMAT_BIT_HAS_MIPMAPS = 1 << 23,
	// 	FORMAT_BIT_DETECT_3D = 1 << 24,
	// 	//FORMAT_BIT_DETECT_SRGB = 1 << 25,
	// 	FORMAT_BIT_DETECT_NORMAL = 1 << 26,
	// 	FORMAT_BIT_DETECT_ROUGNESS = 1 << 27,
	// };
	// set it to "Detect" if the flag is set; else use "Disabled"
	if (p_type == TEXTURE_2D) {
		params["compress/normal_map"] = ((texture_flags & CompressedTexture2D::FORMAT_BIT_DETECT_NORMAL) != 0) ? 0 : 2;
	}
	// Set channel packing

	int channel_pack = 0;
	auto used_channels = decompressed_img->detect_used_channels();
	if (used_channels == Image::USED_CHANNELS_R || used_channels == Image::USED_CHANNELS_RG) {
		channel_pack = 1; // not sRGB friendly
	}
	params["compress/channel_pack"] = channel_pack;

	// Set mipmap settings
	params["mipmaps/generate"] = (texture_flags & CompressedTexture2D::FORMAT_BIT_HAS_MIPMAPS) != 0 || (img->has_mipmaps());
	int limit = -1;
	if (img->has_mipmaps()) {
		int mipmap_count = img->get_mipmap_count();
		int min_width, min_height;
		img->get_format_min_pixel_size(img->get_format(), min_width, min_height);
		if (mipmap_count > 1) {
			int64_t offset, size;
			int last_width, last_height;
			img->get_mipmap_offset_size_and_dimensions(mipmap_count - 1, offset, size, last_width, last_height);
			// it may be a non-power of 2 last mipmap, so it's either min height or width
			if (last_width != min_width && last_height != min_height) {
				limit = mipmap_count;
			}
		}
	}

	params["mipmaps/limit"] = limit;

	if (p_type == TEXTURE_2D) {
		bool is_roughness = (texture_flags & CompressedTexture2D::FORMAT_BIT_DETECT_ROUGNESS) != 0;
		if (is_roughness) {
			// TODO: this puts it into "Detect", which will make the editor detect what it is; maybe we can do better??
			params["roughness/mode"] = 0;
		} else {
			params["roughness/mode"] = 1;
		}

		// TODO: This? Probably not though, it is a destructive process to apply the roughness map
		params["roughness/src_normal"] = "";

		// Set processing options
		params["process/fix_alpha_border"] = false; // default true, but forcing to false to prevent re-fixing
		params["process/premult_alpha"] = false; // default false, and forcing cuz destructive
		params["process/normal_map_invert_y"] = false; // default true, but forcing to false to prevent re-inverting

		// Set HDR settings
		params["process/hdr_as_srgb"] = false;
		params["process/hdr_clamp_exposure"] = false;

		// Set size limit
		params["process/size_limit"] = 0; // Default to no limit

		// Set 3D detection
		int detect_3d_compress_to = 0;
		if (texture_flags & CompressedTexture2D::FORMAT_BIT_DETECT_3D) {
			if (compress_mode == COMPRESS_VRAM_COMPRESSED) {
				detect_3d_compress_to = 1;
			} else if (compress_mode == COMPRESS_BASIS_UNIVERSAL) {
				detect_3d_compress_to = 2;
			}
		}
		params["detect_3d/compress_to"] = detect_3d_compress_to;

		if (ext == "svg") {
			params["svg/scale"] = 1.0;
			// No editor scaling; we want it to stay the same size
			params["editor/scale_with_editor_scale"] = false;
			params["editor/convert_colors_with_editor_theme"] = false;
		}
	}

	// Set the parameters in the import info
	p_import_info->set_params(params);
}

Error TextureExporter::_convert_tex(const String &p_path, const String &dest_path, bool lossy, String &image_format, Ref<ExportReport> report) {
	Error err;
	String dst_dir = dest_path.get_base_dir();
	Ref<Texture2D> tex;
	tex = ResourceCompatLoader::non_global_load(p_path, "", &err);

	if (err == ERR_UNAVAILABLE) {
		// TODO: Not reporting here because we can't get the deprecated format type yet,
		// implement functionality to pass it back
		image_format = "Unknown deprecated image format";
		print_line("Did not convert deprecated Texture resource " + p_path);
		return err;
	}
	ERR_FAIL_COND_V_MSG(err != OK || tex.is_null(), err, "Failed to load texture " + p_path);

	Ref<Image> img = tex->get_image();

	ERR_FAIL_COND_V_MSG(img.is_null(), ERR_PARSE_ERROR, "Failed to load image for texture " + p_path);
	if (report.is_valid()) {
		auto iinfo = report->get_import_info();
		if (iinfo.is_valid()) {
			set_tex_params(iinfo, tex, img, iinfo->get_ver_major(), TEXTURE_2D);
		}
	}
	image_format = Image::get_format_name(img->get_format());
	err = save_image(dest_path, img, lossy);
	if (err == ERR_UNAVAILABLE) {
		return err;
	}
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to save image " + dest_path + " from texture " + p_path);
	print_verbose("Converted " + p_path + " to " + dest_path);
	return OK;
}

Error TextureExporter::_convert_atex(const String &p_path, const String &dest_path, bool lossy, String &image_format, Ref<ExportReport> report) {
	Error err;
	String dst_dir = dest_path.get_base_dir();
	Ref<Texture2D> loaded_tex = ResourceCompatLoader::custom_load(p_path, "", ResourceInfo::GLTF_LOAD, &err, false, ResourceFormatLoader::CACHE_MODE_IGNORE_DEEP);
	// deprecated format
	if (err == ERR_UNAVAILABLE) {
		// TODO: Not reporting here because we can't get the deprecated format type yet,
		// implement functionality to pass it back
		image_format = "Unknown deprecated image format";
		return err;
	}
	ERR_FAIL_COND_V_MSG(err != OK || loaded_tex.is_null(), err, "Failed to load texture " + p_path);
	Ref<AtlasTexture> atex = loaded_tex;
	if (atex.is_null()) {
		// this is not an AtlasTexture, return TextureExporter::_convert_tex
		return _convert_tex(p_path, dest_path, lossy, image_format, report);
	}
	Ref<Texture2D> tex = atex->get_atlas();
	ERR_FAIL_COND_V_MSG(tex.is_null(), ERR_PARSE_ERROR, "Failed to load atlas texture " + p_path);
	Ref<Image> img = tex->get_image();

	ERR_FAIL_COND_V_MSG(img.is_null(), ERR_PARSE_ERROR, "Failed to load image for texture " + p_path);
	ERR_FAIL_COND_V_MSG(img->is_empty(), ERR_FILE_EOF, "Image data is empty for texture " + p_path + ", not saving");
	image_format = Image::get_format_name(img->get_format());

	if (img->is_compressed() && img->has_mipmaps()) {
		img->clear_mipmaps();
	}

	// resize it according to the properties of the atlas
	GDRE_ERR_DECOMPRESS_OR_FAIL(img);
	if (img->get_format() != Image::FORMAT_RGBA8) {
		img->convert(Image::FORMAT_RGBA8);
	}

	auto margin = atex->get_margin();
	auto region = atex->get_region();

	// now we have to add the margin padding
	Ref<Image> new_img = Image::create_empty(atex->get_width(), atex->get_height(), false, img->get_format());
	new_img->blit_rect(img, region, Point2i(margin.position.x, margin.position.y));
	err = save_image(dest_path, new_img, lossy);
	if (err == ERR_UNAVAILABLE) {
		return err;
	}
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to save image " + dest_path + " from texture " + p_path);

	print_verbose("Converted " + p_path + " to " + dest_path);
	return OK;
}

Error TextureExporter::export_file(const String &out_path, const String &res_path) {
	Error err;
	auto res_info = ResourceCompatLoader::get_resource_info(res_path, "", &err);
	if (res_info.is_null() || !handles_import("", res_info->type)) {
		return ERR_FILE_UNRECOGNIZED;
	}
	if (res_info->type == "BitMap") {
		return _convert_bitmap(res_path, out_path, false, nullptr);
	}
	String fmt_name;
	if (res_info->type == "AtlasTexture") {
		return _convert_atex(res_path, out_path, false, fmt_name);
	}
	return _convert_tex(res_path, out_path, false, fmt_name);
}

Error preprocess_images(
		String p_path,
		String dest_path,
		int num_images_w,
		int num_images_h,
		bool lossy,
		Vector<Ref<Image>> &images,
		bool &had_mipmaps,
		bool &detected_alpha,
		bool ignore_dimensions = false) {
	ERR_FAIL_COND_V_MSG(images.size() == 0, ERR_PARSE_ERROR, "No images to concat");
	int layer_count = num_images_w * num_images_h;
	int width = images[0]->get_width();
	int height = images[0]->get_height();

	auto format = images[0]->get_format();
	auto decompressed_fmt = format;

	Ref<Image> ref_img;
	if (layer_count > 0) {
		ref_img = images[0];
		if (!ref_img.is_null()) {
			// dupe to avoid modifying the original
			if (ref_img->is_compressed()) {
				Ref<Image> dupe = ref_img->duplicate();
				GDRE_ERR_DECOMPRESS_OR_FAIL(dupe);
				decompressed_fmt = dupe->get_format();
			}
		}
	}
	if (ref_img.is_null()) {
		return ERR_PARSE_ERROR;
	}
	auto new_format = decompressed_fmt;
	had_mipmaps = layer_count != images.size();
	Vector<Vector<uint8_t>> images_data;
	bool is_hdr = decompressed_fmt >= Image::FORMAT_RF && decompressed_fmt <= Image::FORMAT_RGBE9995;

	Vector<Image::Format> formats;
	detected_alpha = false;
	for (int64_t i = 0; i < layer_count; i++) {
		Ref<Image> img = images[i];
		if (img.is_null()) {
			return ERR_PARSE_ERROR;
		}
		if (img->has_mipmaps()) {
			had_mipmaps = true;
			img->clear_mipmaps();
		}
		GDRE_ERR_DECOMPRESS_OR_FAIL(img);
		detected_alpha = detected_alpha || img->detect_alpha();
		if (is_hdr) {
			new_format = img->get_format();
			formats.push_back(img->get_format());
			images_data.push_back(img->get_data());
		}
		if (!ignore_dimensions) {
			ERR_FAIL_COND_V_MSG(img->get_width() != width || img->get_height() != height, ERR_PARSE_ERROR, "Image " + p_path + " has incorrect dimensions");
		}
	}

	if (!is_hdr || gdre::vector_to_hashset(formats).size() > 1) {
		if (!is_hdr) {
			if (!detected_alpha) {
				new_format = Image::FORMAT_RGB8;
			} else if (detected_alpha) {
				new_format = Image::FORMAT_RGBA8;
			}
		} else {
			new_format = gdre::get_most_popular_value(formats);
			// check if we've detected alpha and if this format supports it
			const bool supports_alpha = new_format == Image::FORMAT_RGBA8 || new_format == Image::FORMAT_RGBA4444 || new_format == Image::FORMAT_RGBAH || new_format == Image::FORMAT_RGBAF;
			if (detected_alpha && !supports_alpha) {
				new_format = Image::FORMAT_RGBA8;
			}
		}
		images_data.clear();
		for (int i = 0; i < layer_count; i++) {
			Ref<Image> img = images[i];
			if (img->get_format() != new_format) {
				img->convert(new_format);
			}
			images_data.push_back(img->get_data());
		}
	}
	return OK;
}

Error save_image_with_mipmaps(const String &dest_path, const Vector<Ref<Image>> &images, int num_images_w, int num_images_h, bool lossy, bool had_mipmaps, int override_width = -1, int override_height = -1) {
	auto new_format = images[0]->get_format();
	int pixel_size = Image::get_format_pixel_size(new_format);
	Vector<Vector<uint8_t>> images_data;
	int max_height = 0;
	for (int i = 0; i < images.size(); i++) {
		ERR_FAIL_COND_V_MSG(images[i].is_null(), ERR_PARSE_ERROR, "Image " + dest_path.get_file() + " is null");
		images_data.push_back(images[i]->get_data());
		max_height = MAX(max_height, images[i]->get_height());
	}

	Vector<uint8_t> new_image_data;
	size_t new_width = override_width != -1 ? override_width : images[0]->get_width() * num_images_w;
	size_t new_height = override_height != -1 ? override_height : images[0]->get_height() * num_images_h;
	size_t new_data_size = Image::get_image_data_size(new_width, new_height, new_format, false);
	new_image_data.resize(new_data_size);
	size_t current_offset = 0;
	for (int row_idx = 0; row_idx < num_images_h; row_idx++) {
		for (int i = 0; i < max_height; i++) {
			for (int img_idx = row_idx * num_images_w; img_idx < (row_idx + 1) * num_images_w; img_idx++) {
				if (images[img_idx]->get_height() <= i) {
					continue;
				}
				size_t copy_size = images[img_idx]->get_width() * pixel_size;
				// We're concatenating the images horizontally; so we have to take a width-sized slice of the image
				// and copy it into the new image data
				size_t start_idx = i * copy_size;
				ERR_FAIL_COND_V(images_data[img_idx].size() < start_idx + copy_size, ERR_PARSE_ERROR);
				memcpy(new_image_data.ptrw() + current_offset, images_data[img_idx].ptr() + start_idx, copy_size);
				current_offset += copy_size;
			}
		}
	}
	DEV_ASSERT(Image::get_image_data_size(new_width, new_height, new_format, false) == new_image_data.size());
	Ref<Image> img = Image::create_from_data(new_width, new_height, false, new_format, new_image_data);
	ERR_FAIL_COND_V_MSG(img.is_null(), ERR_PARSE_ERROR, "Failed to create image for texture " + dest_path.get_file());
	if (had_mipmaps && dest_format_supports_mipmaps(dest_path.get_extension().to_lower())) {
		img->generate_mipmaps();
		DEV_ASSERT(Image::get_image_data_size(new_width, new_height, new_format, true) == img->get_data_size());
	}
	Error err = TextureExporter::save_image(dest_path, img, lossy);
	if (err == ERR_UNAVAILABLE) {
		return err;
	}
	return err;
}

Error TextureExporter::_convert_3d(const String &p_path, const String &dest_path, bool lossy, String &image_format, Ref<ExportReport> report) {
	Error err;
	String dst_dir = dest_path.get_base_dir();
	Ref<Texture3D> tex;
	tex = ResourceCompatLoader::non_global_load(p_path, "", &err);
	Ref<ImportInfo> iinfo;
	if (report.is_valid()) {
		iinfo = report->get_import_info();
	}

	// deprecated format
	if (err == ERR_UNAVAILABLE) {
		image_format = "Unknown deprecated image format";
		print_line("Did not convert deprecated Texture resource " + p_path);
		return err;
	}
	ERR_FAIL_COND_V_MSG(err != OK || tex.is_null(), err, "Failed to load texture " + p_path);
	ERR_FAIL_COND_V_MSG(tex->get_depth() <= 0, ERR_PARSE_ERROR, "Texture " + p_path + " has no layers");

	auto layer_count = tex->get_depth();
	Vector<Ref<Image>> images = tex->get_data();
	Ref<Image> ref_img = images[0]->duplicate();
	ERR_FAIL_COND_V_MSG(images.size() == 0, ERR_PARSE_ERROR, "No images to concat");

	int64_t num_images_w = -1;
	int64_t num_images_h = -1;
	Dictionary params;
	if (iinfo.is_valid()) {
		params = iinfo->get_params();
	}
	bool had_valid_params = false;
	if (iinfo.is_valid()) {
		num_images_w = params.get("slices/horizontal", -1);
		num_images_h = params.get("slices/vertical", -1);
		had_valid_params = num_images_w != -1 && num_images_h != -1;
	}
	if (!had_valid_params) {
		if (layer_count == 64) {
			num_images_w = 8;
			num_images_h = 8;
		} else if (layer_count == 16) {
			num_images_w = 4;
			num_images_h = 4;
		} else if (layer_count == 4) {
			num_images_w = 2;
			num_images_h = 2;
		} else {
			num_images_w = layer_count;
			num_images_h = 1;
		}
	}
	bool had_mipmaps = layer_count != images.size();
	bool detected_alpha = false;
	err = preprocess_images(p_path, dest_path, num_images_w, num_images_h, lossy, images, had_mipmaps, detected_alpha);
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to preprocess images for texture " + p_path);
	err = save_image_with_mipmaps(dest_path, images, num_images_w, num_images_h, lossy, had_mipmaps);
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to save image " + dest_path + " from texture " + p_path);
	set_tex_params(iinfo, tex, ref_img, iinfo->get_ver_major(), TEXTURE_3D);

	if (!had_valid_params && iinfo.is_valid()) {
		iinfo->set_param("slices/horizontal", num_images_w);
		iinfo->set_param("slices/vertical", num_images_h);
		iinfo->set_param("mipmaps/generate", had_mipmaps);
	}
	print_verbose("Converted " + p_path + " to " + dest_path);
	return OK;
}

enum CubemapFormat {
	CUBEMAP_FORMAT_1X6,
	CUBEMAP_FORMAT_2X3,
	CUBEMAP_FORMAT_3X2,
	CUBEMAP_FORMAT_6X1,
};

Ref<Image> crop_transparent(const Ref<Image> &img) {
	ERR_FAIL_COND_V(img.is_null(), img);
	int width = img->get_width();
	int height = img->get_height();

	// check if it's width-wise or height-wise based on the ratio of the width and height
	bool is_horizontal = width > height;
	int64_t num_parts = (is_horizontal ? width / height : height / width);

	int new_width = is_horizontal ? width / num_parts : width;
	int new_height = is_horizontal ? height : height / num_parts;

	// now we need to find the first non-transparent pixel in the image
	int min_x = 0;
	int width_region_start = 0;
	int min_y = 0;
	int height_region_start = 0;

	// first, check to see if the image is entirely transparent
	bool is_entirely_transparent = true;
	for (int y = 0; y < height; y++) {
		for (int x = 0; x < width; x++) {
			if (img->get_pixel(x, y).a > 0.0) {
				is_entirely_transparent = false;
			}
		}
	}
	if (is_entirely_transparent) {
		return Ref<Image>();
	}

	if (is_horizontal) {
		// the transparent pixels start at either at 0,0, new_width,0, or width - new_width,0
		if (img->get_pixel(0, 0).a == 0.0) {
			min_x = 0;
			width_region_start = new_width * (num_parts - 1);
		} else if (img->get_pixel(new_width, 0).a == 0.0) {
			min_x = new_width;
			width_region_start = 0;
		} else if (img->get_pixel(width - new_width, 0).a == 0.0) {
			min_x = width - new_width;
			width_region_start = width - (new_width * (num_parts - 1));
		}
		min_y = 0;
		height_region_start = 0;
	} else {
		// the transparent pixels start at either at 0,0, 0,new_height, or 0,height - new_height
		if (img->get_pixel(0, 0).a == 0.0) {
			min_y = 0;
			height_region_start = new_height * (num_parts - 1);
		} else if (img->get_pixel(0, new_height).a == 0.0) {
			min_y = new_height;
			height_region_start = 0;
		} else if (img->get_pixel(0, height - new_height).a == 0.0) {
			min_y = height - new_height;
			height_region_start = height - (new_height * (num_parts - 1));
		}
		min_x = 0;
		width_region_start = 0;
	}

	return img->get_region(Rect2i(width_region_start, height_region_start, new_width, new_height));
	;
}

Vector<Ref<Image>> fix_cross_cubemaps(const Vector<Ref<Image>> &images, int width, int height, int layer_count, bool detected_alpha) {
	// here is where we fix the "cross" style of cubemaps that got imported all funky
	// check if the images have the same width and height
	Vector<Ref<Image>> fixed_images;
	bool is_horizontal = width > height;
	int64_t num_parts = is_horizontal ? width / height : height / width;
	if (width != height) {
		if (detected_alpha) {
			// we need to fix the images
			for (int i = 0; i < layer_count; i++) {
				Ref<Image> img = images[i];
				if (img->detect_alpha()) {
					Ref<Image> cropped = crop_transparent(img);
					size_t new_width;
					size_t new_height;
					if (!cropped.is_null()) {
						new_width = cropped->get_width();
						new_height = cropped->get_height();
						fixed_images.push_back(cropped);
					} else {
						new_width = 0;
						new_height = 0;
					}
				} else {
					// otherwise, divide it into parts based on the ratio of the width and height
					for (int j = 0; j < num_parts; j++) {
						Rect2i rect;
						if (is_horizontal) {
							rect.position.x = j * width / num_parts;
							rect.size.width = width / num_parts;
							rect.position.y = 0;
							rect.size.height = height;
						} else {
							rect.position.x = 0;
							rect.size.width = width;
							rect.position.y = j * height / num_parts;
							rect.size.height = height / num_parts;
						}
						Ref<Image> part = img->get_region(rect);
						fixed_images.push_back(part);
					}
				}
			}
		}
	}
#if 0
	for (int i = 0; i < fixed_images.size(); i++) {
		Ref<Image> img = fixed_images[i];
		if (img.is_null()) {
			continue;
		}
		auto new_dest = dest_path.get_basename() + "_cropped_" + String::num_int64(i) + "." + dest_path.get_extension();
		Error err = TextureExporter::save_image(new_dest, img, lossy);
	}
#endif
	if (fixed_images.size() > 0) {
		Vector<Ref<Image>> images;
		images.resize(6);
		// X+, X-, Y+, Y-, Z+, Z-
		// this is upside down;
		images.write[0] = fixed_images[3];
		images.write[1] = fixed_images[1];
		images.write[2] = fixed_images[0];
		images.write[3] = fixed_images[5];
		images.write[4] = fixed_images[2];
		images.write[5] = fixed_images[4];

		// arrangement = is_horizontal ? CUBEMAP_FORMAT_6X1 : CUBEMAP_FORMAT_1X6;
		// num_images_w = is_horizontal ? 6 : 1;
		// num_images_h = is_horizontal ? 1 : 6;
		return images;
	} else {
		return {};
	}
}

Error TextureExporter::_convert_layered_2d(const String &p_path, const String &dest_path, bool lossy, String &image_format, Ref<ExportReport> report) {
	Error err;
	String dst_dir = dest_path.get_base_dir();
	Ref<TextureLayered> tex;
	tex = ResourceCompatLoader::non_global_load(p_path, "", &err);
	Ref<ImportInfo> iinfo;
	if (report.is_valid()) {
		iinfo = report->get_import_info();
	}

	if (err == ERR_UNAVAILABLE) {
		image_format = "Unknown deprecated image format";
		print_line("Did not convert deprecated Texture resource " + p_path);
		return err;
	}
	ERR_FAIL_COND_V_MSG(err != OK || tex.is_null(), err, "Failed to load texture " + p_path);

	auto layer_count = tex->get_layers();
	if (layer_count == 0) {
		return ERR_PARSE_ERROR;
	}
	Vector<Ref<Image>> images;
	Ref<Image> ref_img = tex->get_layer_data(0)->duplicate();
	for (int i = 0; i < layer_count; i++) {
		images.push_back(tex->get_layer_data(i));
	}
	ERR_FAIL_COND_V_MSG(images.size() == 0, ERR_PARSE_ERROR, "No images to concat");
#if 0
	for (int i = 0; i < layer_count; i++) {
		Ref<Image> img = images[i];
		auto new_dest = dest_path.get_basename() + "_" + String::num_int64(i) + "." + dest_path.get_extension();
		Error err = TextureExporter::save_image(new_dest, img, lossy);
	}
#endif

	int64_t num_images_w = -1;
	int64_t num_images_h = -1;
	int64_t arrangement = -1;
	int64_t layout = -1;
	int64_t amount = -1;
	int64_t override_width = -1;
	int64_t override_height = -1;
	auto mode = tex->get_layered_type();
	Dictionary params;
	if (iinfo.is_valid()) {
		params = iinfo->get_params();
	}
	// get the resource info
	Ref<ResourceInfo> res_info = ResourceInfo::get_info_from_resource(tex);
	bool had_valid_params = false;
	bool ignore_dimensions = res_info.is_valid() && res_info->get_ver_major() <= 2;

	if (mode == TextureLayered::LAYERED_TYPE_2D_ARRAY) {
		// get the square root of the number of layers; if it's a whole number, then we have a square
		if (iinfo.is_valid()) {
			num_images_w = params.get("slices/horizontal", -1);
			num_images_h = params.get("slices/vertical", -1);
			had_valid_params = num_images_w != -1 && num_images_h != -1;
		}
		if (res_info.is_valid() && res_info->get_type() == "LargeTexture") {
			Vector<Vector2> offsets = res_info->get_extra().get("offsets", Vector<Vector2>());
			Vector2 whole_size = res_info->get_extra().get("whole_size", Vector2(-1, -1));
			override_width = whole_size.x;
			override_height = whole_size.y;
			// get the number of unique individual x and y values
			HashSet<int64_t> unique_x;
			HashSet<int64_t> unique_y;
			for (int i = 0; i < offsets.size(); i++) {
				unique_x.insert(offsets[i].x);
				unique_y.insert(offsets[i].y);
			}
			num_images_w = unique_x.size();
			num_images_h = unique_y.size();
			had_valid_params = true;
		} else if (!had_valid_params) {
			if (layer_count == 64) {
				num_images_w = 8;
				num_images_h = 8;
			} else if (layer_count == 16) {
				num_images_w = 4;
				num_images_h = 4;
			} else if (layer_count == 4) {
				num_images_w = 2;
				num_images_h = 2;
			} else {
				num_images_w = tex->get_layers();
				num_images_h = 1;
			}
		}
	} else if (mode == TextureLayered::LAYERED_TYPE_CUBEMAP || mode == TextureLayered::LAYERED_TYPE_CUBEMAP_ARRAY) {
		if (iinfo.is_valid()) {
			arrangement = params.get("slices/arrangement", -1);
			if (arrangement != -1 && mode == TextureLayered::LAYERED_TYPE_CUBEMAP) {
				had_valid_params = true;
			} else if (arrangement != -1 && mode == TextureLayered::LAYERED_TYPE_CUBEMAP_ARRAY) {
				layout = params.get("slices/layout", -1);
				amount = params.get("slices/amount", -1);
				if (layout != -1 && amount != -1) {
					had_valid_params = true;
				}
			}
		}
		if (!had_valid_params) {
			arrangement = 1;
			layout = 1;
			amount = layer_count / 6;
		}
		if (arrangement == CUBEMAP_FORMAT_1X6) {
			num_images_w = 1;
			num_images_h = 6;
		} else if (arrangement == CUBEMAP_FORMAT_2X3) {
			num_images_w = 2;
			num_images_h = 3;
		} else if (arrangement == CUBEMAP_FORMAT_3X2) {
			num_images_w = 3;
			num_images_h = 2;
		} else if (arrangement == CUBEMAP_FORMAT_6X1) {
			num_images_w = 6;
			num_images_h = 1;
		}
		if (mode == TextureLayered::LAYERED_TYPE_CUBEMAP_ARRAY) {
			if (layout == 0) {
				num_images_w *= amount;
			} else if (layout == 1) {
				num_images_h *= amount;
			}
		}
	}
	bool had_mipmaps = layer_count != images.size();
	bool detected_alpha = false;
	err = preprocess_images(p_path, dest_path, num_images_w, num_images_h, lossy, images, had_mipmaps, detected_alpha, ignore_dimensions);
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to preprocess images for texture " + p_path);
	int width = tex->get_width();
	int height = tex->get_height();
#if 0 // This was an attempt at fixing incorrectly imported cubemaps; if it was incorrectly imported by the original author, we should just leave it be.
	if (mode == TextureLayered::LAYERED_TYPE_CUBEMAP || mode == TextureLayered::LAYERED_TYPE_CUBEMAP_ARRAY) {
		Vector<Ref<Image>> fixed_images = fix_cross_cubemaps(images, width, height, layer_count, detected_alpha);
	}
#endif
	err = save_image_with_mipmaps(dest_path, images, num_images_w, num_images_h, lossy, had_mipmaps, override_width, override_height);
	image_format = Image::get_format_name(images[0]->get_format());
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to concat images for texture " + p_path);
	set_tex_params(iinfo, tex, ref_img, iinfo->get_ver_major(), TEXTURE_LAYERED);

	if (!had_valid_params && iinfo.is_valid()) {
		if (mode == TextureLayered::LAYERED_TYPE_2D_ARRAY) {
			iinfo->set_param("slices/horizontal", num_images_w);
			iinfo->set_param("slices/vertical", num_images_h);
		} else if (mode == TextureLayered::LAYERED_TYPE_CUBEMAP) {
			// 1x6
			iinfo->set_param("slices/arrangement", arrangement);
		} else if (mode == TextureLayered::LAYERED_TYPE_CUBEMAP_ARRAY) {
			// 1x6
			iinfo->set_param("slices/arrangement", arrangement);
			iinfo->set_param("slices/layout", layout);
			iinfo->set_param("slices/amount", layer_count / 6);
		}
		iinfo->set_param("mipmaps/generate", had_mipmaps);
	}
	print_verbose("Converted " + p_path + " to " + dest_path);
	return OK;
}

Ref<ExportReport> TextureExporter::export_resource(const String &output_dir, Ref<ImportInfo> iinfo) {
	String path = iinfo->get_path();
	String source = iinfo->get_source_file();
	bool lossy = false;
	int ver_major = iinfo->get_ver_major();
	int ver_minor = iinfo->get_ver_minor();
	Ref<ExportReport> report = memnew(ExportReport(iinfo));

	// Sonic Colors Unlimited specific hack: We don't support atsc and nx-low formats, so we need to set the path to something else
	if (ver_major == 3 && ver_minor == 1) {
		String format_type = path.get_basename().get_extension();
		Vector<String> banned_types = { "atsc", "nx-low", "atsc-low" };
		if (banned_types.has(format_type)) {
			bool found = false;
			Vector<String> dest_files = iinfo->get_dest_files();
			if (dest_files.size() > 0) {
				String new_path = path.get_basename().get_basename() + ".s3tc" + path.get_extension();
				if (FileAccess::exists(new_path)) {
					path = new_path;
					found = true;
				}
				if (!found) {
					for (auto &dest : dest_files) {
						String fmt = dest.get_basename().get_extension();
						if (!banned_types.has(format_type) && FileAccess::exists(new_path)) {
							path = dest;
							found = true;
							break;
						}
					}
				}
			}
			if (!found) {
				report->set_error(ERR_UNAVAILABLE);
				report->set_message("Cannot convert custom SCU texture format");
				report->set_unsupported_format_type(format_type);
			}
		}
	}
	if (!FileAccess::exists(path)) {
		path = "";
		for (auto &dest : iinfo->get_dest_files()) {
			if (FileAccess::exists(dest)) {
				path = dest;
				break;
			}
		}
		if (path.is_empty()) {
			report->set_error(ERR_FILE_NOT_FOUND);
			report->set_message("No existing textures found for this import");
			report->append_message_detail({ "Possibles:" });
			report->append_message_detail(iinfo->get_dest_files());
			return report;
		}
	}
	// for Godot 2.x resources, we can easily rewrite the metadata to point to a renamed file with a different extension,
	// but this isn't the case for 3.x and greater, so we have to save in the original (lossy) format.
	String source_ext = source.get_extension().to_lower();
	if (source_ext != "png" || ver_major == 2) {
		if (ver_major > 2) {
			if ((source_ext == "jpg" || source_ext == "jpeg")) {
				lossy = true;
				report->set_loss_type(ImportInfo::STORED_LOSSY);
			} else if (source_ext == "webp") {
				// if the engine <3.4, it can't handle lossless encoded WEBPs
				if (ver_major < 4 && !(ver_major == 3 && ver_minor >= 4)) {
					lossy = true;
				}
				report->set_loss_type(ImportInfo::STORED_LOSSY);
			} else if (source_ext == "tga") {
				lossy = false;
			} else if (source_ext == "svg") {
				lossy = true;
				report->set_loss_type(ImportInfo::STORED_LOSSY);
			} else if (source_ext == "dds") {
				lossy = false;
			} else if (source_ext == "exr") {
				lossy = false;
			} else if (source_ext == "bmp") {
				lossy = false;
			} else {
				iinfo->set_export_dest(iinfo->get_export_dest().get_basename() + ".png");
				// If this is version 3-4, we need to rewrite the import metadata to point to the new resource name
				// save it under .assets, which won't be picked up for import by the godot editor
				if (false) {
					// disable this for now
					// iinfo->set_source_file(iinfo->get_export_dest());
				} else {
					if (!iinfo->get_export_dest().replace("res://", "").begins_with(".assets")) {
						String prefix = ".assets";
						if (iinfo->get_export_dest().begins_with("res://")) {
							prefix = "res://.assets";
						}
						iinfo->set_export_dest(prefix.path_join(iinfo->get_export_dest().replace("res://", "")));
					}
				}
			}
		} else { //version 2
			if (!iinfo->is_import()) {
				if (source_ext == "webp" || source_ext == "svg" || source_ext == "jpg" || source_ext == "jpeg") {
					lossy = true;
					report->set_loss_type(ImportInfo::STORED_LOSSY);
				}
			} else {
				iinfo->set_export_dest(iinfo->get_export_dest().get_basename() + ".png");
			}
		}
	}
	report->set_new_source_path(iinfo->get_export_dest());

	Error err = OK;
	String img_format = "bitmap";
	String importer = iinfo->get_importer();
	String dest_path = output_dir.path_join(iinfo->get_export_dest().replace("res://", ""));
	if (importer == "image") {
		ResourceFormatLoaderImage rli;
		Ref<Image> img = rli.load(path, "", &err, false, nullptr, ResourceFormatLoader::CACHE_MODE_IGNORE);
		if (!err && !img.is_null()) {
			img_format = Image::get_format_name(img->get_format());
			err = save_image(dest_path, img, lossy);
		}
	} else if (importer == "texture_atlas") {
		if (ver_major <= 2 && (iinfo->get_type() == "ImageTexture" || iinfo->get_additional_sources().size() > 0)) {
			// this is the sprite sheet for the texture atlas; we can't save it to the original sources, so we save it to another file
			auto new_dest = dest_path.get_base_dir().path_join(iinfo->get_path().get_file());
			new_dest = new_dest.get_basename() + ".ATLAS_SHEET." + dest_path.get_extension();
			iinfo->set_export_dest(new_dest);
			dest_path = new_dest;
			err = _convert_tex(path, dest_path, lossy, img_format, report);
			// Don't rewrite the metadata for this
			report->set_rewrote_metadata(ExportReport::NOT_IMPORTABLE);
		} else {
			err = _convert_atex(path, dest_path, lossy, img_format);
		}
	} else if (importer == "bitmap") {
		err = _convert_bitmap(path, dest_path, lossy);
	} else if (importer == "texture_large" || importer == "2d_array_texture" || importer == "cubemap_array_texture" || importer == "cubemap_texture" || importer == "texture_array") {
		err = _convert_layered_2d(path, dest_path, lossy, img_format, report);
	} else if (importer == "3d_texture" || importer == "texture_3d") {
		err = _convert_3d(path, dest_path, lossy, img_format, report);
	} else if (importer == "texture" || importer == "texture_2d") {
		err = _convert_tex(path, dest_path, lossy, img_format, report);
	} else {
		report->set_error(ERR_UNAVAILABLE);
		report->set_message("Unsupported texture importer: " + importer);
		return report;
	}
	report->set_error(err);
	if (err == ERR_UNAVAILABLE) {
		report->set_unsupported_format_type(img_format);
		report->set_message("Decompression not implemented yet for texture format " + img_format);
		// Already reported in export functions above
		return report;
	} else if (err) {
		return report;
	}
	report->set_saved_path(dest_path);
	// If lossy, also convert it as a png
	bool saving_lossless_copy = false; // TODO: add config option
	if (saving_lossless_copy && lossy && importer == "texture") {
		String dest = iinfo->get_export_dest().get_basename() + ".png";
		if (!dest.replace("res://", "").begins_with(".assets")) {
			String prefix = ".assets";
			if (dest.begins_with("res://")) {
				prefix = "res://.assets";
			}
			dest = prefix.path_join(dest.replace("res://", ""));
		}
		iinfo->set_export_lossless_copy(dest);
		dest_path = output_dir.path_join(dest.replace("res://", ""));
		err = _convert_tex(path, dest_path, false, img_format, nullptr);
		ERR_FAIL_COND_V(err != OK, report);
	}

	return report;
}

void TextureExporter::get_handled_types(List<String> *out) const {
	out->push_back("Texture");
	out->push_back("Texture2D");
	out->push_back("ImageTexture");
	out->push_back("StreamTexture");
	out->push_back("CompressedTexture2D");
	out->push_back("BitMap");
	out->push_back("LargeTexture");
	out->push_back("AtlasTexture");
	out->push_back("StreamTexture");
	out->push_back("StreamTexture3D");
	out->push_back("StreamTextureArray");
	out->push_back("CompressedTexture2D");
	out->push_back("CompressedTexture3D");
	out->push_back("CompressedTextureLayered");
	out->push_back("CompressedTexture2DArray");
	out->push_back("CompressedCubemap");
	out->push_back("CompressedCubemapArray");
	out->push_back("TextureArray");
}

void TextureExporter::get_handled_importers(List<String> *out) const {
	out->push_back("texture");
	out->push_back("texture_2d");
	out->push_back("bitmap");
	out->push_back("image");
	out->push_back("texture_atlas");
	out->push_back("texture_large");
	out->push_back("texture_array");
	out->push_back("cubemap_texture");
	out->push_back("2d_array_texture");
	out->push_back("cubemap_array_texture");
	out->push_back("texture_3d");
	out->push_back("3d_texture");
}

String TextureExporter::get_name() const {
	return "Texture";
}

String TextureExporter::get_default_export_extension(const String &res_path) const {
	return "png";
}
#include "oggstr_exporter.h"
#include "compat/resource_loader_compat.h"
#include "core/error/error_list.h"
#include "core/error/error_macros.h"
#include "core/io/file_access.h"
#include "core/variant/variant.h"
#include "modules/vorbis/audio_stream_ogg_vorbis.h"
#include "utility/common.h"

Error OggStrExporter::get_data_from_ogg_stream(const String &real_src, const Ref<AudioStreamOggVorbis> &sample, Vector<uint8_t> &r_data) {
	Ref<OggPacketSequence> packet_sequence = sample->get_packet_sequence();
	auto page_data = packet_sequence->get_packet_data();
	Vector<uint64_t> page_sizes;
	uint64_t total_estimated_size = 0;
	// page data is a vector of a vector of packedbytearrays
	// need to get all the sizes of the pages
	for (int i = 0; i < page_data.size(); i++) {
		uint64_t page_size = 0;
		// a page is an array of PackedByteArrays
		Array page = page_data[i];
		for (int j = 0; j < page.size(); j++) {
			int pkt_size = ((PackedByteArray)page[j]).size();
			page_size += pkt_size;
			// Header size is 26 + ceil(pkt_size / 255)
			total_estimated_size += pkt_size + (pkt_size / 255) + 100; //just fudge it
		}
		page_sizes.push_back(page_size);
	}
	uint64_t total_pagedata_body_size = [](const Vector<uint64_t> p_counts) {
		uint64_t total = 0;
		for (int i = 0; i < p_counts.size(); i++) {
			total += p_counts[i];
		}
		return total;
	}(page_sizes);
	int64_t total_actual_body_size = 0;
	int64_t total_acc_size = 0;
	auto playback = packet_sequence->instantiate_playback();
	ogg_packet *pkt;
	ogg_stream_state os_en;
	uint32_t id = real_src.hash();
	ogg_stream_init(&os_en, id);

	int64_t page_cursor = 0;
	bool reached_eos = false;
	bool warned = false;
	r_data.resize_initialized(total_estimated_size);
	while (playback->next_ogg_packet(&pkt) && !reached_eos) {
		page_cursor = playback->get_page_number();
		int64_t page_size = page_sizes[page_cursor];
		if (pkt->e_o_s) {
			reached_eos = true;
		}
		ogg_stream_packetin(&os_en, pkt);
		ERR_FAIL_COND_V_MSG(ogg_stream_check(&os_en), ERR_FILE_CORRUPT, "Ogg stream is corrupt.");
		if (os_en.body_fill >= page_size || reached_eos) {
			if (os_en.body_fill < page_size) {
				WARN_PRINT("Reached EOS: Recorded page size is " + itos(page_size) + " but body fill is " + itos(os_en.body_fill) + ".");
				warned = true;
			}
			ogg_page og;
			ERR_FAIL_COND_V_MSG(ogg_stream_flush_fill(&os_en, &og, page_size) == 0, ERR_FILE_CORRUPT, "Could not add page.");
			int64_t cur_pos = total_acc_size;
			total_acc_size += og.header_len + og.body_len;
			total_actual_body_size += og.body_len;
			if (og.body_len != os_en.body_fill) {
				print_verbose("Body fill is " + itos(os_en.body_fill) + " but body len is " + itos(og.body_len) + ".");
			}
			if (total_acc_size > r_data.size()) {
				WARN_PRINT("Resizing data to " + itos(total_acc_size));
				r_data.resize(total_acc_size);
			}
			memcpy(r_data.ptrw() + cur_pos, og.header, og.header_len);
			memcpy(r_data.ptrw() + cur_pos + og.header_len, og.body, og.body_len);
		}
	}
	page_cursor = playback->get_page_number();
	if (total_actual_body_size != total_pagedata_body_size) {
		WARN_PRINT("Actual body size" + itos(total_actual_body_size) + " does not equal the pagedata body size " + itos(total_pagedata_body_size) + ".");
	}
	// resize to the actual size
	r_data.resize(total_acc_size);

	ogg_stream_clear(&os_en);
	ERR_FAIL_COND_V_MSG(!reached_eos, ERR_FILE_CORRUPT, "All packets consumed before EOS.");
	ERR_FAIL_COND_V_MSG(page_cursor < page_sizes.size(), ERR_FILE_CORRUPT, "Did not write all pages before EOS.");
	ERR_FAIL_COND_V_MSG(r_data.size() < 4, ERR_FILE_CORRUPT, "Data is too small to be an Ogg stream.");
	ERR_FAIL_COND_V_MSG(r_data[0] != 'O' || r_data[1] != 'g' || r_data[2] != 'g' || r_data[3] != 'S', ERR_FILE_CORRUPT, "Header is missing in ogg data.");

	return warned ? ERR_PRINTER_ON_FIRE : OK;
}

Vector<uint8_t> OggStrExporter::get_ogg_stream_data(const String &real_src, const Ref<AudioStreamOggVorbis> &sample) {
	Error _err = OK;
	Vector<uint8_t> data;
	_err = get_data_from_ogg_stream(real_src, sample, data);
	if (_err == ERR_PRINTER_ON_FIRE) {
		WARN_PRINT("Ogg stream had warnings: " + sample->get_path());
		_err = OK;
	}
	ERR_FAIL_COND_V_MSG(_err != OK, Vector<uint8_t>(), "Cannot convert packet sequence to raw data.");
	return data;
}

Vector<uint8_t> OggStrExporter::load_ogg_stream_data(const String &real_src, const String &p_path, Ref<AudioStreamOggVorbis> &r_sample, int ver_major, Error *r_err) {
	Error _err = OK;
	if (!r_err) {
		r_err = &_err;
	}
	Vector<uint8_t> data;
	if (ver_major == 0) {
		ver_major = get_ver_major(p_path);
	}
	if (ver_major == 4) {
		r_sample = ResourceCompatLoader::non_global_load(p_path, "", r_err);
		ERR_FAIL_COND_V_MSG(*r_err != OK, Vector<uint8_t>(), "Cannot open resource '" + p_path + "'.");
		*r_err = get_data_from_ogg_stream(real_src, r_sample, data);
		if (*r_err == ERR_PRINTER_ON_FIRE) {
			WARN_PRINT("Ogg stream had warnings: " + p_path);
			*r_err = OK;
			ERR_FAIL_COND_V_MSG(_err != OK, Vector<uint8_t>(), "Cannot convert packet sequence to raw data.");
		}
		ERR_FAIL_COND_V_MSG(*r_err != OK, Vector<uint8_t>(), "Cannot convert packet sequence to raw data.");
	} else {
		auto res = ResourceCompatLoader::fake_load(p_path, "", r_err);
		ERR_FAIL_COND_V_MSG(*r_err, Vector<uint8_t>(), "Cannot load resource '" + p_path + "'.");
		data = res->get("data");
	}
	return data;
}

Error OggStrExporter::_export_file(const String real_src, const String &dst_path, const String &res_path, Ref<AudioStreamOggVorbis> &r_sample, int ver_major) {
	Error err = OK;
	if (ver_major == 0) {
		ver_major = get_ver_major(res_path);
	}
	Vector<uint8_t> data = load_ogg_stream_data(real_src, res_path, r_sample, ver_major, &err);
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to load Ogg stream data from " + res_path);
	err = gdre::ensure_dir(dst_path.get_base_dir());
	Ref<FileAccess> f = FileAccess::open(dst_path, FileAccess::WRITE);
	ERR_FAIL_COND_V_MSG(f.is_null(), ERR_FILE_CANT_WRITE, "Cannot open file '" + dst_path + "' for writing.");
	f->store_buffer(data.ptr(), data.size());
	return OK;
}

Error OggStrExporter::export_file(const String &dst_path, const String &res_path) {
	Ref<AudioStreamOggVorbis> sample;
	return _export_file(dst_path, dst_path, res_path, sample, get_ver_major(res_path));
}

Ref<ExportReport> OggStrExporter::export_resource(const String &output_dir, Ref<ImportInfo> import_info) {
	String src_path = import_info->get_path();
	String dst_path = output_dir.path_join(import_info->get_export_dest().replace("res://", ""));
	// Implement the logic to export the Ogg Vorbis stream to the specified path
	Ref<ExportReport> report = memnew(ExportReport(import_info));
	Ref<AudioStreamOggVorbis> sample;
	Error err = _export_file(import_info->get_source_file(), dst_path, src_path, sample, import_info->get_ver_major());
	if (err != OK) {
		report->set_error(err);
		if (err == ERR_FILE_CORRUPT) {
			report->set_message("The Ogg stream is corrupt: " + src_path);
		} else {
			report->set_message("Failed to export Ogg stream: " + src_path);
		}
		ERR_FAIL_V_MSG(report, report->get_message());
	} else {
		print_verbose("Converted " + src_path + " to " + dst_path);
		report->set_saved_path(dst_path);
		if (sample.is_valid()) { // sample only gets set if ver_major is 4
			import_info->set_param("loop", sample->has_loop());
			import_info->set_param("loop_offset", sample->get_loop_offset());
			import_info->set_param("bpm", sample->get_bpm());
			import_info->set_param("beat_count", sample->get_beat_count());
			import_info->set_param("bar_beats", sample->get_bar_beats());
		}
	}
	return report;
}

void OggStrExporter::get_handled_types(List<String> *out) const {
	out->push_back("AudioStreamOGGVorbis");
	out->push_back("AudioStreamOggVorbis");
}

void OggStrExporter::get_handled_importers(List<String> *out) const {
	out->push_back("ogg_vorbis");
	out->push_back("oggvorbisstr");
}

String OggStrExporter::get_name() const {
	return "OggVorbis";
}

String OggStrExporter::get_default_export_extension(const String &res_path) const {
	return "ogg";
}
#include "mp3str_exporter.h"
#include "compat/resource_loader_compat.h"
#include "exporters/export_report.h"
#include "modules/minimp3/audio_stream_mp3.h"
#include "utility/common.h"

Error Mp3StrExporter::export_file(const String &p_dest_path, const String &p_src_path) {
	Error err;

	Ref<AudioStreamMP3> sample = ResourceCompatLoader::non_global_load(p_src_path, "", &err);
	ERR_FAIL_COND_V_MSG(err != OK, err, "Could not load mp3str file " + p_src_path);

	err = _export_resource(sample, p_dest_path, p_src_path);
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to export mp3str file " + p_src_path);

	print_verbose("Converted " + p_src_path + " to " + p_dest_path);
	return OK;
}

Error Mp3StrExporter::_export_resource(Ref<AudioStreamMP3> sample, const String &p_dest_path, const String &p_src_path) {
	Error err = gdre::ensure_dir(p_dest_path.get_base_dir());
	ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to create dirs for " + p_dest_path);

	Ref<FileAccess> f = FileAccess::open(p_dest_path, FileAccess::WRITE);
	ERR_FAIL_COND_V_MSG(f.is_null(), ERR_CANT_CREATE, "Could not open " + p_dest_path + " for saving");

	PackedByteArray data = sample->get_data();
	f->store_buffer(data.ptr(), data.size());

	return OK;
}

Ref<ExportReport> Mp3StrExporter::export_resource(const String &output_dir, Ref<ImportInfo> import_infos) {
	// Check if the exporter can handle the given importer and resource type

	String src_path = import_infos->get_path();
	String dst_path = output_dir.path_join(import_infos->get_export_dest().replace("res://", ""));
	Ref<ExportReport> report = memnew(ExportReport(import_infos));
	Error err = OK;
	Ref<AudioStreamMP3> sample = ResourceCompatLoader::non_global_load(src_path, "", &err);
	ERR_FAIL_COND_V_MSG(err != OK, report, "Could not load mp3str file " + src_path);
	err = _export_resource(sample, dst_path, src_path);
	report->set_error(err);
	if (err == OK) {
		report->set_saved_path(dst_path);
		if (import_infos->get_ver_major() == 4) {
			import_infos->set_param("loop", sample->has_loop());
			import_infos->set_param("loop_offset", sample->get_loop_offset());
			import_infos->set_param("bpm", sample->get_bpm());
			import_infos->set_param("beat_count", sample->get_beat_count());
			import_infos->set_param("bar_beats", sample->get_bar_beats());
		}
	}

	return report;
}

void Mp3StrExporter::get_handled_types(List<String> *out) const {
	out->push_back("AudioStreamMP3");
}

void Mp3StrExporter::get_handled_importers(List<String> *out) const {
	out->push_back("mp3");
}

String Mp3StrExporter::get_name() const {
	return "MP3";
}

String Mp3StrExporter::get_default_export_extension(const String &res_path) const {
	return "mp3";
}