/*
Copyright (C) 2015-2019 The University of Notre Dame
This software is distributed under the GNU General Public License.
See the file LICENSE for details.
*/

#include "interrupt.h"
#include "console.h"
#include "ioports.h"
#include "clock.h"
#include "string.h"
#include "ata.h"
#include "device.h"
#include "process.h"
#include "mutex.h"

#define ATA_IRQ0	32+14
#define ATA_IRQ1	32+15
#define ATA_IRQ2	32+11
#define ATA_IRQ3	32+9

#define ATA_BASE0	0x1F0
#define ATA_BASE1	0x170
#define ATA_BASE2	0x1E8
#define ATA_BASE3	0x168

#define ATA_TIMEOUT 5000
#define ATA_IDENTIFY_TIMEOUT 1000

#define ATA_DATA	0	/* data register */
#define ATA_ERROR	1	/* error register */
#define ATA_COUNT	2	/* sectors to transfer */
#define ATA_SECTOR	3	/* sector number */
#define ATA_CYL_LO	4	/* low byte of cylinder number */
#define ATA_CYL_HI	5	/* high byte of cylinder number */
#define ATA_FDH		6	/* flags, drive and head */
#define ATA_STATUS	7
#define ATA_COMMAND	7
#define ATA_CONTROL	0x206

#define ATA_FLAGS_ECC	0x80	/* enable error correction */
#define ATA_FLAGS_LBA	0x40	/* enable linear addressing */
#define ATA_FLAGS_SEC	0x20	/* enable 512-byte sectors */
#define ATA_FLAGS_SLV	0x10	/* address the slave drive */

#define ATA_STATUS_BSY	0x80	/* controller busy */
#define ATA_STATUS_RDY	0x40	/* drive ready */
#define ATA_STATUS_WF	0x20	/* write fault */
#define ATA_STATUS_SC	0x10	/* seek complete (obsolete) */
#define ATA_STATUS_DRQ	0x08	/* data transfer request */
#define ATA_STATUS_CRD	0x04	/* corrected data */
#define ATA_STATUS_IDX	0x02	/* index pulse */
#define ATA_STATUS_ERR	0x01	/* error */

#define ATA_COMMAND_IDLE		0x00
#define ATA_COMMAND_READ		0x20	/* read data */
#define ATA_COMMAND_WRITE		0x30	/* write data */
#define ATA_COMMAND_IDENTIFY		0xec

#define ATAPI_COMMAND_IDENTIFY 0xa1
#define ATAPI_COMMAND_PACKET   0xa0

#define ATAPI_FEATURE	1
#define ATAPI_IRR 2
#define ATAPI_SAMTAG 3
#define ATAPI_COUNT_LO 4
#define ATAPI_COUNT_HI 5
#define ATAPI_DRIVE 6

#define SCSI_READ10            0x28
#define SCSI_SENSE             0x03

#define ATA_CONTROL_RESET	0x04
#define ATA_CONTROL_DISABLEINT	0x02

static const int ata_base[4] = { ATA_BASE0, ATA_BASE0, ATA_BASE1, ATA_BASE1 };

static int ata_interrupt_active = 0;
static struct list queue = { 0, 0 };

static struct mutex ata_mutex = MUTEX_INIT;
static int identify_in_progress = 0;

static struct ata_count counters = {{0}};

struct ata_count ata_stats()
{
	return counters;
}

static void ata_interrupt(int intr, int code)
{
	ata_interrupt_active = 1;
	process_wakeup_all(&queue);
}

void ata_reset(int id)
{
	outb(ATA_CONTROL_RESET, ata_base[id] + ATA_CONTROL);
	clock_wait(1);
	outb(0, ata_base[id] + ATA_CONTROL);
	clock_wait(1);
}

static int ata_wait(int id, int mask, int state)
{
	clock_t start, elapsed;
	int t;

	int timeout_millis = identify_in_progress ? ATA_IDENTIFY_TIMEOUT : ATA_TIMEOUT;

	start = clock_read();

	while(1) {
		t = inb(ata_base[id] + ATA_STATUS);
		if((t & mask) == state) {
			return 1;
		}
		if(t & ATA_STATUS_ERR) {
			printf("ata: error\n");
			ata_reset(id);
			return 0;
		}
		elapsed = clock_diff(start, clock_read());
		int elapsed_millis = elapsed.seconds * 1000 + elapsed.millis;
		if(elapsed_millis > timeout_millis) {
			if(!identify_in_progress) {
				printf("ata: timeout\n");
			}
			ata_reset(id);
			return 0;
		}
		process_yield();
	}
}

