/*
This program is distributed under the terms of the 'MIT license'. The text
of this licence follows...

Copyright (c) 2005-2009 J.D.Medhurst (a.k.a. Tixy)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

/**
@file

@brief Test code for forth virtual machine.
@test
*/

/**
@defgroup forth_test Test - Test code for forth virtual machine

@ingroup forth
@{
*/

#include "common.h"
#include "test/test.h"
#include "../forth.h"

/** Path for source code location */
#define SOURCE_ROOT_PATH "forth/"


#include <malloc.h>
#include <stdlib.h>
#include <stdio.h>


#include <unistd.h>
#include <termios.h>

/**
ForthIo implementation which uses posix functions.
*/
class StdForthIo : public ForthIo
	{
public:
	StdForthIo()
		{
		// set new line sequence as CR/LF...
		NewLine[0]=2;
		NewLine[1]=13;
		NewLine[2]=10;

		// if stdin is a terminal then set non-canonical input mode without echo...
		if((IsTerminal = tcgetattr(fileno(stdin),&InitialSettings)==0))
			{
			struct termios settings = InitialSettings;
			settings.c_lflag &= ~(ICANON|ECHO);
			settings.c_cc[VMIN] = 1;
			settings.c_cc[VTIME] = 0;
			tcsetattr(fileno(stdin),TCSANOW,&settings);
			}
		}

	~StdForthIo()
		{
		// restore terminal settings...
		if(IsTerminal)
			tcsetattr(fileno(stdin),TCSANOW,&InitialSettings);
		}


	virtual void ConsoleOut(const CHAR* text,UCELL textLength)
		{
		while(textLength--)
			putchar(*text++);
		}

	virtual CELL ConsoleIn()
		{
		return getchar();
		}

private:
	bool IsTerminal;
	struct termios InitialSettings;
	};


/**
ForthIo implementation used for testing.
This returns pre-determined text for ConsolIn which is  set via SetTestInput.
*/
class TestForthIo : public ForthIo
	{
public:
	TestForthIo()
		: Input(0)
		{
		NewLine[0]=2;
		NewLine[1]=13;
		NewLine[2]=10;
		}

	virtual void ConsoleOut(const CHAR* text,UCELL textLength)
		{
		while(textLength--)
			putchar(*text++);
		}

	/**
	Set the sequence of characters which are to be returned by #ConsoleIn.
	@param input C string (NUL terminated list) of character values.
	*/
	void SetTestInput(const char* input)
		{
		Input = input;
		}

	virtual CELL ConsoleIn()
		{
		if(Input && Input[0])
			return *Input++;
		TEST(false); // no input available!
		return 13; // default to carriage return
		}
private:
	const char* Input;
	};


/**
Read a files contents into memory.
The file is assumed to contain characters of 8 bit size but they are stored
in memory as characters the same size as the Forth VM's \c CHAR.
@param name		The file name.
@param address	On success, this is the address of the memory cell containing the
				read contents. This memory must be freed when it is no longer required.
@param size		On success, this is the number of characters in the file.

@return True, if the read was successful, false otherwise.
*/
bool ReadFile(const char* name,void*& address, unsigned& size)
	{
	FILE* f=fopen(name,"rb");
	if(!f)
		return false;
	if(fseek(f,0,SEEK_END))
		{ fclose(f); return false; }
	size = ftell(f);
	if(fseek(f,0,SEEK_SET))
		{ fclose(f); return false; }
	address = malloc(size*sizeof(CHAR));
	if(!address)
		{ fclose(f); return false; }
	if(size!=fread(address,sizeof(uint8_t),size,f))
		{ free(address); fclose(f); return false; }
	fclose(f);
	CHAR* d=(CHAR*)((CELL)address+size*sizeof(CHAR)/sizeof(uint8_t));
	uint8_t* s=(uint8_t*)((CELL)address+size*sizeof(uint8_t));
	while((void*)d>(void*)s) *--d=*--s;
	return true;
	}


#if BITS_PER_CHAR==16
/** Macro to include string constants in source which match the width of the \c CHAR type.*/
#define STRING(string) L##string
/** Macro to include a line of text as an in-source constant .*/
#define LINE(string) L##string L"\n"
#else
/** Macro to include string constants in source which match the width of the \c CHAR type.*/
#define STRING(string) string
/** Macro to include a line of text as an in-source constant .*/
#define LINE(string) string "\n"
#endif


/**
Test Forth VM derivation to add file \c INCLUDE functionality.
*/
class ForthTest : public Forth
	{
public:
	/**
	Load the contents of a file and \c EVALUATE it.
	@param fileName The name of the file.
	@return 0 or a forth execption value.
	*/
	CELL Include(const char* fileName);

	/**
	Load the contents of a file and \c EVALUATE it a line at a time.
	@param fileName The name of the file.
	@return 0 or a forth execption value.
	*/
	CELL IncludeLines(const char* fileName);
	};


CELL ForthTest::Include(const char* fileName)
	{
	void* file;
	unsigned size;
	if(ReadFile(fileName,file,size))
		{
		CELL result = Evaluate((const CHAR*)file,size);
		free(file);
		return result;
		}
	else
		return -38; // non-existent file
	}


