/* This is implementation is for loading/saving JS files. */
/* After loading it contains the code/bytecode of the loaded file. */

#include "javascript.h"
#include "core/io/file_access_encrypted.h"
#include "javascript_instance.h"
#include "scene/resources/resource_format_text.h"
#include "src/language/javascript_language.h"

ScriptLanguage *JavaScript::get_language() const {
	return JavaScriptLanguage::get_singleton();
}

JavaScript::JavaScript() {
}

JavaScript::~JavaScript() {
}

bool JavaScript::can_instantiate() const {
#ifdef TOOLS_ENABLED
	return is_valid() && (is_tool() || ScriptServer::is_scripting_enabled());
#else
	return is_valid();
#endif
}

StringName JavaScript::get_global_name() const {
	return StringName();
}

StringName JavaScript::get_instance_base_type() const {
	static StringName empty;
	ERR_FAIL_NULL_V(javascript_class, empty);
	ERR_FAIL_NULL_V(javascript_class->native_class, empty);
	return javascript_class->native_class->name;
}

ScriptInstance *JavaScript::instance_create(Object *p_this) {
	JavaScriptBinder *binder = JavaScriptLanguage::get_thread_binder(Thread::get_caller_id());
	ERR_FAIL_NULL_V_MSG(binder, NULL, "Cannot create instance from this thread");
	const JavaScriptClassInfo *cls = NULL;
	JavaScriptError js_err;
	if (!bytecode.is_empty()) {
		cls = binder->parse_javascript_class(bytecode, script_path, false, &js_err);
	} else {
		cls = binder->parse_javascript_class(code, script_path, false, &js_err);
	}
	ERR_FAIL_NULL_V_MSG(cls, NULL, vformat("Cannot parse class from %s", get_script_path()));

	if (!ClassDB::is_parent_class(p_this->get_class_name(), cls->native_class->name)) {
		ERR_FAIL_V_MSG(NULL, vformat("Script inherits from native type '%s', so it can't be instanced in object of type: '%s'", cls->native_class->name, p_this->get_class()));
	}

	JavaScriptGCHandler js_instance = binder->create_js_instance_for_godot_object(cls, p_this);
	ERR_FAIL_NULL_V(js_instance.javascript_object, NULL);

	JavaScriptInstance *instance = memnew(JavaScriptInstance);
	instance->script = Ref<JavaScript>(this);
	instance->owner = p_this;
	instance->binder = binder;
	instance->javascript_object = js_instance;
	instance->javascript_class = cls;
	instance->owner->set_script_instance(instance);
	instances.insert(p_this);
	return instance;
}

PlaceHolderScriptInstance *JavaScript::placeholder_instance_create(Object *p_this) {
#ifdef TOOLS_ENABLED
	PlaceHolderScriptInstance *si = memnew(PlaceHolderScriptInstance(JavaScriptLanguage::get_singleton(), Ref<Script>(this), p_this));
	instances.insert(p_this);
	placeholders.insert(si);
	update_exports();
	return si;
#else
	return NULL;
#endif
}

Error JavaScript::reload(bool p_keep_state) {
	javascript_class = NULL;
	Error err = OK;
	JavaScriptBinder *binder = JavaScriptLanguage::get_thread_binder(Thread::get_caller_id());
#ifdef TOOLS_ENABLED
	// This is a workaround for files generated outside of the godot editor
	if (binder == NULL) {
		binder = JavaScriptLanguage::get_thread_binder(Thread::MAIN_ID);
	}
#endif
	ERR_FAIL_COND_V_MSG(binder == NULL, ERR_INVALID_DATA, "Cannot load script in this thread");
	JavaScriptError js_err;

	// TODO: We should have a setting/option to skip parsing or reading .mjs files which aren't Godot classes for example "chunk-xxx.mjs" build from esbuild

	if (!bytecode.is_empty()) {
		javascript_class = binder->parse_javascript_class(bytecode, script_path, true, &js_err);
	} else {
		javascript_class = binder->parse_javascript_class(code, script_path, true, &js_err);
	}

	if (!javascript_class) {
		err = ERR_PARSE_ERROR;
		ERR_PRINT(binder->error_to_string(js_err));
	} else {
#ifdef TOOLS_ENABLED
		set_last_modified_time(FileAccess::get_modified_time(script_path));
		p_keep_state = true;

		for (Object *owner : instances) {
			HashMap<StringName, Variant> values;
			if (p_keep_state) {
				for (const KeyValue<StringName, JavaScriptProperyInfo> &pair : javascript_class->properties) {
					values.insert(pair.key, owner->get(pair.key));
				}
			}

			ScriptInstance *si = owner->get_script_instance();
			if (si->is_placeholder()) {
				PlaceHolderScriptInstance *psi = static_cast<PlaceHolderScriptInstance *>(si);
				if (javascript_class->tool) {
					_placeholder_erased(psi);
				}
			} else if (!javascript_class->tool) { // from tooled to !tooled
				PlaceHolderScriptInstance *psi = placeholder_instance_create(owner);
				owner->set_script_instance(psi);
				instances.insert(owner);
			}
			if (javascript_class->tool) { // re-create as an instance
				instance_create(owner);
			}

			if (p_keep_state) {
				for (const KeyValue<StringName, Variant> &pair : values) {
					if (const JavaScriptProperyInfo *epi = javascript_class->properties.getptr(pair.key)) {
						const Variant &backup = pair.value;
						owner->set(pair.key, backup.get_type() == epi->type ? backup : epi->default_value);
					}
				}
			}
		}
#endif
	}
	return err;
}

