/*
 * cassette.c - cassette emulation
 *
 * Copyright (C) 2001 Piotr Fusik
 * Copyright (C) 2001-2011 Atari800 development team (see DOC/CREDITS)
 *
 * This file is part of the Atari800 emulator project which emulates
 * the Atari 400, 800, 800XL, 130XE, and 5200 8-bit computers.
 *
 * Atari800 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.
 *
 * Atari800 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 Atari800; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/

#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "atari.h"
#include "cpu.h"
#include "cassette.h"
#include "esc.h"
#include "img_tape.h"
#include "log.h"
#include "util.h"
#include "pokey.h"

static IMG_TAPE_t *cassette_file = NULL;

/* Time till the end of the current tape event (byte or gap), in CPU ticks. */
static SLONG event_time_left = 0;

/* Indicates that there is a SERIN transmission in progress and when it ends,
   the current byte should be copied to POKEY_SERIN. This can be reset by
   rewinding/removing the tape or by resetting POKEY.
   Note that this variable has any meaning when PASSING_GAP is FALSE,
   so it doesn't have to be reset during PASSING_IRG. */
static int pending_serin = FALSE;

/* Indicates that an Inter-Record-Gap is currently being passed. It's set to TRUE
   at the beginning of each block. */
static int passing_gap = FALSE;

/* if penting_serin == TRUE, this holds the byte that is currently loaded from
   tape. It might be later copied to serin_byte. */
static UBYTE pending_serin_byte = 0xff;

/* Byte most recently loaded from tape; will be accessed by SIO_GetByte(). */
static UBYTE serin_byte = 0xff;

char CASSETTE_filename[FILENAME_MAX];
CASSETTE_status_t CASSETTE_status = CASSETTE_STATUS_NONE;
int CASSETTE_write_protect = FALSE;
int CASSETTE_record = FALSE;
int CASSETTE_writable = FALSE;
int CASSETTE_readable = FALSE;

char CASSETTE_description[CASSETTE_DESCRIPTION_MAX];
static int cassette_gapdelay = 0;	/* in ms, includes leader and all gaps */
static int cassette_motor = 0;

int CASSETTE_hold_start_on_reboot = 0;
int CASSETTE_hold_start = 0;
int CASSETTE_press_space = 0;
/* Indicates whether the tape has ended. During saving the value is always 0;
   during loading it is equal to (CASSETTE_GetPosition() >= CASSETTE_GetSize()). */
static int eof_of_tape = 0;

/* Call this function after each change of
   cassette_motor, CASSETTE_status or eof_of_tape. */
static void UpdateFlags(void)
{
	CASSETTE_readable = cassette_motor &&
	                    (CASSETTE_status == CASSETTE_STATUS_READ_WRITE ||
	                     CASSETTE_status == CASSETTE_STATUS_READ_ONLY) &&
	                     !eof_of_tape;
	CASSETTE_writable = cassette_motor &&
	                    CASSETTE_status == CASSETTE_STATUS_READ_WRITE &&
	                    !CASSETTE_write_protect;
}

int CASSETTE_ReadConfig(char *string, char *ptr)
{
	if (strcmp(string, "CASSETTE_FILENAME") == 0)
		Util_strlcpy(CASSETTE_filename, ptr, sizeof(CASSETTE_filename));
	else if (strcmp(string, "CASSETTE_LOADED") == 0) {
		int value = Util_sscanbool(ptr);
		if (value == -1)
			return FALSE;
		CASSETTE_status = (value ? CASSETTE_STATUS_READ_WRITE : CASSETTE_STATUS_NONE);
	}
	else if (strcmp(string, "CASSETTE_WRITE_PROTECT") == 0) {
		int value = Util_sscanbool(ptr);
		if (value == -1)
			return FALSE;
		CASSETTE_write_protect = value;
	}
	else return FALSE;
	return TRUE;
}

