/*
	Copyright (C) 2017 - 2024
	Part of the Battle for Wesnoth Project https://www.wesnoth.org/

	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation; either version 2 of the License, or
	(at your option) any later version.
	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY.

	See the COPYING file for more details.
*/

#include "scripting/lua_formula_bridge.hpp"

#include "game_board.hpp"
#include "scripting/game_lua_kernel.hpp"
#include "scripting/lua_unit.hpp"
#include "scripting/lua_common.hpp"
#include "scripting/lua_team.hpp"
#include "scripting/lua_unit_attacks.hpp"
#include "scripting/lua_unit_type.hpp"
#include "lua/wrapper_lauxlib.h"
#include "formula/callable_objects.hpp"
#include "formula/formula.hpp"
#include "variable.hpp"

#include "resources.hpp"
#include "units/map.hpp"
#include "units/unit.hpp"

static const char formulaKey[] = "formula";

using namespace wfl;

void luaW_pushfaivariant(lua_State* L, variant val);
variant luaW_tofaivariant(lua_State* L, int i);

class lua_callable : public formula_callable {
	lua_State* mState;
	int table_i;
public:
	lua_callable(lua_State* L, int i) : mState(L), table_i(lua_absindex(L,i)) {}
	variant get_value(const std::string& key) const {
		if(key == "__list") {
			std::vector<variant> values;
			std::size_t n = lua_rawlen(mState, table_i);
			if(n == 0) {
				return variant();
			}
			for(std::size_t i = 1; i <= n; i++) {
				lua_pushinteger(mState, i);
				lua_gettable(mState, table_i);
				values.push_back(luaW_tofaivariant(mState, -1));
			}
			return variant(values);
		} else if(key == "__map") {
			std::map<variant,variant> values;
			for(lua_pushnil(mState); lua_next(mState, table_i); lua_pop(mState, 1)) {
				values[luaW_tofaivariant(mState, -2)] = luaW_tofaivariant(mState, -1);
			}
			return variant(values);
		}
		lua_pushlstring(mState, key.c_str(), key.size());
		lua_gettable(mState, table_i);
		variant result = luaW_tofaivariant(mState, -1);
		lua_pop(mState, 1);
		return result;
	}
	void get_inputs(formula_input_vector& inputs) const {
		add_input(inputs, "__list");
		add_input(inputs, "__map");
		for(lua_pushnil(mState); lua_next(mState, table_i); lua_pop(mState,1)) {
			lua_pushvalue(mState, -2);
			bool is_valid_key = (lua_type(mState, -1) == LUA_TSTRING) && !lua_isnumber(mState, -1);
			lua_pop(mState, 1);
			if(is_valid_key) {
				std::string key = lua_tostring(mState, -2);
				if(key.find_first_not_of(formula::id_chars) != std::string::npos) {
					add_input(inputs, key);
				}
			}
		}
	}
	int do_compare(const formula_callable* other) const {
		const lua_callable* lua = dynamic_cast<const lua_callable*>(other);
		if(lua == nullptr) {
			return formula_callable::do_compare(other);
		}
		if(mState == lua->mState) { // Which should always be the case, but let's be safe here
			if(lua_compare(mState, table_i, lua->table_i, LUA_OPEQ)) {
				return 0;
			}
			int top = lua_gettop(mState);
			if(lua_getmetatable(mState, table_i)) {
				lua_getfield(mState, -1, "__lt");
				if(!lua_isnoneornil(mState, -1)) {
					if(lua_getmetatable(mState, lua->table_i)) {
						lua_getfield(mState, -1, "__lt");
						if(!lua_isnoneornil(mState, -1)) {
							lua_settop(mState, top);
							return lua_compare(mState, table_i, lua->table_i, LUA_OPLT) ? -1 : 1;
						}
						if(lua_compare(mState, -4, -2, LUA_OPEQ)) {
							lua_settop(mState, top);
							return 0;
						}
						const void* lhs = lua_topointer(mState, -4);
						const void* rhs = lua_topointer(mState, -2);
						lua_settop(mState, top);
						return lhs < rhs ? -1 : (lhs > rhs ? 1 : 0);
					}
				}
			}
			lua_settop(mState, top);
			return lua_topointer(mState, -2) < lua_topointer(mState, -1) ? -1 : 1;
		}
		return mState < lua->mState ? -1 : 1;
	}
};

