/*****************************************************************************
*           Change Log
*  Date     |  Change
*-----------+-----------------------------------------------------------------
* 29-Dec-94 | [1.600J] <JRJ> Created to implement tape drives.
*****************************************************************************/

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

#include "boolean.h"
#include "btypes.h"
#include "button.h"

#include "alerts.h"
#include "alert.h"
#include "diag.h"
#include "machmem.h"
#include "mach.h"
#include "mem1401.h"

#include "tape.h"
#include "io.h"

boolean tape_transfer_error = false;

/*****************************************************************************
*			tape_io_char_read
* Inputs:
*	tapedrive *tape: Address of tapedrive structure identifying drive.
*
* Effect:
*	Reads the next character from the identified tape drive.  An I/O
*	error causes a tape alert.  EOF sets Tape Indicate.
*
* Returns:
*	boolean indicating success (true) or failure (false).
*****************************************************************************/

boolean tape_io_char_read(tapedrive *tape)
{
	int load_point = false;

	/*
		If the tape unit is not ready, return an error.
	*/
	if(!tape->loaded || tape->indicate.active || !tape->start.active ||
	   tape->file == NULL) {
		sprintf(diag_buffer,"Tape unit %d Not Ready.\n",tape->unit);
		tell(diag_buffer);
		alert(alert_tape);
		return(false);
	}

	/*
		Test for load point.  This is necessary because the
		tape convention sets the bit for IRG on the first
		record (shame on PRP).
	*/
	load_point = (ftell(tape->file) == 0L);

	/*
		read a character.  If an error other than EOF, log an
		alert.  If EOF, set the affected drives Tape Indicate.

		If at load point, strip the IRG bit, too.
	*/
	if(fread(&(tape->buff),1,1,tape->file) != 1) {
		if(!feof(tape->file)) {
			sprintf(diag_buffer,
				"File I/O error %s reading tape unit %d\n",
				strerror(errno),tape->unit);
			tell(diag_buffer);
			alert(alert_tape);
			return(false);
		}
		tape->indicate.active = true;
	}
	else if(load_point) {
		tape->buff &= (~TAPE_IRG);
	}
	return(true);
}

/*****************************************************************************
*			tape_io_char_write
* Inputs:
*	tapedrive *tape:  Address of tapedrive structure identifying drive
*
* Effect:
*	Writes the character in tape->buff, which should already have it's
*	parity properly set.
*
* Returns:
*	boolean indicating success(true) or failure (false).  A host
*	I/O file system write failure can cause a failure (e.g. disk full).
*****************************************************************************/

boolean tape_io_char_write(tapedrive *tape)
{
	assert(tape != NULL && !tape->readonly);

	/*
		If the tape unit is not ready, return an error.
	*/
	if(!tape->loaded || !tape->start.active || tape->file == NULL) {
		sprintf(diag_buffer,"Tape unit %d Not Ready.\n",tape->unit);
		tell(diag_buffer);
		alert(alert_tape);
		tape_transfer_error = true;
		return(false);
	}

	/*
		Write the character
	*/
	if(fwrite(&(tape->buff),1,1,tape->file) != 1) {
		sprintf(diag_buffer,"File I/O error %s writing tape unit %d\n",
			strerror(errno),tape->unit);
		tell(diag_buffer);
		alert(alert_tape);
		tape_transfer_error = true;
		return(false);
	}

	return(true);
}


/*****************************************************************************
*			tape_io_select
* Input: unit (see io.h)
*
* Results:
*	If select is valid (tape unit exists, and is unique), returns a
*	pointer to the tapedrive structure.  Otherwise, issues an alert
*	and returns NULL.
*****************************************************************************/

tapedrive *tape_io_select(void)
{
	int i;
	tapedrive *tape = NULL;
	int select_unit;

	select_unit = io_unit % 10;
	for(i=0 ; i < MAXDRIVES; ++i) {
		if(tapes[i]->unit == select_unit) {
			if(tape != NULL) {
				sprintf(diag_buffer,
					"Tape Unit %d multiple Select.\n",
					io_unit);
				tell(diag_buffer);
				alert(alert_tape);
				return(NULL);
			}
			tape = tapes[i];
		}
	}

	/*
		If no tape unit match, a real 1401 would wait for the
		operator to dial a tape unit and make it ready.  Since
		this program is not multi threaded, we'll call it an
		error.
	*/
	if(tape == NULL) {
		sprintf(diag_buffer,"Tape Unit %d not available.\n",
			select_unit);
		tell(diag_buffer);
		alert(alert_tape);
	}
	return(tape);
}

