// Copyright 2022 Charles Lohr, you may use this file or any portions herein under any of the BSD, MIT, or CC0 licenses.

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#include <SPI.h>
#include <SD.h>

#include "default64mbdtc.h"

// SD card
#define SD_CS_PIN 10

// Just default RAM amount is 64MB.
uint32_t ram_amt = 64*1024*1024;
int fail_on_all_faults = 0;

static int64_t SimpleReadNumberInt( const char * number, int64_t defaultNumber );
static uint64_t GetTimeMicroseconds();
static void ResetKeyboardInput();
static void CaptureKeyboardInput();
static uint32_t HandleException( uint32_t ir, uint32_t retval );
static uint32_t HandleControlStore( uint32_t addy, uint32_t val );
static uint32_t HandleControlLoad( uint32_t addy );
static void HandleOtherCSRWrite( uint8_t * image, uint16_t csrno, uint32_t value );
static int32_t HandleOtherCSRRead( uint8_t * image, uint16_t csrno );
static void MiniSleep();
static int IsKBHit();
static int ReadKBByte();

// This is the functionality we want to override in the emulator.
//  think of this as the way the emulator's processor is connected to the outside world.
#define MINIRV32WARN( x... ) printf( x );
#define MINIRV32_DECORATE  static
#define MINI_RV32_RAM_SIZE ram_amt
#define MINIRV32_IMPLEMENTATION
#define MINIRV32_POSTEXEC( pc, ir, retval ) { if( retval > 0 ) { if( fail_on_all_faults ) { printf( "FAULT\n" ); return 3; } else retval = HandleException( ir, retval ); } }
#define MINIRV32_HANDLE_MEM_STORE_CONTROL( addy, val ) if( HandleControlStore( addy, val ) ) return val;
#define MINIRV32_HANDLE_MEM_LOAD_CONTROL( addy, rval ) rval = HandleControlLoad( addy );
#define MINIRV32_OTHERCSR_WRITE( csrno, value ) HandleOtherCSRWrite( image, csrno, value );
#define MINIRV32_OTHERCSR_READ( csrno, value ) value = HandleOtherCSRRead( image, csrno );

#include "mini-rv32ima.h"

uint8_t * ram_image = 0;
struct MiniRV32IMAState * core;
const char * kernel_command_line = 0;

static void DumpState( struct MiniRV32IMAState * core, uint8_t * ram_image );

