//---------------------------------------------------------------------------
//
//	SCSI Target Emulator RaSCSI (*^..^*)
//	for Raspberry Pi
//
//	Copyright (C) 2014-2020 GIMONS
//  Copyright (C) akuker
//
//  Licensed under the BSD 3-Clause License. 
//  See LICENSE file in the project root folder.
//
//  [ Command thread ]
//
//  This is a background thread that runs, listening for commands on port
//  6868 for commands from the rasctl utility. This also includes the logic
//  for handling the initialization commands that are specified at startup.
//
//---------------------------------------------------------------------------

#include "command_thread.h"

#include "os.h"
#include "rasctl_command.h"
#include "devices/sasihd.h"
#include "devices/scsihd.h"
#include "devices/scsihd_nec.h"
#include "devices/scsihd_apple.h"
#include "devices/scsicd.h"
#include "devices/scsimo.h"
#include "devices/scsi_host_bridge.h"

int Command_Thread::m_monsocket = -1;				// Monitor Socket
pthread_t Command_Thread::m_monthread;				// Monitor Thread

// These objects are currently in rascsi.cpp. They should eventually be 
// broken out.
extern volatile BOOL running;		// Running flag
extern volatile BOOL active;		// Processing flag
void ListDevice(FILE *fp);
Disk* GetDevice(FILE *fp, int id, int un);
BOOL AttachDevice(FILE *fp, Disk *pUnit, int id, int un);
BOOL DetachDevice(FILE *fp, int id, int un);


