/*
** vmframe.cpp
**
**---------------------------------------------------------------------------
** Copyright -2016 Randy Heit
** Copyright 2016 Christoph Oelckers
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
**    notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
**    notice, this list of conditions and the following disclaimer in the
**    documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
**    derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
*/

#include <new>
#include "dobject.h"
#include "v_text.h"
#include "stats.h"
#include "c_dispatch.h"

#include "vmintern.h"
#include "types.h"
#include "jit.h"
#include "c_cvars.h"
#include "version.h"

#ifdef HAVE_VM_JIT
#ifdef __DragonFly__
CUSTOM_CVAR(Bool, vm_jit, false, CVAR_NOINITCALL)
#else
CUSTOM_CVAR(Bool, vm_jit, true, CVAR_NOINITCALL)
#endif
{
	Printf("You must restart " GAMENAME " for this change to take effect.\n");
	Printf("This cvar is currently not saved. You must specify it on the command line.");
}
CUSTOM_CVAR(Bool, vm_jit_aot, true, CVAR_NOINITCALL)
{
	Printf("You must restart " GAMENAME " for this change to take effect.\n");
	Printf("This cvar is currently not saved. You must specify it on the command line.");
}
#else
CVAR(Bool, vm_jit, false, CVAR_NOINITCALL|CVAR_NOSET)
CVAR(Bool, vm_jit_aot, false, CVAR_NOINITCALL|CVAR_NOSET)
FString JitCaptureStackTrace(int framesToSkip, bool includeNativeFrames, int maxFrames) { return FString(); }
void JitRelease() {}
#endif

cycle_t VMCycles[10];
int VMCalls[10];

#if 0
IMPLEMENT_CLASS(VMException, false, false)
#endif

TArray<VMFunction *> VMFunction::AllFunctions;

// Creates the register type list for a function.
// Native functions only need this to assert their parameters in debug mode, script functions use this to load their registers from the VMValues.
void VMFunction::CreateRegUse()
{
#ifdef NDEBUG
	if (VarFlags & VARF_Native) return;	// we do not need this for native functions in release builds.
#endif
	int count = 0;
	if (!Proto)
	{
		//if (RegTypes) return;
		//Printf(TEXTCOLOR_ORANGE "Function without prototype needs register info manually set: %s\n", PrintableName);
		return;
	}
	assert(Proto->isPrototype());

	for (auto arg : Proto->ArgumentTypes)
	{
		count += arg? arg->GetRegCount() : 1;
	}
	uint8_t *regp;
	RegTypes = regp = (uint8_t*)ClassDataAllocator.Alloc(count);
	count = 0;
	for (unsigned i = 0; i < Proto->ArgumentTypes.Size(); i++)
	{
		auto arg = Proto->ArgumentTypes[i];
		auto flg = ArgFlags.Size() > i ? ArgFlags[i] : 0;
		if (arg == nullptr)
		{
			// Marker for start of varargs.
			*regp++ = REGT_NIL;
		}
		else if ((flg & VARF_Out) && !arg->isPointer())
		{
			*regp++ = REGT_POINTER;
		}
		else for (int j = 0; j < arg->GetRegCount(); j++)
		{
			*regp++ = arg->GetRegType();
		}
	}
}

VMScriptFunction::VMScriptFunction(FName name)
{
	Name = name;
	LineInfo = nullptr;
	Code = NULL;
	KonstD = NULL;
	KonstF = NULL;
	KonstS = NULL;
	KonstA = NULL;
	LineInfoCount = 0;
	ExtraSpace = 0;
	CodeSize = 0;
	NumRegD = 0;
	NumRegF = 0;
	NumRegS = 0;
	NumRegA = 0;
	NumKonstD = 0;
	NumKonstF = 0;
	NumKonstS = 0;
	NumKonstA = 0;
	MaxParam = 0;
	NumArgs = 0;
	ScriptCall = &VMScriptFunction::FirstScriptCall;
}

VMScriptFunction::~VMScriptFunction()
{
	if (Code != NULL)
	{
		if (KonstS != NULL)
		{
			for (int i = 0; i < NumKonstS; ++i)
			{
				KonstS[i].~FString();
			}
		}
	}
}

