#include <exec/types.h>
#include <exec/nodes.h>
#include <exec/resident.h>
#include <exec/errors.h>
#include <libraries/expansionbase.h>
#include <libraries/configvars.h>
#include <devices/trackdisk.h>

/*
 * Set SysBase to a local variable, that loads directly from 4 when it
 * has to be reloaded
 */
#define BASE_EXT_DECL
#define BASE_NAME (*(void **)4)
#include <inline/exec.h>

/*
 * This is the main scsi-command executor task. It does the whole
 * scsi-communication and encapsulates hardware usage.
 */

extern struct MsgPort *CreatePort(char *name, int pri);
extern void DeletePort(struct MsgPort *mp);

#include "device.h"

DECL_DPRINTF;
extern void install_int_handler (struct scsi_dev *dev);
extern void remove_int_handler (struct scsi_dev *dev);
extern void wait_interrupt (struct scsi_dev *dev);
extern void clear_interrupt (struct scsi_dev *dev);

void scsi_cmd (struct scsi_dev *dev);
void init_board (struct scsi_dev *dev);

/* this function is used by the int_handler, the rest is not */
int read_scsi_reg (volatile ubyte *board, int reg_num);
static void inline read_scsi_regs (volatile ubyte *board, int reg_start, 
				   ubyte num, ubyte *buf);
static void inline write_scsi_regs (volatile ubyte *board, int reg_start, 
				    ubyte num, ubyte *buf);
static void set_dma_state (volatile ubyte *board, int state, int value);
static void write_scsi_reg (volatile ubyte *board, int reg_num, int value);
static int poll_interrupts (struct scsi_dev *dev, int *status, int length);
static void load_dma_address (volatile ubyte *board, int direction, 
			      void *address);
static int get_cmd_len (int cmd);
static int checkfifo_n_dmareset (volatile ubyte *board);

/*
 * reset states to int-enable & scsi mode
 */
#ifdef A2090
#define REINIT_DMA_CIRCUIT() board[A2090_CNTL_HI] = MRESET|SEL_SCSI|INT_ENABLE 
#endif
#ifdef A2091
#define REINIT_DMA_CIRCUIT()
#endif

/***********************************************************************/

/* this is the handler task, that gets command messages, executes them
 * and replies to them, when they are finished */

