##! Functions related to asynchronous storage operations.

%%{
#include "zeek/Frame.h"
#include "zeek/Trigger.h"
#include "zeek/storage/Backend.h"
#include "zeek/storage/Manager.h"
#include "zeek/storage/ReturnCode.h"

using namespace zeek;
using namespace zeek::storage;

// Utility method for initializing a trigger from a Frame passed into a BIF. This is
// used by the asynchronous methods to make sure the trigger is setup before starting
// the operations. It also does some sanity checking to ensure the trigger is valid.

static zeek::detail::trigger::TriggerPtr init_trigger(zeek::detail::Frame* frame) {
    auto trigger = frame->GetTrigger();

    if ( ! trigger ) {
        emit_builtin_error("Asynchronous storage operations must be called via a when-condition");
        return nullptr;
    }

    if ( auto timeout = trigger->TimeoutValue(); timeout < 0 ) {
        emit_builtin_error("Asynchronous storage operations must specify a timeout block");
        return nullptr;
    }

    frame->SetDelayed();
    trigger->Hold();

    return {NewRef{}, trigger};
}

static void handle_async_result(const IntrusivePtr<Backend>& backend, ResultCallback* cb,
                                const OperationResult& op_result) {
    if ( op_result.code != ReturnCode::IN_PROGRESS || ! backend->SupportsAsync() ) {
        // We need to complete the callback early if:
        // 1. The operation didn't start up successfully. For async operations, this means
        // it didn't report back IN_PROGRESS.
        // 2. The backend doesn't support async. This means we already blocked in order
        // to get here already.
        // Also check for timeout conditions.
        if ( op_result.code == ReturnCode::TIMEOUT )
            cb->Timeout();
        else
            cb->Complete(op_result);
        delete cb;
    }
    else if ( run_state::reading_traces ) {
        // If the backend is truly async and we're reading traces, we need to fake being
        // in sync mode because otherwise time doesn't move forward correctly.
        backend->Poll();
    }
}

%%}

module Storage::Async;

function Storage::Async::__open_backend%(btype: Storage::Backend, options: any, key_type: any, val_type: any%): Storage::OperationResult
	%{
	auto trigger = init_trigger(frame);
	if ( ! trigger )
		return nullptr;

	auto btype_val = IntrusivePtr<EnumVal>{NewRef{}, btype->AsEnumVal()};
	Tag tag{btype_val};

	auto b = storage_mgr->InstantiateBackend(tag);

	if ( ! b.has_value() ) {
		trigger->Cache(
			frame->GetTriggerAssoc(),
			new StringVal(util::fmt("Failed to instantiate backend: %s", b.error().c_str())));
		trigger->Release();
		return nullptr;
	}

	auto bh = make_intrusive<storage::detail::BackendHandleVal>(b.value());

	auto cb = new OpenResultCallback(trigger, frame->GetTriggerAssoc(), bh);
	auto kt = key_type->AsTypeVal()->GetType()->AsTypeType()->GetType();
	auto vt = val_type->AsTypeVal()->GetType()->AsTypeType()->GetType();
	auto options_val = IntrusivePtr<RecordVal>{NewRef{}, options->AsRecordVal()};
	auto op_result = storage_mgr->OpenBackend(b.value(), cb, options_val, kt, vt);

	handle_async_result(b.value(), cb, op_result);

	return nullptr;
	%}

function Storage::Async::__close_backend%(backend: opaque of Storage::BackendHandle%) : Storage::OperationResult
	%{
	auto trigger = init_trigger(frame);
	if ( ! trigger )
		return nullptr;

	auto cb = new ResultCallback(trigger, frame->GetTriggerAssoc());
	auto b = storage::detail::BackendHandleVal::CastFromAny(backend);
	if ( ! b ) {
		cb->Complete(b.error());
		delete cb;
		return nullptr;
	}

	auto op_result = storage_mgr->CloseBackend((*b)->backend, cb);
	handle_async_result((*b)->backend, cb, op_result);

	return nullptr;
	%}

function Storage::Async::__put%(backend: opaque of Storage::BackendHandle, key: any, value: any,
			 overwrite: bool, expire_time: interval%): Storage::OperationResult
	%{
	auto trigger = init_trigger(frame);
	if ( ! trigger )
		return nullptr;

	auto cb = new ResultCallback(trigger, frame->GetTriggerAssoc());
	auto b = storage::detail::BackendHandleVal::CastFromAny(backend);
	if ( ! b ) {
		cb->Complete(b.error());
		delete cb;
		return nullptr;
	}

	if ( expire_time > 0.0 )
		expire_time += run_state::network_time;

	auto key_v = IntrusivePtr<Val>{NewRef{}, key};
	auto val_v = IntrusivePtr<Val>{NewRef{}, value};
	auto op_result = (*b)->backend->Put(cb, key_v, val_v, overwrite, expire_time);
	handle_async_result((*b)->backend, cb, op_result);

	return nullptr;
	%}

function Storage::Async::__get%(backend: opaque of Storage::BackendHandle, key: any%): Storage::OperationResult
	%{
	auto trigger = init_trigger(frame);
	if ( ! trigger )
		return nullptr;

	auto cb = new ResultCallback(trigger, frame->GetTriggerAssoc());
	auto b = storage::detail::BackendHandleVal::CastFromAny(backend);
	if ( ! b ) {
		cb->Complete(b.error());
		delete cb;
		return nullptr;
	}

	auto key_v = IntrusivePtr<Val>{NewRef{}, key};
	auto op_result = (*b)->backend->Get(cb, key_v);
	handle_async_result((*b)->backend, cb, op_result);

	return nullptr;
	%}

function Storage::Async::__erase%(backend: opaque of Storage::BackendHandle, key: any%): Storage::OperationResult
	%{
	auto trigger = init_trigger(frame);
	if ( ! trigger )
		return nullptr;

	auto cb = new ResultCallback(trigger, frame->GetTriggerAssoc());
	auto b = storage::detail::BackendHandleVal::CastFromAny(backend);
	if ( ! b ) {
		cb->Complete(b.error());
		delete cb;
		return nullptr;
	}

	auto key_v = IntrusivePtr<Val>{NewRef{}, key};
	auto op_result = (*b)->backend->Erase(cb, key_v);
	handle_async_result((*b)->backend, cb, op_result);

	return nullptr;
	%}