static void ata_pio_read(int id, void *buffer, int size)
{
	uint16_t *wbuffer = (uint16_t *) buffer;
	while(size > 0) {
		*wbuffer = inw(ata_base[id] + ATA_DATA);
		wbuffer++;
		size -= 2;
	}
}

static void ata_pio_write(int id, const void *buffer, int size)
{
	uint16_t *wbuffer = (uint16_t *) buffer;
	while(size > 0) {
		outw(*wbuffer, ata_base[id] + ATA_DATA);
		wbuffer++;
		size -= 2;
	}
}

static int ata_begin(int id, int command, int nblocks, int offset)
{
	int base = ata_base[id];
	int sector, clow, chigh, flags;

	// enable error correction and linear addressing
	flags = ATA_FLAGS_ECC | ATA_FLAGS_LBA | ATA_FLAGS_SEC;

	// turn on the slave bit for odd-numbered drives
	if(id % 2)
		flags |= ATA_FLAGS_SLV;

	// slice up the linear address in order to fit in the arguments
	sector = (offset >> 0) & 0xff;
	clow = (offset >> 8) & 0xff;
	chigh = (offset >> 16) & 0xff;
	flags |= (offset >> 24) & 0x0f;

	// wait for the disk to calm down
	if(!ata_wait(id, ATA_STATUS_BSY, 0))
		return 0;

	// get the attention of the proper disk
	outb(flags, base + ATA_FDH);

	// wait again for the disk to indicate ready
	// special case: ATAPI identification does not raise RDY flag

	int ready;
	if(command == ATAPI_COMMAND_IDENTIFY) {
		ready = ata_wait(id, ATA_STATUS_BSY, 0);
	} else {
		ready = ata_wait(id, ATA_STATUS_BSY | ATA_STATUS_RDY, ATA_STATUS_RDY);
	}

	if(!ready)
		return 0;

	// send the arguments
	outb(0, base + ATA_CONTROL);
	outb(nblocks, base + ATA_COUNT);
	outb(sector, base + ATA_SECTOR);
	outb(clow, base + ATA_CYL_LO);
	outb(chigh, base + ATA_CYL_HI);
	outb(flags, base + ATA_FDH);

	// execute the command
	outb(command, base + ATA_COMMAND);

	return 1;
}

static int ata_read_unlocked(int id, void *buffer, int nblocks, int offset)
{
	int i;
	if(!ata_begin(id, ATA_COMMAND_READ, nblocks, offset))
		return 0;

	// XXX On fast virtual hardware, waiting for the interrupt
	// doesn't work b/c it has already arrived before we get here.
	// For now, busy wait until a fix is in place.

	// if(ata_interrupt_active) process_wait(&queue);

	for(i = 0; i < nblocks; i++) {
		if(!ata_wait(id, ATA_STATUS_DRQ, ATA_STATUS_DRQ))
			return 0;
		ata_pio_read(id, buffer, ATA_BLOCKSIZE);
		buffer = ((char *) buffer) + ATA_BLOCKSIZE;
		offset++;
	}
	if(!ata_wait(id, ATA_STATUS_BSY, 0))
		return 0;
	return nblocks;
}

int ata_read(int id, void *buffer, int nblocks, int offset)
{
	int result;
	mutex_lock(&ata_mutex);
	result = ata_read_unlocked(id, buffer, nblocks, offset);
	mutex_unlock(&ata_mutex);
	counters.blocks_read[id] += nblocks;
	if (current) {
		current->stats.blocks_read += nblocks;
		current->stats.bytes_read += nblocks*ATA_BLOCKSIZE;
	}
	return result;
}