void luaW_pushfaivariant(lua_State* L, variant val) {
	if(val.is_int()) {
		lua_pushinteger(L, val.as_int());
	} else if(val.is_decimal()) {
		lua_pushnumber(L, val.as_decimal() / 1000.0);
	} else if(val.is_string()) {
		const std::string result_string = val.as_string();
		lua_pushlstring(L, result_string.c_str(), result_string.size());
	} else if(val.is_list()) {
		lua_newtable(L);
		for(const variant& v : val.as_list()) {
			lua_pushinteger(L, lua_rawlen(L, -1) + 1);
			luaW_pushfaivariant(L, v);
			lua_settable(L, -3);
		}
	} else if(val.is_map()) {
		typedef std::map<variant,variant>::value_type kv_type;
		lua_newtable(L);
		for(const kv_type& v : val.as_map()) {
			luaW_pushfaivariant(L, v.first);
			luaW_pushfaivariant(L, v.second);
			lua_settable(L, -3);
		}
	} else if(val.is_callable()) {
		// First try a few special cases
		if(auto u_ref = val.try_convert<unit_callable>()) {
			const unit& u = u_ref->get_unit();
			unit_map::iterator un_it = resources::gameboard->units().find(u.get_location());
			if(&*un_it == &u) {
				luaW_pushunit(L, u.underlying_id());
			} else {
				luaW_pushunit(L, u.side(), u.underlying_id());
			}
		} else if(auto ut_ref = val.try_convert<unit_type_callable>()) {
			const unit_type& ut = ut_ref->get_unit_type();
			luaW_pushunittype(L, ut);
		} else if(auto atk_ref = val.try_convert<attack_type_callable>()) {
			const auto& atk = atk_ref->get_attack_type();
			luaW_pushweapon(L, atk.shared_from_this());
		} else if(auto team_ref = val.try_convert<team_callable>()) {
			auto t = team_ref->get_team();
			luaW_pushteam(L, t);
		} else if(auto loc_ref = val.try_convert<location_callable>()) {
			luaW_pushlocation(L, loc_ref->loc());
		} else {
			// If those fail, convert generically to a map
			auto obj = val.as_callable();
			formula_input_vector inputs;
			obj->get_inputs(inputs);
			lua_newtable(L);
			for(const formula_input& attr : inputs) {
				if(attr.access == formula_access::write_only) {
					continue;
				}
				lua_pushstring(L, attr.name.c_str());
				luaW_pushfaivariant(L, obj->query_value(attr.name));
				lua_settable(L, -3);
			}
		}
	} else if(val.is_null()) {
		lua_pushnil(L);
	}
}

variant luaW_tofaivariant(lua_State* L, int i) {
	switch(lua_type(L, i)) {
		case LUA_TBOOLEAN:
			return variant(lua_tointeger(L, i));
		case LUA_TNUMBER:
			return variant(lua_tonumber(L, i), variant::DECIMAL_VARIANT);
		case LUA_TSTRING:
			return variant(lua_tostring(L, i));
		case LUA_TTABLE:
			return variant(std::make_shared<lua_callable>(L, i));
		case LUA_TUSERDATA:
			static t_string tstr;
			static vconfig vcfg = vconfig::unconstructed_vconfig();
			static map_location loc;
			if(luaW_totstring(L, i, tstr)) {
				return variant(tstr.str());
			} else if(luaW_tovconfig(L, i, vcfg)) {
				return variant(std::make_shared<config_callable>(vcfg.get_parsed_config()));
			} else if(unit* u = luaW_tounit(L, i)) {
				return variant(std::make_shared<unit_callable>(*u));
			} else if(const unit_type* ut = luaW_tounittype(L, i)) {
				return variant(std::make_shared<unit_type_callable>(*ut));
			} else if(const_attack_ptr atk = luaW_toweapon(L, i)) {
				return variant(std::make_shared<attack_type_callable>(*atk));
			} else if(team* t = luaW_toteam(L, i)) {
				return variant(std::make_shared<team_callable>(*t));
			} else if(luaW_tolocation(L, i, loc)) {
				return variant(std::make_shared<location_callable>(loc));
			}
			break;
	}
	return variant();
}

