<commit_before>/*************************************************************************/
/*  project_manager.cpp                                                  */
/*************************************************************************/
/*                       This file is part of:                           */
/*                           GODOT ENGINE                                */
/*                      https://godotengine.org                          */
/*************************************************************************/
/* Copyright (c) 2007-2019 <NAME>, <NAME>.                 */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md)    */
/*                                                                       */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the       */
/* "Software"), to deal in the Software without restriction, including   */
/* without limitation the rights to use, copy, modify, merge, publish,   */
/* distribute, sublicense, and/or sell copies of the Software, and to    */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions:                                             */
/*                                                                       */
/* The above copyright notice and this permission notice shall be        */
/* included in all copies or substantial portions of the Software.       */
/*                                                                       */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
/*************************************************************************/

#include "project_manager.h"

#include "core/io/config_file.h"
#include "core/io/resource_saver.h"
#include "core/io/stream_peer_ssl.h"
#include "core/io/zip_io.h"
#include "core/os/dir_access.h"
#include "core/os/file_access.h"
#include "core/os/keyboard.h"
#include "core/os/os.h"
#include "core/translation.h"
#include "core/version.h"
#include "core/version_hash.gen.h"
#include "editor_scale.h"
#include "editor_settings.h"
#include "editor_themes.h"
#include "scene/gui/center_container.h"
#include "scene/gui/line_edit.h"
#include "scene/gui/margin_container.h"
#include "scene/gui/panel_container.h"
#include "scene/gui/separator.h"
#include "scene/gui/texture_rect.h"
#include "scene/gui/tool_button.h"

class ProjectDialog : public ConfirmationDialog {

	GDCLASS(ProjectDialog, ConfirmationDialog);

public:
	enum Mode {
		MODE_NEW,
		MODE_IMPORT,
		MODE_INSTALL,
		MODE_RENAME
	};

private:
	enum MessageType {
		MESSAGE_ERROR,
		MESSAGE_WARNING,
		MESSAGE_SUCCESS
	};

	enum InputType {
		PROJECT_PATH,
		INSTALL_PATH
	};

	Mode mode;
	Button *browse;
	Button *install_browse;
	Button *create_dir;
	Container *name_container;
	Container *path_container;
	Container *install_path_container;
	Container *rasterizer_container;
	Ref<ButtonGroup> rasterizer_button_group;
	Label *msg;
	LineEdit *project_path;
	LineEdit *project_name;
	LineEdit *install_path;
	TextureRect *status_rect;
	TextureRect *install_status_rect;
	FileDialog *fdialog;
	FileDialog *fdialog_install;
	String zip_path;
	String zip_title;
	AcceptDialog *dialog_error;
	String fav_dir;

	String created_folder_path;

	void set_message(const String &p_msg, MessageType p_type = MESSAGE_SUCCESS, InputType input_type = PROJECT_PATH) {

		msg->set_text(p_msg);
		Ref<Texture> current_path_icon = status_rect->get_texture();
		Ref<Texture> current_install_icon = install_status_rect->get_texture();
		Ref<Texture> new_icon;

		switch (p_type) {

			case MESSAGE_ERROR: {

				msg->add_color_override("font_color", get_color("error_color", "Editor"));
				msg->set_modulate(Color(1, 1, 1, 1));
				new_icon = get_icon("StatusError", "EditorIcons");

			} break;
			case MESSAGE_WARNING: {

				msg->add_color_override("font_color", get_color("warning_color", "Editor"));
				msg->set_modulate(Color(1, 1, 1, 1));
				new_icon = get_icon("StatusWarning", "EditorIcons");

			} break;
			case MESSAGE_SUCCESS: {

				msg->set_modulate(Color(1, 1, 1, 0));
				new_icon = get_icon("StatusSuccess", "EditorIcons");

			} break;
		}

		if (current_path_icon != new_icon && input_type == PROJECT_PATH) {
			status_rect->set_texture(new_icon);
		} else if (current_install_icon != new_icon && input_type == INSTALL_PATH) {
			install_status_rect->set_texture(new_icon);
		}

		set_size(Size2(500, 0) * EDSCALE);
	}

