#pragma once
#include "game/game.hpp"

#include <utils/string.hpp>

namespace ui_scripting
{
	class lightuserdata;
	class userdata_value;
	class userdata;
	class table_value;
	class table;
	class function;
	class script_value;

	namespace
	{
		template <typename T>
		std::string get_typename()
		{
			auto& info = typeid(T);

			if (info == typeid(std::string) ||
				info == typeid(const char*))
			{
				return "string";
			}

			if (info == typeid(lightuserdata))
			{
				return "lightuserdata";
			}

			if (info == typeid(userdata))
			{
				return "userdata";
			}

			if (info == typeid(table))
			{
				return "table";
			}

			if (info == typeid(function))
			{
				return "function";
			}

			if (info == typeid(int) || 
				info == typeid(float) || 
				info == typeid(unsigned int))
			{
				return "number";
			}

			if (info == typeid(bool))
			{
				return "boolean";
			}

			return info.name();
		}
	}

	class hks_reference
	{
	public:
		hks_reference() = default;
		hks_reference(const game::hks::HksObject& value);
		hks_reference(const hks_reference& other) noexcept;
		hks_reference(hks_reference&& other) noexcept;

		hks_reference& operator=(const hks_reference& other) noexcept;
		hks_reference& operator=(hks_reference&& other) noexcept;

		~hks_reference();

		const game::hks::HksObject& get() const;

	private:
		void assign(const game::hks::HksObject& value);
		void release();

		game::hks::HksObject value_{game::hks::TNONE, {}};
		int ref_{};
	};

	using arguments = std::vector<script_value>;
	using event_arguments = std::unordered_map<std::string, script_value>;

	class script_value
	{
	public:
		script_value();
		script_value(const game::hks::HksObject& value);

		script_value(int value);
		script_value(unsigned int value);
		script_value(long long value);
		script_value(unsigned long long value);
		script_value(bool value);

		script_value(float value);
		script_value(double value);

		script_value(const char* value);
		script_value(const char* value, unsigned int len);
		script_value(const std::string& value);

		script_value(const lightuserdata& value);
		script_value(const userdata& value);
		script_value(const table& value);
		script_value(const function& value);

		template <template<class, class> class C, class T, typename TableType = table>
		script_value(const C<T, std::allocator<T>>& container)
		{
			TableType table_{};
			int index = 1;

			for (const auto& value : container)
			{
				table_.set(index++, value);
			}

			game::hks::HksObject obj{};
			obj.t = game::hks::TTABLE;
			obj.v.ptr = table_.ptr;

			this->value_ = obj;
		}

		template <typename F>
		script_value(F f)
			: script_value(function(f))
		{
		}

		template <typename T>
		script_value(const std::optional<T>& optional)
			: script_value(optional.has_value() ? script_value(optional.value()) : script_value())
		{
		}

		bool operator==(const script_value& other) const;

		template<class ...T>
		arguments operator()(T... arguments) const
		{
			return this->as<function>().call({arguments...});
		}

		template <size_t Size>
		table_value operator[](const char(&key)[Size]) const
		{
			return {this->as<table>(), key};
		}

		template <typename T = script_value>
		table_value operator[](const T& key) const
		{
			return {this->as<table>(), key};
		}

		template <typename T>
		bool is() const;

		template <typename T>
		T as() const
		{
			if (!this->is<T>())
			{
				const auto hks_typename = game::hks::s_compilerTypeName[this->get_type() + 2];
				const auto typename_ = get_typename<T>();

				throw std::runtime_error(utils::string::va("%s expected, got %s", 
					typename_.data(), hks_typename));
			}

			return get<T>();
		}

		template <typename T>
		operator T() const
		{
			return this->as<T>();
		}

		const game::hks::HksObjectType get_type() const;
		const game::hks::HksObject& get_raw() const;

		hks_reference value_{};

	private:
		template <typename T>
		T get() const;

	};

	class variadic_args : public arguments
	{
	};

	class function_argument
	{
	public:
		function_argument(const arguments& args, const script_value& value, const int index);

		template <typename T>
		T as() const
		{
			try
			{
				return this->value_.as<T>();
			}
			catch (const std::exception& e)
			{
				throw std::runtime_error(utils::string::va("bad argument #%d (%s)",
					this->index_ + 1, e.what()));
			}
		}

		template <>
		variadic_args as() const
		{
			variadic_args args{};
			for (auto i = this->index_; i < this->values_.size(); i++)
			{
				args.push_back(this->values_[i]);
			}
			return args;
		}

		operator script_value() const
		{
			return this->value_;
		}

		template <typename T>
		operator T() const
		{
			return this->as<T>();
		}

	private:
		arguments values_{};
		script_value value_{};
		int index_{};
	};

	class function_arguments
	{
	public:
		function_arguments(const arguments& values);

		function_argument operator[](const int index) const
		{
			if (index >= values_.size())
			{
				return {values_, {}, index};
			}

			return {values_, values_[index], index};
		}
	private:
		arguments values_{};
	};
}
