/*
 *  scsi.cpp - SCSI Manager
 *
 *  Basilisk II (C) 1997-2008 Christian Bauer
 *
 *  This program 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; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program 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 this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

/*
 *  SEE ALSO
 *    Inside Macintosh: Devices, chapter 3 "SCSI Manager"
 *    Technote DV 24: "Fear No SCSI"
 */

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

#include "sysdeps.h"
#include "cpu_emulation.h"
#include "main.h"
#include "user_strings.h"
#include "scsi.h"

#define DEBUG 0
#include "debug.h"


// Error codes
enum {
	scCommErr = 2,
	scArbNBErr,
	scBadParmsErr,
	scPhaseErr,
	scCompareErr,
	scMgrBusyErr,
	scSequenceErr,
	scBusTOErr,
	scComplPhaseErr
};

// TIB opcodes
enum {
	scInc = 1,
	scNoInc,
	scAdd,
	scMove,
	scLoop,
	scNop,
	scStop,
	scComp
};

// Logical SCSI phases
enum {
	PH_FREE,		// Bus free
	PH_ARBITRATED,	// Bus arbitrated (after SCSIGet())
	PH_SELECTED,	// Target selected (after SCSISelect())
	PH_TRANSFER		// Command sent (after SCSICmd())
};

// Global variables
static int target_id;					// ID of active target
static int phase;						// Logical SCSI phase
static uint16 fake_status;				// Faked 5830 status word
static bool reading;					// Flag: reading from device

const int SG_TABLE_SIZE = 1024;
static int sg_index;					// Index of first unused entry in S/G table
static uint8 *sg_ptr[SG_TABLE_SIZE];	// Scatter/gather table data pointer (host address space)
static uint32 sg_len[SG_TABLE_SIZE];	// Scatter/gather table data length
static uint32 sg_total_length;			// Total data length


/*
 *  Execute TIB, constructing S/G table
 */

static int16 exec_tib(uint32 tib)
{
	for (;;) {

		// Read next opcode and parameters
		uint16 cmd = ReadMacInt16(tib); tib += 2;
		uint32 ptr = ReadMacInt32(tib); tib += 4;
		uint32 len = ReadMacInt32(tib); tib += 4;

#if DEBUG
		const char *cmd_str;
		switch (cmd) {
			case scInc:   cmd_str = "INC  "; break;
			case scNoInc: cmd_str = "NOINC"; break;
			case scAdd:   cmd_str = "ADD  "; break;
			case scMove:  cmd_str = "MOVE "; break;
			case scLoop:  cmd_str = "LOOP "; break;
			case scNop:   cmd_str = "NOP  "; break;
			case scStop:  cmd_str = "STOP "; break;
			case scComp:  cmd_str = "COMP "; break;
			default:      cmd_str = "???  "; break;
		}
		D(bug(" %s(%d) %08x %d\n", cmd_str, cmd, ptr, len));
#endif

		switch (cmd) {
			case scInc:
				WriteMacInt32(tib - 8, ptr + len);
				// fall through to scNoInc
			case scNoInc:
				if ((sg_index > 0) && (Mac2HostAddr(ptr) == sg_ptr[sg_index-1] + sg_len[sg_index-1])) {
					sg_len[sg_index-1] += len;				// Merge to previous entry
				} else {
					if (sg_index == SG_TABLE_SIZE) {
						ErrorAlert(GetString(STR_SCSI_SG_FULL_ERR));
						return -108;
					}
					sg_ptr[sg_index] = Mac2HostAddr(ptr);	// Create new entry
					sg_len[sg_index] = len;
					sg_index++;
				}
				sg_total_length += len;
				break;

			case scAdd:
				WriteMacInt32(ptr, ReadMacInt32(ptr) + len);
				break;

			case scMove:
				WriteMacInt32(len, ReadMacInt32(ptr));
				break;

			case scLoop:
				WriteMacInt32(tib - 4, len - 1);
				if (len - 1 > 0)
					tib += (int32)ptr - 10;
				break;

			case scNop:
				break;

			case scStop:
				return 0;

			case scComp:
				printf("WARNING: Unimplemented scComp opcode\n");
				return scCompareErr;

			default:
				printf("FATAL: Unrecognized TIB opcode %d\n", cmd);
				return scBadParmsErr;
		}
	}
}


/*
 *  Reset SCSI bus
 */

int16 SCSIReset(void)
{
	D(bug("SCSIReset\n"));

	phase = PH_FREE;
	fake_status = 0x0000;	// Bus free
	sg_index = 0;
	target_id = 8;
	return 0;
}