int main(int argc, char** argv) {
    int i;
    long long instct = -1;
    int show_help = 0;
    int time_divisor = 1;
    int fixed_update = 0;
    int do_sleep = 1;
    int single_step = 0;
    int dtb_ptr = 0;
    const char* image_file_name = 0;
    const char* dtb_file_name = 0;

    for (i = 1; i < argc; i++) {
        const char* param = argv[i];
        int param_continue = 0;  // Can combine parameters, like -lpt x
        do {
            if (param[0] == '-' || param_continue) {
                switch (param[1]) {
                    case 'm': if (++i < argc) ram_amt = SimpleReadNumberInt(argv[i], ram_amt); break;
                    case 'c': if (++i < argc) instct = SimpleReadNumberInt(argv[i], -1); break;
                    case 'k': if (++i < argc) kernel_command_line = argv[i]; break;
                    case 'f': image_file_name = (++i < argc) ? argv[i] : 0; break;
                    case 'b': dtb_file_name = (++i < argc) ? argv[i] : 0; break;
                    case 'l': param_continue = 1; fixed_update = 1; break;
                    case 'p': param_continue = 1; do_sleep = 0; break;
                    case 's': param_continue = 1; single_step = 1; break;
                    case 'd': param_continue = 1; fail_on_all_faults = 1; break;
                    case 't': if (++i < argc) time_divisor = SimpleReadNumberInt(argv[i], 1); break;
                    default:
                        if (param_continue)
                            param_continue = 0;
                        else
                            show_help = 1;
                        break;
                }
            }
            else {
                show_help = 1;
                break;
            }
            param++;
        } while (param_continue);
    }

    if (show_help || image_file_name == 0 || time_divisor <= 0) {
        Serial.println("./mini-rv32imaf [parameters]");
        Serial.println("\t-m [ram amount]");
        Serial.println("\t-f [running image]");
        Serial.println("\t-k [kernel command line]");
        Serial.println("\t-b [dtb file, or 'disable']");
        Serial.println("\t-c instruction count");
        Serial.println("\t-s single step with full processor state");
        Serial.println("\t-t time divion base");
        Serial.println("\t-l lock time base to instruction count");
        Serial.println("\t-p disable sleep when wfi");
        Serial.println("\t-d fail out immediately on all faults");
        return 1;
    }

    ram_image = (uint8_t*)malloc(ram_amt);
    if (!ram_image) {
        Serial.println("Error: could not allocate system image.");
        return -4;
    }

restart:
{
    File f = SD.open(image_file_name, FILE_READ);
    if (!f) {
        char buffer[256];
        sprintf(buffer, "Error: \"%s\" not found", image_file_name);
        Serial.println(buffer);
        return -5;
    }

    // Seek to the end of the file to get the file size
    f.seek(f.size());
    long flen = f.position();
    f.seek(0);  // Seek back to the beginning of the file

    if (flen > ram_amt) {
        char buffer[256];
        sprintf(buffer, "Error: Could not fit RAM image (%ld bytes) into %d", flen, ram_amt);
        Serial.println(buffer);
        return -6;
    }

    memset(ram_image, 0, ram_amt);
    size_t bytesRead = 0;
    while (f.available()) {
        ram_image[bytesRead++] = f.read();
    }

    f.close();

    // Handle DTB file (if provided)
    if (dtb_file_name) {
        if (strcmp(dtb_file_name, "disable") == 0) {
            // No DTB reading
        } else {
            f = SD.open(dtb_file_name, FILE_READ);
            if (!f) {
                char buffer[256];
                sprintf(buffer, "Error: \"%s\" not found", dtb_file_name);
                Serial.println(buffer);
                return -5;
            }

            f.seek(f.size());
            long dtblen = f.position();
            f.seek(0);

            dtb_ptr = ram_amt - dtblen - sizeof(struct MiniRV32IMAState);
            bytesRead = 0;
            while (f.available()) {
                ram_image[dtb_ptr + bytesRead++] = f.read();
            }
            f.close();
        }
    } else {
        // Load a default DTB
        dtb_ptr = ram_amt - sizeof(default64mbdtb) - sizeof(struct MiniRV32IMAState);
        memcpy(ram_image + dtb_ptr, default64mbdtb, sizeof(default64mbdtb));
        if (kernel_command_line) {
            strncpy((char*)(ram_image + dtb_ptr + 0xc0), kernel_command_line, 54);
        }
    }
}

CaptureKeyboardInput();

core = (struct MiniRV32IMAState*)(ram_image + ram_amt - sizeof(struct MiniRV32IMAState));
core->pc = MINIRV32_RAM_IMAGE_OFFSET;
core->regs[10] = 0x00; // hart ID
core->regs[11] = dtb_ptr ? (dtb_ptr + MINIRV32_RAM_IMAGE_OFFSET) : 0; // dtb_pa (Must be valid pointer)
core->extraflags |= 3; // Machine-mode.

// Update system ram size in DTB if needed
if (dtb_file_name == 0) {
    uint32_t* dtb = (uint32_t*)(ram_image + dtb_ptr);
    if (dtb[0x13c / 4] == 0x00c0ff03) {
        uint32_t validram = dtb_ptr;
        dtb[0x13c / 4] = (validram >> 24) | (((validram >> 16) & 0xff) << 8) | (((validram >> 8) & 0xff) << 16) | (validram & 0xff);
    }
}

uint64_t rt;
uint64_t lastTime = (fixed_update) ? 0 : (GetTimeMicroseconds() / time_divisor);
int instrs_per_flip = single_step ? 1 : 1024;

for (rt = 0; rt < instct + 1 || instct < 0; rt += instrs_per_flip) {
    uint64_t* this_ccount = ((uint64_t*)&core->cyclel);
    uint32_t elapsedUs = 0;
    if (fixed_update) {
        elapsedUs = *this_ccount / time_divisor - lastTime;
    } else {
        elapsedUs = GetTimeMicroseconds() / time_divisor - lastTime;
    }
    lastTime += elapsedUs;

    if (single_step) {
        DumpState(core, ram_image);
    }

    int ret = MiniRV32IMAStep(core, ram_image, 0, elapsedUs, instrs_per_flip);
    switch (ret) {
        case 0:
            break;
        case 1:
            if (do_sleep) MiniSleep();
            *this_ccount += instrs_per_flip;
            break;
        case 3:
            instct = 0;
            break;
        case 0x7777:
            goto restart;  // syscon code for restart
        case 0x5555:
            printf("POWEROFF@0x%08x%08x\n", core->cycleh, core->cyclel);
            return 0; // syscon code for power-off
        default:
            printf("Unknown failure\n");
            break;
    }
}

DumpState(core, ram_image);
}