/*****************************************************************************
*			tape_io_read
* Inputs:
*	boolean load_mode: True if this is a load mode (word mark) read
*	(Unit is implicit.  See io.h)
*
* Effect:
*	Implements tape read I/O.  Reads data until IRG or until it reaches
*	a Group Mark + Word Mark already in core.  If the IRG is reached before
*	a GM+WM in core, a Group Mark is inserted after the record in core.
*	After execution, the A address register contains %4x.  The B address
*	register contains the address of the Group Mark (or GM + WM) +1.
*
*	In load mode, a Word Separator character turns into a word mark over
*	the subsequent tape character.  All other word marks are cleared if in
*	load mode.
*
*	Data is expected to be in BCD.  IRG's are represented by a high order
*	bit on the first character of the following record (0x80).  Data should
*	be *EVEN* parity.  A Tape Mark is represented as a single character
*	record (octal 0217).
*
*	If a Tape Mark is read, the affected tape's "Tape Indicate" is set.
*	If I/O is attempted with Tape Indicate already set, an alert is raised.
*	If the I/O operation attempts to wrap core, an alert is raised.
*	If a real C I/O error is encountered, an alert is raised.
*	PC end of file is treated like a tape mark.
*
* Returns: boolean.  Normally true.  False to halt machine.
*
******************************************************************************/

boolean tape_io_read(boolean load_mode)
{
	boolean storing = true;		/* If we haven't hit GM+WM */
	int wm=0;			/* Used to save wm for load mode */
	tapedrive *tape = NULL;		/* handy pointer to tapedrive */

	/*
		Reset the transfer error (parity error) flag, which is
		supposed to be reset before any tape operation.
	*/
	tape_transfer_error = false;
	IO_indicator(alert_tape);

	/*
		Select tape.  Quit if that fails (with tape alert set).
		Plus, we have no choice but to halt machine, if that happens,
		because this is not a multi-threaded simulator.
	*/
	if((tape = tape_io_select()) == NULL) {
		return(false);
	}


	/*
		Loop to process tape characters...
	*/
	while(true) {

		/*
			If we set Tape Indicate before, then this is an
			attempt to read past the TM or the reel.  Error!

			Still, we return true to let processor continue.
		*/
		if(tape->indicate.active) {
			if(diagnostics_on) {
				sprintf(diag_buffer,
				  "Attempt to read past EOF/TM on drive %d\n",
				  tape->unit);
				tell(diag_buffer);
			}
			alert(alert_tape);
			return(true);
		}

		/*
			If we were in an IRG before, use that character.
			Otherwise, ask for one from the tape drive.

			If we get a parity error or end of file, return
			true to let processor continue.  (alert will have
			already been set).
		*/
		if(tape->at_irg) {
			tape->at_irg = false;
		}
		else {
			if(!tape_io_char_read(tape)) {
				return(true);
			}
		}

		/*
			If we got a tape mark, set Tape Indicate.
			(That also ends the operation right quick!)
			Also set Tape Indicate if the read reasulted in EOF
			If the read resulted in a parity error, set the
			tape alert lite, too.

			Even if lites are lit, we'll return true to let the
			processor continue.
		*/
		if(tape->buff == BCD_TM || tape->indicate.active) {
			tape->indicate.active = true;
			if(tape_transfer_error) {
				alert(alert_tape);
			}
			return(true);
		}

		/*
			Time to check the tape character's parity...
		*/
		if(tape_parity(tape->buff) != 0) {
			sprintf(diag_buffer,"Tape parity error on unit %d\n",
				tape->unit);
			tell(diag_buffer);
			tape_transfer_error = true;
		}


		/*
			If we're in load mode, and we get a Word Separator,
			set the wordmark flag, and read another character.
		*/
		wm=0;
		if(load_mode && tape->buff == BCD_WS) {
			wm = word_mark;
			if(!tape_io_char_read(tape) ||
			   (tape->buff & TAPE_IRG)) {
				sprintf(diag_buffer,
				   "Trailing WS in load mode on drive %d\n",
				   tape->unit);
				tell(diag_buffer);
				return(true);
			}
		}

		/*
			If we are going to try and store data, validate the
			B address.
		*/
		if(storing && bad_address(B_addr)) {
			cycle = cycle_B;
			return(false);
		}

		/*
			If end of record, and we haven't hit GM+WM, then
			insert a Group Mark (if we're storing data).
			(If load mode, also clear wm before we store).

			In any event, handle an IRG, by setting the flag
			in the tapedrive structure and saving the character
			sans the IRG flag.

			If, during the read, we had a parity error, now is
			the time to set the tape alert lite (though the CPU
			is not halted for tape alerts).
		*/
		if(tape->buff & TAPE_IRG) {
			if(storing) {
				if(load_mode) {
					clear_wm(B_addr);
				}
				memory[B_addr] = WM(memory[B_addr]) | BCD_GM;
				++B_addr;
			}
			tape->at_irg = true;
			tape->buff &= (~TAPE_IRG);
			if(tape_transfer_error) {
				alert(alert_tape);
			}
			return(true);
		}

		/*
			If we're storing data away, and hit a GM+WM, stop
			storing data.
		*/
		if(storing && WM(memory[B_addr]) &&
		   BA8421(memory[B_addr]) == BCD_GM) {
			storing = false;
			++B_addr;
		}

		/*
			Finally!  If we are still storing data, stuff
			it.  In load mode, we clear the WM first, and
			use the wm flag (set if previous char was WS).

			Here is also where we translate the alternate space
			tape character back into the original space bit
			configuration.

			The check bit isn't actually stored in simulatory
			memory.
		*/
		if(storing) {
			if(!load_mode) {
				wm = WM(memory[B_addr]);
			}
			if( BA8421(tape->buff) == BCD_AS) {
				tape->buff = BCD_SP;
			}
			memory[B_addr] = wm | BA8421(tape->buff);
			++B_addr;
		}
	}
}

