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

#include "../../hardware.h"
#include "../../menu.h"
#include "../../file_io.h"
#include "../../debug.h"
#include "../../user_io.h"
#include "../../fpga_io.h"
#include "st_tos.h"

#define CONFIG_FILENAME  "ATARIST0.CFG"

const char* tos_mem[] = { "512kb", "1mb", "2mb", "4mb", "8mb", "14mb", "--", "--" };
const char* tos_scanlines[] = { "Off","25%","50%","75%" };
const char* tos_stereo[] = { "Mono","Stereo" };
const char* tos_chipset[] = { "ST","STE","MegaSTE","STEroids" };
const char* tos_chipset_short[] = { "ST","STe","MST","ST+" };

typedef struct {
	unsigned long system_ctrl;  // system control word
	char tos_img[1024];
	char cart_img[1024];
	char acsi_img[2][1024];
	char video_adjust[2];
	char cdc_control_redirect;
} tos_config_t;

static tos_config_t config;

fileTYPE hdd_image[2] = {};

static unsigned char dma_buffer[512];

static const char *acsi_cmd_name(int cmd) {
	static const char *cmdname[] = {
		"Test Drive Ready", "Restore to Zero", "Cmd $2", "Request Sense",
		"Format Drive", "Read Block limits", "Reassign Blocks", "Cmd $7",
		"Read Sector", "Cmd $9", "Write Sector", "Seek Block",
		"Cmd $C", "Cmd $D", "Cmd $E", "Cmd $F",
		"Cmd $10", "Cmd $11", "Inquiry", "Verify",
		"Cmd $14", "Mode Select", "Cmd $16", "Cmd $17",
		"Cmd $18", "Cmd $19", "Mode Sense", "Start/Stop Unit",
		"Cmd $1C", "Cmd $1D", "Cmd $1E", "Cmd $1F",
		// extended commands supported by ICD feature:
		"Cmd $20", "Cmd $21", "Cmd $22",
		"Read Format Capacities", "Cmd $24", "Read Capacity (10)",
		"Cmd $26", "Cmd $27", "Read (10)", "Read Generation",
		"Write (10)", "Seek (10)"
	};

	if (cmd > 0x2b) return NULL;

	return cmdname[cmd];
}

static int uart_mode = 0;
static void set_control(uint32_t ctrl)
{
	ctrl = uart_mode ? (ctrl | TOS_CONTROL_REDIR0) : (ctrl & ~TOS_CONTROL_REDIR0);

	spi_uio_cmd_cont(UIO_SET_STATUS2);
	spi32w(ctrl);
	DisableIO();
}

void tos_update_sysctrl(uint32_t ctrl)
{
	config.system_ctrl = ctrl;
	set_control(config.system_ctrl);
}

unsigned long tos_system_ctrl()
{
	return config.system_ctrl;
}

void tos_uart_mode(int enable)
{
	uart_mode = enable;
	set_control(config.system_ctrl);
}

static void memory_read(uint8_t *data, uint32_t words)
{
	EnableIO();
	spi8(ST_READ_MEMORY);

	// transmitted bytes must be multiple of 2 (-> words)
	uint16_t *buf = (uint16_t*)data;
	while (words--) *buf++ = spi_w(0);

	DisableIO();
}

static void memory_write(uint8_t *data, uint32_t words)
{
	EnableIO();
	spi8(ST_WRITE_MEMORY);

	uint16_t *buf = (uint16_t*)data;
	while (words--) spi_w(*buf++);

	DisableIO();
}

static void dma_ack(uint8_t status)
{
	EnableIO();
	spi8(ST_ACK_DMA);
	spi8(status);
	DisableIO();
}

static void dma_nak()
{
	EnableIO();
	spi8(ST_NAK_DMA);
	DisableIO();
}

