##! Definitions of built-in functions that allow the scripting layer to
##! change the value of options and to be notified when option values change.

module Option;

%%{
#include "zeek/NetVar.h"
#include "zeek/broker/Data.h"

static bool call_option_handlers_and_set_value(zeek::StringVal* name, const zeek::detail::IDPtr& i,
                                               zeek::ValPtr val, zeek::StringVal* location)
	{
	// when shutting down, don't call back into script land handlers as
	// these may use resources that have already been shutdown.
	if ( zeek::run_state::terminating )
		return false;

	if ( i->HasOptionHandlers() )
		{
		for ( auto handler_function : i->GetOptionHandlers() )
			{
			bool add_loc = handler_function->GetType()->ParamList()->GetTypes().size() == 3;
			zeek::Args vl;
			vl.reserve(2 + add_loc);
			vl.emplace_back(zeek::NewRef{}, name);
			vl.emplace_back(val);

			if ( add_loc )
				vl.emplace_back(zeek::NewRef{}, location);

			val = handler_function->Invoke(&vl); // consumed by next call.

			if ( ! val )
				{
				// Someone messed up, don't change value and just return
				return false;
				}
			}
		}

	// clone to prevent changes
	i->SetVal(val->Clone());
	return true;
	}
%%}

## Set an option to a new value. This change will also cause the option change
## handlers to be called.
##
## ID: The ID of the option to update.
##
## val: The new value of the option.
##
## location: Optional parameter detailing where this change originated from.
##
## Returns: true on success, false when an error occurred.
##
## .. zeek:see:: Option::set_change_handler Config::set_value
##
## .. note:: :zeek:id:`Option::set` only works on one node and does not distribute
##           new values across a cluster. The higher-level :zeek:id:`Config::set_value`
##           supports clusterization and should typically be used instead of this
##           lower-level function.
function Option::set%(ID: string, val: any, location: string &default=""%): bool
	%{
	const auto& i = zeek::detail::global_scope()->Find(ID->CheckString());
	if ( ! i )
		{
		zeek::emit_builtin_error(zeek::util::fmt("Could not find ID named '%s'", ID->CheckString()));
		return zeek::val_mgr->False();
		}

	if ( ! i->HasVal() )
		{
		// should be impossible because initialization is enforced
		zeek::emit_builtin_error(zeek::util::fmt("ID '%s' has no value", ID->CheckString()));
		return zeek::val_mgr->False();
		}

	if ( ! i->IsOption() )
		{
		zeek::emit_builtin_error(zeek::util::fmt("ID '%s' is not an option", ID->CheckString()));
		return zeek::val_mgr->False();
		}

	if ( same_type(val->GetType(), zeek::Broker::detail::DataVal::ScriptDataType()) )
		{
		auto valptr = val->AsRecordVal()->GetField(0);
		auto dv = static_cast<zeek::Broker::detail::DataVal*>(valptr.get());
		auto val_from_data = dv->castTo(i->GetType().get());

		if ( ! val_from_data )
			{
			zeek::emit_builtin_error(zeek::util::fmt("Incompatible type for set of ID '%s': got broker data '%s', need '%s'",
			                             ID->CheckString(), dv->data.get_type_name(),
			                             type_name(i->GetType()->Tag())));
			return zeek::val_mgr->False();
			}

		auto rval = call_option_handlers_and_set_value(ID, i, std::move(val_from_data), location);
		return zeek::val_mgr->Bool(rval);
		}

	if ( ! same_type(i->GetType(), val->GetType()) )
		{
		if ( i->GetType()->Tag() == TYPE_TABLE &&
		     val->GetType()->Tag() == TYPE_TABLE &&
		     val->GetType()->AsTableType()->IsUnspecifiedTable() )
			{
			// Just coerce an empty/unspecified table to the right type.
			auto tv = zeek::make_intrusive<zeek::TableVal>(
			        zeek::cast_intrusive<TableType>(i->GetType()),
			        i->GetVal()->AsTableVal()->GetAttrs());
			auto rval = call_option_handlers_and_set_value(ID, i, std::move(tv), location);
			return zeek::val_mgr->Bool(rval);
			}

		zeek::emit_builtin_error(zeek::util::fmt("Incompatible type for set of ID '%s': got '%s', need '%s'",
		                             ID->CheckString(), type_name(val->GetType()->Tag()),
		                             type_name(i->GetType()->Tag())));
		return zeek::val_mgr->False();
		}

	auto rval = call_option_handlers_and_set_value(ID, i, {zeek::NewRef{}, val}, location);
	return zeek::val_mgr->Bool(rval);
	%}

