//
// Digital-to-analogue details, specific to the 48k Spectrum
// with a 3.5MHz frequency, and to the Sinclair (default) ROM tape loader routines
//
//            pulse
// -----     -------
//     |     |     |
//     |     |     |
//     -------     ---
//      pulse
// 
// Each block is as follows (ROM routine is edge-triggered, so initial level doesn't matter):
// 
// 1. lead tone
//     header blocks - 8063 pulses each 2168 tstates long, or
//       data blocks - 3223 pulses each 2168 tstates long
// 2. sync pulses
//     one pulse of 667 tstates, then
//     one pulse of 735 tstates
// 3. data
//     logical 0 bit - 2 pulses of 855 tstates each
//     logical 1 bit - 2 pulses of 1710 tstates each
//     MSB first
//
#define __ADJUSTMENT (0)

#define _LEAD_TONE_PULSE_COUNT__HEADER 8063
#define _LEAD_TONE_PULSE_COUNT__DATA 3223
#define _LEAD_TONE_TSTATES_PER_PULSE 2168 + __ADJUSTMENT
#define _SYNC_PULSE1_PULSE_COUNT 1
#define _SYNC_PULSE1_TSTATES_PER_PULSE 667 + __ADJUSTMENT
#define _SYNC_PULSE2_PULSE_COUNT 1
#define _SYNC_PULSE2_TSTATES_PER_PULSE 735 + __ADJUSTMENT
#define _DATA_BIT_LOGICAL_0_PULSE_COUNT 2
#define _DATA_BIT_LOGICAL_0_TSTATES_PER_PULSE 855 + __ADJUSTMENT
#define _DATA_BIT_LOGICAL_1_PULSE_COUNT 2
#define _DATA_BIT_LOGICAL_1_TSTATES_PER_PULSE 1710 + __ADJUSTMENT

#define _SILENCE_BEFORE_LEAD_TSTATES 2625000    // about 750ms at a clock speed of 3.5MHz

#include <SDL.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys\types.h> 
#include <sys\stat.h>

#include "Constants.h"
#include "Timing.h"
#include "TapLoader.h"
#include "Io.h"
#include "Log.h"
#include "Cpu.h"
#include "Memory.h"

static Uint8* _tapFileBuffer = NULL;
static Uint32 _tapFileSize = 0;
static Uint8 _tapLoadedFile = 0;

Uint8 _tapEarLineBitValue;
struct tapRunState _tapRunState;

#define _HEADER_SPECTRUM_BYTE_LENGTH    19      // 17 for the data to load, 1 for checksum, 1 for flag
#define _HEADER_FILENAME_LENGTH 10
#define _DEBUG_BUFFER_SIZE 256
static char _debugBuffer[_DEBUG_BUFFER_SIZE];

// tightly coupled to save-states, because save-states need to know how many bytes are sufficient
// to represent tape block index
#define _MAX_TAP_BLOCKS 1000
static struct tapBlock** _tapBlocks = NULL;
static Uint16 _tapBlocksCount = 0;

// invoked when tape stops
void (*_taploader_on_stop_handler)();
// invoked when tape plays
void (*_taploader_on_play_handler)();

#define _TURBO_ON_FACTOR 100
#define _TURBO_OFF_FACTOR 1

Uint8 _tapLoader_is_play_continuous = 0;

// used to flicker between red and cyan during loading, but when tape is stopped
static Uint8 _tapLoader_idle_border_effect_colour = 2;
// relied upon to turn on or off the idle tape border effect
static Uint8 _tapLoader_rom_is_sinclair = 0;

// what the consumer has requested for when the tape is playing
Uint8 _tapLoader_desired_tstate_turbo_factor = _TURBO_OFF_FACTOR;
Uint64 _taploader_instruction_counter = 0;

void taploader_set_desired_turbo(Uint8 isEnabled) {
    if (isEnabled) {
        _tapLoader_desired_tstate_turbo_factor = _TURBO_ON_FACTOR;
    }
    else {
        _tapLoader_desired_tstate_turbo_factor = _TURBO_OFF_FACTOR;
    }
}

// seek to the start of a block, and stops tape
void _taploader_seek(Uint16 blockIndex) {
    if (blockIndex >= _tapBlocksCount) {
        return;
    }

    taploader_press_stop();
    _tapRunState.block = _tapBlocks[blockIndex];
}