	String _test_path() {

		DirAccess *d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
		String valid_path, valid_install_path;
		if (d->change_dir(project_path->get_text()) == OK) {
			valid_path = project_path->get_text();
		} else if (d->change_dir(project_path->get_text().strip_edges()) == OK) {
			valid_path = project_path->get_text().strip_edges();
		} else if (project_path->get_text().ends_with(".zip")) {
			if (d->file_exists(project_path->get_text())) {
				valid_path = project_path->get_text();
			}
		} else if (project_path->get_text().strip_edges().ends_with(".zip")) {
			if (d->file_exists(project_path->get_text().strip_edges())) {
				valid_path = project_path->get_text().strip_edges();
			}
		}

		if (valid_path == "") {
			set_message(TTR("The path does not exist."), MESSAGE_ERROR);
			memdelete(d);
			get_ok()->set_disabled(true);
			return "";
		}

		if (mode == MODE_IMPORT && valid_path.ends_with(".zip")) {
			if (d->change_dir(install_path->get_text()) == OK) {
				valid_install_path = install_path->get_text();
			} else if (d->change_dir(install_path->get_text().strip_edges()) == OK) {
				valid_install_path = install_path->get_text().strip_edges();
			}

			if (valid_install_path == "") {
				set_message(TTR("The path does not exist."), MESSAGE_ERROR, INSTALL_PATH);
				memdelete(d);
				get_ok()->set_disabled(true);
				return "";
			}
		}

		if (mode == MODE_IMPORT || mode == MODE_RENAME) {

			if (valid_path != "" && !d->file_exists("project.godot")) {

				if (valid_path.ends_with(".zip")) {
					FileAccess *src_f = NULL;
					zlib_filefunc_def io = zipio_create_io_from_file(&src_f);

					unzFile pkg = unzOpen2(valid_path.utf8().get_data(), &io);
					if (!pkg) {

						set_message(TTR("Error opening package file, not in zip format."), MESSAGE_ERROR);
						memdelete(d);
						get_ok()->set_disabled(true);
						unzClose(pkg);
						return "";
					}

					int ret = unzGoToFirstFile(pkg);
					while (ret == UNZ_OK) {
						unz_file_info info;
						char fname[16384];
						ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, NULL, 0, NULL, 0);

						if (String(fname).ends_with("project.godot")) {
							break;
						}

						ret = unzGoToNextFile(pkg);
					}

					if (ret == UNZ_END_OF_LIST_OF_FILE) {
						set_message(TTR("Invalid '.zip' project file, does not contain a 'project.godot' file."), MESSAGE_ERROR);
						memdelete(d);
						get_ok()->set_disabled(true);
						unzClose(pkg);
						return "";
					}

					unzClose(pkg);

					// check if the specified install folder is empty, even though this is not an error, it is good to check here
					d->list_dir_begin();
					bool is_empty = true;
					String n = d->get_next();
					while (n != String()) {
						if (n != "." && n != "..") {
							is_empty = false;
							break;
						}
						n = d->get_next();
					}
					d->list_dir_end();

					if (!is_empty) {

						set_message(TTR("Please choose an empty folder."), MESSAGE_WARNING, INSTALL_PATH);
						memdelete(d);
						get_ok()->set_disabled(true);
						return "";
					}

				} else {
					set_message(TTR("Please choose a 'project.godot' or '.zip' file."), MESSAGE_ERROR);
					memdelete(d);
					install_path_container->hide();
					get_ok()->set_disabled(true);
					return "";
				}

			} else if (valid_path.ends_with("zip")) {

				set_message(TTR("Directory already contains a Godot project."), MESSAGE_ERROR, INSTALL_PATH);
				memdelete(d);
				get_ok()->set_disabled(true);
				return "";
			}

		} else {

			// check if the specified folder is empty, even though this is not an error, it is good to check here
			d->list_dir_begin();
			bool is_empty = true;
			String n = d->get_next();
			while (n != String()) {
				if (n != "." && n != "..") { // i don't know if this is enough to guarantee an empty dir
					is_empty = false;
					break;
				}
				n = d->get_next();
			}
			d->list_dir_end();

			if (!is_empty) {

				set_message(TTR("Please choose an empty folder."), MESSAGE_ERROR);
				memdelete(d);
				get_ok()->set_disabled(true);
				return "";
			}
		}