bool JavaScript::instance_has(const Object *p_this) const {
	return instances.has(const_cast<Object *>(p_this));
}

#ifdef TOOLS_ENABLED
void JavaScript::_placeholder_erased(PlaceHolderScriptInstance *p_placeholder) {
	instances.erase(p_placeholder->get_owner());
	placeholders.erase(p_placeholder);
}
#endif

bool JavaScript::has_method(const StringName &p_method) const {
	if (!javascript_class)
		return false;
	return javascript_class->methods.getptr(p_method) != NULL;
}

MethodInfo JavaScript::get_method_info(const StringName &p_method) const {
	MethodInfo mi;
	ERR_FAIL_NULL_V(javascript_class, mi);
	if (const MethodInfo *ptr = javascript_class->methods.getptr(p_method)) {
		mi = *ptr;
	}
	return mi;
}

bool JavaScript::is_tool() const {
	if (!javascript_class)
		return false;
	return javascript_class->tool;
}

void JavaScript::get_script_method_list(List<MethodInfo> *p_list) const {
	if (!javascript_class)
		return;
	for (const KeyValue<StringName, MethodInfo> &pair : javascript_class->methods) {
		p_list->push_back(pair.value);
	}
}

void JavaScript::get_script_property_list(List<PropertyInfo> *p_list) const {
	if (!javascript_class)
		return;
	for (const KeyValue<StringName, JavaScriptProperyInfo> &pair : javascript_class->properties) {
		p_list->push_back(pair.value);
	}
}

bool JavaScript::get_property_default_value(const StringName &p_property, Variant &r_value) const {
	if (!javascript_class)
		return false;

	if (const JavaScriptProperyInfo *pi = javascript_class->properties.getptr(p_property)) {
		r_value = pi->default_value;
		return true;
	}

	return false;
}

void JavaScript::update_exports() {
#ifdef TOOLS_ENABLED
	if (!javascript_class)
		return;

	List<PropertyInfo> props;
	HashMap<StringName, Variant> values;
	for (const KeyValue<StringName, JavaScriptProperyInfo> &pair : javascript_class->properties) {
		const JavaScriptProperyInfo &pi = pair.value;
		props.push_back(pi);
		values[pair.key] = pi.default_value;
	}

	for (PlaceHolderScriptInstance *s : placeholders) {
		s->update(props, values);
	}
#endif
}

bool JavaScript::has_script_signal(const StringName &p_signal) const {
	if (!javascript_class)
		return false;
	return javascript_class->signals.has(p_signal);
}

void JavaScript::get_script_signal_list(List<MethodInfo> *r_signals) const {
	if (!javascript_class)
		return;
	for (const KeyValue<StringName, MethodInfo> &pair : javascript_class->signals) {
		r_signals->push_back(pair.value);
	}
}

bool JavaScript::is_valid() const {
	return javascript_class != NULL;
}

void JavaScript::_bind_methods() {
}