//////////////////////////////////////////////////////////////////////////
// Platform-specific functionality
//////////////////////////////////////////////////////////////////////////


#if defined(WINDOWS) || defined(WIN32) || defined(_WIN32)

#include <windows.h>
#include <conio.h>

#define strtoll _strtoi64

static void CaptureKeyboardInput()
{
	system(""); // Poorly documented tick: Enable VT100 Windows mode.
}

static void ResetKeyboardInput()
{
}

static void MiniSleep()
{
	Sleep(1);
}

static uint64_t GetTimeMicroseconds()
{
	static LARGE_INTEGER lpf;
	LARGE_INTEGER li;

	if( !lpf.QuadPart )
		QueryPerformanceFrequency( &lpf );

	QueryPerformanceCounter( &li );
	return ((uint64_t)li.QuadPart * 1000000LL) / (uint64_t)lpf.QuadPart;
}


static int IsKBHit()
{
	return _kbhit();
}

static int ReadKBByte()
{
	// This code is kind of tricky, but used to convert windows arrow keys
	// to VT100 arrow keys.
	static int is_escape_sequence = 0;
	int r;
	if( is_escape_sequence == 1 )
	{
		is_escape_sequence++;
		return '[';
	}

	r = _getch();

	if( is_escape_sequence )
	{
		is_escape_sequence = 0;
		switch( r )
		{
			case 'H': return 'A'; // Up
			case 'P': return 'B'; // Down
			case 'K': return 'D'; // Left
			case 'M': return 'C'; // Right
			case 'G': return 'H'; // Home
			case 'O': return 'F'; // End
			default: return r; // Unknown code.
		}
	}
	else
	{
		switch( r )
		{
			case 13: return 10; //cr->lf
			case 224: is_escape_sequence = 1; return 27; // Escape arrow keys
			default: return r;
		}
	}
}

#else

//#include <sys/ioctl.h> -- NOT A THING ON ARDUINO MEGA
//#include <termios.h> -- NOT A THING ON ARDUINO MEGA
#include <unistd.h>
#include <signal.h>
//#include <sys/time.h> -- NOT A THING ON ARDUINO MEGA

// MEGA Fixes
#include <Wire.h>
#include <RTClib.h>

// Real Time Clock
RTC_DS1307 rtc;

// usleep
static void usleep(long long time)
{
    delayMicroseconds(time);  // Arduino's function to sleep for microseconds
}

// TODO:
/*
Replace time with RTC
*/

static void CtrlC()
{
	DumpState( core, ram_image);
	exit( 0 );
}

// Override keyboard, so we can capture all keyboard input for the VM.
static void CaptureKeyboardInput()
{
	// Hook exit, because we want to re-enable keyboard.
	atexit(ResetKeyboardInput);
	//signal(SIGINT, CtrlC);

	//struct termios term;
	//tcgetattr(0, &term);
	//term.c_lflag &= ~(ICANON | ECHO); // Disable echo as well
	//tcsetattr(0, TCSANOW, &term);
}

static void ResetKeyboardInput()
{
	// Re-enable echo, etc. on keyboard.
	//struct termios term;
	//tcgetattr(0, &term);
	//term.c_lflag |= ICANON | ECHO;
	//tcsetattr(0, TCSANOW, &term);
}

