/*
 * ioc_ata.c - ATA driver implementation
 *
 * Copyright (c) 2017 Maxime Villard
 *
 * This file is part of ALMOS-MKH.
 *
 * ALMOS-MKH is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2.0 of the License.
 *
 * ALMOS-MKH is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with ALMOS-MKH; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include <chdev.h>
#include <dev_ioc.h>
#include <hal_drivers.h>
#include <hal_kentry.h>
#include <thread.h>
#include <spinlock.h>

#include <hal_internal.h>

#define PIO_ATA_CBR_BASE	0x1F0
#	define ATA_DATA		0x000
#	define ATA_ERRFEAT	0x001 /* two regs */
#	define ATA_SCR		0x002
#	define ATA_SNR		0x003 /* lba0 */
#	define ATA_CLR		0x004 /* lba1 */
#	define ATA_CHR		0x005 /* lba2 */
#	define ATA_DHR		0x006 /* drive | lba3 */
#	define ATA_SR		0x007
#		define ATA_SR_ERR	0x01
#		define ATA_SR_IDX	0x02
#		define ATA_SR_CORR	0x04
#		define ATA_SR_DRQ	0x08
#		define ATA_SR_DSC	0x10
#		define ATA_SR_DF	0x20
#		define ATA_SR_DRDY	0x40
#		define ATA_SR_BSY	0x80
#	define ATA_CR		0x007 /* two regs */

#define ATA_CMD_READ_SECTORS_RETRY	0x20
#define ATA_CMD_READ_SECTORS_NORETRY	0x21
#define ATA_CMD_WRITE_SECTORS_RETRY	0x30
#define ATA_CMD_WRITE_SECTORS_NORETRY	0x31
#define ATA_CMD_IDENTIFY		0xEC

static inline uint16_t ata_data_read()
{
	return in16(PIO_ATA_CBR_BASE + ATA_DATA);
}

static inline void ata_data_write(uint16_t val)
{
	out16(PIO_ATA_CBR_BASE + ATA_DATA, val);
}

static inline uint8_t ata_cbr_read(uint32_t reg)
{
	return in8(PIO_ATA_CBR_BASE + reg);
}

static inline void ata_cbr_write(uint32_t reg, uint8_t val)
{
	out8(PIO_ATA_CBR_BASE + reg, val);
}

static inline int ata_wait()
{
	uint8_t status;

	while (1) {
		status = ata_cbr_read(ATA_SR);
		if (status & ATA_SR_DRQ)
			break;
	}

	return ((status & ATA_SR_ERR) ? -1 : 0);
}

static void ata_prepare(uint8_t slave, uint32_t lba, uint8_t count)
{
	ata_cbr_write(ATA_ERRFEAT, 0x00); /* NULL byte to port 0x1F1 */
	ata_cbr_write(ATA_SCR, count); /* sector count */

	/* set the lba */
	ata_cbr_write(ATA_SNR, (lba >> 0) & 0xFF);
	ata_cbr_write(ATA_CLR, (lba >> 8) & 0xFF);
	ata_cbr_write(ATA_CHR, (lba >> 16)& 0xFF);

	/* set the drive and lba3 */
	ata_cbr_write(ATA_DHR, 0xE0 | (slave << 4) | ((lba >> 24) & 0x0F));
}

static int ata_read(uint8_t count, char *buf)
{
	uint16_t tmpword;
	size_t idx, n;

	for (n = 0; n < count; n++) {
		/* wait for the drive to signal that it's ready */
		if (ata_wait() == -1)
			x86_panic("ata_wait");

		/* read one block */
		for (idx = 0; idx < 256; idx++) {
			tmpword = ata_data_read();
			buf[n * 512 + idx * 2] = (uint8_t)(tmpword & 0xFF);
			buf[n * 512 + idx * 2 + 1] = (uint8_t)(tmpword >> 8);
		}
	}

	return 0;
}