void
scsi_cmd (struct scsi_dev *dev)
{
  struct scsi_msg *sm;
  struct MsgPort *mp;
  int i, cmd_len, status;
  volatile ubyte *board = (ubyte *)dev->sc_base;
  ulong length;

DPRINTF(("handler-task: dev = $%lx, board = $%lx", dev, board));

  /* set up our interrupt handler */
  install_int_handler (dev);

  /* put the board into a defined state */
  init_board (dev);

  /* for now, just assume it worked... */
  mp = CreatePort("scsi_cmd.port",0);
  dev->sc_hmsgport = mp;
  PutMsg(&dev->sc_msgport, (struct Message *)&dev->sc_hmessage);
  
  for (;;)
    {
      WaitPort(mp);
      while (sm = (struct scsi_msg *) GetMsg(mp))
	{
DPRINTF(("handler-task(%ld): cmd: %lx, scsi-cmd: %lx",
	 sm->scm_unit, sm->scm_cmd, sm->scm_scsi_cmd->scsi_Command[0]));

	  if (sm->scm_cmd == SCM_CMD_SHUTDOWN)
	    goto shutdown;
	  else if (sm->scm_cmd == SCM_CMD_EXEC_SCSI)
	    {
	      /*
	       * first check, that we won't try to send a command
	       * to the controller: 
	       */
	      if (sm->scm_unit == 7)
		{
		  sm->scm_cmd = HFERR_SelfUnit;
		  ReplyMsg((struct Message*)sm);
		  continue;
		}

	      REINIT_DMA_CIRCUIT();

	      /* since the user can specify an scsi_Length of up to 
	       * 2**24 byte, and the 2090-DMA circuit only stands
	       * 64K, I'll limit any given Length to these 64K .. */
	      length = sm->scm_scsi_cmd->scsi_Length;
	      if (length > DMA_MAX_TRANSFER) length = DMA_MAX_TRANSFER;

	      /* both, the 2090 and the 2091 can't handle more than
	       * 24bit addresses as DMA target */
	      if (length && (ulong)sm->scm_scsi_cmd->scsi_Data >= 0x01000000)
		AutoAutoRequest(SCSI_NAME,
				"Your DMA target is above $1000000",
				"This is not supported, this transfer will fail!",
				"Continue", "Continue");

	      /* execute the SCSICommand attached to the message */
	      if (length)
		load_dma_address (board,
				  sm->scm_scsi_cmd->scsi_Flags & SCSIF_READ,
				  sm->scm_scsi_cmd->scsi_Data);

	      /* 
	       * copy the CDB (ignoring scsi_CmdLength... an SCSI command
	       * has a fixed length, determined from its group code) 
	       */
	      cmd_len = get_cmd_len (sm->scm_scsi_cmd->scsi_Command[0]);
	      write_scsi_regs (board, WD_CDB_1, cmd_len,
			       sm->scm_scsi_cmd->scsi_Command);
	      sm->scm_scsi_cmd->scsi_CmdActual = cmd_len;
	      
	      /* set transfer length in WD chip, DMA is already ok */
	      write_scsi_regs (board, WD_TRANSFER_COUNT_MSB, 3,
	      		       ((ubyte *)&length)+1);

	      write_scsi_reg (board, WD_DESTINATION_ID, sm->scm_unit);

	      clear_interrupt (dev);
	      write_scsi_reg (board, WD_COMMAND, 
			      WD_CMD_SELECT_WITHOUT_ATN_AND_TRANSFER);

	      /*
	       * this field serves two purposes, when the message is sent,
	       * it is used as a global command, when the message is
	       * returned, it contains the io_Error value 
	       */
	      sm->scm_cmd = poll_interrupts (dev, &status, length);

	      /*
	       * get the value of the transfer counter after the execution
	       * of the command
	       */
	      cmd_len = 0;
	      read_scsi_regs (board, WD_TRANSFER_COUNT_MSB, 3,
	      		       ((ubyte *)&cmd_len)+1);

	      /* and deduce the actual transferred amount of data */
	      sm->scm_scsi_cmd->scsi_Actual = length - cmd_len;

	      sm->scm_scsi_cmd->scsi_Status = status;
	      /*
	       * if CHECK_CONDITION is set, and the user wanted to
	       * AUTOSENSE, do a REQUEST_SENSE now 
	       */
	      if ((status & 2) == 2 &&
		  (sm->scm_scsi_cmd->scsi_Flags & SCSIF_AUTOSENSE))
		{
DPRINTF(("handler-task(%ld): need autosense information", sm->scm_unit));
		  load_dma_address (board,
				    DIRECTION_READ,
				    sm->scm_scsi_cmd->scsi_SenseData);
		  /* REQUEST_SENSE */
		  write_scsi_regs (board, WD_CDB_1, 4, (ubyte*)"\3\0\0\0");
		  if (sm->scm_scsi_cmd->scsi_Flags & SCSIF_OLDAUTOSENSE)
		    cmd_len = 4;
		  else
		    cmd_len = sm->scm_scsi_cmd->scsi_SenseLength;
 		  write_scsi_reg (board, WD_CDB_5, 
		    (sm->scm_scsi_cmd->scsi_Flags & SCSIF_OLDAUTOSENSE) ?
		    0 : cmd_len);
 		  write_scsi_reg (board, WD_CDB_6, 0);

	          write_scsi_regs (board, WD_TRANSFER_COUNT_MSB, 3,
	      		       ((ubyte *)&cmd_len)+1);

		  /* needed the second time ?? */
		  write_scsi_reg (board, WD_DESTINATION_ID, sm->scm_unit);
		  
		  clear_interrupt (dev);
	          write_scsi_reg (board, WD_COMMAND, 
			      WD_CMD_SELECT_WITHOUT_ATN_AND_TRANSFER);

		  poll_interrupts (dev, &status, cmd_len);
		  
	          cmd_len = 0;
	          read_scsi_regs (board, WD_TRANSFER_COUNT_MSB, 3,
	      		          ((ubyte *)&cmd_len)+1);
		  sm->scm_scsi_cmd->scsi_SenseActual =
		    sm->scm_scsi_cmd->scsi_SenseLength - cmd_len;
DPRINTF(("\013handler-task(%ld): autosense information = $%lx", 
	sm->scm_unit, *(ulong *)sm->scm_scsi_cmd->scsi_SenseData));
		}
	      ReplyMsg ((struct Message*)sm);
	    }
	}
    }

shutdown :
  Forbid ();
  ReplyMsg ((struct Message *)sm);
  DeletePort (mp);
  remove_int_handler (dev);
  RemTask (FindTask (0));
}