void taploader_start() {
	_tapEarLineBitValue = 1;

    int romHash = memory_compute_hash(0, SPECTRUM_ROM_SIZE);
    if (romHash == SINCLAIR_48K_ROM__CHECKSUM) {
        _tapLoader_rom_is_sinclair = 1;
    }
    else {
        _tapLoader_rom_is_sinclair = 0;
        log_write("Warning: idle tape border effects disabled; ROM is not Sinclair");
    }
}

void taploader_press_stop() {
    _tapRunState.type = Stopped;
    timing_apply_tstate_factor(_TURBO_OFF_FACTOR);  // restore CPU to normal speed when tape stops
    if (_taploader_on_stop_handler != NULL) {
        _taploader_on_stop_handler();
    }
}

void taploader_set_on_stop_handler(void (*handler)()) {
    _taploader_on_stop_handler = handler;
}

void taploader_set_on_play_handler(void (*handler)()) {
    _taploader_on_play_handler = handler;
}

void taploader_seek_next() {
    Uint16 blockIndex = _tapRunState.block->blockIndex + 1;
    if (blockIndex >= _tapBlocksCount) {
        blockIndex = 0;
    }
    _taploader_seek(blockIndex);
}

void taploader_seek_previous() {
    Uint16 blockIndex = _tapRunState.block->blockIndex;
    if (blockIndex == 0) {
        blockIndex = _tapBlocksCount - 1;
    }
    else {
        blockIndex--;
    }
    _taploader_seek(blockIndex);
}

const char* _taploader_translate_block_type(Uint8 flagByte) {
    switch (flagByte) {
    case 0x00:
        return "[hdr]";
    case 0xFF:
        return "[dat]";
    default:
        return "[???]";
    }
}

void taploader_destroy() {
    taploader_press_stop();

    if (_tapFileBuffer != NULL) {
        free(_tapFileBuffer);
    }

    // free TAP blocks array
    if (_tapBlocks) {
        for (int i = 0; i < _tapBlocksCount; i++) {
            struct tapBlock* block = _tapBlocks[i];

            // Don't need MSVC's antics here
            #pragma warning(push)
            #pragma warning(disable: 6001)
            if (block->type == Header) {
                free(block->headerInfo->filename);
                free(block->headerInfo);
            }
            free(block);
            #pragma warning(pop)
        }
        free(_tapBlocks);
    }
}

Uint16 taploader_get_block_count() {
    return _tapBlocksCount;
}

struct tapRunState taploader_get_state() {
    return _tapRunState;
}

// simulates pressing play on the tape player
// tape is guaranteed to be at the start of a block
void taploader_press_play(Uint8 isContinuous) {
    if (_tapRunState.type != Stopped) {
        // NOOP when tape is already playing
        return;
    }

    _tapLoader_is_play_continuous = isContinuous;

    _tapRunState.type = Silence;    // first state of the "play" sequence
    timing_apply_tstate_factor(_tapLoader_desired_tstate_turbo_factor);  // hasten or revert CPU to its normal speed, as desired
    _tapRunState.tstatesRemaining = (Sint64)_SILENCE_BEFORE_LEAD_TSTATES;
    if (_taploader_on_play_handler != NULL) {
        _taploader_on_play_handler();
    }
}

// assumes current run state ran out of pulses
void _taploader_next_data_bit(Uint8 isInitial) {
    // next bit
    _tapRunState.bitNumber--;

    if (isInitial) {
        _tapRunState.type = DataBytes;
        _tapRunState.bitNumber = 7; // MSB first
        _tapRunState.dataBytePointer = _tapRunState.block->tapBlockBytes + 2;   // skip over 2 TAP block size bytes
    }

    // are we at the end of a byte?
    if (_tapRunState.bitNumber == -1) {
        // yes, so we're done with this byte

        if (_tapRunState.dataBytePointer >= _tapRunState.block->tapBlockBytes + _tapRunState.block->tapBlockSize) {
            // it was the last byte, so we're done this entire block
            taploader_press_stop();
            return;
        }

        // we still have more bytes in this block, so move to the next byte
        _tapRunState.dataBytePointer++;
        // .. and start from the MSB
        _tapRunState.bitNumber = 7;
    }

    // reset counters
    Uint8 bit = *(_tapRunState.dataBytePointer) & (1 << _tapRunState.bitNumber);
    _tapRunState.pulsesRemaining = bit ? _DATA_BIT_LOGICAL_1_PULSE_COUNT : _DATA_BIT_LOGICAL_0_PULSE_COUNT;
    _tapRunState.tstatesPerPulse = bit ? _DATA_BIT_LOGICAL_1_TSTATES_PER_PULSE : _DATA_BIT_LOGICAL_0_TSTATES_PER_PULSE;
    _tapRunState.tstatesRemaining = _tapRunState.tstatesPerPulse;
    if (!isInitial) {
        _tapEarLineBitValue ^= 1;
    }
}