static int ata_write(uint8_t count, char *buf)
{
	uint16_t tmpword;
	size_t idx, n;

	for (n = 0; n < count; n++) {
		/* wait for the drive to signal that it's ready */
		if (ata_wait() == -1)
			x86_panic("ata_wait");

		/* write one block */
		for (idx = 0; idx < 256; idx++) {
			tmpword = (buf[n * 512 + idx * 2 + 1] << 8) |
			           buf[n * 512 + idx * 2];
			ata_data_write(tmpword);
		}
	}

	return 0;
}

static uint16_t bswap16(uint16_t x)
{
	return ((x << 8) & 0xFF00) | ((x >> 8) & 0x00FF);
}

static void ata_init()
{
	uint8_t id[512];
	uint16_t tmpw, *p;
	size_t idx;
	char *model;
	uint8_t ncyl;
	int ret;

	ata_prepare(0, 0, 0);
	ata_cbr_write(ATA_CR, ATA_CMD_IDENTIFY);

	/* wait for the drive to signal that it's ready */
	ret = ata_wait();
	if (ret == -1)
		x86_panic("-> unable to identify ATA\n");

	for (idx = 0; idx < 256; idx++) {
		tmpw = ata_data_read();
		id[idx * 2]     = (uint8_t)((tmpw >> 0) & 0xFF);
		id[idx * 2 + 1] = (uint8_t)((tmpw >> 8) & 0xFF);
	}

	ncyl = id[0] & 0x1;
	x86_printf("-> cyl: %z\n", (uint64_t)ncyl);

	for (idx = 27*2; idx < 46*2; idx += 2) {
		p = (uint16_t *)(&id[idx]);
		*p = bswap16(*p);
	}

	model = &id[27*2];
	id[46*2] = '\0';
	x86_printf("-> ATA model: '%s'\n", model);
}

/* -------------------------------------------------------------------------- */

static void ioc_ata_cmd(xptr_t th_xp);
extern void x86_ioapic_ata0();

void ioc_ata_init(chdev_t *chdev)
{
	chdev->cmd = &ioc_ata_cmd;
	chdev->isr = &x86_ioapic_ata0;
	ata_init();
}

static void ioc_ata_cmd(xptr_t th_xp)
{
	uint32_t   cmd_type;     // IOC_READ / IOC_WRITE / IOC_SYNC_READ
	uint32_t   lba;          // command argument
	uint32_t   count;        // command argument
	xptr_t     buf_xp;       // command argument

	// get client thread cluster and local pointer
	cxy_t      th_cxy = GET_CXY( th_xp );
	thread_t * th_ptr = (thread_t *)GET_PTR( th_xp );

	// get command arguments and extended pointer on IOC device
	cmd_type =         hal_remote_lw ( XPTR( th_cxy , &th_ptr->ioc_cmd.type   ) );
	lba      =         hal_remote_lw ( XPTR( th_cxy , &th_ptr->ioc_cmd.lba    ) );
	count    =         hal_remote_lw ( XPTR( th_cxy , &th_ptr->ioc_cmd.count  ) );
	buf_xp   = (xptr_t)hal_remote_lwd( XPTR( th_cxy , &th_ptr->ioc_cmd.buf_xp ) );

	/* execute operation */
	ata_prepare(0, lba, count);
	if (cmd_type == IOC_WRITE) {
		ata_cbr_write(ATA_CR, ATA_CMD_WRITE_SECTORS_RETRY);
	} else {
		ata_cbr_write(ATA_CR, ATA_CMD_READ_SECTORS_RETRY);
	}

	/* waiting policy depends on the command type */
	if (cmd_type == IOC_SYNC_READ) {
		ata_read(count, (char *)buf_xp);
	} else {
		x86_panic("!IOC_SYNC_READ not supported");
	}
}

void ioc_ata_isr(hal_cpu_context_t *ctx)
{
	x86_printf("rip = %Z\n", ctx->tf_rip);

	x86_panic((char *)__func__);
}

