/*
 * E-UAE - The portable Amiga Emulator
 *
 * SCSI layer back-end for AmigaOS hosts
 *
 * Copyright 2005-2006 Richard Drummond
 *           2005 Sigbjørn Skjæret (68k/MorphOS device-scanning)
 */

#include "sysconfig.h"
#include "sysdeps.h"

#include "options.h"
#include "include/memory.h"
#include "threaddep/thread.h"
#include "blkdev.h"
#include "scsidev.h"
#include "sleep.h"
#include "gui.h"

#include <devices/scsidisk.h>
#include <proto/exec.h>


//#define DEBUG_ME
#ifdef  DEBUG_ME
#define DEBUG_LOG    write_log
#else
#define DEBUG_LOG(...) do { ; } while (0);
#endif

#define MAX_DRIVES	16


struct IOContext {
    struct MsgPort  *msgport;
    struct IOStdReq *ioreq;
    struct SCSICmd   cmd;
    uae_u8          *scsibuf;
};

/*
 * Private per-device data
 */
struct scsidevdata {
    const char       *device;
    int               unit;
/*
 * Aargh! AmigaOS device I/O requires the task that opened the device
 * be the task that sends I/O requests. Thus, since we want to be able
 * to access each device from two tasks, we need to maintain two I/O
 * contexts - one for each task.
 *
 * TODO: Rework the upper layers of the SCSI wrapper so we can get
 * rid of this.
 */
    struct Task      *main_task;
    struct IOContext  main_ioc;

    struct Task      *local_task;
    struct IOContext  local_ioc;
};

static struct scsidevdata drives[MAX_DRIVES];
static int total_drives;


static int open_iocontext (const char *device, int unit, struct IOContext *ioc)
{
    int error = -1;

    DEBUG_LOG ("SCSIDEV: Opening device '%s' unit %d...", device, unit);

    if ((ioc->msgport = CreateMsgPort ())) {
	if ((ioc->ioreq = (struct IOStdReq*) CreateIORequest (ioc->msgport, sizeof(struct IOStdReq)))) {
	    error = OpenDevice ((char *)device,
				unit,
				(struct IORequest*)ioc->ioreq,
				0);
	    if (error == 0) {
		ioc->scsibuf = AllocMem (DEVICE_SCSI_BUFSIZE, MEMF_ANY);

		if (ioc->scsibuf) {
		    DEBUG_LOG ("Success\n");

		    return 0;
		}
		CloseDevice ((struct IORequest *)ioc->ioreq);
	    }
	    DeleteIORequest ((struct IORequest *)ioc->ioreq);
	    ioc->ioreq = 0;
	}
	DeleteMsgPort (ioc->msgport);
	ioc->msgport = 0;
    }

    DEBUG_LOG ("Failed. Error=%d\n", error);

    return error;
}

static void close_iocontext (struct IOContext *ioc)
{
    if (ioc->scsibuf) {
	FreeMem (ioc->scsibuf, DEVICE_SCSI_BUFSIZE);
	ioc->scsibuf = 0;
    }

    if (ioc->ioreq) {
	CloseDevice ((struct IORequest *)ioc->ioreq);
	DeleteIORequest ((struct IORequest *)ioc->ioreq);
	ioc->ioreq = 0;
    }

    if (ioc->msgport) {
	DeleteMsgPort (ioc->msgport);
	ioc->msgport = 0;
    }
}

static struct IOContext *get_iocontext (int unitnum)
{
    struct scsidevdata *sdd  = &drives[unitnum];
    struct Task        *self = FindTask (NULL);
    struct IOContext   *ioc  = NULL;

    if (self == sdd->main_task) {
	DEBUG_LOG ("SCSIDEV: Doing I/O in main task\n");

	ioc = &(sdd->main_ioc);
    } else if (self == sdd->local_task) {
	DEBUG_LOG ("SCSIDEV: Doing I/O in local task\n");
	ioc = &(sdd->local_ioc);
    }