CELL ForthTest::IncludeLines(const char* fileName)
	{
	static CELL XT_INTERPRET_LINES = 0;

	static const CHAR InterpretLines[] =
		{
		// Function which interprets source a line at a time...
		LINE("DECIMAL")
		LINE(":NONAME   ( c-addr u -- )")
		LINE("	(source) 2! 0 >IN !")
		LINE("	SOURCE CHARS + >R")
		LINE("	BEGIN")
		LINE("		SOURCE DROP >IN @ CHARS +")
		LINE("		DUP R@ U<")
		LINE("	WHILE")
		LINE("	 	R@ OVER - 1 CHARS / (source) 2!")
	//	LINE("		BASE @ HEX R@ . (source) 2@ CHARS + . CR BASE !")
		LINE("		0 >IN ! 10 PARSE 2DROP >IN @ (source) ! 0 >IN !")
	//	LINE("		SOURCE TYPE KEY DROP")
		LINE("		INTERPRET")
		LINE("	REPEAT")
		LINE("	R> 2DROP")
		LINE(";")
		};

	if(!XT_INTERPRET_LINES)
		{
		// Create word to interpret a file a line at a time...
		CELL result = Evaluate(InterpretLines,sizeof(InterpretLines)/sizeof(CHAR));
		if(result)
			return result;
		XT_INTERPRET_LINES = Pop(1)[0];  // Pop xt for INTERPRET-LINES
		}

	void* file;
	unsigned size;
	if(ReadFile(fileName,file,size))
		{
		// Interpret file a line at a time...
		Push((CELL*)&file,1);
		Push((CELL*)&size,1);
		CELL result = Execute(XT_INTERPRET_LINES);
		free(file);
		return result;
		}
	else
		return -38; // non-existent file
	}


/**
Construct counted string arguments in the form <CODE> (CHAR*)string,(CELL)stringLength </CODE>
@param string A quoted string, e.g. <CODE> "A string" </CODE>
*/
#define COUNTED_STRING(string) (CHAR*)STRING(string),(sizeof(string)-1)


/**
Top level test function. Call this to perform all tests.

@param interactive	True, if the user will interact with the tests,
					false if tests are to run non-interactively.
*/
void TestForth(bool interactive)
	{
	// make i/o object...
	union
		{
		ForthIo* io;
		StdForthIo* ioStd;
		TestForthIo* ioTest;
		};
	if(interactive)
		ioStd = new StdForthIo;
	else
		ioTest = new TestForthIo;
	TEST(io);

	// construct test VM...
	unsigned size = 1<<20; // 1Meg of RAM
	void* start =  malloc(size);
	ForthTest* forth = (ForthTest*)Forth::Construct(start,size,io);

	// test Push and Pop...
	CELL args[2];
	args[0] = 888;
	forth->Push(args,1);
	args[0] = 999;
	forth->Push(args,1);
	args[0] = 11;
	args[1] = 123;
	forth->Push(args,2);
	const CELL* out;
	out=forth->Pop(0); // pop zero arguments, i.e. just get a peek at the stack
	TEST(out[0]==11);
	TEST(out[1]==123);
	TEST(out[2]==999);
	TEST(out[3]==888);
	TEST(!forth->Evaluate(COUNTED_STRING("- SWAP"))); // stack effect is: ( 888 999 123 11 -- 888 112 999 )
	out=forth->Pop(0);
	TEST(out[0]==999);
	TEST(out[1]==112);
	TEST(out[2]==888);
	out=forth->Pop(2);
	TEST(out[0]==999);
	TEST(out[1]==112);
	out=forth->Pop(1);
	TEST(out[0]==888);

	// load additional forth source...
	TEST(0==forth->Include(SOURCE_ROOT_PATH "core-ext.f"));
	TEST(0==forth->Include(SOURCE_ROOT_PATH "search.f"));
	TEST(0==forth->Include(SOURCE_ROOT_PATH "programming-tools.f"));

	// ANS wordset tests...
	TEST(0==forth->Include(SOURCE_ROOT_PATH "test/tester.f"));
	if(!interactive)
		ioTest->SetTestInput("Testing, Testing, 124\b3\n");
	TEST(0==forth->IncludeLines(SOURCE_ROOT_PATH "test/core.f"));
	TEST(0==forth->IncludeLines(SOURCE_ROOT_PATH "test/core-ext.f"));
	TEST(0==forth->IncludeLines(SOURCE_ROOT_PATH "test/search.f"));
	TEST(0==forth->IncludeLines(SOURCE_ROOT_PATH "test/search-ext.f"));

#if 0 // set true to test ARM assembler and disassembler...
	TEST(0==forth->Include(SOURCE_ROOT_PATH "arm-disasm.f"));
	TEST(0==forth->Include(SOURCE_ROOT_PATH "arm-asm.f"));
	if(!interactive)
		ioTest->SetTestInput("\n");
	TEST(0==forth->Include(SOURCE_ROOT_PATH "test/arm-asm-disasm-test.f"));
#endif

	// start interactive console...
	if(!interactive)
		ioTest->SetTestInput("2 2 + .\nBYE\n");
	forth->Quit();

	// cleanup...
	free(forth);
	if(interactive)
		delete ioStd;
	else
		delete ioTest;
	}

/** @} */ // End of group