/**
 * Evaluates a formula in the formula engine.
 * - Arg 1: Formula string.
 * - Arg 2: optional context; can be a unit or a Lua table.
 * - Ret 1: Result of the formula.
 */
int lua_formula_bridge::intf_eval_formula(lua_State *L)
{
	bool need_delete = false;
	fwrapper* form;
	if(void* ud = luaL_testudata(L, 1, formulaKey)) {
		form = static_cast<fwrapper*>(ud);
	} else {
		need_delete = true;
		form = new fwrapper(luaL_checkstring(L, 1));
	}
	std::shared_ptr<formula_callable> context, fallback;
	if(unit* u = luaW_tounit(L, 2)) {
		context.reset(new unit_callable(*u));
	} else if(const unit_type* ut = luaW_tounittype(L, 2)) {
		context.reset(new unit_type_callable(*ut));
	} else if(const_attack_ptr atk = luaW_toweapon(L, 2)) {
		context.reset(new attack_type_callable(*atk));
	} else if(team* t = luaW_toteam(L, 2)) {
		context.reset(new team_callable(*t));
	} else if(lua_istable(L, 2)) {
		context.reset(new lua_callable(L, 2));
	} else {
		context.reset(new map_formula_callable);
	}
	variant result = form->evaluate(*context);
	luaW_pushfaivariant(L, result);
	if(need_delete) {
		delete form;
	}
	return 1;
}

int lua_formula_bridge::intf_compile_formula(lua_State* L)
{
	if(!lua_isstring(L, 1)) {
		luaW_type_error(L, 1, "string");
	}
	new(L) fwrapper(lua_tostring(L, 1));
	luaL_setmetatable(L, formulaKey);
	return 1;
}

lua_formula_bridge::fwrapper::fwrapper(const std::string& code)
	: formula_ptr(new formula(code))
{
}

std::string lua_formula_bridge::fwrapper::str() const
{
	if(formula_ptr) {
		return formula_ptr->str();
	}
	return "";
}

variant lua_formula_bridge::fwrapper::evaluate(const formula_callable& variables, formula_debugger* fdb) const
{
	if(formula_ptr) {
		return formula_ptr->evaluate(variables, fdb);
	}
	return variant();
}

static int impl_formula_collect(lua_State* L)
{
	lua_formula_bridge::fwrapper* form = static_cast<lua_formula_bridge::fwrapper*>(lua_touserdata(L, 1));
	form->~fwrapper();
	return 0;
}

static int impl_formula_tostring(lua_State* L)
{
	lua_formula_bridge::fwrapper* form = static_cast<lua_formula_bridge::fwrapper*>(lua_touserdata(L, 1));
	const std::string str = form->str();
	lua_pushlstring(L, str.c_str(), str.size());
	return 1;
}

std::string lua_formula_bridge::register_metatables(lua_State* L)
{
	luaL_newmetatable(L, formulaKey);
	lua_pushcfunction(L, impl_formula_collect);
	lua_setfield(L, -2, "__gc");
	lua_pushcfunction(L, impl_formula_tostring);
	lua_setfield(L, -2, "__tostring");
	lua_pushcfunction(L, intf_eval_formula);
	lua_setfield(L, -2, "__call");
	lua_pushstring(L, "formula");
	lua_setfield(L, -2, "__metatable");

	return "Adding formula metatable...\n";
}