    return ioc;
}


static int add_device (const char *device, int unit)
{
   int result = 0;

   if (total_drives < MAX_DRIVES) {
	struct scsidevdata *sdd = &drives[total_drives];

	memset (sdd, 0, sizeof (struct scsidevdata));

	sdd->device    = my_strdup (device); // FIXME: This will leak.
	sdd->unit      = unit;
	sdd->main_task = FindTask (NULL);

	total_drives++;
	result = 1;
    }
    return result;
}

static int execscsicmd (struct IOContext *ioc, const uae_u8 *cmd_data, int cmd_len,
			uae_u8 *inbuf, int inlen, int *outlen)
{
    struct IOStdReq  *ioreq = ioc->ioreq;
    struct SCSICmd   *cmd   = &(ioc->cmd);
    int error;

    if (ioc->ioreq == 0)
	return -1;

    memset (cmd, 0, sizeof (struct SCSICmd));

    ioreq->io_Length  = sizeof(struct SCSICmd);
    ioreq->io_Data    = (APTR)cmd;
    ioreq->io_Command = HD_SCSICMD;

    DEBUG_LOG ("SCSIDEV: execscicmd data=%08lx len=%d, inbuf=%08lx"\
	       " inlen=%d\n", cmd_data, cmd_len, inbuf, inlen);

    if (inbuf) {
	cmd->scsi_Data   = (UWORD*)inbuf;
	cmd->scsi_Length = inlen;
	cmd->scsi_Flags  = SCSIF_READ | SCSIF_AUTOSENSE;
	memset (inbuf, 0, inlen);
    } else
	cmd->scsi_Flags  = SCSIF_WRITE | SCSIF_AUTOSENSE;

    cmd->scsi_Command   = (UBYTE*)cmd_data;
    cmd->scsi_CmdLength = cmd_len;

    DEBUG_LOG ("SCSIDEV: sending command: 0x%2x\n", cmd->scsi_Command[0]);

	gui_flicker_led (LED_CD, 0, 0);

    error = DoIO ((struct IORequest *)ioreq);

    DEBUG_LOG ("SCSIDEV: result: %d\n", error);
    DEBUG_LOG ("SCSIDEV: actual: %d\n", cmd->scsi_Actual);

    if (outlen)
	*outlen = cmd->scsi_Actual;

    return error;
}

static const uae_u8 *execscsicmd_out (int unitnum, const uae_u8 *cmd_data, int cmd_len)
{
    int error;
    struct IOContext *ioc = get_iocontext (unitnum);

    DEBUG_LOG ("SCSIDEV: unit=%d: execscsicmd_out\n", unitnum);

    error = execscsicmd (get_iocontext (unitnum), cmd_data, cmd_len, 0, 0, 0);

    if (error == 0)
	return cmd_data;

    return 0;
}

static const uae_u8 *execscsicmd_in (int unitnum, const uae_u8 *cmd_data, int cmd_len, int *outlen)
{
    int error;
    struct IOContext *ioc = get_iocontext (unitnum);

    DEBUG_LOG ("SCSIDEV: unit=%d: execscsicmd_in\n", unitnum);

    error = execscsicmd (ioc, cmd_data, cmd_len, ioc->scsibuf, DEVICE_SCSI_BUFSIZE, outlen);

    if (error == 0)
	return ioc->scsibuf;

    return 0;
}