// invoked when a tstate counter has elapsed
// returns 0 when state didn't change, or non-0 otherwise
Uint8 _taploader_handle_end_of_pulse() {
    Uint8 stateChanged = 0;

    switch (_tapRunState.type) {
    case Stopped:
        // Stopped state doesn't automatically flow into any other state;
        // it has to be exited manually
        break;
    case Silence:
        // at the end of silence begins the lead tone
        _tapRunState.type = LeadTone;
        Sint64 pulseCount = 0;
        switch (_tapRunState.block->type) {
        case Header:
            pulseCount = _LEAD_TONE_PULSE_COUNT__HEADER;
            break;
        case Data:
            pulseCount = _LEAD_TONE_PULSE_COUNT__DATA;
            break;
        }
        _tapRunState.pulsesRemaining = pulseCount;
        _tapRunState.tstatesPerPulse = _LEAD_TONE_TSTATES_PER_PULSE;
        _tapRunState.tstatesRemaining = _tapRunState.tstatesPerPulse;
        stateChanged = 1;
        break;
    case LeadTone:
        // current pulse has finished (by running out of tstates)
        _tapRunState.pulsesRemaining--;

        if (_tapRunState.pulsesRemaining == 0) {
            // the current pulse was the last pulse of the lead tone, so the lead tone is over
            // and we're beginning sync pulse 1
            _tapRunState.type = SyncPulse1;
            _tapRunState.pulsesRemaining = _SYNC_PULSE1_PULSE_COUNT;
            _tapRunState.tstatesPerPulse = _SYNC_PULSE1_TSTATES_PER_PULSE;
            _tapRunState.tstatesRemaining = _tapRunState.tstatesPerPulse;
            stateChanged = 1;
            _tapEarLineBitValue ^= 1;
        }
        else {
            // lead tone continues with another pulse, so we flip the EAR line level
            // and remain in the lead tone
            _tapEarLineBitValue ^= 1;
            _tapRunState.tstatesRemaining = _tapRunState.tstatesPerPulse;
        }
        break;
    case SyncPulse1:
        // current pulse has finished (by running out of tstates)
        _tapRunState.pulsesRemaining--;

        if (_tapRunState.pulsesRemaining == 0) {
            // sync pulse 1 is over and we begin sync pulse 2
            _tapRunState.type = SyncPulse2;
            _tapRunState.pulsesRemaining = _SYNC_PULSE2_PULSE_COUNT;
            _tapRunState.tstatesPerPulse = _SYNC_PULSE2_TSTATES_PER_PULSE;
            _tapRunState.tstatesRemaining = _tapRunState.tstatesPerPulse;
            stateChanged = 1;
            _tapEarLineBitValue ^= 1;
        }
        else {
            // NOTE: this is here for consistency/debugging sake, since this state has a
            //       single pulse
            //       that is, this code is not normally reached
            // sync pulse continues with another pulse, so we flip the EAR line level
            // and remain in the sync pulse
            _tapEarLineBitValue ^= 1;
            _tapRunState.tstatesRemaining = _tapRunState.tstatesPerPulse;
        }
        break;
    case SyncPulse2:
        // current pulse has finished (by running out of tstates)
        _tapRunState.pulsesRemaining--;

        if (_tapRunState.pulsesRemaining == 0) {
            // sync pulse 2 is over and we begin data bytes
            _taploader_next_data_bit(1);
            _tapEarLineBitValue ^= 1;
            stateChanged = 1;
        }
        else {
            // NOTE: this is here for consistency/debugging sake, since this state has a
            //       single pulse
            //       that is, this code is not normally reached
            // sync pulse continues with another pulse, so we flip the EAR line level
            // and remain in the sync pulse
            _tapEarLineBitValue ^= 1;
            _tapRunState.tstatesRemaining = _tapRunState.tstatesPerPulse;
        }
        break;
    case DataBytes:
        // current pulse has finished (by running out of tstates)
        _tapRunState.pulsesRemaining--;

        if (_tapRunState.pulsesRemaining == 0) {
            _taploader_next_data_bit(0);
            if (_tapRunState.type == Stopped) {
                // continue playing next block, unless we've just finished playing the last one
                if (_tapRunState.block->blockIndex < _tapBlocksCount - 1) {
                    // there are still blocks ahead to play

                    // move to next block
                    taploader_seek_next();

                    if (_tapLoader_is_play_continuous) {
                        // play it
                        taploader_press_play(1);
                    }
                }
                else {
                    taploader_press_stop();
                }

                stateChanged = 1;
            }
        }
        else {
            // we continue with another pulse, so we flip the EAR line level
            _tapEarLineBitValue ^= 1;
            _tapRunState.tstatesRemaining = _tapRunState.tstatesPerPulse;
        }
        break;
    default:
        break;
    }

    return stateChanged;
}