void CASSETTE_WriteConfig(FILE *fp)
{
	fprintf(fp, "CASSETTE_FILENAME=%s\n", CASSETTE_filename);
	fprintf(fp, "CASSETTE_LOADED=%d\n", CASSETTE_status != CASSETTE_STATUS_NONE);
	fprintf(fp, "CASSETTE_WRITE_PROTECT=%d\n", CASSETTE_write_protect);
}

int CASSETTE_Initialise(int *argc, char *argv[])
{
	int i;
	int j;
	int protect = FALSE; /* Is write-protect requested in command line? */

	for (i = j = 1; i < *argc; i++) {
		int i_a = (i + 1 < *argc);		/* is argument available? */
		int a_m = FALSE;			/* error, argument missing! */

		if (strcmp(argv[i], "-tape") == 0) {
			if (i_a) {
				Util_strlcpy(CASSETTE_filename, argv[++i], sizeof(CASSETTE_filename));
				CASSETTE_status = CASSETTE_STATUS_READ_WRITE;
				/* Reset any write-protection read from config file. */
				CASSETTE_write_protect = FALSE;
			}
			else a_m = TRUE;
		}
		else if (strcmp(argv[i], "-boottape") == 0) {
			if (i_a) {
				Util_strlcpy(CASSETTE_filename, argv[++i], sizeof(CASSETTE_filename));
				CASSETTE_status = CASSETTE_STATUS_READ_WRITE;
				/* Reset any write-protection read from config file. */
				CASSETTE_write_protect = FALSE;
				CASSETTE_hold_start = 1;
			}
			else a_m = TRUE;
		}
		else if (strcmp(argv[i], "-tape-readonly") == 0)
			protect = TRUE;
		else {
			if (strcmp(argv[i], "-help") == 0) {
				Log_print("\t-tape <file>      Insert cassette image");
				Log_print("\t-boottape <file>  Insert cassette image and boot it");
				Log_print("\t-tape-readonly    Mark the attached cassette image as read-only");
			}
			argv[j++] = argv[i];
		}

		if (a_m) {
			Log_print("Missing argument for '%s'", argv[i]);
			return FALSE;
		}
	}

	*argc = j;

	/* If CASSETTE_status was set in this function or in CASSETTE_ReadConfig(),
	   then tape is to be mounted. */
	if (CASSETTE_status != CASSETTE_STATUS_NONE && CASSETTE_filename[0] != '\0') {
		/* Tape is mounted unprotected by default - overrun it if needed. */
		protect = protect || CASSETTE_write_protect;
		if (!CASSETTE_Insert(CASSETTE_filename)) {
			CASSETTE_status = CASSETTE_STATUS_NONE;
			Log_print("Cannot open cassette image %s", CASSETTE_filename);
		}
		else if (protect)
			CASSETTE_ToggleWriteProtect();
	}

	return TRUE;
}

void CASSETTE_Exit(void)
{
	CASSETTE_Remove();
}

int CASSETTE_Insert(const char *filename)
{
	int writable;
	char const *description;

	IMG_TAPE_t *file = IMG_TAPE_Open(filename, &writable, &description);
	if (file == NULL)
		return FALSE;

	CASSETTE_Remove();
	cassette_file = file;
	/* Guard against providing CASSETTE_filename as parameter. */
	if (CASSETTE_filename != filename)
		strcpy(CASSETTE_filename, filename);
	eof_of_tape = 0;

	CASSETTE_status = (writable ? CASSETTE_STATUS_READ_WRITE : CASSETTE_STATUS_READ_ONLY);
	event_time_left = 0;
	pending_serin = FALSE;
	passing_gap = FALSE;

	if (description != NULL)
		Util_strlcpy(CASSETTE_description, description, sizeof(CASSETTE_description));
	CASSETTE_write_protect = FALSE;
	CASSETTE_record = FALSE;
	UpdateFlags();
	cassette_gapdelay = 0;

	return TRUE;
}

