#include "three_phase_task.h"
#include "../external_copy.h"

using namespace v8;
using std::unique_ptr;

namespace catbox {

/**
 * CalleeInfo implementation
 */
ThreePhaseTask::CalleeInfo::CalleeInfo(
	Local<Promise::Resolver> resolver,
	Local<Context> context,
	Local<StackTrace> stack_trace
) : remotes(resolver, context, stack_trace) {
	IsolateEnvironment* env = IsolateEnvironment::GetCurrent();
	if (env->IsDefault()) {
		async = node::EmitAsyncInit(env->GetIsolate(), resolver->GetPromise(), v8_symbol("catbox-v8"));
	}
}

ThreePhaseTask::CalleeInfo::~CalleeInfo() {
	IsolateEnvironment* env = IsolateEnvironment::GetCurrent();
	if (env->IsDefault()) {
		node::EmitAsyncDestroy(env->GetIsolate(), async);
	}
}

/**
 * Wrapper around node's version of the same class which does nothing if this isn't the node
 * isolate.
 *
 * nb: CallbackScope sets up a v8::TryCatch so if you need to catch an exception do this *before*
 * the v8::TryCatch.
 */
struct CallbackScope {
	unique_ptr<node::CallbackScope> scope;

	CallbackScope(node::async_context async, Local<Object> resource) {
		IsolateEnvironment* env = IsolateEnvironment::GetCurrent();
		if (env->IsDefault()) {
			scope = std::make_unique<node::CallbackScope>(env->GetIsolate(), resource, async);
		}
	}
};

/**
 * Phase2Runner implementation
 */
ThreePhaseTask::Phase2Runner::Phase2Runner(
	unique_ptr<ThreePhaseTask> self,
	unique_ptr<CalleeInfo> info
) :
	self(std::move(self)),
	info(std::move(info))	{}

ThreePhaseTask::Phase2Runner::~Phase2Runner() {
	if (!did_run) {
		// The task never got to run
		struct Phase3Orphan : public Runnable {
			unique_ptr<ThreePhaseTask> self;
			unique_ptr<CalleeInfo> info;

			Phase3Orphan(
				unique_ptr<ThreePhaseTask> self,
				unique_ptr<CalleeInfo> info
			) :
				self(std::move(self)),
				info(std::move(info)) {}

			void Run() final {
				// Revive our persistent handles
				Isolate* isolate = Isolate::GetCurrent();
				auto context_local = info->remotes.Deref<1>();
				Context::Scope context_scope(context_local);
				auto promise_local = info->remotes.Deref<0>();
				CallbackScope callback_scope(info->async, promise_local);
				// Throw from promise
				Local<Object> error = Exception::Error(v8_string("Isolate is disposed")).As<Object>();
				StackTraceHolder::AttachStack(error, info->remotes.Deref<2>());
				Unmaybe(promise_local->Reject(context_local, error));
				isolate->RunMicrotasks();
			}
		};
		// Schedule a throw task back in first isolate
		auto holder = info->remotes.GetIsolateHolder();
		holder->ScheduleTask(
			std::make_unique<Phase3Orphan>(
				std::move(self),
				std::move(info)
			), false, true
		);
	}
}

void ThreePhaseTask::Phase2Runner::Run() {

	// This class will be used if Phase2() throws an error
	struct Phase3Failure : public Runnable {
		unique_ptr<ThreePhaseTask> self;
		unique_ptr<CalleeInfo> info;
		unique_ptr<ExternalCopy> error;

		Phase3Failure(
			unique_ptr<ThreePhaseTask> self,
			unique_ptr<CalleeInfo> info,
			unique_ptr<ExternalCopy> error
		) :
			self(std::move(self)),
			info(std::move(info)),
			error(std::move(error)) {}

		void Run() final {
			// Revive our persistent handles
			Isolate* isolate = Isolate::GetCurrent();
			auto context_local = info->remotes.Deref<1>();
			Context::Scope context_scope(context_local);
			auto promise_local = info->remotes.Deref<0>();
			CallbackScope callback_scope(info->async, promise_local);
			Local<Value> rejection;
			if (error) {
				rejection = error->CopyInto();
			} else {
				rejection = Exception::Error(v8_string("An exception was thrown. Sorry I don't know more."));
			}
			if (rejection->IsObject()) {
				StackTraceHolder::ChainStack(rejection.As<Object>(), info->remotes.Deref<2>());
			}
			// If Reject fails then I think that's bad..
			Unmaybe(promise_local->Reject(context_local, rejection));
			isolate->RunMicrotasks();
		}
	};

	// This is called if Phase2() does not throw
	struct Phase3Success : public Runnable {
		unique_ptr<ThreePhaseTask> self;
		unique_ptr<CalleeInfo> info;

		Phase3Success(
			unique_ptr<ThreePhaseTask> self,
			unique_ptr<CalleeInfo> info
		) :
			self(std::move(self)),
			info(std::move(info)) {}

		void Run() final {
			Isolate* isolate = Isolate::GetCurrent();
			auto context_local = info->remotes.Deref<1>();
			Context::Scope context_scope(context_local);
			auto promise_local = info->remotes.Deref<0>();
			CallbackScope callback_scope(info->async, promise_local);
			FunctorRunners::RunCatchValue([&]() {
				// Final callback
				Unmaybe(promise_local->Resolve(context_local, self->Phase3()));
			}, [&](Local<Value> error) {
				// Error was thrown
				if (error->IsObject()) {
					StackTraceHolder::AttachStack(error.As<Object>(), info->remotes.Deref<2>());
				}
				Unmaybe(promise_local->Reject(context_local, error));
			});
			isolate->RunMicrotasks();
		}
	};

	did_run = true;
	FunctorRunners::RunCatchExternal(IsolateEnvironment::GetCurrent()->DefaultContext(), [ this ]() {
		// Continue the task
		self->Phase2();
		IsolateEnvironment::GetCurrent()->TaskEpilogue();
		auto holder = info->remotes.GetIsolateHolder();
		holder->ScheduleTask(std::make_unique<Phase3Success>(std::move(self), std::move(info)), false, true);
	}, [ this ](unique_ptr<ExternalCopy> error) {

		// Schedule a task to enter the first isolate so we can throw the error at the promise
		auto holder = info->remotes.GetIsolateHolder();
		holder->ScheduleTask(std::make_unique<Phase3Failure>(std::move(self), std::move(info), std::move(error)), false, true);
	});
}

/**
 * Phase2RunnerIgnored implementation
 */
ThreePhaseTask::Phase2RunnerIgnored::Phase2RunnerIgnored(unique_ptr<ThreePhaseTask> self) : self(std::move(self)) {}

void ThreePhaseTask::Phase2RunnerIgnored::Run() {
	TryCatch try_catch(Isolate::GetCurrent());
	try {
		self->Phase2();
		IsolateEnvironment::GetCurrent()->TaskEpilogue();
	} catch (const js_runtime_error& cc_error) {}
}

/**
 * RunSync implementation
 */
Local<Value> ThreePhaseTask::RunSync(IsolateHolder& second_isolate, bool allow_async) {
	// Grab a reference to second isolate
	auto second_isolate_ref = second_isolate.GetIsolate();
	if (!second_isolate_ref) {
		throw js_generic_error("Isolated is disposed");
	}
	if (second_isolate_ref->GetIsolate() == Isolate::GetCurrent()) {
		if (allow_async) {
			throw js_generic_error("This function may not be called from the default thread");
		}
		// Shortcut when calling a sync method belonging to the currently entered isolate. This avoids
		// the deadlock protection below
		Phase2();

	} else {

		bool is_recursive = Locker::IsLocked(second_isolate_ref->GetIsolate());
		if (IsolateEnvironment::Executor::IsDefaultThread() || is_recursive) {
			if (allow_async) {
				throw js_generic_error("This function may not be called from the default thread");
			}

			// This is the simple sync runner case
			unique_ptr<ExternalCopy> error;
			{
				IsolateEnvironment::Executor::Lock lock(*second_isolate_ref);

				// Run handle tasks first
				std::queue<std::unique_ptr<Runnable>> handle_tasks;
				{
					IsolateEnvironment::Scheduler::Lock scheduler_lock(second_isolate_ref->scheduler);
					handle_tasks = scheduler_lock.TakeHandleTasks();
				}
				while (!handle_tasks.empty()) {
					handle_tasks.front()->Run();
					handle_tasks.pop();
				}

				// Now run the actual work
				FunctorRunners::RunCatchExternal(second_isolate_ref->DefaultContext(), [ this, is_recursive, &second_isolate_ref ]() {
					// Run Phase2 and externalize errors
					Phase2();
					if (!is_recursive) {
						second_isolate_ref->TaskEpilogue();
					}
				}, [ &error ](unique_ptr<ExternalCopy> error_inner) {

					// We need to stash the error in the outer unique_ptr because the executor lock is still up
					error = std::move(error_inner);
				});
			}

			if (error) {
				// Throw to outer isolate
				Isolate* isolate = Isolate::GetCurrent();
				Local<Value> error_copy = error->CopyInto();
				if (error_copy->IsObject()) {
					StackTraceHolder::ChainStack(error_copy.As<Object>(), StackTrace::CurrentStackTrace(isolate, 10));
				}
				isolate->ThrowException(error_copy);
				throw js_runtime_error();
			}

		} else if (second_isolate_ref->IsDefault()) {

			// In this case we asyncronously call the default thread and suspend this thread
			struct AsyncRunner : public Runnable {
				bool allow_async = false;
				bool did_run = false;
				bool is_async = false;
				ThreePhaseTask& self;
				IsolateEnvironment::Scheduler::AsyncWait& wait;
				unique_ptr<ExternalCopy>& error;

				AsyncRunner(
					ThreePhaseTask& self,
					IsolateEnvironment::Scheduler::AsyncWait& wait,
					bool allow_async,
					unique_ptr<ExternalCopy>& error
				) : allow_async(allow_async), self(self), wait(wait), error(error) {}

				AsyncRunner(const AsyncRunner&) = delete;
				AsyncRunner& operator=(const AsyncRunner&) = delete;

				~AsyncRunner() final {
					if (!did_run) {
						error = std::make_unique<ExternalCopyError>(ExternalCopyError::ErrorType::Error, "Isolate is disposed");
					}
					if (!is_async) {
						wait.Wake();
					}
					wait.Ready();
				}

				void Run() final {
					did_run = true;
					FunctorRunners::RunCatchExternal(IsolateEnvironment::GetCurrent()->DefaultContext(), [ this ]() {
						// Now in the default thread
						if (allow_async) {
							is_async = self.Phase2Async(wait);
						} else {
							self.Phase2();
						}
						IsolateEnvironment::GetCurrent()->TaskEpilogue();
					}, [ this ](unique_ptr<ExternalCopy> error) {
						this->error = std::move(error);
					});
				}
			};

			Isolate* isolate = Isolate::GetCurrent();
			unique_ptr<ExternalCopy> error;
			{
				// Setup condition variable to sleep this thread
				IsolateEnvironment& env = *IsolateEnvironment::GetCurrent();
				IsolateEnvironment::Scheduler::AsyncWait wait(env.scheduler);
				// Scope to unlock v8 in this thread and set up the wait
				IsolateEnvironment::Executor::Unlock unlocker(env);
				// Run it and sleep
				second_isolate.ScheduleTask(std::make_unique<AsyncRunner>(*this, wait, allow_async, error), false, true);
				wait.Wait();
			}

			// At this point thread synchronization is done and Phase2() has finished
			if (error) {
				Local<Value> error_copy = error->CopyInto();
				if (error_copy->IsObject()) {
					StackTraceHolder::ChainStack(error_copy.As<Object>(), StackTrace::CurrentStackTrace(isolate, 10));
				}
				isolate->ThrowException(error_copy);
				throw js_runtime_error();
			}

		} else {

			// ~ very specific error message ~
			throw js_generic_error(
				"Calling a synchronous catbox-v8 function on a non-default isolate from within an asynchronous catbox-v8 function is not allowed."
			);
		}
	}

	// Final phase
	return Phase3();
}

} // namespace catbox