void _taploader_debug() {
    //
    // I've kept this around because these specific spots in the ZX Spectrum's ROM
    // were extremely useful while debugging tape timings
    // 
    
    //if (cpu_regs()->PC == 0x58f && _tapRunState.pulsesRemaining == 0) {
    //    // LD_SYNC: we're starting to wait for first sync pulse
    //    cpu_regs()->PC = cpu_regs()->PC;
    //}
    //if (cpu_regs()->PC == 0x59b && _tapRunState.type != DataBytes) {
    //    // LD_SYNC: we're starting to wait for second sync pulse
    //    cpu_regs()->PC = cpu_regs()->PC;
    //}
    //if (cpu_regs()->PC == 0x59f) {
    //    // LD_SYNC: immediately after second sync pulse
    //    cpu_regs()->PC = cpu_regs()->PC;
    //}
    //if (cpu_regs()->PC == 0x556 && _tapRunState.type == DataBytes) {
    //    // EXPECTED TO BE IN DATA BYTES STATE
    //    // LD_BYTES: CPU is at the start of loading data bytes
    //    cpu_regs()->PC = cpu_regs()->PC;
    //}
    //if (cpu_regs()->PC == 0x5d9 && _tapRunState.type == DataBytes) {
    //    // EXPECTED TO BE IN DATA BYTES STATE
    //    // LD_8_BITS: CPU has read one whole byte
    //    // DEBUG! this is reached immediately after the CPU reads a complete byte from tape
    //    Uint8 lastReadByteFromTape = cpu_regs()->L;
    //    Uint16 remainingBytesFromTape = *(cpu_regs()->DE);
    //    cpu_regs()->PC = cpu_regs()->PC;
    //}
}

inline void _taploader_oscillate_border_colour() {
    if (_tapLoader_idle_border_effect_colour == 2) {
        _tapLoader_idle_border_effect_colour = 5;
    }
    else {
        _tapLoader_idle_border_effect_colour = 2;
    }
    io_set8_border_colour_uncontended(_tapLoader_idle_border_effect_colour);
}

void taploader_on_instruction_tstates_elapsed__tap_independent_border_effect(struct instruction* instruction) {
    if (_tapRunState.type != Stopped) {
        return;
    }

    // tape is stopped

    // when inside a loop in various tape-access ROM routines, oscillate the border colour
    // to simulate the A/C noise which would cause that effect on real hardware

    // ... but only for known ROMs, whose routines' addresses are known
    if (!_tapLoader_rom_is_sinclair) {
        return;
    }

    // are we inside "LD-EDGE-1" ROM routine (used by LOAD, VERIFY, MERGE)
    if (instruction->startPc == 0x05E7) {
        // yes, so we're awaiting tape signal
        if (rand() % 128 == 0) {
            // only oscillate border colour occasionally
            _taploader_oscillate_border_colour();
        }
    }
}

void taploader_on_instruction_tstates_elapsed(struct instruction* instruction) {
    if (!_tapLoadedFile) {
        return;
    }

    _taploader_instruction_counter++;

    taploader_on_instruction_tstates_elapsed__tap_independent_border_effect(instruction);

    if (_tapRunState.type == Stopped) {
        return;
    }

    // check infrequently if the turbo state has changed while loading
    if ((_taploader_instruction_counter & 0x10000) == 0x10000) {
        timing_apply_tstate_factor(_tapLoader_desired_tstate_turbo_factor);  // hasten or revert CPU to its normal speed, as desired
    }

	_tapRunState.tstatesRemaining -= instruction->tstatesElapsed;

    _taploader_debug();

    if (_tapRunState.tstatesRemaining <= 0) {
        _taploader_handle_end_of_pulse();
    }

    io_ear_write1_uncontended(_tapEarLineBitValue);
}