static void handle_acsi(unsigned char *buffer)
{
	static uint8_t buf[65536];

	static uint8_t asc[2] = { 0,0 };
	uint8_t target = buffer[10] >> 5;
	uint8_t device = buffer[1] >> 5;
	uint8_t cmd = buffer[0];
	uint32_t lba = 256 * 256 * (buffer[1] & 0x1f) + 256 * buffer[2] + buffer[3];
	uint32_t length = buffer[4];
	if (length == 0) length = 256;

	if (0)
	{
		tos_debugf("ACSI: target %d.%d, \"%s\" (%02x)", target, device, acsi_cmd_name(cmd), cmd);
		tos_debugf("ACSI: lba %u (%x), length %u", lba, lba, length);
	}

	// only a harddisk on ACSI 0/1 is supported
	// ACSI 0/1 is only supported if a image is loaded
	if (((target < 2) && (hdd_image[target].size != 0)))
	{
		uint32_t blocks = hdd_image[target].size / 512;

		// only lun0 is fully supported
		switch (cmd) {
		case 0x25:
			if (device == 0) {
				bzero(dma_buffer, 512);
				dma_buffer[0] = (blocks - 1) >> 24;
				dma_buffer[1] = (blocks - 1) >> 16;
				dma_buffer[2] = (blocks - 1) >> 8;
				dma_buffer[3] = (blocks - 1) >> 0;
				dma_buffer[6] = 2;  // 512 bytes per block

				memory_write(dma_buffer, 4);

				dma_ack(0x00);
				asc[target] = 0x00;
			}
			else {
				dma_ack(0x02);
				asc[target] = 0x25;
			}
			break;

		case 0x00: // test drive ready
		case 0x04: // format
			if (device == 0) {
				asc[target] = 0x00;
				dma_ack(0x00);
			}
			else {
				asc[target] = 0x25;
				dma_ack(0x02);
			}
			break;

		case 0x03: // request sense
			if (device != 0)
				asc[target] = 0x25;

			bzero(dma_buffer, 512);
			dma_buffer[7] = 0x0b;
			if (asc[target] != 0) {
				dma_buffer[2] = 0x05;
				dma_buffer[12] = asc[target];
			}
			memory_write(dma_buffer, 9); // 18 bytes
			dma_ack(0x00);
			asc[target] = 0x00;
			break;

		case 0x08: // read sector
		case 0x28: // read (10)
			if (device == 0)
			{
				if (cmd == 0x28)
				{
					lba =
						256 * 256 * 256 * buffer[2] +
						256 * 256 * buffer[3] +
						256 * buffer[4] +
						buffer[5];

					length = 256 * buffer[7] + buffer[8];
					//	  iprintf("READ(10) %d, %d\n", lba, length);
				}

				if (lba + length <= blocks)
				{
					DISKLED_ON;
					FileSeekLBA(&hdd_image[target], lba);
					while (length)
					{
						uint32_t len = length;
						if (len > 128) len = 128;
						length -= len;

						len *= 512;
						FileReadAdv(&hdd_image[target], buf, len);
						memory_write(buf, len / 2);
					}
					DISKLED_OFF;

					dma_ack(0x00);
					asc[target] = 0x00;
				}
				else
				{
					tos_debugf("ACSI: read (%u+%d) exceeds device limits (%u)", lba, length, blocks);
					dma_ack(0x02);
					asc[target] = 0x21;
				}
			}
			else
			{
				dma_ack(0x02);
				asc[target] = 0x25;
			}
			break;

		case 0x0a: // write sector
		case 0x2a: // write (10)
			if (device == 0)
			{
				if (cmd == 0x2a)
				{
					lba =
						256 * 256 * 256 * buffer[2] +
						256 * 256 * buffer[3] +
						256 * buffer[4] +
						buffer[5];

					length = 256 * buffer[7] + buffer[8];

					//	  iprintf("WRITE(10) %d, %d\n", lba, length);
				}

				if (lba + length <= blocks)
				{
					DISKLED_ON;
					FileSeekLBA(&hdd_image[target], lba);
					while (length)
					{
						uint32_t len = length;
						if (len > 128) len = 128;
						length -= len;

						len *= 512;
						memory_read(buf, len / 2);
						FileWriteAdv(&hdd_image[target], buf, len);
					}
					DISKLED_OFF;
					dma_ack(0x00);
					asc[target] = 0x00;
				}
				else {
					tos_debugf("ACSI: write (%u+%d) exceeds device limits (%u)", lba, length, blocks);
					dma_ack(0x02);
					asc[target] = 0x21;
				}
			}
			else {
				dma_ack(0x02);
				asc[target] = 0x25;
			}
			break;

		case 0x12: // inquiry
			tos_debugf("ACSI: Inquiry %.11s", hdd_image[target].name);
			bzero(dma_buffer, 512);
			dma_buffer[2] = 2;                                   // SCSI-2
			dma_buffer[4] = length - 5;                          // len
			memcpy(dma_buffer + 8, "MISTer  ", 8);               // Vendor
			memcpy(dma_buffer + 16, "                ", 16);     // Clear device entry
			memcpy(dma_buffer + 16, hdd_image[target].name, 11); // Device
			memcpy(dma_buffer + 32, "ATH ", 4);                  // Product revision
			memcpy(dma_buffer + 36, VDATE "  ", 8);              // Serial number
			if (device != 0) dma_buffer[0] = 0x7f;
			memory_write(dma_buffer, length / 2);
			dma_ack(0x00);
			asc[target] = 0x00;
			break;

		case 0x1a: // mode sense
			if (device == 0) {
				tos_debugf("ACSI: mode sense, blocks = %u", blocks);
				bzero(dma_buffer, 512);
				dma_buffer[3] = 8;            // size of extent descriptor list
				dma_buffer[5] = blocks >> 16;
				dma_buffer[6] = blocks >> 8;
				dma_buffer[7] = blocks;
				dma_buffer[10] = 2;           // byte 1 of block size in bytes (512)
				memory_write(dma_buffer, length / 2);
				dma_ack(0x00);
				asc[target] = 0x00;
			}
			else {
				asc[target] = 0x25;
				dma_ack(0x02);
			}
			break;

#if 0
		case 0x1f: // ICD command?
			tos_debugf("ACSI: ICD command %s ($%02x)",
				acsi_cmd_name(buffer[1] & 0x1f), buffer[1] & 0x1f);
			asc[target] = 0x05;
			dma_ack(0x02);
			break;
#endif

		default:
			tos_debugf("ACSI: >>>>>>>>>>>> Unsupported command <<<<<<<<<<<<<<<<");
			asc[target] = 0x20;
			dma_ack(0x02);
			break;
		}
	}
	else {
		tos_debugf("ACSI: Request for unsupported target");

		// tell acsi state machine that io controller is done
		// but don't generate a acsi irq
		dma_nak();
	}
}