void VMScriptFunction::Alloc(int numops, int numkonstd, int numkonstf, int numkonsts, int numkonsta, int numlinenumbers)
{
	assert(Code == NULL);
	assert(numops > 0);
	assert(numkonstd >= 0 && numkonstd <= 65535);
	assert(numkonstf >= 0 && numkonstf <= 65535);
	assert(numkonsts >= 0 && numkonsts <= 65535);
	assert(numkonsta >= 0 && numkonsta <= 65535);
	assert(numlinenumbers >= 0 && numlinenumbers <= 65535);
	void *mem = ClassDataAllocator.Alloc(numops * sizeof(VMOP) +
						 numkonstd * sizeof(int) +
						 numkonstf * sizeof(double) +
						 numkonsts * sizeof(FString) +
						 numkonsta * sizeof(FVoidObj) +
						 numlinenumbers * sizeof(FStatementInfo));
	Code = (VMOP *)mem;
	mem = (void *)((VMOP *)mem + numops);

	if (numlinenumbers > 0)
	{
		LineInfo = (FStatementInfo*)mem;
		LineInfoCount = numlinenumbers;
		mem = LineInfo + numlinenumbers;
	}
	else
	{
		LineInfo = nullptr;
		LineInfoCount = 0;
	}
	if (numkonstd > 0)
	{
		KonstD = (int *)mem;
		mem = (void *)((int *)mem + numkonstd);
	}
	else
	{
		KonstD = NULL;
	}
	if (numkonstf > 0)
	{
		KonstF = (double *)mem;
		mem = (void *)((double *)mem + numkonstf);
	}
	else
	{
		KonstF = NULL;
	}
	if (numkonsts > 0)
	{
		KonstS = (FString *)mem;
		for (int i = 0; i < numkonsts; ++i)
		{
			::new(&KonstS[i]) FString;
		}
		mem = (void *)((FString *)mem + numkonsts);
	}
	else
	{
		KonstS = NULL;
	}
	if (numkonsta > 0)
	{
		KonstA = (FVoidObj *)mem;
	}
	else
	{
		KonstA = NULL;
	}
	CodeSize = numops;
	NumKonstD = numkonstd;
	NumKonstF = numkonstf;
	NumKonstS = numkonsts;
	NumKonstA = numkonsta;
}

void VMScriptFunction::InitExtra(void *addr)
{
	char *caddr = (char*)addr;

	for (auto tao : SpecialInits)
	{
		tao.first->InitializeValue(caddr + tao.second, nullptr);
	}
}

void VMScriptFunction::DestroyExtra(void *addr)
{
	char *caddr = (char*)addr;

	for (auto tao : SpecialInits)
	{
		tao.first->DestroyValue(caddr + tao.second);
	}
}

int VMScriptFunction::AllocExtraStack(PType *type)
{
	int address = ((ExtraSpace + type->Align - 1) / type->Align) * type->Align;
	ExtraSpace = address + type->Size;
	type->SetDefaultValue(nullptr, address, &SpecialInits);
	return address;
}

int VMScriptFunction::PCToLine(const VMOP *pc)
{
	int PCIndex = int(pc - Code);
	if (LineInfoCount == 1) return LineInfo[0].LineNumber;
	for (unsigned i = 1; i < LineInfoCount; i++)
	{
		if (LineInfo[i].InstructionIndex > PCIndex)
		{
			return LineInfo[i - 1].LineNumber;
		}
	}
	return -1;
}

static bool CanJit(VMScriptFunction *func)
{
	// Asmjit has a 256 register limit. Stay safely away from it as the jit compiler uses a few for temporaries as well.
	// Any function exceeding the limit will use the VM - a fair punishment to someone for writing a function so bloated ;)

	if(func->blockJit) return false;

	int maxregs = 200;
	if (func->NumRegA + func->NumRegD + func->NumRegF + func->NumRegS < maxregs)
		return true;

	Printf(TEXTCOLOR_ORANGE "%s is using too many registers (%d of max %d)! Function will not use native code.\n", func->PrintableName, func->NumRegA + func->NumRegD + func->NumRegF + func->NumRegS, maxregs);

	return false;
}

void VMScriptFunction::JitCompile()
{
	if(!(VarFlags & VARF_Abstract))
	{
	#ifdef HAVE_VM_JIT
		if (vm_jit && CanJit(this))
		{
			ScriptCall = ::JitCompile(this);
			if (!ScriptCall)
				ScriptCall = VMExec;
		}
		else
	#endif // HAVE_VM_JIT
		{
			ScriptCall = VMExec;
		}
	}
}