// reads a header-type block
// tapBuffer is a ptr to a ptr to block's second byte (aka immediately after flag)
// blockSize is a count of Spectrum bytes, including checksum and flag
struct tapBlock* _taploader_readblock__header(Uint8** tapBuffer, Uint16 blockSize, Uint8 flagByte) {
    Uint8* ptrToHeaderByte0 = *tapBuffer;
    
    // header byte 0: block type: 0=Program, 1=Number array, 2=Character array, 3=Code
    Uint8 blockType = **tapBuffer;
    (*tapBuffer)++;

    // header bytes 1-10: file name, padded with 0x20
    char* filename = (char*)malloc((_HEADER_FILENAME_LENGTH+1) * sizeof(char) + 1);
    if (filename) {
        SDL_memcpy(filename, (char*)(*tapBuffer), _HEADER_FILENAME_LENGTH);
        filename[_HEADER_FILENAME_LENGTH] = 0; // terminate
    }
    (*tapBuffer) += _HEADER_FILENAME_LENGTH;

    // header bytes 11-12: length of data block
    Uint16 dataBlockLength = (**(Uint16**)tapBuffer);
    (*tapBuffer) += 2;

    // header bytes 13-14: parameter1
    Uint16 parameter1 = (**(Uint16**)tapBuffer);
    (*tapBuffer) += 2;

    // header bytes 15-16: parameter2
    Uint16 parameter2 = (**(Uint16**)tapBuffer);
    (*tapBuffer) += 2;

    // byte 0 after header: checksum
    Uint8 checksum = **tapBuffer;
    (*tapBuffer)++;

    // *tapBuffer now points to first byte immediately after block

    // build result
    struct tapHeaderBlock* header = (struct tapHeaderBlock*)malloc(1 * sizeof(struct tapHeaderBlock));
    struct tapBlock* wrapper = (struct tapBlock*)malloc(1 * sizeof(struct tapBlock));
    if (header && wrapper) {
        header->type = (enum tapHeaderType)blockType;
        header->filename = filename;
        header->dataBlockLength = dataBlockLength;
        header->parameter1 = parameter1;
        header->parameter2 = parameter2;

        wrapper->type = Header;
        wrapper->flagByte = flagByte;
        wrapper->headerInfo = header;
        wrapper->tapBlockSize = blockSize + 2;      // plus 2 initial TAP-specific size bytes (unknown to ZX Spectrum)
        wrapper->blockDataSize = blockSize;
        wrapper->blockRawDataSize = blockSize - 2;  // minus 2 ZX Spectrum metadata bytes (block type, checksum)
        wrapper->tapBlockBytes = ptrToHeaderByte0 - 3;  // rewind 3 bytes to point to first size byte
        wrapper->tapBlockOffset = (Uint32)(wrapper->tapBlockBytes - _tapFileBuffer);
        wrapper->tapBlockEndOffset = (Uint32)(wrapper->tapBlockOffset + wrapper->tapBlockSize - 1);
    }
    return wrapper;
}

// reads a data-type block
// tapBuffer is a ptr to a ptr to block's second byte (aka immediately after flag)
struct tapBlock* _taploader_readblock__data(Uint8** tapBuffer, Uint16 blockSize, Uint8 flagByte) {
    Uint8* ptrToDataByte0 = *tapBuffer;

    (*tapBuffer) += blockSize - 1;  // - 1 because we're already on the second byte of the block
    // *tapBuffer now points to first byte immediately after block

    // build result
    struct tapBlock* result = (struct tapBlock*)malloc(1 * sizeof(struct tapBlock));
    if (result) {
        result->blockRawDataSize = blockSize - 2; // minus 2 ZX Spectrum metadata bytes (block type, checksum)
        result->blockDataSize = blockSize;
        result->tapBlockSize = blockSize + 2;     // plus 2 initial size bytes
        result->tapBlockBytes = ptrToDataByte0 - 3;  // rewind 3 bytes to point to first size byte
        result->headerInfo = NULL;
        result->type = Data;
        result->flagByte = flagByte;
        result->tapBlockOffset = (Uint32)(result->tapBlockBytes - _tapFileBuffer);
        result->tapBlockEndOffset = (Uint32)(result->tapBlockOffset + result->tapBlockSize - 1);
    }
    return result;
}

// assumes *tapBuffer points to the start of a block
// moves *tapBuffer to immediately after the block it reads
struct tapBlock* _taploader_readblock(Uint8** tapBuffer) {
    // bytes 0-1: size of header, little endian
    Uint16 blockSize = (**(Uint16**)tapBuffer);
    (*tapBuffer) += 2;