static void get_dmastate()
{
	uint8_t buffer[16];

	EnableIO();
	spi8(ST_GET_DMASTATE);
	spi_read(buffer, 16, 0);
	DisableIO();

	if (buffer[10] & 0x01) handle_acsi(buffer);
}

static void fill_tx(uint16_t fill, uint32_t len, int index)
{
	user_io_set_index(index);
	user_io_set_download(1);

	len /= 2;
	EnableFpga();
	spi8(UIO_FILE_TX_DAT);
	while(len--) spi_w(fill);
	DisableFpga();

	user_io_set_download(0);
}

void tos_load_cartridge(const char *name)
{
	if (name) strncpy(config.cart_img, name, 11);

	// erase that ram area to remove any previously uploaded
	// image
	tos_debugf("Erasing cart memory");
	fill_tx(0xff, 128 * 1024, 2);

	// upload cartridge
	if (config.cart_img[0] && FileExists(config.cart_img))
	{
		user_io_file_tx(config.cart_img, 2);
		tos_debugf("%s uploaded", config.cart_img);
		return;
	}
}

char tos_cartridge_is_inserted()
{
	return config.cart_img[0];
}

void tos_poll()
{
	static unsigned long timer = 0;

	get_dmastate();

	// check the user button
	if (!user_io_osd_is_visible() && user_io_user_button())
	{
		if (!timer) timer = GetTimer(1000);
		else if (timer != 1)
		{
			if (CheckTimer(timer))
			{
				tos_reset(1);
				timer = 1;
			}
		}
	}
	else
	{
		timer = 0;
	}
}