/*
 *  Arbitrate bus
 */

int16 SCSIGet(void)
{
	D(bug("SCSIGet\n"));
	if (phase != PH_FREE)
		return scMgrBusyErr;

	phase = PH_ARBITRATED;
	fake_status = 0x0040;	// Bus busy
	reading = false;
	sg_index = 0;			// Flush S/G table
	sg_total_length = 0;
	return 0;
}


/*
 *  Select SCSI device
 */

int16 SCSISelect(int id)
{
	D(bug("SCSISelect %d\n", id));
	if (phase != PH_ARBITRATED)
		return scSequenceErr;

	// ID valid?
	if (id < 0 || id > 7)
		return scBadParmsErr;

	// Target present?
	target_id = id;
	if (!scsi_is_target_present(target_id)) {
		phase = PH_FREE;
		fake_status = 0x0000;	// Bus free
		return scCommErr;
	}

	phase = PH_SELECTED;
	fake_status = 0x006a;		// Target selected, command phase
	return 0;
}


/*
 *  Send SCSI command
 */

int16 SCSICmd(int cmd_length, uint8 *cmd)
{
#if DEBUG
	switch (cmd_length) {
		case 6:
			D(bug("SCSICmd len 6, cmd %02x %02x %02x %02x %02x %02x\n", cmd[0], cmd[1], cmd[2], cmd[3], cmd[4], cmd[5]));
			break;
		case 10:
			D(bug("SCSICmd len 10, cmd %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", cmd[0], cmd[1], cmd[2], cmd[3], cmd[4], cmd[5], cmd[6], cmd[7], cmd[8], cmd[9]));
			break;
		case 12:
			D(bug("SCSICmd len 12, cmd %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", cmd[0], cmd[1], cmd[2], cmd[3], cmd[4], cmd[5], cmd[6], cmd[7], cmd[8], cmd[9], cmd[10], cmd[11]));
			break;
		default:
			D(bug("SCSICmd bogus length %d\n", cmd_length));
			break;
	}
#endif

	if (phase != PH_SELECTED)
		return scPhaseErr;

	// Commdn length valid?
	if (cmd_length != 6 && cmd_length != 10 && cmd_length != 12)
		return scBadParmsErr;

	// Set command, extract LUN
	scsi_set_cmd(cmd_length, cmd);

	// Extract LUN, set target
	if (!scsi_set_target(target_id, (cmd[1] >> 5) & 7)) {
		phase = PH_FREE;
		fake_status = 0x0000;	// Bus free
		return scCommErr;
	}

	phase = PH_TRANSFER;
	fake_status = 0x006e;		// Target selected, data phase
	return 0;
}


/*
 *  Read data
 */

int16 SCSIRead(uint32 tib)
{
	D(bug("SCSIRead TIB %08lx\n", tib));
	if (phase != PH_TRANSFER)
		return scPhaseErr;

	// Execute TIB, fill S/G table
	reading = true;
	return exec_tib(tib);
}


/*
 *  Write data
 */

int16 SCSIWrite(uint32 tib)
{
	D(bug("SCSIWrite TIB %08lx\n", tib));
	if (phase != PH_TRANSFER)
		return scPhaseErr;

	// Execute TIB, fill S/G table
	return exec_tib(tib);
}


/*
 *  Wait for command completion (we're actually doing everything in here...)
 */

int16 SCSIComplete(uint32 timeout, uint32 message, uint32 stat)
{
	D(bug("SCSIComplete wait %d, msg %08lx, stat %08lx\n", timeout, message, stat));
	WriteMacInt16(message, 0);
	if (phase != PH_TRANSFER)
		return scPhaseErr;

	// Send command, process S/G table
	uint16 scsi_stat = 0;
	bool success = scsi_send_cmd(sg_total_length, reading, sg_index, sg_ptr, sg_len, &scsi_stat, timeout);
	WriteMacInt16(stat, scsi_stat);

	// Complete command
	phase = PH_FREE;
	fake_status = 0x0000;	// Bus free
	return success ? 0 : scCommErr;
}


/*
 *  Get bus status
 */

uint16 SCSIStat(void)
{
	D(bug("SCSIStat returns %04x\n", fake_status));
	return fake_status;
}


/*
 *  SCSI Manager busy?
 */

int16 SCSIMgrBusy(void)
{
//	D(bug("SCSIMgrBusy\n"));
	return phase != PH_FREE;
}