static void MiniSleep()
{
	usleep(500);
}

static uint64_t GetTimeMicroseconds()
{
    DateTime now = rtc.now();  // Get the current time from the RTC
    
    // Get the number of seconds since epoch (1970-01-01 00:00:00 UTC)
    uint32_t seconds = now.unixtime();
    
    // Convert seconds to microseconds
    uint64_t microseconds = (uint64_t)seconds * 1000000LL;
    
    // Add the current microseconds (DS1307 is only accurate to seconds, no sub-second precision)
    microseconds += now.second() * 1000000LL;  // This is a rough approximation
    
    return microseconds;
}

static int is_eofd;

static int ReadKBByte()
{
	if( is_eofd ) return 0xffffffff;
	char rxchar = 0;
	//int rread = read(fileno(stdin), (char*)&rxchar, 1);

	//if( rread > 0 ) // Tricky: getchar can't be used with arrow keys.
	//	return rxchar;
	//else
		return -1;
}

static int IsKBHit()
{
	if( is_eofd ) return -1;
	int byteswaiting;
	//ioctl(0, FIONREAD, &byteswaiting);
	//if( !byteswaiting && write( fileno(stdin), 0, 0 ) != 0 ) { is_eofd = 1; return -1; } // Is end-of-file for 
	return !!byteswaiting;
}


#endif


//////////////////////////////////////////////////////////////////////////
// Rest of functions functionality
//////////////////////////////////////////////////////////////////////////

static uint32_t HandleException( uint32_t ir, uint32_t code )
{
	// Weird opcode emitted by duktape on exit.
	if( code == 3 )
	{
		// Could handle other opcodes here.
	}
	return code;
}

static uint32_t HandleControlStore( uint32_t addy, uint32_t val )
{
	if( addy == 0x10000000 ) //UART 8250 / 16550 Data Buffer
	{
		printf( "%c", val );
		fflush( stdout );
	}
	else if( addy == 0x11004004 ) //CLNT
		core->timermatchh = val;
	else if( addy == 0x11004000 ) //CLNT
		core->timermatchl = val;
	else if( addy == 0x11100000 ) //SYSCON (reboot, poweroff, etc.)
	{
		core->pc = core->pc + 4;
		return val; // NOTE: PC will be PC of Syscon.
	}
	return 0;
}


static uint32_t HandleControlLoad( uint32_t addy )
{
	// Emulating a 8250 / 16550 UART
	if( addy == 0x10000005 )
		return 0x60 | IsKBHit();
	else if( addy == 0x10000000 && IsKBHit() )
		return ReadKBByte();
	else if( addy == 0x1100bffc ) // https://chromitem-soc.readthedocs.io/en/latest/clint.html
		return core->timerh;
	else if( addy == 0x1100bff8 )
		return core->timerl;
	return 0;
}

static void HandleOtherCSRWrite( uint8_t * image, uint16_t csrno, uint32_t value )
{
	if( csrno == 0x136 )
	{
		printf( "%d", value ); fflush( stdout );
	}
	if( csrno == 0x137 )
	{
		printf( "%08x", value ); fflush( stdout );
	}
	else if( csrno == 0x138 )
	{
		//Print "string"
		uint32_t ptrstart = value - MINIRV32_RAM_IMAGE_OFFSET;
		uint32_t ptrend = ptrstart;
		if( ptrstart >= ram_amt )
			printf( "DEBUG PASSED INVALID PTR (%08x)\n", value );
		while( ptrend < ram_amt )
		{
			if( image[ptrend] == 0 ) break;
			ptrend++;
		}
		if( ptrend != ptrstart )
			fwrite( image + ptrstart, ptrend - ptrstart, 1, stdout );
	}
	else if( csrno == 0x139 )
	{
		putchar( value ); fflush( stdout );
	}
}

static int32_t HandleOtherCSRRead( uint8_t * image, uint16_t csrno )
{
	if( csrno == 0x140 )
	{
		if( !IsKBHit() ) return -1;
		return ReadKBByte();
	}
	return 0;
}