static int atapi_begin(int id, void *data, int length)
{
	int base = ata_base[id];
	int flags;

	// enable error correction and linear addressing
	flags = ATA_FLAGS_ECC | ATA_FLAGS_LBA | ATA_FLAGS_SEC;

	// turn on the slave bit for odd-numbered drives
	if(id % 2)
		flags |= ATA_FLAGS_SLV;

	// wait for the disk to calm down
	if(!ata_wait(id, ATA_STATUS_BSY, 0))
		return 0;

	// get the attention of the proper disk
	outb(flags, base + ATA_FDH);

	// wait again for the disk to indicate ready
	if(!ata_wait(id, ATA_STATUS_BSY, 0))
		return 0;

	// send the arguments
	outb(0, base + ATAPI_FEATURE);
	outb(0, base + ATAPI_IRR);
	outb(0, base + ATAPI_SAMTAG);
	outb(length & 0xff, base + ATAPI_COUNT_LO);
	outb(length >> 8, base + ATAPI_COUNT_HI);

	// execute the command
	outb(ATAPI_COMMAND_PACKET, base + ATA_COMMAND);

	// wait for ready
	if(!ata_wait(id, ATA_STATUS_BSY | ATA_STATUS_DRQ, ATA_STATUS_DRQ))
		return 0;

	// send the ATAPI packet
	ata_pio_write(id, data, length);

	return 1;
}

static int atapi_read_unlocked(int id, void *buffer, int nblocks, int offset)
{
	uint8_t packet[12];
	int length = sizeof(packet);
	int i;

	packet[0] = SCSI_READ10;
	packet[1] = 0;
	packet[2] = offset >> 24;
	packet[3] = offset >> 16;
	packet[4] = offset >> 8;
	packet[5] = offset >> 0;
	packet[6] = 0;
	packet[7] = nblocks >> 8;
	packet[8] = nblocks >> 0;
	packet[9] = 0;
	packet[10] = 0;
	packet[11] = 0;

	if(!atapi_begin(id, packet, length))
		return 0;

	// XXX On fast virtual hardware, waiting for the interrupt
	// doesn't work b/c it has already arrived before we get here.
	// For now, busy wait until a fix is in place.

	// if(ata_interrupt_active) process_wait(&queue);

	for(i = 0; i < nblocks; i++) {
		if(!ata_wait(id, ATA_STATUS_DRQ, ATA_STATUS_DRQ))
			return 0;
		ata_pio_read(id, buffer, ATAPI_BLOCKSIZE);
		buffer = ((char *) buffer) + ATAPI_BLOCKSIZE;
		offset++;
	}

	return 1;
}

int atapi_read(int id, void *buffer, int nblocks, int offset)
{
	int result;
	mutex_lock(&ata_mutex);
	result = atapi_read_unlocked(id, buffer, nblocks, offset);
	mutex_unlock(&ata_mutex);
	counters.blocks_read[id] += nblocks;
	if (current) {
		current->stats.blocks_read += nblocks;
		current->stats.bytes_read += nblocks * ATAPI_BLOCKSIZE;
	}
	return result;
}

static int ata_write_unlocked(int id, const void *buffer, int nblocks, int offset)
{
	int i;
	if(!ata_begin(id, ATA_COMMAND_WRITE, nblocks, offset))
		return 0;
	for(i = 0; i < nblocks; i++) {
		if(!ata_wait(id, ATA_STATUS_DRQ, ATA_STATUS_DRQ))
			return 0;
		ata_pio_write(id, buffer, ATA_BLOCKSIZE);
		buffer = ((char *) buffer) + ATA_BLOCKSIZE;
		offset++;
	}
	// XXX On fast virtual hardware, waiting for the interrupt
	// doesn't work b/c it has already arrived before we get here.
	// For now, busy wait until a fix is in place.

	// if(ata_interrupt_active) process_wait(&queue);

	if(!ata_wait(id, ATA_STATUS_BSY, 0))
		return 0;
	return nblocks;
}

int ata_write(int id, const void *buffer, int nblocks, int offset)
{
	int result;
	mutex_lock(&ata_mutex);
	result = ata_write_unlocked(id, buffer, nblocks, offset);
	mutex_unlock(&ata_mutex);
	counters.blocks_written[id] += nblocks;
	if (current) {
		current->stats.blocks_written += nblocks;
		current->stats.bytes_written += nblocks * ATA_BLOCKSIZE;
	}
	return result;
}

/*
ata_probe sends an IDENTIFY DEVICE command to the device.
If a device is connected, it will respond with 512 bytes
of identifying data, described on page 48 of the ATA-3 standard.
If no response comes within the timeout window, we assume
the the device is simply not connected.
*/