int VMScriptFunction::FirstScriptCall(VMFunction *func, VMValue *params, int numparams, VMReturn *ret, int numret)
{
	// [Player701] Check that we aren't trying to call an abstract function.
	// This shouldn't happen normally, but if it does, let's catch this explicitly
	// rather than let GZDoom crash.
	if (func->VarFlags & VARF_Abstract)
	{
		ThrowAbortException(X_OTHER, "attempt to call abstract function %s.", func->PrintableName);
	}
	
	static_cast<VMScriptFunction*>(func)->JitCompile();

	return func->ScriptCall(func, params, numparams, ret, numret);
}

int VMNativeFunction::NativeScriptCall(VMFunction *func, VMValue *params, int numparams, VMReturn *returns, int numret)
{
	try
	{
		VMCycles[0].Unclock();
		numret = static_cast<VMNativeFunction *>(func)->NativeCall(VM_INVOKE(params, numparams, returns, numret, func->RegTypes));
		VMCycles[0].Clock();

		return numret;
	}
	catch (CVMAbortException &err)
	{
		err.MaybePrintMessage();
		err.stacktrace.AppendFormat("Called from %s\n", func->PrintableName);
		throw;
	}
}

//===========================================================================
//
// VMFrame :: InitRegS
//
// Initialize the string registers of a newly-allocated VMFrame.
//
//===========================================================================

void VMFrame::InitRegS()
{
	FString *regs = GetRegS();
	for (int i = 0; i < NumRegS; ++i)
	{
		::new(&regs[i]) FString;
	}
}

//===========================================================================
//
// VMFrameStack - Constructor
//
//===========================================================================

VMFrameStack::VMFrameStack()
{
	Blocks = NULL;
	UnusedBlocks = NULL;
}

//===========================================================================
//
// VMFrameStack - Destructor
//
//===========================================================================

VMFrameStack::~VMFrameStack()
{
	while (PopFrame() != NULL)
	{ }
	if (Blocks != NULL)
	{
		BlockHeader *block, *next;
		for (block = Blocks; block != NULL; block = next)
		{
			next = block->NextBlock;
			delete[] (VM_UBYTE *)block;
		}
		Blocks = NULL;
	}
	if (UnusedBlocks != NULL)
	{
		BlockHeader *block, *next;
		for (block = UnusedBlocks; block != NULL; block = next)
		{
			next = block->NextBlock;
			delete[] (VM_UBYTE *)block;
		}
		UnusedBlocks = NULL;
	}
}

//===========================================================================
//
// VMFrameStack :: AllocFrame
//
// Allocates a frame from the stack suitable for calling a particular
// function.
//
//===========================================================================

VMFrame *VMFrameStack::AllocFrame(VMScriptFunction *func)
{
	VMFrame *frame = Alloc(func->StackSize);
	frame->Func = func;
	frame->NumRegD = func->NumRegD;
	frame->NumRegF = func->NumRegF;
	frame->NumRegS = func->NumRegS;
	frame->NumRegA = func->NumRegA;
	frame->MaxParam = func->MaxParam;
	frame->Func = func;
	frame->InitRegS();
	if (func->SpecialInits.Size())
	{
		func->InitExtra(frame->GetExtra());
	}
	return frame;
}

//===========================================================================
//
// VMFrameStack :: Alloc
//
// Allocates space for a frame. Its size will be rounded up to a multiple
// of 16 bytes.
//
//===========================================================================

VMFrame *VMFrameStack::Alloc(int size)
{
	BlockHeader *block;
	VMFrame *frame, *parent;

	size = (size + 15) & ~15;
	block = Blocks;
	if (block != NULL)
	{
		parent = block->LastFrame;
	}
	else
	{
		parent = NULL;
	}
	if (block == NULL || ((VM_UBYTE *)block + block->BlockSize) < (block->FreeSpace + size))
	{ // Not enough space. Allocate a new block.
		int blocksize = ((sizeof(BlockHeader) + 15) & ~15) + size;
		BlockHeader **blockp;
		if (blocksize < BLOCK_SIZE)
		{
			blocksize = BLOCK_SIZE;
		}
		for (blockp = &UnusedBlocks, block = *blockp; block != NULL; block = block->NextBlock)
		{
			if (block->BlockSize >= blocksize)
			{
				break;
			}
		}
		if (block != NULL)
		{
			*blockp = block->NextBlock;
		}
		else
		{
			block = (BlockHeader *)new VM_UBYTE[blocksize];
			block->BlockSize = blocksize;
		}
		block->InitFreeSpace();
		block->LastFrame = NULL;
		block->NextBlock = Blocks;
		Blocks = block;
	}
	frame = (VMFrame *)block->FreeSpace;
	memset(frame, 0, size);
	frame->ParentFrame = parent;
	block->FreeSpace += size;
	block->LastFrame = frame;
	return frame;
}