		set_message("");
		set_message("", MESSAGE_SUCCESS, INSTALL_PATH);
		memdelete(d);
		get_ok()->set_disabled(false);
		return valid_path;
	}

	void _path_text_changed(const String &p_path) {

		String sp = _test_path();
		if (sp != "") {

			// If the project name is empty or default, infer the project name from the selected folder name
			if (project_name->get_text() == "" || project_name->get_text() == TTR("New Game Project")) {
				sp = sp.replace("\\", "/");
				int lidx = sp.find_last("/");

				if (lidx != -1) {
					sp = sp.substr(lidx + 1, sp.length()).capitalize();
				}
				if (sp == "" && mode == MODE_IMPORT)
					sp = TTR("Imported Project");

				project_name->set_text(sp);
				_text_changed(sp);
			}
		}

		if (created_folder_path != "" && created_folder_path != p_path) {
			_remove_created_folder();
		}
	}

	void _file_selected(const String &p_path) {

		String p = p_path;
		if (mode == MODE_IMPORT) {
			if (p.ends_with("project.godot")) {
				p = p.get_base_dir();
				install_path_container->hide();
				get_ok()->set_disabled(false);
			} else if (p.ends_with(".zip")) {
				install_path->set_text(p.get_base_dir());
				install_path_container->show();
				get_ok()->set_disabled(false);
			} else {
				set_message(TTR("Please choose a 'project.godot' or '.zip' file."), MESSAGE_ERROR);
				get_ok()->set_disabled(true);
				return;
			}
		}
		String sp = p.simplify_path();
		project_path->set_text(sp);
		_path_text_changed(sp);
		if (p.ends_with(".zip")) {
			install_path->call_deferred("grab_focus");
		} else {
			get_ok()->call_deferred("grab_focus");
		}
	}

	void _path_selected(const String &p_path) {

		String p = p_path;
		String sp = p.simplify_path();
		project_path->set_text(sp);
		_path_text_changed(sp);
		get_ok()->call_deferred("grab_focus");
	}

	void _install_path_selected(const String &p_path) {
		String p = p_path;
		String sp = p.simplify_path();
		install_path->set_text(sp);
		_path_text_changed(sp);
		get_ok()->call_deferred("grab_focus");
	}

	void _browse_path() {

		fdialog->set_current_dir(project_path->get_text());

		if (mode == MODE_IMPORT) {

			fdialog->set_mode(FileDialog::MODE_OPEN_FILE);
			fdialog->clear_filters();
			fdialog->add_filter("project.godot ; " VERSION_NAME " Project");
			fdialog->add_filter("*.zip ; Zip File");
		} else {
			fdialog->set_mode(FileDialog::MODE_OPEN_DIR);
		}
		fdialog->popup_centered_ratio();
	}

	void _browse_install_path() {
		fdialog_install->set_current_dir(install_path->get_text());
		fdialog_install->set_mode(FileDialog::MODE_OPEN_DIR);
		fdialog_install->popup_centered_ratio();
	}

	void _create_folder() {

		if (project_name->get_text() == "" || created_folder_path != "" || project_name->get_text().ends_with(".") || project_name->get_text().ends_with(" ")) {
			set_message(TTR("Invalid Project Name."), MESSAGE_WARNING);
			return;
		}

		DirAccess *d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
		if (d->change_dir(project_path->get_text()) == OK) {

			if (!d->dir_exists(project_name->get_text())) {

				if (d->make_dir(project_name->get_text()) == OK) {

					d->change_dir(project_name->get_text());
					String dir_str = d->get_current_dir();
					project_path->set_text(dir_str);
					_path_text_changed(dir_str);
					created_folder_path = d->get_current_dir();
					create_dir->set_disabled(true);
				} else {

					dialog_error->set_text(TTR("Couldn't create folder."));
					dialog_error->popup_centered_minsize();
				}
			} else {

				dialog_error->set_text(TTR("There is already a folder in this path with the specified name."));
				dialog_error->popup_centered_minsize();
			}
		}

		memdelete(d);
	}

	void _text_changed(const String &p_text) {

		if (mode != MODE_NEW)
			return;

		_test_path();

		if (p_text == "")
			set_message(TTR("It would be a good idea to name your project."), MESSAGE_WARNING);
	}

	void ok_pressed() {

		String dir = project_path->get_text();

		if (mode == MODE_RENAME) {

			String dir2 = _test_path();
			if (dir2 == "") {
				set_message(TTR("Invalid project path (changed anything?)."), MESSAGE_ERROR);
				return;
			}

			ProjectSettings *current = memnew(ProjectSettings);

			int err = current->setup(dir2, "");
			if (err != OK) {
				set_message(vformat(TTR("Couldn't load project.godot in project path (error %d). It may be missing or corrupted."), err), MESSAGE_ERROR);
			} else {
				ProjectSettings::CustomMap edited_settings;
				edited_settings["application/config/name"] = project_name->get_text();

				if (current->save_custom(dir2.plus_file("project.godot"), edited_settings, Vector<String>(), true) != OK) {
					set_message(TTR("Couldn't edit project.godot in project path."), MESSAGE_ERROR);
				}
			}

			hide();
			emit_signal("projects_updated");

		} else {

			if (mode == MODE_IMPORT) {

				if (project_path->get_text().ends_with(".zip")) {

					mode = MODE_INSTALL;
					ok_pressed();

					return;
				}

			} else {
				if (mode == MODE_NEW) {

					ProjectSettings::CustomMap initial_settings;
					if (rasterizer_button_group->get_pressed_button()->get_meta("driver_name") == "GLES3") {
						initial_settings["rendering/quality/driver/driver_name"] = "GLES3";
					} else {
						initial_settings["rendering/quality/driver/driver_name"] = "GLES2";
						initial_settings["rendering/vram_compression/import_etc2"] = false;
						initial_settings["rendering/vram_compression/import_etc"] = true;
					}
					initial_settings["application/config/name"] = project_name->get_text();
					initial_settings["application/config/icon"] = "res://icon.png";
					initial_settings["rendering/environment/default_environment"] = "res://default_env.tres";

					if (ProjectSettings::get_singleton()->save_custom(dir.plus_file("project.godot"), initial_settings, Vector<String>(), false) != OK) {
						set_message(TTR("Couldn't create project.godot in project path."), MESSAGE_ERROR);
					} else {
						ResourceSaver::save(dir.plus_file("icon.png"), get_icon("DefaultProjectIcon", "EditorIcons"));

						FileAccess *f = FileAccess::open(dir.plus_file("default_env.tres"), FileAccess::WRITE);
						if (!f) {
							set_message(TTR("Couldn't create project.godot in project path."), MESSAGE_ERROR);
						} else {
							f->store_line("[gd_resource type=\"Environment\" load_steps=2 format=2]");
							f->store_line("[sub_resource type=\"ProceduralSky\" id=1]");
							f->store_line("[resource]");
							f->store_line("background_mode = 2");
							f->store_line("background_sky = SubResource( 1 )");
							memdelete(f);
						}
					}

				} else if (mode == MODE_INSTALL) {

					if (project_path->get_text().ends_with(".zip")) {
						dir = install_path->get_text();
						zip_path = project_path->get_text();
					}

					FileAccess *src_f = NULL;
					zlib_filefunc_def io = zipio_create_io_from_file(&src_f);

					unzFile pkg = unzOpen2(zip_path.utf8().get_data(), &io);
					if (!pkg) {

						dialog_error->set_text(TTR("Error opening package file, not in zip format."));
						dialog_error->popup_centered_minsize();
						return;
					}

					int ret = unzGoToFirstFile(pkg);

					Vector<String> failed_files;

					int idx = 0;
					while (ret == UNZ_OK) {

						//get filename
						unz_file_info info;
						char fname[16384];
						ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, NULL, 0, NULL, 0);

						String path = fname;

						int depth = 1; //stuff from github comes with tag
						bool skip = false;
						while (depth > 0) {
							int pp = path.find("/");
							if (pp == -1) {
								skip = true;
								break;
							}
							path = path.substr(pp + 1, path.length());
							depth--;
						}

						if (skip || path == String()) {
							//
						} else if (path.ends_with("/")) { // a dir

							path = path.substr(0, path.length() - 1);

							DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
							da->make_dir(dir.plus_file(path));
							memdelete(da);

						} else {

							Vector<uint8_t> data;
							data.resize(info.uncompressed_size);

							//read
							unzOpenCurrentFile(pkg);
							unzReadCurrentFile(pkg, data.ptrw(), data.size());
							unzCloseCurrentFile(pkg);

							FileAccess *f = FileAccess::open(dir.plus_file(path), FileAccess::WRITE);

							if (f) {
								f->store_buffer(data.ptr(), data.size());
								memdelete(f);
							} else {
								failed_files.push_back(path);
							}
						}

						idx++;
						ret = unzGoToNextFile(pkg);
					}

					unzClose(pkg);

					if (failed_files.size()) {
						String msg = TTR("The following files failed extraction from package:") + "\n\n";
						for (int i = 0; i < failed_files.size(); i++) {

							if (i > 15) {
								msg += "\nAnd " + itos(failed_files.size() - i) + " more files.";
								break;
							}
							msg += failed_files[i] + "\n";
						}

						dialog_error->set_text(msg);
						dialog_error->popup_centered_minsize();

					} else if (!project_path->get_text().ends_with(".zip")) {
						dialog_error->set_text(TTR("Package installed successfully!"));
						dialog_error->popup_centered_minsize();
					}
				}
			}

			dir = dir.replace("\\", "/");
			if (dir.ends_with("/"))
				dir = dir.substr(0, dir.length() - 1);
			String proj = dir.replace("/", "::");
			EditorSettings::get_singleton()->set("projects/" + proj, dir);
			EditorSettings::get_singleton()->save();

			hide();
			emit_signal("project_created", dir);
		}
	}

	void _remove_created_folder() {

		if (created_folder_path != "") {
			DirAccess *d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
			d->remove(created_folder_path);
			memdelete(d);

			create_dir->set_disabled(false);
			created_folder_path = "";
		}
	}

	void cancel_pressed() {

		_remove_created_folder();

		project_path->clear();
		_path_text_changed("");
		project_name->clear();
		_text_changed("");

		if (status_rect->get_texture() == get_icon("StatusError", "EditorIcons"))
			msg->show();

		if (install_status_rect->get_texture() == get_icon("StatusError", "EditorIcons"))
			msg->show();
	}

	void _notification(int p_what) {

		if (p_what == MainLoop::NOTIFICATION_WM_QUIT_REQUEST)
			_remove_created_folder();
	}