## Set a change handler for an option. The change handler will be
## called anytime :zeek:id:`Option::set` is called for the option.
##
## ID: The ID of the option for which change notifications are desired.
##
## on_change: The function that will be called when a change occurs. The
##            function can choose to receive two or three parameters: the first
##            parameter is a string containing *ID*, the second parameter is
##            the new option value. The third, optional, parameter is the
##            location string as passed to Option::set. Note that the global
##            value is not yet changed when the function is called. The passed
##            function has to return the new value that it wants the option to
##            be set to. This enables it to reject changes, or change values
##            that are being set. When several change handlers are set for an
##            option they are chained; the second change handler will see the
##            return value of the first change handler as the "new value".
##
## priority: The priority of the function that was added; functions with higher
##           priority are called first, functions with the same priority are
##           called in the order in which they were added.
##
## Returns: true when the change handler was set, false when an error occurred.
##
## .. zeek:see:: Option::set
function Option::set_change_handler%(ID: string, on_change: any, priority: int &default=0%): bool
	%{
	const auto& i = zeek::detail::global_scope()->Find(ID->CheckString());
	if ( ! i )
		{
		zeek::emit_builtin_error(zeek::util::fmt("Could not find ID named '%s'", ID->CheckString()));
		return zeek::val_mgr->False();
		}

	if ( ! i->IsOption() )
		{
		zeek::emit_builtin_error(zeek::util::fmt("ID '%s' is not an option", ID->CheckString()));
		return zeek::val_mgr->False();
		}

	if ( on_change->GetType()->Tag() != TYPE_FUNC )
		{
		zeek::emit_builtin_error(zeek::util::fmt("Option::on_change needs function argument; got '%s' for ID '%s'",
			                     type_name(on_change->GetType()->Tag()), ID->CheckString()));
		return zeek::val_mgr->False();
		}

	if ( on_change->GetType()->AsFuncType()->Flavor() != zeek::FUNC_FLAVOR_FUNCTION )
		{
		zeek::emit_builtin_error("Option::on_change needs function argument; not hook or event");
		return zeek::val_mgr->False();
		}

	const auto& args = on_change->GetType()->AsFuncType()->ParamList()->GetTypes();
	if ( args.size() < 2 || args.size() > 3 )
		{
		zeek::emit_builtin_error(zeek::util::fmt("Wrong number of arguments for passed function in Option::on_change for ID '%s'; expected 2 or 3, got %zu",
		                             ID->CheckString(), args.size()));
		return zeek::val_mgr->False();
		}

	if ( args[0]->Tag() != TYPE_STRING )
		{
		zeek::emit_builtin_error(zeek::util::fmt("First argument of passed function has to be string in Option::on_change for ID '%s'; got '%s'",
		                             ID->CheckString(), type_name(args[0]->Tag())));
		return zeek::val_mgr->False();
		}

	if ( ! same_type(args[1], i->GetType()) )
		{
		zeek::emit_builtin_error(zeek::util::fmt("Second argument of passed function has to be %s in Option::on_change for ID '%s'; got '%s'",
		                             type_name(i->GetType()->Tag()), ID->CheckString(),
		                             type_name(args[1]->Tag())));
		return zeek::val_mgr->False();
		}

	if ( args.size() == 3 && args[2]->Tag() != TYPE_STRING )
		{
		zeek::emit_builtin_error(zeek::util::fmt("Third argument of passed function has to be string in Option::on_change for ID '%s'; got '%s'",
			ID->CheckString(), type_name(args[2]->Tag())));
		return zeek::val_mgr->False();
		}

	if ( ! same_type(on_change->GetType()->AsFuncType()->Yield(), i->GetType()) )
		{
		zeek::emit_builtin_error(zeek::util::fmt("Passed function needs to return type '%s' for ID '%s'; got '%s'",
		                             type_name(i->GetType()->Tag()), ID->CheckString(),
		                             type_name(on_change->GetType()->AsFuncType()->Yield()->Tag())));
		return zeek::val_mgr->False();
		}

	auto func = on_change->As<FuncVal*>()->AsFuncPtr();
	i->AddOptionHandler(func, -priority);
	return zeek::val_mgr->True();
	%}

## Helper function that converts a set (of arbitrary index type) to
## a "vector of any".
##
## v: an "any" type corresponding to a set.
##
## Returns: a vector-of-any with one element for each member of v.
function Option::any_set_to_any_vec%(v: any%): any_vec
	%{
	auto ret_type = make_intrusive<VectorType>(base_type(TYPE_ANY));
	auto ret = make_intrusive<VectorVal>(ret_type);
	const auto& t = v->GetType();

	if ( ! t->IsSet() )
		{
		zeek::emit_builtin_error("argument is not a set");
		return ret;
		}

	auto lv = v->AsTableVal()->ToListVal();
	auto n = lv->Length();

	for ( int i = 0; i < n; ++i )
		ret->Assign(i, lv->Idx(i));

	return ret;
	%}