const char *tos_get_disk_name(int index)
{
	const char *name = 0;
	if(index <= 1) name = get_image_name(index);
	else
	{
		if (!hdd_image[index & 1].size) name = 0;
		else
		{
			name = strrchr(hdd_image[index & 1].name, '/');
			if (!name) name = hdd_image[index & 1].name; else name++;
		}
	}

	return name ? name : "* no disk *";
}

const char *tos_get_image_name()
{
	char *p = strrchr(config.tos_img, '/');
	return p ? p+1 : config.tos_img;
}

const char *tos_get_cartridge_name()
{
	if (!config.cart_img[0])  return "* no cartridge *";
	char *p = strrchr(config.cart_img, '/');
	return p ? p + 1 : config.cart_img;
}

char tos_disk_is_inserted(int index)
{
	if (index <= 1) return (get_image_name(index) != NULL);
	return hdd_image[index & 1].size != 0;
}

static void tos_select_hdd_image(int i, const char *name)
{
	tos_debugf("Select ACSI%c image %s", '0' + i, name);

	strcpy(config.acsi_img[i], name);
	if (!strlen(name))
	{
		FileClose(&hdd_image[i]);
		hdd_image[i].size = 0;
		config.system_ctrl &= ~(TOS_ACSI0_ENABLE << i);
	}
	else
	{
		if (FileOpenEx(&hdd_image[i], name, (O_RDWR | O_SYNC)))
		{
			config.system_ctrl |= (TOS_ACSI0_ENABLE << i);
		}
	}

	// update system control
	set_control(config.system_ctrl);
}

void tos_insert_disk(int index, const char *name)
{
	static int wpins = 0;

	if (index <= 1)
	{
		user_io_file_mount(name, index);
		if (tos_disk_is_inserted(index))
		{
			if (!index) wpins &= ~TOS_CONTROL_FDC_WR_PROT_A;
			else wpins &= ~TOS_CONTROL_FDC_WR_PROT_B;
		}
		else
		{
			if (!index) wpins |= TOS_CONTROL_FDC_WR_PROT_A;
			else wpins |= TOS_CONTROL_FDC_WR_PROT_B;
		}

		set_control(config.system_ctrl ^ (wpins & (TOS_CONTROL_FDC_WR_PROT_A | TOS_CONTROL_FDC_WR_PROT_B)));
	}
	else tos_select_hdd_image(index & 1, name);
}

// force ejection of all disks (SD card has been removed)
void tos_eject_all()
{
	for (int i = 0; i < 4; i++) tos_insert_disk(i, "");
}

void tos_reset(char cold)
{
	tos_update_sysctrl(config.system_ctrl | TOS_CONTROL_CPU_RESET);  // set reset
	if (cold)
	{
		// clear first 16k
		tos_debugf("Clear first 16k");
		fill_tx(0, 16 * 1024, 3);

		// upload and verify tos image
		int len = FileLoad(config.tos_img, 0, 0);
		if (len)
		{
			tos_debugf("TOS.IMG:\n  size = %d", len);

			if (len >= 256 * 1024) user_io_file_tx(config.tos_img, 0);
			else if (len == 192 * 1024) user_io_file_tx(config.tos_img, 1);
			else tos_debugf("WARNING: Unexpected TOS size!");
		}
		else
		{
			tos_debugf("Unable to find tos.img");
			return;
		}

		tos_load_cartridge(NULL);

		for (int i = 0; i < 2; i++)
		{
			if (FileExists(config.acsi_img[i]))
			{
				tos_select_hdd_image(i, config.acsi_img[i]);
			}
		}
	}
	tos_update_sysctrl(config.system_ctrl & ~TOS_CONTROL_CPU_RESET);  // release reset
}

