/*
 * tape.c - Tape unit emulation.
 *
 * Written by
 *  Ettore Perazzoli <ettore@comm2000.it>
 *  Andreas Boose <viceteam@t-online.de>
 *
 * Based on older code by
 *  Jouko Valta <jopi@stekt.oulu.fi>
 *
 * This file is part of VICE, the Versatile Commodore Emulator.
 * See README for copyright notice.
 *
 *  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.
 *
 */

#include "vice.h"

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

#include "archdep.h"
#include "autostart.h"
#include "datasette.h"
#include "lib.h"
#include "log.h"
#include "machine.h"
#include "maincpu.h"
#include "mem.h"
#include "network.h"
#include "t64.h"
#include "tap.h"
#include "tape-internal.h"
#include "tape.h"
#include "tapeimage.h"
#include "tapeport.h"
#include "traps.h"
#include "types.h"
#include "uiapi.h"
#include "vice-event.h"

/* #define DEBUG_TAPE */

/* Cassette Format Constants */
#define CAS_TYPE_OFFSET 0
#define CAS_STAD_OFFSET 1       /* start address */
#define CAS_ENAD_OFFSET 3       /* end address */
#define CAS_NAME_OFFSET 5       /* filename */

/* CPU addresses for tape routine variables.  */
static uint16_t buffer_pointer_addr;
static uint16_t st_addr;
static uint16_t verify_flag_addr;
static uint16_t stal_addr;
static uint16_t eal_addr;
static uint16_t kbd_buf_addr;
static uint16_t kbd_buf_pending_addr;
static int irqval;
static uint16_t irqtmp;

/* Flag: has tape been initialized?  */
static int tape_is_initialized = 0;

/* Tape traps to be installed.  */
static const trap_t *tape_traps;

/* Logging goes here.  */
static log_t tape_log = LOG_DEFAULT;

/* The tape image for device 1. */
tape_image_t *tape_image_dev[TAPEPORT_MAX_PORTS] = { NULL };

/* ------------------------------------------------------------------------- */

static inline void set_st(uint8_t b)
{
    mem_store(st_addr, (uint8_t)(mem_read(st_addr) | b));
}

/* ------------------------------------------------------------------------- */

void tape_traps_install(void)
{
    const trap_t *p;

    if (tape_traps != NULL) {
        for (p = tape_traps; p->func != NULL; p++) {
            traps_add(p);
        }
    }
}

void tape_traps_deinstall(void)
{
    const trap_t *p;

    if (tape_traps != NULL) {
        for (p = tape_traps; p->func != NULL; p++) {
            traps_remove(p);
        }
    }
}

static void tape_init_vars(const tape_init_t *init)
{
    /* Set addresses of tape routine variables.  */
    st_addr = init->st_addr;
    buffer_pointer_addr = init->buffer_pointer_addr;
    verify_flag_addr = init->verify_flag_addr;
    irqtmp = init->irqtmp;
    irqval = init->irqval;
    stal_addr = init->stal_addr;
    eal_addr = init->eal_addr;

    kbd_buf_addr = init->kbd_buf_addr;
    kbd_buf_pending_addr = init->kbd_buf_pending_addr;

    tape_traps = init->trap_list;
}

/* Initialize the tape emulation, using the traps in `trap_list'.  */
/* FIXME: This should be passed through a struct.  */
int tape_init(const tape_init_t *init)
{
    int i;

    if (tape_log == LOG_DEFAULT) {
        tape_log = log_open("Tape");
    }

    tape_internal_init();
    tape_image_init();

    for (i = 0; i < TAPEPORT_MAX_PORTS; i++) {
        lib_free(tape_image_dev[i]);
        tape_image_dev[i] = lib_calloc(1, sizeof(tape_image_t));
    }

    tap_init(init);

    tape_init_vars(init);
    tape_traps_install();

    tape_is_initialized = 1;
    return 0;
}

/* re-init the traps, needed to switch between different configurations in x128 */
int tape_reinit(const tape_init_t *init)
{
    if (tape_is_initialized == 0) {
        return -1;
    }

    tape_traps_deinstall();
    tape_traps = NULL;

    tape_init_vars(init);
    tape_traps_install();

    return 0;
}

void tape_shutdown(void)
{
    int i;

    for (i = 0; i < TAPEPORT_MAX_PORTS; i++) {
        lib_free(tape_image_dev[i]);
    }
}