//===========================================================================
//
// VMFrameStack :: PopFrame
//
// Pops the top frame off the stack, returning a pointer to the new top
// frame.
//
//===========================================================================

VMFrame *VMFrameStack::PopFrame()
{
	if (Blocks == NULL)
	{
		return NULL;
	}
	VMFrame *frame = Blocks->LastFrame;
	if (frame == NULL)
	{
		return NULL;
	}
	auto Func = static_cast<VMScriptFunction *>(frame->Func);
	if (Func->SpecialInits.Size())
	{
		Func->DestroyExtra(frame->GetExtra());
	}
	// Free any string registers this frame had.
	FString *regs = frame->GetRegS();
	for (int i = frame->NumRegS; i != 0; --i)
	{
		(regs++)->~FString();
	}
	VMFrame *parent = frame->ParentFrame;
	if (parent == NULL)
	{
		// Popping the last frame off the stack.
		if (Blocks != NULL)
		{
			assert(Blocks->NextBlock == NULL);
			Blocks->LastFrame = NULL;
			Blocks->InitFreeSpace();
		}
		return NULL;
	}
	if ((VM_UBYTE *)parent < (VM_UBYTE *)Blocks || (VM_UBYTE *)parent >= (VM_UBYTE *)Blocks + Blocks->BlockSize)
	{ // Parent frame is in a different block, so move this one to the unused list.
		BlockHeader *next = Blocks->NextBlock;
		assert(next != NULL);
		assert((VM_UBYTE *)parent >= (VM_UBYTE *)next && (VM_UBYTE *)parent < (VM_UBYTE *)next + next->BlockSize);
		Blocks->NextBlock = UnusedBlocks;
		UnusedBlocks = Blocks;
		Blocks = next;
	}
	else
	{
		Blocks->LastFrame = parent;
		Blocks->FreeSpace = (VM_UBYTE *)frame;
	}
	return parent;
}

//===========================================================================
//
// VMFrameStack :: Call
//
// Calls a function, either native or scripted. If an exception occurs while
// executing, the stack is cleaned up. If trap is non-NULL, it is set to the
// VMException that was caught and the return value is negative. Otherwise,
// any caught exceptions will be rethrown. Under normal termination, the
// return value is the number of results from the function.
//
//===========================================================================

int VMCall(VMFunction *func, VMValue *params, int numparams, VMReturn *results, int numresults/*, VMException **trap*/)
{
#if 0
	try
#endif
	{	
		if (func->VarFlags & VARF_Native)
		{
			return static_cast<VMNativeFunction *>(func)->NativeCall(VM_INVOKE(params, numparams, results, numresults, func->RegTypes));
		}
		else
		{
			auto code = static_cast<VMScriptFunction *>(func)->Code;
			// handle empty functions consisting of a single return explicitly so that empty virtual callbacks do not need to set up an entire VM frame.
			// code cann be null here in case of some non-fatal DECORATE errors.
			if (code == nullptr || code->word == (0x00808000|OP_RET))
			{
				return 0;
			}
			else if (code->word == (0x00048000|OP_RET))
			{
				if (numresults == 0) return 0;
				results[0].SetInt(static_cast<VMScriptFunction *>(func)->KonstD[0]);
				return 1;
			}
			else
			{
				VMCycles[0].Clock();

				auto sfunc = static_cast<VMScriptFunction *>(func);
				int numret = sfunc->ScriptCall(sfunc, params, numparams, results, numresults);
				VMCycles[0].Unclock();
				return numret;
			}
		}
	}
#if 0
	catch (VMException *exception)
	{
		if (trap != NULL)
		{
			*trap = exception;
			return -1;
		}
		throw;
	}
#endif
}

int VMCallWithDefaults(VMFunction *func, TArray<VMValue> &params, VMReturn *results, int numresults/*, VMException **trap = NULL*/)
{
	if (func->DefaultArgs.Size() > params.Size())
	{
		auto oldp = params.Size();
		params.Resize(func->DefaultArgs.Size());
		for (unsigned i = oldp; i < params.Size(); i++)
		{
			params[i] = func->DefaultArgs[i];
		}
	}
	return VMCall(func, params.Data(), params.Size(), results, numresults);
}


// Exception stuff for the VM is intentionally placed there, because having this in vmexec.cpp would subject it to inlining
// which we do not want because it increases the local stack requirements of Exec which are already too high.
FString CVMAbortException::stacktrace;

