/*
 *  scsi.cpp - SCSI Manager
 *
 *  Basilisk II (C) 1997-1999 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;

		D(bug(" %d %08x %d\n", cmd, ptr, len));

		switch (cmd) {
			case scInc:
				WriteMacInt32(tib - 8, ptr + len);
			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) {
		target_id = id;

		// Target present?
		if (scsi_is_target_present(target_id)) {
			phase = PH_SELECTED;
			fake_status = 0x006a;			// Target selected, command phase
			return 0;
		}
	}

	// Error
	phase = PH_FREE;
	fake_status = 0x0000;		// Bus free
	return scCommErr;
}


/*
 *  Send SCSI command
 */

int16 SCSICmd(int cmd_length, uint8 *cmd)
{
	D(bug("SCSICmd len %d, cmd %08x%08x%08x\n", cmd_length, ntohl(0[(uint32 *)cmd]), ntohl(1[(uint32 *)cmd]), ntohl(2[(uint32 *)cmd])));
	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;
}