/*
 * IMPORTANT: not all HFERR_ that could be detected are reported. This is
 * to try to ensure, that the bus is always in a defined state, when this
 * function returns. Only a timeout-condition on select and a disconnect
 * will cause this function to return. 
 */

int
poll_interrupts (struct scsi_dev *dev, int *cmd_status, int length)
{
  int status;
  int do_wait = 1; /* only needed after a command has been started */
  volatile ubyte *board = (volatile ubyte *)dev->sc_base;
  
  for (;;)
    {
      if (do_wait) 
        {
	  wait_interrupt (dev);
	  do_wait = 0;
	}

      status = read_scsi_reg (board, WD_SCSI_STATUS);

      switch (status)
	{
	case 0x00: /* after RESET */
	case 0x16: /* COMMAND COMPLETED */
	  break;
	case 0x1f: /* INFO OUT COMPLETED */
	  do_wait = 1;
	  clear_interrupt (dev);
	  write_scsi_reg (board, WD_COMMAND,
			  WD_CMD_TRANSFER_INFO|WD_CMD_SBT_MODE);
	  while (! (read_scsi_reg (board, WD_AUXILIARY_STATUS) & 1)) ;
	  read_scsi_reg (board, WD_DATA);
	  break;
	case 0x20:
	  do_wait = 1;
	  clear_interrupt (dev);
	  write_scsi_reg (board, WD_COMMAND, WD_CMD_NEGATE_ACK);
	  break;
	case 0x42: /* SELECT TIMEOUT */
	  if (length) checkfifo_n_dmareset (board);
	  return HFERR_SelTimeout;
	case 0x4b: /* requested unusual info out */
	  do_wait = 1;
	  clear_interrupt (dev);
	  write_scsi_reg (board, WD_COMMAND,
			  WD_CMD_TRANSFER_INFO|WD_CMD_SBT_MODE);
	  while (! (read_scsi_reg (board, WD_AUXILIARY_STATUS) & 1)) ;
	  *cmd_status = read_scsi_reg (board, WD_DATA);
	  /*
	   * no (!) return here... the old 2090 device used to return
	   * in such conditions, leaving a device in a connected state,
	   * that would later timeout and leave the bus in a 
	   * broken state..
	   */
	  break;
	default:
	  /* printf("strange-status: $%x\n", status); */
	  break;
	case 0x85: /* DISCONNECT */
	  if (!length || checkfifo_n_dmareset (board))
	    {
	      /* 0x60 == COMMAND_COMPLETED PHASE */
	      if (read_scsi_reg (board, WD_COMMAND_PHASE) == 0x60)
		{
		  *cmd_status = read_scsi_reg (board, WD_TARGET_LUN);
		  return *cmd_status ? HFERR_BadStatus : 0;
		}
	      else
		return HFERR_Phase;
	    }
	  else
	    return HFERR_DMA;
	}
    }
}