CVMAbortException::CVMAbortException(EVMAbortException reason, const char *moreinfo, va_list ap)
{
	SetMessage("VM execution aborted: ");
	switch (reason)
	{
	case X_READ_NIL:
		AppendMessage("tried to read from address zero.");
		break;

	case X_WRITE_NIL:
		AppendMessage("tried to write to address zero.");
		break;

	case X_TOO_MANY_TRIES:
		AppendMessage("too many try-catch blocks.");
		break;

	case X_ARRAY_OUT_OF_BOUNDS:
		AppendMessage("array access out of bounds.");
		break;

	case X_DIVISION_BY_ZERO:
		AppendMessage("division by zero.");
		break;

	case X_BAD_SELF:
		AppendMessage("invalid self pointer.");
		break;

	case X_FORMAT_ERROR:
		AppendMessage("string format failed.");
		break;

	case X_OTHER:
		// no prepended message.
		break;

	default:
	{
		size_t len = strlen(m_Message);
		mysnprintf(m_Message + len, MAX_ERRORTEXT - len, "Unknown reason %d", reason);
		break;
	}
	}
	if (moreinfo != nullptr)
	{
		// [Player701] avoid double space
		if (reason != X_OTHER)
		{
			AppendMessage(" ");
		}
		size_t len = strlen(m_Message);
		myvsnprintf(m_Message + len, MAX_ERRORTEXT - len, moreinfo, ap);
	}

	if (vm_jit)
		stacktrace = JitCaptureStackTrace(1, false);
	else
		stacktrace = "";
}

// Print this only once on the first catch block.
void CVMAbortException::MaybePrintMessage()
{
	auto m = GetMessage();
	if (m != nullptr)
	{
		Printf(PRINT_NONOTIFY | PRINT_BOLD, TEXTCOLOR_RED "%s\n", m);
		SetMessage("");
	}
}


[[noreturn]] void ThrowAbortException(EVMAbortException reason, const char *moreinfo, ...)
{
	va_list ap;
	va_start(ap, moreinfo);
	throw CVMAbortException(reason, moreinfo, ap);
}

[[noreturn]] void ThrowAbortException(VMScriptFunction *sfunc, VMOP *line, EVMAbortException reason, const char *moreinfo, ...)
{
	va_list ap;
	va_start(ap, moreinfo);

	CVMAbortException err(reason, moreinfo, ap);

	err.stacktrace.AppendFormat("Called from %s at %s, line %d\n", sfunc->PrintableName, sfunc->SourceFileName.GetChars(), sfunc->PCToLine(line));
	throw err;
}

DEFINE_ACTION_FUNCTION(DObject, ThrowAbortException)
{
	PARAM_PROLOGUE;
	FString s = FStringFormat(VM_ARGS_NAMES);
	ThrowAbortException(X_OTHER, s.GetChars());
}

[[noreturn]] void NullParam(const char *varname)
{
	ThrowAbortException(X_READ_NIL, "In function parameter %s", varname);
}

#if 0
void ThrowVMException(VMException *x)
{
	throw x;
}
#endif


void ClearGlobalVMStack()
{
	while (GlobalVMStack.PopFrame() != nullptr)
	{
	}
}


ADD_STAT(VM)
{
	double added = 0;
	int addedc = 0;
	double peak = 0;
	for (auto d : VMCycles)
	{
		added += d.TimeMS();
		peak = max<double>(peak, d.TimeMS());
	}
	for (auto d : VMCalls) addedc += d;
	memmove(&VMCycles[1], &VMCycles[0], 9 * sizeof(cycle_t));
	memmove(&VMCalls[1], &VMCalls[0], 9 * sizeof(int));
	VMCycles[0].Reset();
	VMCalls[0] = 0;
	return FStringf("VM time in last 10 tics: %f ms, %d calls, peak = %f ms", added, addedc, peak);
}

//-----------------------------------------------------------------------------
//
//
//
//-----------------------------------------------------------------------------
CCMD(vmengine)
{
	if (argv.argc() == 2)
	{
		if (stricmp(argv[1], "default") == 0)
		{
			VMSelectEngine(VMEngine_Default);
			return;
		}
		else if (stricmp(argv[1], "checked") == 0)
		{
			VMSelectEngine(VMEngine_Checked);
			return;
		}
		else if (stricmp(argv[1], "unchecked") == 0)
		{
			VMSelectEngine(VMEngine_Unchecked);
			return;
		}
	}
	Printf("Usage: vmengine <default|checked|unchecked>\n");
}