static int execscsicmd_direct (int unitnum, uaecptr acmd)
{
    int sactual = 0;
    struct IOContext *ioc   =  get_iocontext (unitnum);
    struct IOStdReq  *ioreq =  ioc->ioreq;
    struct SCSICmd   *cmd   = &(ioc->cmd);

    uaecptr scsi_data         = get_long (acmd + 0);
    uae_u32 scsi_len          = get_long (acmd + 4);
    uaecptr scsi_cmd          = get_long (acmd + 12);
    int     scsi_cmd_len      = get_word (acmd + 16);
    int     scsi_cmd_len_orig = scsi_cmd_len;
    uae_u8  scsi_flags        = get_byte (acmd + 20);
    uaecptr scsi_sense        = get_long (acmd + 22);
    uae_u16 scsi_sense_len    = get_word (acmd + 26);
    int     io_error          = 0;
    int     parm;

    addrbank *bank_data    = &get_mem_bank (scsi_data);
    addrbank *bank_cmd	   = &get_mem_bank (scsi_cmd);

    uae_u8   *scsi_datap;
    uae_u8   *scsi_datap_org;

    DEBUG_LOG ("SCSIDEV: unit=%d: execscsicmd_direct\n", unitnum);

    /* do transfer directly to and from Amiga memory */
    if (!bank_data || !bank_data->check (scsi_data, scsi_len))
	return -5; /* IOERR_BADADDRESS */

    memset (cmd, 0, sizeof (cmd));

    ioreq->io_Length  = sizeof(struct SCSICmd);
    ioreq->io_Data    = (APTR)cmd;
    ioreq->io_Command = HD_SCSICMD;

    scsi_datap = scsi_datap_org
	= scsi_len ? bank_data->xlateaddr (scsi_data) : 0;

    cmd->scsi_Data        = (UWORD*) scsi_datap;
    cmd->scsi_Length      = scsi_len;
    cmd->scsi_Flags       = scsi_flags;
    cmd->scsi_Command     = bank_cmd->xlateaddr (scsi_cmd);
    cmd->scsi_CmdLength   = scsi_cmd_len;
    cmd->scsi_SenseData   = scsi_sense ? get_real_address (scsi_sense) : 0;
    cmd->scsi_SenseLength = scsi_sense_len;

    DEBUG_LOG ("SCSIDEV: sending command: 0x%2x\n", cmd->scsi_Command[0]);

    io_error = DoIO ((struct IORequest *)ioreq);

    DEBUG_LOG ("SCSIDEV: error: %d actual %d\n", io_error, cmd->scsi_Actual);

	gui_flicker_led (LED_CD, 0, 1);

    put_long (acmd + 8,  cmd->scsi_Actual);
    put_word (acmd + 18, cmd->scsi_CmdActual);
    put_byte (acmd + 21, cmd->scsi_Status);
    put_byte (acmd + 28, cmd->scsi_SenseActual);

    return io_error;
}

static int check_device (const char *device, int unit)
{
    int result = 0;
    struct IOContext ioc;


    if (open_iocontext (device, unit, &ioc) == 0) {
	const uae_u8 INQUIRY_CMD[6] = {0x12,0,0,0,36,0};
	uae_u8 inqbuf[36];
	int outlen;

	memset (inqbuf, 0, sizeof inqbuf);

	if (execscsicmd (&ioc, INQUIRY_CMD, sizeof INQUIRY_CMD, inqbuf, sizeof inqbuf, &outlen) == 0) {
	    int         type     =                 inqbuf[0] & 0x1F;
	    const char *vendor   = (const char *) &inqbuf[8];
	    const char *prod_id  = (const char *) &inqbuf[16];
	    const char *prod_rev = (const char *) &inqbuf[32];

	    write_log ("%-16.16s %3d: '%-8.8s' '%-16.16s' '%-4.4s' %s\n",
		       device, unit, vendor, prod_id, prod_rev, type == 5 ? "CD-ROM" : "");
	    if (type == 5)
		result = 1;

	    close_iocontext (&ioc);
	}
    }

    return result;
}

static int media_check (int unitnum)
{
    int media;
    const uae_u8 TEST_UNIT_READY_CMD[6] = {0,0,0,0,0,0};

    media = execscsicmd_out (unitnum, TEST_UNIT_READY_CMD, sizeof(TEST_UNIT_READY_CMD)) ? 1 : 0;

    DEBUG_LOG ("SCSIDEV: media check :%d\n", media);

    return media;
}