int tape_deinstall(void)
{
    int i;

    if (!tape_is_initialized) {
        return -1;
    }

    for (i = 0; i < TAPEPORT_MAX_PORTS; i++) {
        if (tape_image_dev[i]->name != NULL && tape_image_dev[i]->type == TAPE_TYPE_T64) {
            tape_image_detach_internal(i + 1);
        }
    }

    tape_traps_deinstall();

    tape_traps = NULL;

    tape_is_initialized = 0;

    return 0;
}

/* ------------------------------------------------------------------------- */

/* Tape traps.  These functions implement the standard kernal replacements
   for the tape functions.  Every emulator can either use these traps, or
   install its own ones, by passing an appropriate `trap_list' to
   `tape_init()'.  */

/* NOTE: When loading from tape, the kernal will always load absolute when the
         tape header type is 3 (except on PET and CBM2 - which will always load
         absolute, no matter what). Because of this we will change the default
         header type to type 1 IF autostart is in progress AND loading to basic
         start was requested by the respective option. */
static int default_tape_header_type(void)
{
    if (autostart_in_progress() && (autostart_tape_basic_load == 1)) {
        return TAPE_CAS_TYPE_BAS;
    }
    return machine_tape_type_default();
}

/* Find the next Tape Header and load it onto the Tape Buffer.  */
int tape_find_header_trap(void)
{
    int err;
    uint8_t *cassette_buffer;

    cassette_buffer = mem_ram + (mem_read(buffer_pointer_addr) | (mem_read((uint16_t)(buffer_pointer_addr + 1)) << 8));

    if (tape_image_dev[TAPEPORT_PORT_1]->name == NULL
        || tape_image_dev[TAPEPORT_PORT_1]->type != TAPE_TYPE_T64) {
        err = 1;
    } else {
        t64_t *t64;
        t64_file_record_t *rec;

        t64 = (t64_t *)tape_image_dev[TAPEPORT_PORT_1]->data;
        rec = NULL;

        err = 0;
        do {
            if (t64_seek_to_next_file(t64, 1) < 0) {
                err = 1;
                break;
            }

            rec = t64_get_current_file_record(t64);
        } while (rec->entry_type != T64_FILE_RECORD_NORMAL);

        if (!err) {
            cassette_buffer[CAS_TYPE_OFFSET] = default_tape_header_type();
            cassette_buffer[CAS_STAD_OFFSET] = rec->start_addr & 0xff;
            cassette_buffer[CAS_STAD_OFFSET + 1] = rec->start_addr >> 8;
            cassette_buffer[CAS_ENAD_OFFSET] = rec->end_addr & 0xff;
            cassette_buffer[CAS_ENAD_OFFSET + 1] = rec->end_addr >> 8;
            memcpy(cassette_buffer + CAS_NAME_OFFSET,
                   rec->cbm_name, T64_REC_CBMNAME_LEN);
        }
    }

    if (err) {
        cassette_buffer[CAS_TYPE_OFFSET] = TAPE_CAS_TYPE_EOF;
    }

    mem_store(st_addr, 0);      /* Clear the STATUS word.  */
    mem_store(verify_flag_addr, 0);

    if (irqtmp) {
        mem_store(irqtmp, (uint8_t)(irqval & 0xff));
        mem_store((uint16_t)(irqtmp + 1), (uint8_t)((irqval >> 8) & 0xff));
    }

    /* Check if STOP has been pressed.  */
    {
        int i, n = mem_read(kbd_buf_pending_addr);

        maincpu_set_carry(0);
        for (i = 0; i < n; i++) {
            if (mem_read((uint16_t)(kbd_buf_addr + i)) == 0x3) {
                maincpu_set_carry(1);
                break;
            }
        }
    }

    maincpu_set_zero(1);
    return 1;
}

