/*
** 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 "templates.h"
#include "vmintern.h"
#include "types.h"

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

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

TArray<VMFunction *> VMFunction::AllFunctions;


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;
}

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;
}

//===========================================================================
//
// 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*/)
{
	bool allocated = false;
	try
	{	
		if (func->VarFlags & VARF_Native)
		{
			return static_cast<VMNativeFunction *>(func)->NativeCall(params, func->DefaultArgs, numparams, results, numresults);
		}
		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();
				VMCalls[0]++;
				auto &stack = GlobalVMStack;
				stack.AllocFrame(static_cast<VMScriptFunction *>(func));
				allocated = true;
				VMFillParams(params, stack.TopFrame(), numparams);
				int numret = VMExec(&stack, code, results, numresults);
				stack.PopFrame();
				VMCycles[0].Unclock();
				return numret;
			}
		}
	}
#if 0
	catch (VMException *exception)
	{
		if (allocated)
		{
			PopFrame();
		}
		if (trap != NULL)
		{
			*trap = exception;
			return -1;
		}
		throw;
	}
#endif
	catch (...)
	{
		if (allocated)
		{
			GlobalVMStack.PopFrame();
		}
		throw;
	}
}

// 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)
	{
		AppendMessage(" ");
		size_t len = strlen(m_Message);
		myvsnprintf(m_Message + len, MAX_ERRORTEXT - len, moreinfo, ap);
	}
	stacktrace = "";
}

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


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

DEFINE_ACTION_FUNCTION(DObject, ThrowAbortException)
{
	PARAM_PROLOGUE;
	FString s = FStringFormat(param, defaultparam, numparam, ret, numret);
	ThrowAbortException(X_OTHER, s.GetChars());
	return 0;
}

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

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


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");
}