static int ata_identify(int id, int command, void *buffer)
{
	int result;
	identify_in_progress = 1;
	if(ata_begin(id, command, 0, 0) && ata_wait(id, ATA_STATUS_DRQ, ATA_STATUS_DRQ)) {
		ata_pio_read(id, buffer, 512);
		result = 1;
	} else {
		result = 0;
	}
	identify_in_progress = 0;
	return result;
}


static int ata_probe_internal( int id, int kind, int *nblocks, int *blocksize, char *name )
{
	uint16_t buffer[256];
	char *cbuffer = (char *) buffer;

	/*
	   First check for 0xff in the controller status register,
	   which would indicate that there is nothing attached.
	 */

	uint8_t t = inb(ata_base[id] + ATA_STATUS);
	if(t == 0xff) {
		printf("ata unit %d: nothing attached\n", id);
		return 0;
	}

	/* Now reset the unit to check for register signatures. */
	ata_reset(id);

	/* Clear the buffer to receive the identify data. */
	memset(cbuffer, 0, 512);

	int result = 0;

	/* Do either an ATA or ATAPI identify, or do both if kind is zero */

	if(kind==ATA_COMMAND_IDENTIFY || kind==0) {
		result = ata_identify(id, ATA_COMMAND_IDENTIFY, cbuffer);
		if(result) {
			*nblocks = buffer[1] * buffer[3] * buffer[6];
			printf("%d logical cylinders\n", buffer[1]);
			printf("%d logical heads\n", buffer[3]);
			printf("%d logical sectors/track\n", buffer[6]);
			*blocksize = ATA_BLOCKSIZE;
		}
	}

	if(kind==ATAPI_COMMAND_IDENTIFY || (kind==0 && result==0) ) {
		result = ata_identify(id, ATAPI_COMMAND_IDENTIFY, cbuffer);
		if(result) {
			// XXX use SCSI sense to get media size
			*nblocks = 337920;
			*blocksize = ATAPI_BLOCKSIZE;
		}
	}

	if(!result) {
		printf("ata unit %d: not connected\n", id);
		return 0;
	}

	/* Now byte-swap the data so as the generate byte-ordered strings */
	uint32_t i;
	for(i = 0; i < 512; i += 2) {
		t = cbuffer[i];
		cbuffer[i] = cbuffer[i + 1];
		cbuffer[i + 1] = t;
	}
	cbuffer[256] = 0;

	/* Vendor supplied name is at byte 54 */
	strcpy(name,&cbuffer[54]);
	name[40] = 0;

	/* Get disk size in megabytes*/
	uint32_t mbytes = (*nblocks) / KILO * (*blocksize) / KILO;

	printf("%s unit %d: %s %u sectors %u MB %s\n",
	       (*blocksize)==512 ? "ata" : "atapi",
	       id,
	       (*blocksize)==512 ? "disk" : "cdrom",
	       *nblocks, mbytes, name);
	return 1;
}

int ata_probe( int id, int *nblocks, int *blocksize, char *name )
{
	return ata_probe_internal(id,ATA_COMMAND_IDENTIFY,nblocks,blocksize,name);
}

int atapi_probe( int id, int *nblocks, int *blocksize, char *name )
{
	return ata_probe_internal(id,ATAPI_COMMAND_IDENTIFY,nblocks,blocksize,name);
}

static struct device_driver ata_driver = {
	.name          = "ata",
	.probe         = ata_probe,
	.read          = ata_read,
	.read_nonblock = ata_read,
	.write         = ata_write,
	.multiplier    = 8
};

static struct device_driver atapi_driver = {
	.name          = "atapi",
	.probe         = atapi_probe,
	.read          = atapi_read,
	.read_nonblock = atapi_read,
};

void ata_init()
{
	int i;

	int nblocks;
	int blocksize = 0;

	char longname[256];
	for (int i = 0; i < 4; i++) {
		counters.blocks_read[i] = 0;
		counters.blocks_written[i] = 0;
	}

	printf("ata: setting up interrupts\n");

	interrupt_register(ATA_IRQ0, ata_interrupt);
	interrupt_enable(ATA_IRQ0);

	interrupt_register(ATA_IRQ1, ata_interrupt);
	interrupt_enable(ATA_IRQ1);

	printf("ata: probing devices\n");

	for(i = 0; i < 4; i++) {
		ata_probe_internal(i, 0, &nblocks, &blocksize, longname);
	}

	device_driver_register(&ata_driver);
	device_driver_register(&atapi_driver);
}