void
init_board (struct scsi_dev *dev)
{
  volatile ubyte *board = (volatile ubyte *)dev->sc_base;

#ifdef A2090
  /* ST506 init stuff... shudder.. */

  int i;
  int try;
  ulong foo_addr = 0x79000; /* aehm... */

  /* from 2620 monitor.. hope it works:-)) */
  for (i = 0; i < 2; i++)
    {
      try = 2500000;
DPRINTF(("st506: 1part"))
      *(unsigned short *)&board[0x50] = foo_addr >> (1 + (i==1?8:0));
      board[A2090_CNTL_HI] = MRESET|HCBP;  /* 506 stuff... */
      while (!(board[A2090_CNTL_HI] & (1<<7)) && try-- > 0) ;
      try = 2500000;
DPRINTF(("st506: 2part"))
      board[A2090_CNTL_HI] = MRESET;
      while ((board[A2090_CNTL_HI] & (1<<7)) && try-- > 0) ;
    }

  board[A2090_CNTL_HI] = MRESET|SEL_SCSI;
#endif
#ifdef A3000
  board[0x0b] = 0;
  board[0x3f] = 0;
  board[0x03] = 0;
  board[0x1b] = 0;
  board[0x0b] = 12;  /* what this might do..... */
#endif

  read_scsi_reg (board, WD_SCSI_STATUS);

  REINIT_DMA_CIRCUIT();
  
  write_scsi_reg (board, WD_OWN_ID, 7);
  write_scsi_reg (board, WD_COMMAND, WD_CMD_RESET);

DPRINTF(("init: wait_interrupt"))
  wait_interrupt (dev);
  
DPRINTF(("init: set controller into dma mode"))
  do
    write_scsi_reg (board, WD_CONTROL, 0x80); /* set chip into DMA mode */
  while (read_scsi_reg (board, WD_CONTROL) != 0x80);

/*  write_scsi_reg (board, WD_TIMEOUT_PERIOD, 0xff); /* timeout ~~2s */
  write_scsi_reg (board, WD_TIMEOUT_PERIOD, 0x20); /* timeout minimal */

  read_scsi_reg (board, WD_SCSI_STATUS);

  write_scsi_reg (board, WD_SYNCHRONOUS_TRANSFER, 0x40); /* no sync */

#ifdef A2090
  checkfifo_n_dmareset (board);
#endif
}

/***********************************************************************/

static int
get_cmd_len (int cmd)
{
  switch (cmd >> 5)
    {
    case 0: return 6;
    case 1: return 10;
    default: /* assume maximum */
    case 5: return 12;
    }
}

static void
load_dma_address (volatile ubyte *board, int direction, void *address)
{
#ifdef A2090
  ulong data;
  
  data = (ulong)address >> 1;
  if (direction == DIRECTION_WRITE) data |= 0x800000;
  set_dma_state (board, DMA_LD_UP_ADDR, data >> 16);
  set_dma_state (board, DMA_LD_MID_ADDR, data >> 8);
  set_dma_state (board, DMA_LD_LO_ADDR, data);
  board[SCSI_PCSS] = DMA_OPEN_FIFO;
#endif
#ifdef A2091
  if (direction == DIRECTION_WRITE)
    *(ushort *)&board[0x42] = 0x38;
  else
    *(ushort *)&board[0x42] = 0x30;
  
  *(ulong *)&board[0x80] = 0;
  *(ulong *)&board[0x84] = (ulong) address;
  *(ushort *)&board[0xe0] = 1;
#endif
#ifdef A3000
  if (direction == DIRECTION_WRITE)
    *(ushort *)&board[0x42] = 0x38;
  else
    *(ushort *)&board[0x42] = 0x30;
  
  *(ulong *)&board[0x80] = 0;
  *(ulong *)&board[0x0c] = (ulong) address;
  *(ushort *)&board[0xe0] = 1;
#endif
}