static int open_scsi_device (int unitnum)
{
    int result = 0;

    DEBUG_LOG ("SCSIDEV: unit=%d: open_scsi_device\n", unitnum);

    if (unitnum < total_drives) {
	struct scsidevdata *sdd = &drives[unitnum];
	struct IOContext   *ioc = &(sdd->main_ioc);

	if (ioc->ioreq) {
	    DEBUG_LOG ("Already open\n");

	    result = 1;
	} else {
	    if (!open_iocontext (sdd->device, sdd->unit, ioc))
		result = 1;
	}
    }

    return result;
}

static void close_scsi_device (int unitnum)
{
    DEBUG_LOG ("SCSIDEV: unit=%d: close_scsi_device\n", unitnum);

    if (unitnum < total_drives) {
	struct scsidevdata *sdd  = &drives[unitnum];
	struct Task        *self = FindTask (NULL);

	if (self == sdd->main_task)
	    close_iocontext (&(sdd->main_ioc));
    }
}

static int open_device_thread (int unitnum)
{
    int result = 0;

    DEBUG_LOG ("SCSIDEV: unit=%d: open_device_thread\n", unitnum);

    if (unitnum < total_drives) {
	struct scsidevdata *sdd = &drives[unitnum];
	struct IOContext   *ioc = &(sdd->local_ioc);

	if (ioc->ioreq) {
	    result = 1;

	    if (sdd->local_task == FindTask (NULL)) {
		DEBUG_LOG ("Already open\n");
	    } else {
		write_log ("SCSIDEV: Warning: Orphaned I/O context.\n");
		open_iocontext (sdd->device, sdd->unit, ioc);
		sdd->local_task = FindTask (NULL);
	    }
	} else {
	    if (!open_iocontext (sdd->device, sdd->unit, ioc)) {
		result = 1;
		sdd->local_task = FindTask (NULL);
	    }
	}
    }

    return result;
}

static void close_device_thread (int unitnum)
{
    DEBUG_LOG ("SCSIDEV: unit=%d: close_device_thread\n", unitnum);

    if (unitnum < total_drives) {
	struct scsidevdata *sdd  = &drives[unitnum];
	struct Task        *self = FindTask (NULL);

	if(sdd->local_ioc.ioreq) {
	    if (self == sdd->local_task) {
		close_iocontext (&(sdd->local_ioc));
		sdd->local_task = NULL;
	    } else
		write_log ("SCSIDEV: Warning: Failed to close local I/O context.\n");
	}
    }
}

static void close_scsi_bus (void)
{
    DEBUG_LOG ("SCSIDEV: close_scsi_bus\n");

    /* Not currently used */
}

#ifdef __amigaos4__
static int find_devices (void)
{
    struct FileSystemData *filesys;
    char cd_device[8];
    int i;

    /* block 'please insert volume CDx: requesters */
    struct Process *self = (struct Process *) FindTask (NULL);
    APTR old_windptr = self->pr_WindowPtr;
    self->pr_WindowPtr = (APTR)-1;

    /* Scan for device names of the form CDx: */
    for (i=0; i<10; i++) {
	sprintf (cd_device, "CD%d:\n", i);

	DEBUG_LOG ("Looking for %s\n", cd_device);

	if ((filesys = GetDiskFileSystemData (cd_device))) {
	    if (check_device (filesys->fsd_DeviceName, filesys->fsd_DeviceUnit))
		add_device (filesys->fsd_DeviceName, filesys->fsd_DeviceUnit);

	    FreeDiskFileSystemData (filesys);
	}
    }

    self->pr_WindowPtr = old_windptr;

    return total_drives;
}
#else
#include <exec/lists.h>
#include <proto/dos.h>
#include <dos/dos.h>
#include <dos/dosextens.h>
#include <dos/filehandler.h>
#include <string.h>