Ref<Resource> ResourceFormatLoaderJavaScript::load(const String &p_path, const String &p_original_path, Error *r_error, bool p_use_sub_threads, float *r_progress, CacheMode p_cache_mode) {
	Error err = OK;
	Ref<JavaScriptModule> module = ResourceFormatLoaderJavaScriptModule::load_static(p_path, p_original_path, &err);
	if (r_error)
		*r_error = err;

	if (err == ERR_FILE_NOT_FOUND) {
		return module;
	}

	ERR_FAIL_COND_V_MSG(err != OK, Ref<Resource>(), "Cannot load script file '" + p_path + "'.");
	Ref<JavaScript> javaScript;
	javaScript.instantiate();
	javaScript->set_script_path(p_path);
	javaScript->bytecode = module->get_bytecode();
	javaScript->set_source_code(module->get_source_code());
	err = javaScript->reload();
	if (r_error)
		*r_error = err;
	ERR_FAIL_COND_V_MSG(err != OK, Ref<Resource>(), "Parse source code from file '" + p_path + "' failed.");
#ifdef TOOLS_ENABLED
	JavaScriptLanguage::get_singleton()->get_scripts().insert(javaScript);
#endif
	return javaScript;
}

void ResourceFormatLoaderJavaScript::get_recognized_extensions(List<String> *p_extensions) const {
	p_extensions->push_front(EXT_JSCLASS);
}

void ResourceFormatLoaderJavaScript::get_recognized_extensions_for_type(const String &p_type, List<String> *p_extensions) const {
	get_recognized_extensions(p_extensions);
}

bool ResourceFormatLoaderJavaScript::handles_type(const String &p_type) const {
	return p_type == JavaScript::get_class_static();
}

String ResourceFormatLoaderJavaScript::get_resource_type(const String &p_path) const {
	String el = p_path.get_extension().to_lower();
	if (el == EXT_JSCLASS)
		return JavaScript::get_class_static();
	return "";
}

bool ResourceFormatLoaderJavaScript::recognize_path(const String &p_path, const String &p_for_type) const {
	return p_path.get_extension() == EXT_JSCLASS;
}

Error ResourceFormatSaverJavaScript::save(const Ref<Resource> &p_resource, const String &p_path, uint32_t p_flags) {
	Ref<JavaScript> javaScript = p_resource;
	ERR_FAIL_COND_V(javaScript.is_null(), ERR_INVALID_PARAMETER);

	String source = javaScript->get_source_code();

	Error err;
	Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::WRITE, &err);
	ERR_FAIL_COND_V_MSG(err, err, "Cannot save file '" + p_path + "'.");
	file->store_string(source);
	if (file->get_error() != OK && file->get_error() != ERR_FILE_EOF) {
		return ERR_CANT_CREATE;
	}

	if (ScriptServer::is_reload_scripts_on_save_enabled()) {
		javaScript->reload();
	}

	return OK;
}

void ResourceFormatSaverJavaScript::get_recognized_extensions(const Ref<Resource> &p_resource, List<String> *p_extensions) const {
	if (Object::cast_to<JavaScript>(*p_resource)) {
		p_extensions->push_back(EXT_JSCLASS);
	}
}

bool ResourceFormatSaverJavaScript::recognize(const Ref<Resource> &p_resource) const {
	return Object::cast_to<JavaScript>(*p_resource) != nullptr;
}

void JavaScriptModule::_bind_methods() {
	ClassDB::bind_method(D_METHOD("set_script_path", "script_path"), &JavaScriptModule::set_script_path);
	ClassDB::bind_method(D_METHOD("get_script_path"), &JavaScriptModule::get_script_path);
	ClassDB::bind_method(D_METHOD("set_source_code", "source_code"), &JavaScriptModule::set_source_code);
	ClassDB::bind_method(D_METHOD("get_source_code"), &JavaScriptModule::get_source_code);
	ClassDB::bind_method(D_METHOD("set_bytecode", "bytecode"), &JavaScriptModule::set_bytecode);
	ClassDB::bind_method(D_METHOD("get_bytecode"), &JavaScriptModule::get_bytecode);
	ADD_PROPERTY(PropertyInfo(Variant::STRING, "script_path"), "set_script_path", "get_script_path");
}

JavaScriptModule::JavaScriptModule() {
	set_source_code("module.exports = {};\n");
}

Ref<Resource> ResourceFormatLoaderJavaScriptModule::load(const String &p_path, const String &p_original_path, Error *r_error, bool p_use_sub_threads, float *r_progress, ResourceFormatLoader::CacheMode p_cache_mode) {
	return ResourceFormatLoaderJavaScriptModule::load_static(p_path, p_original_path, r_error);
}

void ResourceFormatLoaderJavaScriptModule::get_recognized_extensions(List<String> *p_extensions) const {
	p_extensions->push_front(EXT_JSMODULE);
}