static int64_t SimpleReadNumberInt( const char * number, int64_t defaultNumber )
{
    if ( !number || !number[0] ) return defaultNumber;

    int radix = 10;
    if ( number[0] == '0' )
    {
        char nc = number[1];
        number += 2;
        if ( nc == 0 ) return 0;  // "0" is just 0
        else if ( nc == 'x' ) radix = 16;  // Hexadecimal
        else if ( nc == 'b' ) radix = 2;   // Binary
        else { number--; radix = 8; }      // Octal
    }

    int64_t ret = 0;
    bool negative = false;

    // Handle negative numbers
    if (number[0] == '-') {
        negative = true;
        number++;
    }

    // Loop through the string and convert to number
    while (*number)
    {
        char c = *number++;
        int digit;

        // Convert char to corresponding digit based on radix
        if (c >= '0' && c <= '9') {
            digit = c - '0';
        } 
        else if (c >= 'a' && c <= 'f' && radix == 16) {
            digit = c - 'a' + 10;
        }
        else if (c >= 'A' && c <= 'F' && radix == 16) {
            digit = c - 'A' + 10;
        } 
        else if (c == 'b' && radix == 2) {
            digit = 1;  // Binary handling
        }
        else if (c == '0' && radix == 8) {
            digit = 0;  // Octal handling
        } else {
            // If the character is not valid for the current radix, return default number
            return defaultNumber;
        }

        // Make sure the digit is within the correct range for the radix
        if (digit >= radix) {
            return defaultNumber;
        }

        ret = ret * radix + digit;
    }

    return negative ? -ret : ret; // Handle negative numbers
}


static void DumpState( struct MiniRV32IMAState * core, uint8_t * ram_image )
{
	uint32_t pc = core->pc;
	uint32_t pc_offset = pc - MINIRV32_RAM_IMAGE_OFFSET;
	uint32_t ir = 0;

	printf( "PC: %08x ", pc );
	if( pc_offset >= 0 && pc_offset < ram_amt - 3 )
	{
		ir = *((uint32_t*)(&((uint8_t*)ram_image)[pc_offset]));
		printf( "[0x%08x] ", ir ); 
	}
	else
		printf( "[xxxxxxxxxx] " ); 
	uint32_t * regs = core->regs;
	printf( "Z:%08x ra:%08x sp:%08x gp:%08x tp:%08x t0:%08x t1:%08x t2:%08x s0:%08x s1:%08x a0:%08x a1:%08x a2:%08x a3:%08x a4:%08x a5:%08x ",
		regs[0], regs[1], regs[2], regs[3], regs[4], regs[5], regs[6], regs[7],
		regs[8], regs[9], regs[10], regs[11], regs[12], regs[13], regs[14], regs[15] );
	printf( "a6:%08x a7:%08x s2:%08x s3:%08x s4:%08x s5:%08x s6:%08x s7:%08x s8:%08x s9:%08x s10:%08x s11:%08x t3:%08x t4:%08x t5:%08x t6:%08x\n",
		regs[16], regs[17], regs[18], regs[19], regs[20], regs[21], regs[22], regs[23],
		regs[24], regs[25], regs[26], regs[27], regs[28], regs[29], regs[30], regs[31] );
}

void setup() {
	// Setup External Devices
	Serial.begin(9600);
	Wire.begin();
	
	// Initialize the RTC
	if (!rtc.begin()) {
			Serial.println("Couldn't find RTC");
			while (1);
	}
	
	// Check if the RTC lost power and set the time if necessary
	if (!rtc.isrunning()) {
			Serial.println("RTC is NOT running, setting the time...");
			// Set the RTC to a known time (use your desired start time)
			rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
	}

  if (!SD.begin(SD_CS_PIN)) {
    Serial.println("SD card initialization failed!");
    return;
  }

  Serial.println("SD card initialized.");

	// Call main
	char* arg1 = "program";
	char* argv[] = {arg1};
	main(1, argv);
}


void loop() {

}
GND5VSDASCLSQWRTCDS1307+