void CASSETTE_Remove(void)
{
	if (cassette_file != NULL) {
		IMG_TAPE_Close(cassette_file);
		cassette_file = NULL;
	}
	CASSETTE_status = CASSETTE_STATUS_NONE;
	CASSETTE_description[0] = '\0';
	UpdateFlags();
}

int CASSETTE_CreateCAS(const char *filename, const char *description) {
	IMG_TAPE_t *file = IMG_TAPE_Create(filename, description);
	if (file == NULL)
		return FALSE;

	CASSETTE_Remove(); /* Unmount any previous tape image. */
	cassette_file = file;
	Util_strlcpy(CASSETTE_filename, filename, sizeof(CASSETTE_filename));
	if (description != NULL)
		Util_strlcpy(CASSETTE_description, description, sizeof(CASSETTE_description));
	CASSETTE_status = CASSETTE_STATUS_READ_WRITE;
	event_time_left = 0;
	pending_serin = FALSE;
	passing_gap = FALSE;
	cassette_gapdelay = 0;
	eof_of_tape = 0;
	CASSETTE_record = TRUE;
	CASSETTE_write_protect = FALSE;
	UpdateFlags();

	return TRUE;
}

unsigned int CASSETTE_GetPosition(void)
{
	if (cassette_file == NULL)
		return 0;
	return IMG_TAPE_GetPosition(cassette_file) + 1;
}

unsigned int CASSETTE_GetSize(void)
{
	if (cassette_file == NULL)
		return 0;
	return IMG_TAPE_GetSize(cassette_file);
}

void CASSETTE_Seek(unsigned int position)
{
	if (cassette_file != NULL) {
		if (position > 0)
			position --;
		IMG_TAPE_Seek(cassette_file, position);

		event_time_left = 0;
		pending_serin = FALSE;
		passing_gap = FALSE;
		eof_of_tape = 0;
		CASSETTE_record = FALSE;
		UpdateFlags();
	}
}

int CASSETTE_GetByte(void)
{
	return serin_byte;
}

int CASSETTE_IOLineStatus(void)
{
	/* if motor off and EOF return always 1 (equivalent the mark tone) */
	if (!CASSETTE_readable || CASSETTE_record) {
		return 1;
	}

	return IMG_TAPE_SerinStatus(cassette_file, event_time_left);
}

void CASSETTE_PutByte(int byte)
{
	if (!ESC_enable_sio_patch && CASSETTE_writable && CASSETTE_record)
		IMG_TAPE_WriteByte(cassette_file, byte, POKEY_AUDF[POKEY_CHAN3] + POKEY_AUDF[POKEY_CHAN4]*0x100);
}

void CASSETTE_TapeMotor(int onoff)
{
	if (cassette_motor != onoff) {
		if (CASSETTE_record && CASSETTE_writable)
			/* Recording disabled, flush the tape */
			IMG_TAPE_Flush(cassette_file);
		cassette_motor = onoff;
		UpdateFlags();
	}
}

int CASSETTE_ToggleWriteProtect(void)
{
	if (CASSETTE_status != CASSETTE_STATUS_READ_WRITE)
		return FALSE;
	CASSETTE_write_protect = !CASSETTE_write_protect;
	UpdateFlags();
	return TRUE;
}

int CASSETTE_ToggleRecord(void)
{
	if (CASSETTE_status == CASSETTE_STATUS_NONE)
		return FALSE;
	CASSETTE_record = !CASSETTE_record;
	if (CASSETTE_record)
		eof_of_tape = FALSE;
	else if (CASSETTE_writable)
		/* Recording disabled, flush the tape */
		IMG_TAPE_Flush(cassette_file);
	event_time_left = 0;
	pending_serin = FALSE;
	passing_gap = FALSE;
	UpdateFlags();
	/* Return FALSE to indicate that recording will not work. */
	return !CASSETTE_record || (CASSETTE_status == CASSETTE_STATUS_READ_WRITE && !CASSETTE_write_protect);
}

