#include "DeerStudio/EditorEngine/ServiceScript/ServiceScriptObject.h"

#include "DeerStudio/EditorEngine.h"
#include "DeerStudio/EditorEngine/ErrorHandle.h"

#include "DeerStudio/EditorEngine/ServiceScript/ServiceScriptGenericFunction.h"

#include "angelscript.h"
#include "scriptbuilder.h"

#include <functional>
#include <sstream>
#include <string>

namespace Deer {
	namespace EditorEngine {
		ServiceScriptObject::ServiceScriptObject(
		    asITypeInfo* _type, asIScriptContext* _scriptContext)
		    : type(_type), scriptContext(_scriptContext) {
			// Constructor
			// "type@ type()"
			std::string callString;
			const std::string ns(type->GetNamespace());
			if (ns != "") {
				callString += ns;
				callString += "::";
			}
			callString += type->GetName();
			callString += "@ ";
			if (ns != "") {
				callString += ns;
				callString += "::";
			}
			callString += type->GetName();
			callString += "()";

			asIScriptFunction* factory =
			    type->GetFactoryByDecl(callString.c_str());

			AS_CHECK(scriptContext->Prepare(factory));
			AS_CHECK(scriptContext->Execute());

			// Return value contains the ref to a asIScriptObject in the
			// location provided
			object =
			    *(asIScriptObject**)scriptContext->GetAddressOfReturnValue();
			if (!object) {
				DEER_EDITOR_ENGINE_ERROR("Could not create object",
				                         type->GetName());
				return;
			}
			object->AddRef();

			updateFunction = type->GetMethodByDecl("void onUpdate()");
			initFunction = type->GetMethodByDecl("void onInit()");

			scriptContext->Unprepare();
		}

		ServiceScriptObject::~ServiceScriptObject() {
			if (object)
				object->Release();
		}

		void ServiceScriptObject::init() {
			if (!initFunction)
				return;

			AS_CHECK(scriptContext->Prepare(initFunction));
			AS_CHECK(scriptContext->SetObject(object));
			AS_CHECK(scriptContext->Execute());
			AS_CHECK(scriptContext->Unprepare());
		}

		void ServiceScriptObject::update() {
			if (!updateFunction)
				return;

			AS_CHECK(scriptContext->Prepare(updateFunction));
			AS_CHECK(scriptContext->SetObject(object));
			AS_CHECK(scriptContext->Execute());
			AS_CHECK(scriptContext->Unprepare());
		}

		ServiceScriptObject::ServiceScriptObject(
		    ServiceScriptObject&& other) noexcept {
			type = other.type;
			object = other.object;
			updateFunction = other.updateFunction;
			initFunction = other.initFunction;
			scriptContext = other.scriptContext;

			other.type = nullptr;
			other.object = nullptr;
			other.updateFunction = nullptr;
			other.initFunction = nullptr;
			other.scriptContext = nullptr;
		}

		ServiceScriptObject&
		ServiceScriptObject::operator=(ServiceScriptObject&& other) noexcept {
			if (&other != this) {
				type = other.type;
				object = other.object;
				updateFunction = other.updateFunction;
				initFunction = other.initFunction;
				scriptContext = other.scriptContext;

				other.type = nullptr;
				other.object = nullptr;
				other.updateFunction = nullptr;
				other.initFunction = nullptr;
				other.scriptContext = nullptr;
			}
			return *this;
		}

		void ServiceScriptObject::bindFunctions() {
			for (int i = 0; i < type->GetMethodCount(); i++) {
				asIScriptFunction* func = type->GetMethodByIndex(i);
				std::vector<std::string> metadata =
				    scriptBuilder.GetMetadataForTypeMethod(type->GetTypeId(),
				                                           func);

				bool containsServiceAPI = false;
				for (std::string& str : metadata) {
					if (str != "ServiceAPI")
						continue;
					containsServiceAPI = true;
					break;
				}

				if (!containsServiceAPI)
					continue;

				DEER_CORE_INFO("Registering function from object {0}.{1}",
				               type->GetName(), func->GetName());

				asITypeInfo* retType =
				    EditorEngine::scriptEngine->GetTypeInfoById(
				        func->GetReturnTypeId());

				std::stringstream ext_dec;

				// This means its void
				if (retType == nullptr)
					ext_dec << "void ";
				else
					ext_dec << retType->GetName() << " ";

				ext_dec << func->GetName();
				ext_dec << "(";

				for (int i = 0; i < func->GetParamCount(); i++) {
					if (i != 0)
						ext_dec << ", ";

					int typeId;
					const char* name;

					func->GetParam(i, &typeId, nullptr, &name);
					asITypeInfo* paramType =
					    scriptEngine->GetTypeInfoById(typeId);

					const char* typeName =
					    scriptEngine->GetTypeDeclaration(typeId, false);

					ext_dec << typeName;

					if (name != nullptr)
						ext_dec << " " << name;
				}

				ext_dec << ")";

				DEER_CORE_INFO(ext_dec.str().c_str());

				ServiceScriptFunction* serviceScriptFunction =
				    new ServiceScriptFunction();

				serviceScriptFunction->apiFunction = func;
				serviceScriptFunction->scriptContext = scriptContext;
				serviceScriptFunction->scriptObject = object;

				AS_CHECK_ADDITIONAL_INFO(
				    scriptEngine->RegisterGlobalFunction(
				        ext_dec.str().c_str(), asFUNCTION(apiFunction),
				        asCALL_GENERIC, serviceScriptFunction),
				    ext_dec.str().c_str());
			}
		}
	} // namespace EditorEngine
} // namespace Deer