void tos_upload(const char *name)
{
	if(name) strcpy(config.tos_img, name);
	tos_reset(1);
}

// load/init configuration
void tos_config_load(int slot)
{
	char name[64] = { CONFIG_FILENAME };

	static char last_slot = 0;
	char new_slot;

	tos_eject_all();

	new_slot = (slot == -1) ? last_slot : slot;
	memset(&config, 0, sizeof(config));

	// set default values
	config.system_ctrl = TOS_MEMCONFIG_1M | TOS_CONTROL_VIDEO_COLOR | TOS_CONTROL_BORDER;
	strcpy(config.tos_img, user_io_get_core_path());
	strcat(config.tos_img, "/TOS.IMG");

	// try to load config
	name[7] = '0' + new_slot;
	int len = FileLoadConfig(name, 0, 0);
	tos_debugf("Configuration file size: %d (should be %d)", len, sizeof(tos_config_t));
	if (len == sizeof(tos_config_t)) FileLoadConfig(name, &config, sizeof(tos_config_t));

	// ethernet is auto detected later
	config.system_ctrl &= ~TOS_CONTROL_ETHERNET;
}

// save configuration
void tos_config_save(int slot)
{
	char name[64] = { CONFIG_FILENAME };
	name[7] = '0' + slot;
	FileSaveConfig(name, &config, sizeof(config));
}

// configuration file check
int tos_config_exists(int slot)
{
	char name[64] = { CONFIG_FILENAME };
	name[7] = '0' + slot;
	return FileLoadConfig(name, 0, 0);
}

const char* tos_get_cfg_string(int num)
{
	static char str[256];

	char name[64] = { CONFIG_FILENAME };
	name[7] = '0' + num;

	static tos_config_t tmp;
	memset(&tmp, 0, sizeof(tmp));

	int len = FileLoadConfig(name, 0, 0);
	if (len == sizeof(tos_config_t))
	{
		FileLoadConfig(name, &tmp, sizeof(tos_config_t));

		memset(str, 0, sizeof(str));
		strcat(str, tos_mem[(tmp.system_ctrl >> 1) & 7]);
		strcat(str, " ");
		if (tmp.acsi_img[0][0]) strcat(str, "H0 ");
		if (tmp.acsi_img[1][0]) strcat(str, "H1 ");
		if (tmp.cart_img[0]) strcat(str, "CR ");
		strcat(str, tos_chipset_short[(tmp.system_ctrl >> 23) & 3]);
		strcat(str, " ");
		if (!((tmp.system_ctrl >> 23) & 3) && (tmp.system_ctrl & TOS_CONTROL_BLITTER)) strcat(str, "B ");
		if (tmp.system_ctrl & TOS_CONTROL_VIKING) strcat(str, "V ");
		if (tmp.system_ctrl & TOS_CONTROL_VIDEO_AR) strcat(str, "W ");
		strcat(str, (tmp.system_ctrl & TOS_CONTROL_VIDEO_COLOR) ? "C " : "M ");
		if (!(tmp.system_ctrl & TOS_CONTROL_BORDER)) strcat(str, "F ");
		int sl = (tmp.system_ctrl >> 20) & 3;
		if (sl) sprintf(str + strlen(str), "S%d ", sl);
		strcat(str, (tmp.system_ctrl & TOS_CONTROL_STEREO) ? "AS " : "AM ");

		if (tmp.tos_img[0])
		{
			char *p = strrchr(tmp.tos_img, '/');
			if (!p) p = tmp.tos_img; else p++;
			int len = strlen(p);
			if (len >= 4) len -= 4;
			memcpy(str + strlen(str), p, len);
		}
		return str;
	}

	return "< empty slot >";
}