static void CassetteWrite(int num_ticks)
{
	if (CASSETTE_writable)
		IMG_TAPE_WriteAdvance(cassette_file, num_ticks);
}

/* Sets the stamp of next SERIN IRQ event and loads new record if necessary.
   Returns TRUE if a new byte was loaded and POKEY_SERIN should be updated.
   The function assumes that current_block <= max_block. */
static int CassetteRead(int num_ticks)
{
	if (CASSETTE_readable) {
		int loaded = FALSE; /* Function's return value */
		event_time_left -= num_ticks;
		while (event_time_left < 0) {
			unsigned int length;
			if (!passing_gap && pending_serin) {
				serin_byte = pending_serin_byte;
				/* A byte is loaded, return TRUE so it gets stored in POKEY_SERIN. */
				loaded = TRUE;
			}

			/* If POKEY is in reset state, no serial I/O occurs. */
			pending_serin = (POKEY_SKCTL & 0x03) != 0;

			if (!IMG_TAPE_Read(cassette_file, &length, &passing_gap, &pending_serin_byte)) {
				eof_of_tape = 1;
				UpdateFlags();
				return loaded;
			}

			event_time_left += length;
		}
		return loaded;
	}
	return FALSE;
}

int CASSETTE_AddScanLine(void)
{
	/* increment elapsed cassette time */
	if (CASSETTE_record) {
		CassetteWrite(114);
		return FALSE;
	} else
		return CassetteRead(114);
}

void CASSETTE_ResetPOKEY(void)
{
	/* Resetting POKEY stops any serial transmission. */
	pending_serin = FALSE;
	pending_serin_byte = 0xff;
}

/* --- Functions for loading/saving with SIO patch --- */

int CASSETTE_AddGap(int gaptime)
{
	cassette_gapdelay += gaptime;
	if (cassette_gapdelay < 0)
		cassette_gapdelay = 0;
	return cassette_gapdelay;
}

/* Indicates that a loading leader is expected by the OS */
void CASSETTE_LeaderLoad(void)
{
	if (CASSETTE_record)
		CASSETTE_ToggleRecord();
	CASSETTE_TapeMotor(TRUE);
	cassette_gapdelay = 9600;
}

/* indicates that a save leader is written by the OS */
void CASSETTE_LeaderSave(void)
{
	if (!CASSETTE_record)
	CASSETTE_ToggleRecord();
	CASSETTE_TapeMotor(TRUE);
	cassette_gapdelay = 19200;
}

int CASSETTE_ReadToMemory(UWORD dest_addr, int length)
{
	CASSETTE_TapeMotor(1);
	if (!CASSETTE_readable)
		return 0;

	/* Convert wait_time to ms ( wait_time * 1000 / 1789790 ) and subtract. */
	cassette_gapdelay -= event_time_left / 1789; /* better accuracy not needed */
	if (!IMG_TAPE_SkipToData(cassette_file, cassette_gapdelay)) {
		/* Ignore the eventual error, assume it is the end of file */
		cassette_gapdelay = 0;
		eof_of_tape = 1;
		UpdateFlags();
		return 0;
	}
	cassette_gapdelay = 0;

	/* Load bytes */
	switch (IMG_TAPE_ReadToMemory(cassette_file, dest_addr, length)) {
	case TRUE:
		return TRUE;
	case -1: /* Read error/EOF */
		eof_of_tape = 1;
		UpdateFlags();
		/* FALLTHROUGH */
	default: /* case FALSE */
		return FALSE;
	}
}

int CASSETTE_WriteFromMemory(UWORD src_addr, int length)
{
	int result;
	CASSETTE_TapeMotor(1);
	if (!CASSETTE_writable)
		return 0;

	result = IMG_TAPE_WriteFromMemory(cassette_file, src_addr, length, cassette_gapdelay);
	cassette_gapdelay = 0;
	return result;
}


/*
vim:ts=4:sw=4:
*/
