#pragma once
#include "util.h"
#include "../external_copy.h"

namespace catbox {

/**
 * Helpers to run a function and catch the various exceptions defined above
 */
namespace FunctorRunners {

template <typename F>
void RunBarrier(F fn) {
	// Runs a function and converts C++ errors to immediate v8 errors. Pretty much the same as
	// `RunCallback` but with no return value.
	try {
		fn();
	} catch (const js_fatal_error& cc_error) {
		// Execution is terminating
	} catch (const js_error_ctor_base& cc_error) {
		v8::Isolate::GetCurrent()->ThrowException(cc_error.ConstructError());
	} catch (const js_runtime_error& cc_error) {
		// A JS error is waiting in the isolate
	}
}

template <typename F, typename T>
void RunCallback(T& info, F fn) {
	// This function is used when C++ code is invoked from a JS callback. We are given an instance of
	// `FunctionCallbackInfo`, or `PropertyCallbackInfo` which is used to give the return value to v8.
	// C++ exceptions will be caught, converted to JS exceptions, and then thrown back to JS.
	try {
		v8::Local<v8::Value> result = fn();
		if (result.IsEmpty()) {
			throw std::logic_error("Callback returned empty Local<> but did not set exception");
		}
		info.GetReturnValue().Set(result);
	} catch (const js_error_ctor_base& cc_error) {
		v8::Local<v8::Value> error = cc_error.ConstructError();
		if (!error.IsEmpty()) {
			v8::Isolate::GetCurrent()->ThrowException(error);
		}
	} catch (const js_runtime_error& err) {
	}
}

template <typename F1, typename F2>
void RunCatchExternal(v8::Local<v8::Context> default_context, F1 fn1, F2 fn2) {
	// This function will call `fn1()` and if that fails it will convert the caught error to an
	// `ExternalCopy` and call `fn2(err)`
	v8::TryCatch try_catch(v8::Isolate::GetCurrent());
	try {
		try {
			fn1();
		} catch (const js_type_error& cc_error) {
			// The following errors are just various C++ strings with an error type
			fn2(std::make_unique<ExternalCopyError>(ExternalCopyError::ErrorType::TypeError, cc_error.GetMessage(), cc_error.GetStackTrace()));
		} catch (const js_range_error& cc_error) {
			fn2(std::make_unique<ExternalCopyError>(ExternalCopyError::ErrorType::RangeError, cc_error.GetMessage(), cc_error.GetStackTrace()));
		} catch (const js_generic_error& cc_error) {
			fn2(std::make_unique<ExternalCopyError>(ExternalCopyError::ErrorType::Error, cc_error.GetMessage(), cc_error.GetStackTrace()));
		} catch (const js_error_message& cc_error) {
			fn2(std::make_unique<ExternalCopyError>(ExternalCopyError::ErrorType::Error, cc_error.GetMessage()));
		} catch (const js_runtime_error& cc_error) {
			// If this is caught it means the error needs to be copied out of v8
			assert(try_catch.HasCaught());
			v8::Context::Scope context_scope(default_context);
			std::unique_ptr<ExternalCopy> error = ExternalCopy::CopyIfPrimitiveOrError(try_catch.Exception());
			if (error) {
				fn2(std::move(error));
			} else {
				fn2((std::make_unique<ExternalCopyError>(ExternalCopyError::ErrorType::Error,
					"An object was thrown from supplied code within catbox-v8, but that object was not an instance of `Error`."
				)));
			}
		}
	} catch (...) {
		if (try_catch.HasCaught()) {
			try_catch.ReThrow();
		}
		throw;
	}
}

template <typename F1, typename F2>
void RunCatchValue(F1 fn1, F2 fn2) {
	// This function will call `fn1()` and if that fails it will convert the caught error to a v8
	// value and pass it to fn3().
	//
	// *Fatal errors are swallowed*
	//
	v8::TryCatch try_catch(v8::Isolate::GetCurrent());
	try {
		try {
			fn1();
		} catch (const js_fatal_error& cc_error) {
			return;
		} catch (const js_error_ctor_base& cc_error) {
			// A C++ error thrown and needs to be internalized into v8
			fn2(cc_error.ConstructError());
		} catch (const js_runtime_error& cc_error) {
			// A JS error is waiting in the isolate
			assert(try_catch.HasCaught());
			v8::Local<v8::Value> error = try_catch.Exception();
			try_catch.Reset();
			fn2(error);
		}
	} catch (...) {
		if (try_catch.HasCaught()) {
			try_catch.ReThrow();
		}
		throw;
	}
}

}; // namespace FunctorRunners
}; // namespace catbox