protected:
	static void _bind_methods() {

		ClassDB::bind_method("_browse_path", &ProjectDialog::_browse_path);
		ClassDB::bind_method("_create_folder", &ProjectDialog::_create_folder);
		ClassDB::bind_method("_text_changed", &ProjectDialog::_text_changed);
		ClassDB::bind_method("_path_text_changed", &ProjectDialog::_path_text_changed);
		ClassDB::bind_method("_path_selected", &ProjectDialog::_path_selected);
		ClassDB::bind_method("_file_selected", &ProjectDialog::_file_selected);
		ClassDB::bind_method("_install_path_selected", &ProjectDialog::_install_path_selected);
		ClassDB::bind_method("_browse_install_path", &ProjectDialog::_browse_install_path);
		ADD_SIGNAL(MethodInfo("project_created"));
		ADD_SIGNAL(MethodInfo("projects_updated"));
	}

public:
	void set_zip_path(const String &p_path) {
		zip_path = p_path;
	}
	void set_zip_title(const String &p_title) {
		zip_title = p_title;
	}

	void set_mode(Mode p_mode) {

		mode = p_mode;
	}

	void set_project_path(const String &p_path) {
		project_path->set_text(p_path);
	}

	void show_dialog() {

		if (mode == MODE_RENAME) {

			project_path->set_editable(false);
			browse->hide();
			install_browse->hide();

			set_title(TTR("Rename Project"));
			get_ok()->set_text(TTR("Rename"));
			name_container->show();
			status_rect->hide();
			msg->hide();
			install_path_container->hide();
			install_status_rect->hide();
			rasterizer_container->hide();
			get_ok()->set_disabled(false);

			ProjectSettings *current = memnew(ProjectSettings);

			int err = current->setup(project_path->get_text(), "");
			if (err != OK) {
				set_message(vformat(TTR("Couldn't load project.godot in project path (error %d). It may be missing or corrupted."), err), MESSAGE_ERROR);
				status_rect->show();
				msg->show();
				get_ok()->set_disabled(true);
			} else if (current->has_setting("application/config/name")) {
				String proj = current->get("application/config/name");
				project_name->set_text(proj);
				_text_changed(proj);
			}

			project_name->call_deferred("grab_focus");

			create_dir->hide();

		} else {

			fav_dir = EditorSettings::get_singleton()->get("filesystem/directories/default_project_path");
			if (fav_dir != "") {
				project_path->set_text(fav_dir);
				fdialog->set_current_dir(fav_dir);
			} else {
				DirAccess *d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
				project_path->set_text(d->get_current_dir());
				fdialog->set_current_dir(d->get_current_dir());
				memdelete(d);
			}
			String proj = TTR("New Game Project");
			project_name->set_text(proj);
			_text_changed(proj);

			project_path->set_editable(true);
			browse->set_disabled(false);
			browse->show();
			install_browse->set_disabled(false);
			install_browse->show();
			create_dir->show();
			status_rect->show();
			install_status_rect->show();
			msg->show();

			if (mode == MODE_IMPORT) {
				set_title(TTR("Import Existing Project"));
				get_ok()->set_text(TTR("Import & Edit"));
				name_container->hide();
				install_path_container->hide();
				rasterizer_container->hide();
				project_path->grab_focus();

			} else if (mode == MODE_NEW) {

				set_title(TTR("Create New Project"));
				get_ok()->set_text(TTR("Create & Edit"));
				name_container->show();
				install_path_container->hide();
				rasterizer_container->show();
				project_name->call_deferred("grab_focus");
				project_name->call_deferred("select_all");

			} else if (mode == MODE_INSTALL) {

				set_title(TTR("Install Project:") + " " + zip_title);
				get_ok()->set_text(TTR("Install & Edit"));
				name_container->show();
				install_path_container->hide();
				rasterizer_container->hide();
				project_path->grab_focus();<commit_msg>Set a default value for project name when installing a project.<commit_after>/*************************************************************************/
/*  project_manager.cpp                                                  */
/*************************************************************************/
/*                       This file is part of:                           */
/*                           GODOT ENGINE                                */
/*                      https://godotengine.org                          */
/*************************************************************************/
/* Copyright (c) 2007-2019 <NAME>, <NAME>.                 */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md)    */
/*                                                                       */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the       */
/* "Software"), to deal in the Software without restriction, including   */
/* without limitation the rights to use, copy, modify, merge, publish,   */
/* distribute, sublicense, and/or sell copies of the Software, and to    */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions:                                             */
/*                                                                       */
/* The above copyright notice and this permission notice shall be        */
/* included in all copies or substantial portions of the Software.       */
/*                                                                       */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
/*************************************************************************/

#include "project_manager.h"

#include "core/io/config_file.h"
#include "core/io/resource_saver.h"
#include "core/io/stream_peer_ssl.h"
#include "core/io/zip_io.h"
#include "core/os/dir_access.h"
#include "core/os/file_access.h"
#include "core/os/keyboard.h"
#include "core/os/os.h"
#include "core/translation.h"
#include "core/version.h"
#include "core/version_hash.gen.h"
#include "editor_scale.h"
#include "editor_settings.h"
#include "editor_themes.h"
#include "scene/gui/center_container.h"
#include "scene/gui/line_edit.h"
#include "scene/gui/margin_container.h"
#include "scene/gui/panel_container.h"
#include "scene/gui/separator.h"
#include "scene/gui/texture_rect.h"
#include "scene/gui/tool_button.h"

class ProjectDialog : public ConfirmationDialog {

	GDCLASS(ProjectDialog, ConfirmationDialog);

public:
	enum Mode {
		MODE_NEW,
		MODE_IMPORT,
		MODE_INSTALL,
		MODE_RENAME
	};

private:
	enum MessageType {
		MESSAGE_ERROR,
		MESSAGE_WARNING,
		MESSAGE_SUCCESS
	};

	enum InputType {
		PROJECT_PATH,
		INSTALL_PATH
	};

	Mode mode;
	Button *browse;
	Button *install_browse;
	Button *create_dir;
	Container *name_container;
	Container *path_container;
	Container *install_path_container;
	Container *rasterizer_container;
	Ref<ButtonGroup> rasterizer_button_group;
	Label *msg;
	LineEdit *project_path;
	LineEdit *project_name;
	LineEdit *install_path;
	TextureRect *status_rect;
	TextureRect *install_status_rect;
	FileDialog *fdialog;
	FileDialog *fdialog_install;
	String zip_path;
	String zip_title;
	AcceptDialog *dialog_error;
	String fav_dir;

	String created_folder_path;

	void set_message(const String &p_msg, MessageType p_type = MESSAGE_SUCCESS, InputType input_type = PROJECT_PATH) {

		msg->set_text(p_msg);
		Ref<Texture> current_path_icon = status_rect->get_texture();
		Ref<Texture> current_install_icon = install_status_rect->get_texture();
		Ref<Texture> new_icon;

		switch (p_type) {

			case MESSAGE_ERROR: {

				msg->add_color_override("font_color", get_color("error_color", "Editor"));
				msg->set_modulate(Color(1, 1, 1, 1));
				new_icon = get_icon("StatusError", "EditorIcons");

			} break;
			case MESSAGE_WARNING: {

				msg->add_color_override("font_color", get_color("warning_color", "Editor"));
				msg->set_modulate(Color(1, 1, 1, 1));
				new_icon = get_icon("StatusWarning", "EditorIcons");

			} break;
			case MESSAGE_SUCCESS: {

				msg->set_modulate(Color(1, 1, 1, 0));
				new_icon = get_icon("StatusSuccess", "EditorIcons");

			} break;
		}

		if (current_path_icon != new_icon && input_type == PROJECT_PATH) {
			status_rect->set_texture(new_icon);
		} else if (current_install_icon != new_icon && input_type == INSTALL_PATH) {
			install_status_rect->set_texture(new_icon);
		}

		set_size(Size2(500, 0) * EDSCALE);
	}

	String _test_path() {

		DirAccess *d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
		String valid_path, valid_install_path;
		if (d->change_dir(project_path->get_text()) == OK) {
			valid_path = project_path->get_text();
		} else if (d->change_dir(project_path->get_text().strip_edges()) == OK) {
			valid_path = project_path->get_text().strip_edges();
		} else if (project_path->get_text().ends_with(".zip")) {
			if (d->file_exists(project_path->get_text())) {
				valid_path = project_path->get_text();
			}
		} else if (project_path->get_text().strip_edges().ends_with(".zip")) {
			if (d->file_exists(project_path->get_text().strip_edges())) {
				valid_path = project_path->get_text().strip_edges();
			}
		}

		if (valid_path == "") {
			set_message(TTR("The path does not exist."), MESSAGE_ERROR);
			memdelete(d);
			get_ok()->set_disabled(true);
			return "";
		}

		if (mode == MODE_IMPORT && valid_path.ends_with(".zip")) {
			if (d->change_dir(install_path->get_text()) == OK) {
				valid_install_path = install_path->get_text();
			} else if (d->change_dir(install_path->get_text().strip_edges()) == OK) {
				valid_install_path = install_path->get_text().strip_edges();
			}

			if (valid_install_path == "") {
				set_message(TTR("The path does not exist."), MESSAGE_ERROR, INSTALL_PATH);
				memdelete(d);
				get_ok()->set_disabled(true);
				return "";
			}
		}

		if (mode == MODE_IMPORT || mode == MODE_RENAME) {

			if (valid_path != "" && !d->file_exists("project.godot")) {

				if (valid_path.ends_with(".zip")) {
					FileAccess *src_f = NULL;
					zlib_filefunc_def io = zipio_create_io_from_file(&src_f);

					unzFile pkg = unzOpen2(valid_path.utf8().get_data(), &io);
					if (!pkg) {

						set_message(TTR("Error opening package file, not in zip format."), MESSAGE_ERROR);
						memdelete(d);
						get_ok()->set_disabled(true);
						unzClose(pkg);
						return "";
					}

					int ret = unzGoToFirstFile(pkg);
					while (ret == UNZ_OK) {
						unz_file_info info;
						char fname[16384];
						ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, NULL, 0, NULL, 0);

						if (String(fname).ends_with("project.godot")) {
							break;
						}

						ret = unzGoToNextFile(pkg);
					}

					if (ret == UNZ_END_OF_LIST_OF_FILE) {
						set_message(TTR("Invalid '.zip' project file, does not contain a 'project.godot' file."), MESSAGE_ERROR);
						memdelete(d);
						get_ok()->set_disabled(true);
						unzClose(pkg);
						return "";
					}

					unzClose(pkg);

					// check if the specified install folder is empty, even though this is not an error, it is good to check here
					d->list_dir_begin();
					bool is_empty = true;
					String n = d->get_next();
					while (n != String()) {
						if (n != "." && n != "..") {
							is_empty = false;
							break;
						}
						n = d->get_next();
					}
					d->list_dir_end();

					if (!is_empty) {

						set_message(TTR("Please choose an empty folder."), MESSAGE_WARNING, INSTALL_PATH);
						memdelete(d);
						get_ok()->set_disabled(true);
						return "";
					}

				} else {
					set_message(TTR("Please choose a 'project.godot' or '.zip' file."), MESSAGE_ERROR);
					memdelete(d);
					install_path_container->hide();
					get_ok()->set_disabled(true);
					return "";
				}

			} else if (valid_path.ends_with("zip")) {

				set_message(TTR("Directory already contains a Godot project."), MESSAGE_ERROR, INSTALL_PATH);
				memdelete(d);
				get_ok()->set_disabled(true);
				return "";
			}

		} else {

			// check if the specified folder is empty, even though this is not an error, it is good to check here
			d->list_dir_begin();
			bool is_empty = true;
			String n = d->get_next();
			while (n != String()) {
				if (n != "." && n != "..") { // i don't know if this is enough to guarantee an empty dir
					is_empty = false;
					break;
				}
				n = d->get_next();
			}
			d->list_dir_end();

			if (!is_empty) {

				set_message(TTR("Please choose an empty folder."), MESSAGE_ERROR);
				memdelete(d);
				get_ok()->set_disabled(true);
				return "";
			}
		}

		set_message("");
		set_message("", MESSAGE_SUCCESS, INSTALL_PATH);
		memdelete(d);
		get_ok()->set_disabled(false);
		return valid_path;
	}

	void _path_text_changed(const String &p_path) {

		String sp = _test_path();
		if (sp != "") {

			// If the project name is empty or default, infer the project name from the selected folder name
			if (project_name->get_text() == "" || project_name->get_text() == TTR("New Game Project")) {
				sp = sp.replace("\\", "/");
				int lidx = sp.find_last("/");

				if (lidx != -1) {
					sp = sp.substr(lidx + 1, sp.length()).capitalize();
				}
				if (sp == "" && mode == MODE_IMPORT)
					sp = TTR("Imported Project");

				project_name->set_text(sp);
				_text_changed(sp);
			}
		}

		if (created_folder_path != "" && created_folder_path != p_path) {
			_remove_created_folder();
		}
	}

	void _file_selected(const String &p_path) {

		String p = p_path;
		if (mode == MODE_IMPORT) {
			if (p.ends_with("project.godot")) {
				p = p.get_base_dir();
				install_path_container->hide();
				get_ok()->set_disabled(false);
			} else if (p.ends_with(".zip")) {
				install_path->set_text(p.get_base_dir());
				install_path_container->show();
				get_ok()->set_disabled(false);
			} else {
				set_message(TTR("Please choose a 'project.godot' or '.zip' file."), MESSAGE_ERROR);
				get_ok()->set_disabled(true);
				return;
			}
		}
		String sp = p.simplify_path();
		project_path->set_text(sp);
		_path_text_changed(sp);
		if (p.ends_with(".zip")) {
			install_path->call_deferred("grab_focus");
		} else {
			get_ok()->call_deferred("grab_focus");
		}
	}

	void _path_selected(const String &p_path) {

		String p = p_path;
		String sp = p.simplify_path();
		project_path->set_text(sp);
		_path_text_changed(sp);
		get_ok()->call_deferred("grab_focus");
	}

	void _install_path_selected(const String &p_path) {
		String p = p_path;
		String sp = p.simplify_path();
		install_path->set_text(sp);
		_path_text_changed(sp);
		get_ok()->call_deferred("grab_focus");
	}

	void _browse_path() {

		fdialog->set_current_dir(project_path->get_text());

		if (mode == MODE_IMPORT) {

			fdialog->set_mode(FileDialog::MODE_OPEN_FILE);
			fdialog->clear_filters();
			fdialog->add_filter("project.godot ; " VERSION_NAME " Project");
			fdialog->add_filter("*.zip ; Zip File");
		} else {
			fdialog->set_mode(FileDialog::MODE_OPEN_DIR);
		}
		fdialog->popup_centered_ratio();
	}

	void _browse_install_path() {
		fdialog_install->set_current_dir(install_path->get_text());
		fdialog_install->set_mode(FileDialog::MODE_OPEN_DIR);
		fdialog_install->popup_centered_ratio();
	}

	void _create_folder() {

		if (project_name->get_text() == "" || created_folder_path != "" || project_name->get_text().ends_with(".") || project_name->get_text().ends_with(" ")) {
			set_message(TTR("Invalid Project Name."), MESSAGE_WARNING);
			return;
		}

		DirAccess *d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
		if (d->change_dir(project_path->get_text()) == OK) {

			if (!d->dir_exists(project_name->get_text())) {

				if (d->make_dir(project_name->get_text()) == OK) {

					d->change_dir(project_name->get_text());
					String dir_str = d->get_current_dir();
					project_path->set_text(dir_str);
					_path_text_changed(dir_str);
					created_folder_path = d->get_current_dir();
					create_dir->set_disabled(true);
				} else {

					dialog_error->set_text(TTR("Couldn't create folder."));
					dialog_error->popup_centered_minsize();
				}
			} else {

				dialog_error->set_text(TTR("There is already a folder in this path with the specified name."));
				dialog_error->popup_centered_minsize();
			}
		}

		memdelete(d);
	}

	void _text_changed(const String &p_text) {

		if (mode != MODE_NEW)
			return;

		_test_path();

		if (p_text == "")
			set_message(TTR("It would be a good idea to name your project."), MESSAGE_WARNING);
	}

	void ok_pressed() {

		String dir = project_path->get_text();

		if (mode == MODE_RENAME) {

			String dir2 = _test_path();
			if (dir2 == "") {
				set_message(TTR("Invalid project path (changed anything?)."), MESSAGE_ERROR);
				return;
			}

			ProjectSettings *current = memnew(ProjectSettings);

			int err = current->setup(dir2, "");
			if (err != OK) {
				set_message(vformat(TTR("Couldn't load project.godot in project path (error %d). It may be missing or corrupted."), err), MESSAGE_ERROR);
			} else {
				ProjectSettings::CustomMap edited_settings;
				edited_settings["application/config/name"] = project_name->get_text();

				if (current->save_custom(dir2.plus_file("project.godot"), edited_settings, Vector<String>(), true) != OK) {
					set_message(TTR("Couldn't edit project.godot in project path."), MESSAGE_ERROR);
				}
			}

			hide();
			emit_signal("projects_updated");

		} else {

			if (mode == MODE_IMPORT) {

				if (project_path->get_text().ends_with(".zip")) {

					mode = MODE_INSTALL;
					ok_pressed();

					return;
				}

			} else {
				if (mode == MODE_NEW) {

					ProjectSettings::CustomMap initial_settings;
					if (rasterizer_button_group->get_pressed_button()->get_meta("driver_name") == "GLES3") {
						initial_settings["rendering/quality/driver/driver_name"] = "GLES3";
					} else {
						initial_settings["rendering/quality/driver/driver_name"] = "GLES2";
						initial_settings["rendering/vram_compression/import_etc2"] = false;
						initial_settings["rendering/vram_compression/import_etc"] = true;
					}
					initial_settings["application/config/name"] = project_name->get_text();
					initial_settings["application/config/icon"] = "res://icon.png";
					initial_settings["rendering/environment/default_environment"] = "res://default_env.tres";

					if (ProjectSettings::get_singleton()->save_custom(dir.plus_file("project.godot"), initial_settings, Vector<String>(), false) != OK) {
						set_message(TTR("Couldn't create project.godot in project path."), MESSAGE_ERROR);
					} else {
						ResourceSaver::save(dir.plus_file("icon.png"), get_icon("DefaultProjectIcon", "EditorIcons"));

						FileAccess *f = FileAccess::open(dir.plus_file("default_env.tres"), FileAccess::WRITE);
						if (!f) {
							set_message(TTR("Couldn't create project.godot in project path."), MESSAGE_ERROR);
						} else {
							f->store_line("[gd_resource type=\"Environment\" load_steps=2 format=2]");
							f->store_line("[sub_resource type=\"ProceduralSky\" id=1]");
							f->store_line("[resource]");
							f->store_line("background_mode = 2");
							f->store_line("background_sky = SubResource( 1 )");
							memdelete(f);
						}
					}

				} else if (mode == MODE_INSTALL) {

					if (project_path->get_text().ends_with(".zip")) {
						dir = install_path->get_text();
						zip_path = project_path->get_text();
					}

					FileAccess *src_f = NULL;
					zlib_filefunc_def io = zipio_create_io_from_file(&src_f);

					unzFile pkg = unzOpen2(zip_path.utf8().get_data(), &io);
					if (!pkg) {

						dialog_error->set_text(TTR("Error opening package file, not in zip format."));
						dialog_error->popup_centered_minsize();
						return;
					}

					int ret = unzGoToFirstFile(pkg);

					Vector<String> failed_files;

					int idx = 0;
					while (ret == UNZ_OK) {

						//get filename
						unz_file_info info;
						char fname[16384];
						ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, NULL, 0, NULL, 0);

						String path = fname;

						int depth = 1; //stuff from github comes with tag
						bool skip = false;
						while (depth > 0) {
							int pp = path.find("/");
							if (pp == -1) {
								skip = true;
								break;
							}
							path = path.substr(pp + 1, path.length());
							depth--;
						}

						if (skip || path == String()) {
							//
						} else if (path.ends_with("/")) { // a dir

							path = path.substr(0, path.length() - 1);

							DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
							da->make_dir(dir.plus_file(path));
							memdelete(da);

						} else {

							Vector<uint8_t> data;
							data.resize(info.uncompressed_size);

							//read
							unzOpenCurrentFile(pkg);
							unzReadCurrentFile(pkg, data.ptrw(), data.size());
							unzCloseCurrentFile(pkg);

							FileAccess *f = FileAccess::open(dir.plus_file(path), FileAccess::WRITE);

							if (f) {
								f->store_buffer(data.ptr(), data.size());
								memdelete(f);
							} else {
								failed_files.push_back(path);
							}
						}

						idx++;
						ret = unzGoToNextFile(pkg);
					}

					unzClose(pkg);

					if (failed_files.size()) {
						String msg = TTR("The following files failed extraction from package:") + "\n\n";
						for (int i = 0; i < failed_files.size(); i++) {

							if (i > 15) {
								msg += "\nAnd " + itos(failed_files.size() - i) + " more files.";
								break;
							}
							msg += failed_files[i] + "\n";
						}

						dialog_error->set_text(msg);
						dialog_error->popup_centered_minsize();

					} else if (!project_path->get_text().ends_with(".zip")) {
						dialog_error->set_text(TTR("Package installed successfully!"));
						dialog_error->popup_centered_minsize();
					}
				}
			}

			dir = dir.replace("\\", "/");
			if (dir.ends_with("/"))
				dir = dir.substr(0, dir.length() - 1);
			String proj = dir.replace("/", "::");
			EditorSettings::get_singleton()->set("projects/" + proj, dir);
			EditorSettings::get_singleton()->save();

			hide();
			emit_signal("project_created", dir);
		}
	}

	void _remove_created_folder() {

		if (created_folder_path != "") {
			DirAccess *d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
			d->remove(created_folder_path);
			memdelete(d);

			create_dir->set_disabled(false);
			created_folder_path = "";
		}
	}

	void cancel_pressed() {

		_remove_created_folder();

		project_path->clear();
		_path_text_changed("");
		project_name->clear();
		_text_changed("");

		if (status_rect->get_texture() == get_icon("StatusError", "EditorIcons"))
			msg->show();

		if (install_status_rect->get_texture() == get_icon("StatusError", "EditorIcons"))
			msg->show();
	}

	void _notification(int p_what) {

		if (p_what == MainLoop::NOTIFICATION_WM_QUIT_REQUEST)
			_remove_created_folder();
	}

protected:
	static void _bind_methods() {

		ClassDB::bind_method("_browse_path", &ProjectDialog::_browse_path);
		ClassDB::bind_method("_create_folder", &ProjectDialog::_create_folder);
		ClassDB::bind_method("_text_changed", &ProjectDialog::_text_changed);
		ClassDB::bind_method("_path_text_changed", &ProjectDialog::_path_text_changed);
		ClassDB::bind_method("_path_selected", &ProjectDialog::_path_selected);
		ClassDB::bind_method("_file_selected", &ProjectDialog::_file_selected);
		ClassDB::bind_method("_install_path_selected", &ProjectDialog::_install_path_selected);
		ClassDB::bind_method("_browse_install_path", &ProjectDialog::_browse_install_path);
		ADD_SIGNAL(MethodInfo("project_created"));
		ADD_SIGNAL(MethodInfo("projects_updated"));
	}

public:
	void set_zip_path(const String &p_path) {
		zip_path = p_path;
	}
	void set_zip_title(const String &p_title) {
		zip_title = p_title;
	}

	void set_mode(Mode p_mode) {

		mode = p_mode;
	}

	void set_project_path(const String &p_path) {
		project_path->set_text(p_path);
	}

	void show_dialog() {

		if (mode == MODE_RENAME) {

			project_path->set_editable(false);
			browse->hide();
			install_browse->hide();

			set_title(TTR("Rename Project"));
			get_ok()->set_text(TTR("Rename"));
			name_container->show();
			status_rect->hide();
			msg->hide();
			install_path_container->hide();
			install_status_rect->hide();
			rasterizer_container->hide();
			get_ok()->set_disabled(false);

			ProjectSettings *current = memnew(ProjectSettings);

			int err = current->setup(project_path->get_text(), "");
			if (err != OK) {
				set_message(vformat(TTR("Couldn't load project.godot in project path (error %d). It may be missing or corrupted."), err), MESSAGE_ERROR);
				status_rect->show();
				msg->show();
				get_ok()->set_disabled(true);
			} else if (current->has_setting("application/config/name")) {
				String proj = current->get("application/config/name");
				project_name->set_text(proj);
				_text_changed(proj);
			}

			project_name->call_deferred("grab_focus");

			create_dir->hide();

		} else {

			fav_dir = EditorSettings::get_singleton()->get("filesystem/directories/default_project_path");
			if (fav_dir != "") {
				project_path->set_text(fav_dir);
				fdialog->set_current_dir(fav_dir);
			} else {
				DirAccess *d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
				project_path->set_text(d->get_current_dir());
				fdialog->set_current_dir(d->get_current_dir());
				memdelete(d);
			}
			String proj = TTR("New Game Project");
			project_name->set_text(proj);
			_text_changed(proj);

			project_path->set_editable(true);
			browse->set_disabled(false);
			browse->show();
			install_browse->set_disabled(false);
			install_browse->show();
			create_dir->show();
			status_rect->show();
			install_status_rect->show();
			msg->show();

			if (mode == MODE_IMPORT) {
				set_title(TTR("Import Existing Project"));
				get_ok()->set_text(TTR("Import & Edit"));
				name_container->hide();
				install_path_container->hide();
				rasterizer_container->hide();
				project_path->grab_focus();

			} else if (mode == MODE_NEW) {

				set_title(TTR("Create New Project"));
				get_ok()->set_text(TTR("Create & Edit"));
				name_container->show();
				install_path_container->hide();
				rasterizer_container->show();
				project_name->call_deferred("grab_focus");
				project_name->call_deferred("select_all");

			} else if (mode == MODE_INSTALL) {

				set_title(TTR("Install Project:") + " " + zip_title);
				get_ok()->set_text(TTR("Install & Edit"));
				project_name->set_text(zip_title);
				name_container->show();
				install_path_container->hide();
				rasterizer_container->hide();
				project_path->grab_focus();
{'fraction_non_alphanumeric': 0.16211860530958033, 'fraction_numerical': 0.003876559879783087, 'mean_word_length': 3.4416715031921066, 'pattern_counts': {'":': 2, '<': 25, '<?xml version=': 0, '>': 402, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 26, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1175975', 'n_tokens_mistral': 18497, 'n_tokens_neox': 15078, 'n_words': 2874}