int tape_find_header_trap_plus4(void)
{
    int err;
    uint8_t *cassette_buffer;

    cassette_buffer = mem_ram + buffer_pointer_addr;

    if (tape_image_dev[TAPEPORT_PORT_1]->name == NULL
        || tape_image_dev[TAPEPORT_PORT_1]->type != TAPE_TYPE_T64) {
        err = 1;
    } else {
        t64_t *t64;
        t64_file_record_t *rec;

        t64 = (t64_t *)tape_image_dev[TAPEPORT_PORT_1]->data;
        rec = NULL;

        err = 0;
        do {
            if (t64_seek_to_next_file(t64, 1) < 0) {
                err = 1;
                break;
            }

            rec = t64_get_current_file_record(t64);
        } while (rec->entry_type != T64_FILE_RECORD_NORMAL);

        if (!err) {
            mem_store(0xF8, default_tape_header_type());
            cassette_buffer[CAS_STAD_OFFSET - 1] = rec->start_addr & 0xff;
            cassette_buffer[CAS_STAD_OFFSET] = rec->start_addr >> 8;
            cassette_buffer[CAS_ENAD_OFFSET - 1] = rec->end_addr & 0xff;
            cassette_buffer[CAS_ENAD_OFFSET] = rec->end_addr >> 8;
            memcpy(cassette_buffer + CAS_NAME_OFFSET - 1,
                   rec->cbm_name, T64_REC_CBMNAME_LEN);
        }
    }

    if (err) {
        mem_store(0xF8, TAPE_CAS_TYPE_EOF);
    }

    mem_store(0xb6, 0x33);
    mem_store(0xb7, 0x03);

    mem_store(st_addr, 0);      /* Clear the STATUS word.  */
    mem_store(verify_flag_addr, 0);

    /* Check if STOP has been pressed.  */
    {
        int i, n = mem_read(kbd_buf_pending_addr);

        maincpu_set_carry(0);
        for (i = 0; i < n; i++) {
            if (mem_read((uint16_t)(kbd_buf_addr + i)) == 0x3) {
                maincpu_set_carry(1);
                break;
            }
        }
    }

    maincpu_set_zero(1);
    return 1;
}

/* Cassette Data transfer trap.

   XR flags the function to be performed on IRQ:

   08   Write tape
   0a   Write tape leader
   0c   Normal keyscan
   0e   Read tape

   Luckily enough, these values are valid for all the machines.  */
int tape_receive_trap(void)
{
    int len;
    uint16_t start, end;
    uint8_t st;

    start = (mem_read(stal_addr) | (mem_read((uint16_t)(stal_addr + 1)) << 8));
    end = (mem_read(eal_addr) | (mem_read((uint16_t)(eal_addr + 1)) << 8));

    switch (maincpu_get_x()) {
        case 0x0e:
            {
                int amount;

                len = (int)(end - start);
                amount = t64_read((t64_t *)tape_image_dev[TAPEPORT_PORT_1]->data, mem_ram + (int)start, len);
                if (amount == len) {
                    st = 0x40;  /* EOF */
                } else {
                    st = 0x10;

                    log_warning(tape_log,
                                "Unexpected end of tape: file may be truncated.");
                }
            }
            break;
        default:
            log_error(tape_log, "Kernal command %x not supported.",
                      maincpu_get_x());
            st = 0x40;
            break;
    }

    /* Set registers and flags like the Kernal routine does.  */

    if (irqtmp) {
        mem_store(irqtmp, (uint8_t)(irqval & 0xff));
        mem_store((uint16_t)(irqtmp + 1), (uint8_t)((irqval >> 8) & 0xff));
    }

    set_st(st);                 /* EOF and possible errors */

    maincpu_set_carry(0);
    maincpu_set_interrupt(0);
    return 1;
}

int tape_receive_trap_plus4(void)
{
    uint16_t start, end, len;
    uint8_t st;

    start = (mem_read(stal_addr) | (mem_read((uint16_t)(stal_addr + 1)) << 8));
    end = (mem_read(eal_addr) | (mem_read((uint16_t)(eal_addr + 1)) << 8));

    /* Read block.  */
    len = end - start;

    if (t64_read((t64_t *)tape_image_dev[TAPEPORT_PORT_1]->data,
                 mem_ram + (int) start, (int)len) == (int) len) {
        st = 0x40;      /* EOF */
    } else {
        st = 0x10;

        log_warning(tape_log,
                    "Unexpected end of tape: file may be truncated.");
    }

    /* Set registers and flags like the Kernal routine does.  */


    set_st(st);                 /* EOF and possible errors */
    return 1;
}

const char *tape_get_file_name(int port)
{
    if (tape_image_dev[port] == NULL) {
        return "";
    }

    return tape_image_dev[port]->name;
}

int tape_tap_attached(int port)
{
    if (tape_image_dev[port]->name != NULL
        && tape_image_dev[port]->type == TAPE_TYPE_TAP) {
        return 1;
    }

    return 0;
}

/* ------------------------------------------------------------------------- */