//---------------------------------------------------------------------------
//
//	Initialization
//
//---------------------------------------------------------------------------
BOOL Command_Thread::Init()
{

#ifndef BAREMETAL
	struct sockaddr_in server;
	int yes;

	// Create socket for monitor
	m_monsocket = socket(PF_INET, SOCK_STREAM, 0);
	memset(&server, 0, sizeof(server));
	server.sin_family = PF_INET;
	server.sin_port   = htons(Rasctl_Command::PortNumber);
	server.sin_addr.s_addr = htonl(INADDR_ANY);

	// allow address reuse
	yes = 1;
	if (setsockopt(
		m_monsocket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0){
		return FALSE;
	}

	// Bind
	if (bind(m_monsocket, (struct sockaddr *)&server,
		sizeof(struct sockaddr_in)) < 0) {
		FPRT(stderr, "Error : Already running?\n");
		return FALSE;
	}

	// Create Monitor Thread
	pthread_create(&m_monthread, NULL, MonThread, NULL);


#endif // BAREMETAL

	return TRUE;
}

//---------------------------------------------------------------------------
//
//	Close/Cleanup the command thread
//
//---------------------------------------------------------------------------
void Command_Thread::Close(){
	// Close the monitor socket
	if (m_monsocket >= 0) {
		close(m_monsocket);
	}
}


//---------------------------------------------------------------------------
//
//	Argument Parsing
//
//---------------------------------------------------------------------------
#ifdef BAREMETAL
BOOL ParseConfig(int argc, char* argv[])
{
	FRESULT fr;
	FIL fp;
	char line[512];
	int id;
	int un;
	int type;
	char *argID;
	char *argPath;
	int len;
	char *ext;

	// Mount the SD card
	fr = f_mount(&fatfs, "", 1);
	if (fr != FR_OK) {
		FPRT(stderr, "Error : SD card mount failed.\n");
		return FALSE;
	}

	// If there is no setting file, the processing is interrupted
	fr = f_open(&fp, "rascsi.ini", FA_READ);
	if (fr != FR_OK) {
		return FALSE;
	}

	// Start Decoding

	while (TRUE) {
		// Get one Line
		memset(line, 0x00, sizeof(line));
		if (f_gets(line, sizeof(line) -1, &fp) == NULL) {
			break;
		}

		// Delete the CR/LF
		len = strlen(line);
		while (len > 0) {
			if (line[len - 1] != '\r' && line[len - 1] != '\n') {
				break;
			}
			line[len - 1] = '\0';
			len--;
		}

		// Get the ID and Path
		argID = &line[0];
		argPath = &line[4];
		line[3] = '\0';

		// Check if the line is an empty string
		if (argID[0] == '\0' || argPath[0] == '\0') {
			continue;
		}

		if (strlen(argID) == 3 && xstrncasecmp(argID, "id", 2) == 0) {
			// ID or ID Format

			// Check that the ID number is valid (0-7)
			if (argID[2] < '0' || argID[2] > '7') {
				FPRT(stderr,
					"Error : Invalid argument(IDn n=0-7) [%c]\n", argID[2]);
				goto parse_error;
			}

			// The ID unit is good
            id = argID[2] - '0';
			un = 0;
		} else if (xstrncasecmp(argID, "hd", 2) == 0) {
			// HD or HD format

			if (strlen(argID) == 3) {
				// Check that the HD number is valid (0-9)
				if (argID[2] < '0' || argID[2] > '9') {
					FPRT(stderr,
						"Error : Invalid argument(HDn n=0-15) [%c]\n", argID[2]);
					goto parse_error;
				}

				// ID was confirmed
				id = (argID[2] - '0') / UnitNum;
				un = (argID[2] - '0') % UnitNum;
			} else if (strlen(argID) == 4) {
				// Check that the HD number is valid (10-15)
				if (argID[2] != '1' || argID[3] < '0' || argID[3] > '5') {
					FPRT(stderr,
						"Error : Invalid argument(HDn n=0-15) [%c]\n", argID[2]);
					goto parse_error;
				}

				// The ID unit is good - create the id and unit number
				id = ((argID[3] - '0') + 10) / UnitNum;
				un = ((argID[3] - '0') + 10) % UnitNum;
				argPath++;
			} else {
				FPRT(stderr,
					"Error : Invalid argument(IDn or HDn) [%s]\n", argID);
				goto parse_error;
			}
		} else {
			FPRT(stderr,
				"Error : Invalid argument(IDn or HDn) [%s]\n", argID);
			goto parse_error;
		}

		// Skip if there is already an active device
		if (disk[id * UnitNum + un] &&
			!disk[id * UnitNum + un]->IsNULL()) {
			continue;
		}

		// Initialize device type
		type = -1;

		// Check ethernet and host bridge
		if (xstrcasecmp(argPath, "bridge") == 0) {
			type = 4;
		} else {
			// Check the path length
			len = strlen(argPath);
			if (len < 5) {
				FPRT(stderr,
					"Error : Invalid argument(File path is short) [%s]\n",
					argPath);
				goto parse_error;
			}

			// Does the file have an extension?
			if (argPath[len - 4] != '.') {
				FPRT(stderr,
					"Error : Invalid argument(No extension) [%s]\n", argPath);
				goto parse_error;
			}

			// Figure out what the type is
			ext = &argPath[len - 3];
			if (xstrcasecmp(ext, "hdf") == 0 ||
				xstrcasecmp(ext, "hds") == 0 ||
				xstrcasecmp(ext, "hdn") == 0 ||
				xstrcasecmp(ext, "hdi") == 0 || xstrcasecmp(ext, "nhd") == 0 ||
				xstrcasecmp(ext, "hda") == 0) {
				// HD(SASI/SCSI)
				type = 0;
			} else if (strcasecmp(ext, "mos") == 0) {
				// MO
				type = 2;
			} else if (strcasecmp(ext, "iso") == 0) {
				// CD
				type = 3;
			} else {
				// Cannot determine the file type
				FPRT(stderr,
					"Error : Invalid argument(file type) [%s]\n", ext);
				goto parse_error;
			}
		}

		// Execute the command
		if (!ExecuteCommand(stderr, id, un, 0, type, argPath)) {
			goto parse_error;
		}
	}

	// Close the configuration file
	f_close(&fp);

	// Display the device list
	ListDevice(stdout);

	return TRUE;

parse_error:

	// Close the configuration file
	f_close(&fp);

	return FALSE;
}
#else
#endif  // BAREMETAL

//---------------------------------------------------------------------------
//
//	Monitor Thread
//
//---------------------------------------------------------------------------
void *Command_Thread::MonThread(void *param)
{
#ifndef BAREMETAL
	struct sched_param schedparam;
	struct sockaddr_in client;
	socklen_t len;
	int fd;
	FILE *fp;
	char buf[BUFSIZ];
	char *p;
	Rasctl_Command *new_command;


	// Scheduler Settings
	schedparam.sched_priority = 0;
#if defined(__linux__)
	// sched_setscheduler is only available on Linux
	sched_setscheduler(0, SCHED_IDLE, &schedparam);
#endif
	// Set the affinity to a specific processor core
	FixCpu(2);

	// Setup the monitor socket to receive commands
	listen(m_monsocket, 1);

	while (running) {
		// Wait for connection
		memset(&client, 0, sizeof(client));
		len = sizeof(client);
		fd = accept(m_monsocket, (struct sockaddr*)&client, &len);
		if (fd < 0) {
			break;
		}

		// Fetch the command
		fp = fdopen(fd, "r+");
		p = fgets(buf, BUFSIZ, fp);

		// If we received a command....
		if (p) {
			// Decode the received buffer into a Command object
			new_command = Rasctl_Command::DeSerialize(p,BUFSIZ);

			// If the command was successfully parsed
			if(new_command != nullptr){
				// Execute the command
				ExecuteCommand(fp, new_command);
			}
	}

		// Release the connection
		fclose(fp);
		close(fd);
	}
#endif	// BAREMETAL

	return NULL;
}


//---------------------------------------------------------------------------
//
//	Command Processing
//
//---------------------------------------------------------------------------
BOOL Command_Thread::ExecuteCommand(FILE *fp, Rasctl_Command *incoming_command)
{
	Filepath filepath;
	BOOL result = FALSE;

	switch(incoming_command->cmd){
		case rasctl_cmd_shutdown:
			result = DoShutdown(fp,incoming_command);
			break;
		case rasctl_cmd_list:
			result = DoList(fp,incoming_command);
		break;
		case rasctl_cmd_attach:
			result = DoAttach(fp,incoming_command);
			break;
		case rasctl_cmd_insert:
			result = DoInsert(fp,incoming_command);
			break;
		case rasctl_cmd_eject:
			result = DoEject(fp,incoming_command);
			break;
		case rasctl_cmd_protect:
			result = DoProtect(fp,incoming_command);
			break;
		case rasctl_cmd_detach:
			result = DoDetach(fp,incoming_command);
			break;
		default:
			fprintf(fp, "UNKNOWN COMMAND RECEIVED\n");
	}

	return result;
}

BOOL Command_Thread::DoShutdown(FILE *fp, Rasctl_Command *incoming_command){
	fprintf(fp, "Stopping the RaSCSI application\n");
	running = FALSE;
	return TRUE;
}

BOOL Command_Thread::DoList(FILE *fp, Rasctl_Command *incoming_command){
	ListDevice(fp);
	return TRUE;
}

BOOL Command_Thread::DoAttach(FILE *fp, Rasctl_Command *incoming_command){
	Filepath filepath;
	Disk *pUnit = nullptr;

	// Create a new drive, based upon type
	switch (incoming_command->type) {
		case rasctl_dev_sasi_hd:		// HDF
			pUnit = new SASIHD();
			break;
		case rasctl_dev_scsi_hd:		// HDS/HDI
			pUnit = new SCSIHD();
			break;
		case rasctl_dev_scsi_hd_appl:   // HDA
			pUnit = new SCSIHD_APPLE();
			break;
		case rasctl_dev_scsi_hd_nec:    // HDN/NHD
			pUnit = new SCSIHD_NEC();
			break;
		case rasctl_dev_mo:
			pUnit = new SCSIMO();
			break;
		case rasctl_dev_cd:
			pUnit = new SCSICD();
			break;
		case rasctl_dev_br:
			pUnit = new SCSIBR();
			break;
		default:
			FPRT(fp,	"Error : Invalid device type\n");
			return FALSE;
	}

	// drive checks files
	switch(incoming_command->type){
		case rasctl_dev_mo:
		case rasctl_dev_scsi_hd_nec:
		case rasctl_dev_sasi_hd:
		case rasctl_dev_scsi_hd:
		case rasctl_dev_cd:
		case rasctl_dev_scsi_hd_appl:
			if((incoming_command->file != nullptr) && (strnlen(incoming_command->file,_MAX_PATH) > 0)){
				// Set the Path
				filepath.SetPath(incoming_command->file);

				// Open the file path
				if (!pUnit->Open(filepath)) {
					FPRT(fp, "Error : File open error [%s]\n", incoming_command->file);
					delete pUnit;
					return FALSE;
				}

			}
		break;
		case rasctl_dev_br:
		case rasctl_dev_invalid:
			// Do nothing
		break;
	}


	// Set the cache to write-through
	pUnit->SetCacheWB(FALSE);

	// Map the controller
	return AttachDevice(fp, pUnit, incoming_command->id, incoming_command->un);	
}

BOOL Command_Thread::DoDetach(FILE *fp, Rasctl_Command *incoming_command){
	return DetachDevice(fp, incoming_command->id, incoming_command->un);
}

BOOL Command_Thread::DoInsert(FILE *fp, Rasctl_Command *incoming_command){

	Filepath filepath;
	Disk *pUnit = GetDevice(fp, incoming_command->id, incoming_command->un);


	// Does the device exist?
	if (pUnit == nullptr) {
		FPRT(fp, "Error : No such device\n");
		return FALSE;
	}

	// Valid only for MO or CD
	if (pUnit->GetID() != MAKEID('S', 'C', 'M', 'O') &&
		pUnit->GetID() != MAKEID('S', 'C', 'C', 'D')) {
		FPRT(fp, "Error : Insert operation denied(Device isn't removable)\n");
		return FALSE;
	}

	// Set the file path
	filepath.SetPath(incoming_command->file);

	// Open the file
	if (!pUnit->Open(filepath)) {
		FPRT(fp, "Error : File open error [%s]\n", incoming_command->file);
		return FALSE;
	}
	
	return TRUE;
}
BOOL Command_Thread::DoEject(FILE *fp, Rasctl_Command *incoming_command){
	Disk *pUnit = GetDevice(fp, incoming_command->id, incoming_command->un);

	// Does the device exist?
	if (pUnit == nullptr) {
		FPRT(fp, "Error : No such device\n");
		return FALSE;
	}

	// Valid only for MO or CD
	if (pUnit->GetID() != MAKEID('S', 'C', 'M', 'O') &&
		pUnit->GetID() != MAKEID('S', 'C', 'C', 'D')) {
		FPRT(fp, "Error : Operation denied(Deveice isn't removable)\n");
		return FALSE;
	}
	pUnit->Eject(TRUE);
	return TRUE;
}

BOOL Command_Thread::DoProtect(FILE *fp, Rasctl_Command *incoming_command){

	Disk *pUnit = GetDevice(fp, incoming_command->id, incoming_command->un);

	// Does the device exist?
	if (pUnit == nullptr) {
		FPRT(fp, "Error : No such device\n");
		return FALSE;
	}

	// Valid only for MO
	if (pUnit->GetID() != MAKEID('S', 'C', 'M', 'O')) {
		FPRT(fp, "Error : Write Protect denied(Device isn't a MO drive)\n");
		return FALSE;
	}
	pUnit->WriteP(!pUnit->IsWriteP());
	return TRUE;
}











BOOL Command_Thread::HasSuffix(const char* string, const char* suffix) {
	int string_len = strlen(string);
	int suffix_len = strlen(suffix);
	return (string_len >= suffix_len)
		&& (xstrcasecmp(string + (string_len - suffix_len), suffix) == 0);
}