void ResourceFormatLoaderJavaScriptModule::get_recognized_extensions_for_type(const String &p_type, List<String> *p_extensions) const {
	get_recognized_extensions(p_extensions);
}

bool ResourceFormatLoaderJavaScriptModule::handles_type(const String &p_type) const {
	return p_type == JavaScriptModule::get_class_static();
}

String ResourceFormatLoaderJavaScriptModule::get_resource_type(const String &p_path) const {
	String el = p_path.get_extension().to_lower();
	if (el == EXT_JSMODULE)
		return JavaScriptModule::get_class_static();
	return "";
}

Ref<Resource> ResourceFormatLoaderJavaScriptModule::load_static(const String &p_path, const String &p_original_path, Error *r_error) {
	Error err = ERR_FILE_CANT_OPEN;
	bool fileExists = FileAccess::exists(p_path);
	if (!fileExists) {
		*r_error = ERR_FILE_NOT_FOUND;
		WARN_PRINT("Cannot find file '" + p_path + "'. Maybe you deleted the file.");
		return Ref<Resource>();
	}

	Ref<JavaScriptModule> module;
	module.instantiate();
	module->set_script_path(p_path);

	if (p_path.ends_with("." EXT_JSMODULE) || p_path.ends_with("." EXT_JSCLASS) || p_path.ends_with("." EXT_JSON)) {
		String code = FileAccess::get_file_as_string(p_path, &err);
		if (r_error)
			*r_error = err;
		ERR_FAIL_COND_V_MSG(err != OK, Ref<Resource>(), "Cannot load source code from file '" + p_path + "'.");
		module->set_source_code(code);
	}

// TODO: Check what this block is for
#if 0
	else if (p_path.ends_with("." EXT_JSMODULE_BYTECODE) || p_path.ends_with("." EXT_JSCLASS_BYTECODE)) {
		module->set_bytecode(FileAccess::get_file_as_array(p_path, &err));
		if (r_error) *r_error = err;
		ERR_FAIL_COND_V_MSG(err != OK, Ref<Resource>(), "Cannot load bytecode from file '" + p_path + "'.");
	} else if (p_path.ends_with("." EXT_JSMODULE_ENCRYPTED) || p_path.ends_with("." EXT_JSCLASS_ENCRYPTED)) {
		Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::READ);
		if (fa.is_valid() && fa->is_open()) {
			Ref<FileAccessEncrypted> fae = memnew(FileAccessEncrypted);
			Vector<uint8_t> key;
			key.resize(32);
			for (int i = 0; i < key.size(); i++) {
				key.write[i] = script_encryption_key[i];
			}
			err = fae->open_and_parse(fa, key, FileAccessEncrypted::MODE_READ);
			if (err == OK) {
				Vector<uint8_t> encrypted_code;
				encrypted_code.resize(fae->get_length());
				fae->get_buffer(encrypted_code.ptrw(), encrypted_code.size());

				String code;
				if (code.parse_utf8((const char *)encrypted_code.ptr(), encrypted_code.size())) {
					err = ERR_PARSE_ERROR;
				} else {
					module->set_source_code(code);
				}
			}
		} else {
			err = ERR_CANT_OPEN;
		}
	}
#endif

	if (r_error)
		*r_error = err;
	ERR_FAIL_COND_V(err != OK, Ref<Resource>());
	return module;
}

Error ResourceFormatSaverJavaScriptModule::save(const Ref<Resource> &p_resource, const String &p_path, uint32_t p_flags) {
	Ref<JavaScriptModule> module = p_resource;
	ERR_FAIL_COND_V(module.is_null(), ERR_INVALID_PARAMETER);
	String source = module->get_source_code();
	Error err;
	Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::WRITE, &err);
	ERR_FAIL_COND_V_MSG(err, err, "Cannot save file '" + p_path + "'.");
	file->store_string(source);
	if (file->get_error() != OK && file->get_error() != ERR_FILE_EOF) {
		return ERR_CANT_CREATE;
	}
	return OK;
}

void ResourceFormatSaverJavaScriptModule::get_recognized_extensions(const Ref<Resource> &p_resource, List<String> *p_extensions) const {
	if (Object::cast_to<JavaScriptModule>(*p_resource)) {
		p_extensions->push_back(EXT_JSMODULE);
	}
}

bool ResourceFormatSaverJavaScriptModule::recognize(const Ref<Resource> &p_resource) const {
	return Object::cast_to<JavaScriptModule>(*p_resource) != nullptr;
}