#ifndef NEWLIST
#define NEWLIST NewList
#endif

#ifndef ADDTAIL
#define ADDTAIL AddTail
#endif

struct MyDevNode
{
    struct MinNode node;
    UBYTE DevName[256];
    ULONG DevUnit;
};

static int find_devices (void)
{
    struct DosList *dl;
    struct MyDevNode *mdn, *mdn2;
    struct MinList mdl;

    NEWLIST ((struct List *)&mdl);
    dl = LockDosList (LDF_DEVICES | LDF_READ);

    while ((dl = NextDosEntry (dl, LDF_DEVICES))) {
	struct DeviceNode *dn = (struct DeviceNode *) dl;
	struct FileSysStartupMsg *fs = BADDR (dn->dn_Startup);

	if (TypeOfMem (fs)) {
	    UBYTE *devname = BADDR (fs->fssm_Device);

	    if (TypeOfMem (devname) && devname[0] != 0) {
		mdn2 = AllocVec (sizeof(*mdn2), MEMF_PUBLIC);

		for (mdn = (struct MyDevNode *)mdl.mlh_Head; mdn->node.mln_Succ;
		     mdn = (struct MyDevNode *)mdn->node.mln_Succ) {

		    if (strncmp (mdn->DevName, devname+1, devname[0]) == 0
			&& mdn->DevUnit == fs->fssm_Unit) {

			FreeVec (mdn2);
			mdn2 = NULL;
			break;
		    }
		}

		if (mdn2) {
		    memcpy (mdn2->DevName, devname+1, devname[0]);
		    mdn2->DevName[devname[0]] = 0;
		    mdn2->DevUnit = fs->fssm_Unit;
		    ADDTAIL ((struct List *)&mdl, (struct Node *)&mdn2->node);
		}
	    }
	}
    }

    UnLockDosList (LDF_DEVICES | LDF_READ);

    for (mdn  = (struct MyDevNode *)mdl.mlh_Head;
	 mdn2 = (struct MyDevNode *)mdn->node.mln_Succ; (mdn = mdn2)) {

	if (check_device (mdn->DevName, mdn->DevUnit))
	    add_device (mdn->DevName, mdn->DevUnit);

	FreeVec(mdn);
    }

    return total_drives;
}
#endif

static int open_scsi_bus (int flags)
{
    int num_devices = 0;

    DEBUG_LOG ("SCSIDEV: open_scsi_bus\n");

    num_devices = find_devices ();

    write_log ("SCSIDEV: %d device(s) found\n", num_devices);

    return num_devices;
}

static struct device_info *info_device (int unitnum, struct device_info *di)
{
    DEBUG_LOG ("SCSIDEV: unit=%d: info_device\n", unitnum);

    if (unitnum < total_drives) {
	struct scsidevdata *sdd = &drives[unitnum];

	di->bus		    = 0;
	di->target	    = unitnum;
	di->lun		    = 0;
	di->media_inserted  = media_check (unitnum);
	di->write_protected = 1;
	di->bytespersector  = 2048;
	di->cylinders	    = 1;
	di->type	    = INQ_ROMD; /* We only support CD/DVD drives for now */
	di->id		    = unitnum + 1;

	snprintf (di->label, 60, "%s:%d", sdd->device, sdd->unit);
    } else
	di = 0;

    return di;
}

static int check_isatapi (int unitnum)
{
    return 0; // FIXME
//    return drives[unitnum].isatapi;
}

struct device_functions devicefunc_scsi_amiga = {
    open_scsi_bus,
    close_scsi_bus,
    open_scsi_device,
    close_scsi_device,
    info_device,
    execscsicmd_out,
    execscsicmd_in,
    execscsicmd_direct,
    0, 0, 0, 0, 0, 0, 0,
    check_isatapi,
    open_device_thread,
    close_device_thread
};