/*****************************************************************************
*			tape_io_write
* Inputs:
*	boolean load_mode: True if this is load mode (word separator) write
*	(Unit is implicit.  See io.h)
*
* Effect:
*	Implements tape write I/O.  Write data until a Group Mark + Word Mark
*	in core (end of core is an error).  After execution, the A address
*	register contains %4x.  The B address register contains the address
*	of the GM+WM +1.
*
*	In load mode, Word Separator characters are written out before each
*	character in core that has a word mark.  (The word mark bits them-
*	selves are never written in either move mode or load mode).  Data is
*	written with *EVEN* parity BCD.
*
* Returns:
*	boolean.  Normally true, unless an error occures which should halt the
*	machine.  (Note that most Tape I/O errors do *NOT* halt a 1401 CPU).
*****************************************************************************/

boolean tape_io_write(boolean load_mode)
{
	tapedrive *tape = NULL;
	boolean first_char = true;		/* For setting IRG bit */
	unsigned char c;

	/*
		Select the tape unit.  Quit if that fails.  Normally a 1401
		would wait until a drive was dialed to the correct number,
		but since the simulator is not multi-tasking, we can't do that,
		so we should just halt the processor.
	*/
	if((tape = tape_io_select()) == NULL) {
		return(false);
	}

	/*
		If the write ring is in, set the transfer error flag.
	*/
	if(tape->readonly) {
		tape_transfer_error = true;
		alert(alert_tape);
		return(true);
	}

	tape_transfer_error = false;

	/*
		Tape write loop
	*/
	while(true) {

		/*
			Validate the address
		*/
		if(bad_address(B_addr)) {
			cycle = cycle_B;
			alert(alert_process);
			return(false);
		}

		/*
			If core has a GM + WM, we are done.  Leave B address
			register just past the GM + WM.
		*/
		c = memory[B_addr++];
		if(WM(c) && BA8421(c) == BCD_GM) {
			if(fflush(tape->file) == EOF) {
				sprintf(diag_buffer,
				   "fflush() error %s on drive %d\n",
				   strerror(errno),tape->unit);
				tell(diag_buffer);
				alert(alert_tape);
				tape_transfer_error = true;
			}
			return(true);
		}

		/*
			If load mode and there is a Word Mark, write out a
			word separator character.  Quite if the I/O fails.
			If it's the first character of the record, also set
			the IRG bit flag.
		*/
		if(load_mode && WM(c)) {
			tape->buff = BCD_WS;	/* EVEN parity already */
			if(first_char) {
				tape->buff |= TAPE_IRG;
				first_char = false;
			}
			if(!tape_io_char_write(tape)) {
				return(true);
			}
		}

		/*
			Put the character into the tape buffer and set the
			parity bit for EVEN parity.  Set IRG bit for the first
			character of each record.

			Also, spaces get written as alternate space on tape.
		*/
		tape->buff = BA8421(c);
		if(tape->buff == BCD_SP) {
			tape->buff = BCD_AS;
		}
		if(tape_parity(tape->buff) != 0) {
			tape->buff |= TAPE_C_bit;
		}
		if(first_char) {
			tape->buff |= TAPE_IRG;
			first_char = false;
		}

		/*
			Write out the character.  Quit on a problem.
		*/
		if(!tape_io_char_write(tape)) {
			return(true);
		}
	}
}

/*****************************************************************************
*			tape_parity
* Input:
*	A character from tape
*
* Returns:
*	0 if Even parity, 1 if Odd parity
*****************************************************************************/

int tape_parity(unsigned char c)
{
	int bits = 0;
	unsigned char bit;

	if(TAPE_C_bit & c) {
		++bits;
	}

	c = BA8421(c);
	for(bit=0x20; bit != 0; bit >>= 1) {
		if(bit & c) {
			++bits;
		}
	}
	return(bits % 2);
}