    // byte 2: flag byte, 0=header, 0xFF=data
    Uint8 flagByte = **tapBuffer;
    (*tapBuffer)++;

    struct tapBlock* block = NULL;
    // read block based on its type (header or data)
    if (flagByte == 0 && blockSize == _HEADER_SPECTRUM_BYTE_LENGTH) {
        // flag byte indicates header, and block size is correct for a header
        //     therefore it's a proper header
        block = _taploader_readblock__header(tapBuffer, blockSize, flagByte);

        // the reason for the block size check is that in some cases (such as Lemmings
        // levels), header blocks have flag byte = 0 (indicating a header), but a
        // nonstandard size - making them "improper" header blocks, which we
        // just handle as data
    }
    else {
        block = _taploader_readblock__data(tapBuffer, blockSize, flagByte);
    }

    block->flagByteHumanReadable = _taploader_translate_block_type(block->flagByte);

    return block;
}

void _taploader_enumerate_blocks() {
    log_write("\nEnumerating TAP blocks...");
    log_write("      TAP Length: length of the TAP block, which is larger than ZX Spectrum blocks.");
    log_write("     Data Length: number of bytes loaded into memory");
    log_write("");
    log_write("     TAP Length = Data Length + 2 (TAP header) + 1 (Spectrum flag) + 1 (Spectrum checksum)");
    log_write("");

    _tapBlocks = (struct tapBlock**)malloc(_MAX_TAP_BLOCKS * sizeof(struct tapBlock*));
    if (!_tapBlocks) {
        log_write("Error: unable to allocate memory for TAP block array");
        return;
    }

    log_write("Slot\tType\tFlag\tName\t\tStart\t\tTAP Length\tEnd\t\t\tData Length");
    log_write("----\t----\t----\t----\t\t-----\t\t----------\t---\t\t\t-----------");
    Uint8* cursor = _tapFileBuffer;
    while (_tapBlocksCount < _MAX_TAP_BLOCKS) {
        if ((Sint64)cursor - (Sint64)_tapFileBuffer >= (Sint64)_tapFileSize) {
            break;
        }
        struct tapBlock* block = _taploader_readblock(&cursor);
        _tapBlocks[_tapBlocksCount] = block;
        block->blockIndex = _tapBlocksCount;

        sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, 
            "%u\t\t%s\t0x%02X\t%s\t0x%08X\t0x%04X\t\t0x%08X\t0x%08X",
            _tapBlocksCount,
            block->flagByteHumanReadable,
            block->flagByte,
            block->type == Header ? block->headerInfo->filename : "\t\t",
            block->tapBlockOffset,
            block->tapBlockSize,
            block->tapBlockEndOffset,
            block->blockRawDataSize);
        log_write(_debugBuffer);

        _tapBlocksCount++;
    }

    if (_tapBlocksCount == _MAX_TAP_BLOCKS) {
        sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, 
            "Warning: max block count exceeded in TAP; loading only first %u blocks", _MAX_TAP_BLOCKS);
        log_write(_debugBuffer);
    }
}

Uint8 taploader_load(char* filename) {
    log_write("Starting TAP load");
    log_write(filename);
    struct stat info;
    if (stat(filename, &info) != 0) {
        log_write("Error: TAP file not found");
        return 0;
    }

    _tapFileSize = info.st_size;
    if (_tapFileSize < 3) {
        log_write("Error: TAP file must be at least 3 bytes long");
        return 0;
    }

    _tapFileBuffer = (Uint8*)malloc(_tapFileSize);
    if (_tapFileBuffer == NULL) {
        log_write("Error: unable to allocate memory for TAP file");
        return 0;
    }
    FILE* fp;
    errno_t result = fopen_s(&fp, filename, "rb");
    if (result) {
        log_write("Error: could not open TAP file");
        return 0;
    }
    // try to read a single block of info.st_size bytes
    size_t blocks_read = fread(_tapFileBuffer, _tapFileSize, 1, fp);
    if (blocks_read != 1) {
        log_write("Error: could not read from TAP file");
        return 0;
    }
    fclose(fp);
    
    sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, 
        "Loaded TAP file of %u bytes (0x%08X)", _tapFileSize, _tapFileSize);
    log_write(_debugBuffer);

    _taploader_enumerate_blocks();
    _taploader_seek(0);
    _tapLoadedFile = 1;
    return 1;
}

Uint8 taploader_is_file_loaded() {
    return _tapLoadedFile;
}