int
read_scsi_reg (volatile ubyte *board, int reg_num)
{
  int result;

  /* need to ensure, that register access is single threaded, the
   * board-interrupt will also try to use read_scsi_reg () .. */
  Disable();
  board[SCSI_CS_CNTL] = reg_num;
  result = board[SCSI_CS_DATA];
  Enable();
  return result;
}

static void inline
read_scsi_regs (volatile ubyte *board, int reg_start, ubyte num, ubyte *buf)
{
  /* need to ensure, that register access is single threaded, the
   * board-interrupt will also try to read those registers */
  Disable();
  board[SCSI_CS_CNTL] = reg_start;
  while (num --) *buf++ = board[SCSI_CS_DATA];
  Enable();
}

static void inline
write_scsi_regs (volatile ubyte *board, int reg_start, ubyte num, ubyte *buf)
{
  /* need to ensure, that register access is single threaded, the
   * board-interrupt will also try to read those registers */
  Disable();
  board[SCSI_CS_CNTL] = reg_start;
  while (num --) board[SCSI_CS_DATA] = *buf++;
  Enable();
}

static void
write_scsi_reg (volatile ubyte *board, int reg_num, int value)
{
  Disable();
  board[SCSI_CS_CNTL] = reg_num;
  board[SCSI_CS_DATA] = value;
  Enable();
}

static int
checkfifo_n_dmareset (volatile ubyte *board)
{
#ifdef A2090
  ubyte i;
  int status;

  board[SCSI_PCSS] = DMA_READ_STATUS;
  for (i = 0; i <= 10; i++) ;
  status = board[SCSI_PCSD];
  for (i = 0; i <= 10; i++) ;
  board[SCSI_PCSS] = DMA_RESET_1;
  for (i = 0; i <= 10; i++) ;
  board[SCSI_PCSS] = DMA_RESET_2;  
  for (i = 0; i <= 10; i++) ;

  return status & 0x20;
#endif
#ifdef A2091
  *(ushort *)&board[0xe8] = 1;
  while (!(board[0x41] & 1)) ;
  *(ushort *)&board[0xe4] = 1;
  *(ushort *)&board[0xe2] = 1;
  return 1; /* there seem to be no possible DMA errors.. GREAT!! */
#endif  
}

#ifdef A2090
static void
set_dma_state (volatile ubyte *board, int state, int addr)
{
  ubyte i;

  board[SCSI_PCSS] = state;
  /* busy loop */
  for (i = 0; i <= 10; i++) ;
  
  board[SCSI_PCSD] = addr;
  /* busy loop */
  for (i = 0; i <= 10; i++) ;
}
#endif

/***********************************************************************/
/***********************************************************************/

#ifdef NEED_DUMP_REGS
void
dump_regs(char *title)
{
  ubyte *board = get_board_address((int *)board);

  printf("%s: tgt-lun = $%2x, cmd-ph = $%2x, sync = $%2x,\nsrc-id = $%2x, stat = $%x, data = $%2x, aux = $%2x\n",
	title,
	read_scsi_reg(board, WD_TARGET_LUN),
	read_scsi_reg(board, WD_COMMAND_PHASE),
	read_scsi_reg(board, WD_SYNCHRONOUS_TRANSFER),
	read_scsi_reg(board, WD_SOURCE_ID),
	read_scsi_reg(board, WD_SCSI_STATUS),
	read_scsi_reg(board, WD_DATA),
	read_scsi_reg(board, WD_AUXILIARY_STATUS));
  /* wait for middle mouse button to be released */
  while (!(*(unsigned short *)0xdff016 & (1 << 8))) ;
}

void
init_controller (ubyte *board)
{
}
#endif