/* Detach.  */
int tape_image_detach_internal(unsigned int unit)
{
    int retval = 0;
    char event_data[2];

    if ((unit != 1) && (unit != 2)) {
        return -1;
    }

    /* before detaching the tape image, press STOP */
    datasette_control(unit - 1, DATASETTE_CONTROL_STOP);

    if ((tape_image_dev[unit - 1] == NULL) ||
        (tape_image_dev[unit - 1]->name == NULL)) {
        return 0;
    }

    switch (tape_image_dev[unit - 1]->type) {
        case TAPE_TYPE_T64:
            log_message(tape_log,
                        "Detaching T64 image `%s'.", tape_image_dev[unit - 1]->name);
            break;
        case TAPE_TYPE_TAP:
            log_message(tape_log,
                        "Detaching TAP image `%s'.", tape_image_dev[unit - 1]->name);
            datasette_set_tape_image(unit - 1, NULL);

            tape_traps_install();
            break;
        default:
            log_error(tape_log, "Unknown tape type %u.",
                      tape_image_dev[unit - 1]->type);
    }

    retval = tape_image_close(tape_image_dev[unit - 1]);

    ui_display_tape_current_image(unit - 1, "");

    event_data[0] = (char)unit;
    event_data[1] = 0;

    event_record(EVENT_ATTACHTAPE, (void *)event_data, 2);

    return retval;
}

int tape_image_detach(unsigned int unit)
{
    char event_data[2];

    if (unit != 1 && unit != 2) {
        return -1;
    }

    event_data[0] = (char)unit;
    event_data[1] = 0;

    if (event_playback_active()) {
        return -1;
    }

    if (network_connected()) {
        network_event_record(EVENT_ATTACHTAPE, (void *)event_data, 2);
        return 0;
    }

    return tape_image_detach_internal(unit);
}

void tape_image_detach_all(void)
{
    tape_image_detach(1);
    tape_image_detach(2);
}

/* Attach.  */
static int tape_image_attach_internal(unsigned int unit, const char *name)
{
    tape_image_t tape_image;

    if (unit != 1 && unit != 2) {
        return -1;
    }

    if (!name || !*name) {
        return -1;
    } else {
        /* Make sure this tape isn't already in the other datasette */
        unsigned int other_index = 2 - unit;
        if (tape_image_dev[other_index] &&
            tape_image_dev[other_index]->name &&
            archdep_real_path_equal(tape_image_dev[other_index]->name, name)) {
            log_error(tape_log, "File `%s' already mounted on other tape unit", name);
            return -1;
        }
    }

    tape_image.name = lib_strdup(name);
    tape_image.read_only = 0;

    if (tape_image_open(&tape_image) < 0) {
        lib_free(tape_image.name);
        log_error(tape_log, "Cannot open file `%s'", name);
        return -1;
    }

    /* detach any attached image, this will also press STOP */
    tape_image_detach_internal(unit);

    memcpy(tape_image_dev[unit - 1], &tape_image, sizeof(tape_image_t));

    ui_display_tape_current_image(unit - 1, tape_image_dev[unit - 1]->name);

    switch (tape_image_dev[unit - 1]->type) {
        case TAPE_TYPE_T64:
            log_message(tape_log, "T64 image '%s' attached.", name);
            break;
        case TAPE_TYPE_TAP:
            datasette_set_tape_image(unit - 1, (tap_t *)tape_image_dev[unit - 1]->data);
            log_message(tape_log, "TAP image '%s' attached.", name);
            log_message(tape_log, "TAP image version: %i, system: %i.",
                        ((tap_t *)tape_image_dev[unit - 1]->data)->version,
                        ((tap_t *)tape_image_dev[unit - 1]->data)->system);
            tape_traps_deinstall();
            break;
        default:
            log_error(tape_log, "Unknown tape type %u.",
                      tape_image_dev[unit - 1]->type);
            return -1;
    }

    event_record_attach_image(unit, 0, name, tape_image.read_only);

    return 0;
}

int tape_image_attach(unsigned int unit, const char *name)
{
    if (event_playback_active()) {
        return -1;
    }

    if (network_connected()) {
        network_attach_image(unit, name);
        return 0;
    }

    return tape_image_attach_internal(unit, name);
}

void tape_image_event_playback(unsigned int unit, const char *filename)
{
    if (filename == NULL || filename[0] == 0) {
        tape_image_detach_internal(unit);
    } else {
        tape_image_attach_internal(unit, filename);
    }
}
