// transfer.c
// data transfer over AMY messages

#include <stdlib.h>
#include <ctype.h>
#include "transfer.h"
#include "amy_midi.h"    // for midi_out, MAX_SYSEX_BYTES
#include "sequencer.h"   // for sequencer_midi_stop/start
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdbool.h>

#ifdef __EMSCRIPTEN__
#include "emscripten.h"
#endif

// per platform file reading / writing
// posix (linux, mac)
// micropython / littlefs - tulip & amybard
// (later) arduino / SPIFFS / SD
// if yours isnt' here, you just write your own amy_external_fopen_hook etc


// mac / linux / generic posix 

#if defined(__unix__) || defined(__APPLE__) || defined(_POSIX_VERSION)

// Map from FILE * to a uint32_t handle to pass to AMY

static FILE *g_files[MAX_OPEN_FILES]; // index 1..MAX_OPEN_FILES-1 used
#ifdef __EMSCRIPTEN__
static uint32_t g_em_handle[MAX_OPEN_FILES];
static uint32_t g_em_pos[MAX_OPEN_FILES];
#endif

#ifndef __EMSCRIPTEN__
static uint32_t alloc_handle(FILE *f) {
    for (uint32_t i = 1; i < MAX_OPEN_FILES; i++) {
        if (g_files[i] == NULL) {
            g_files[i] = f;
            return i;
        }
    }
    return HANDLE_INVALID; // table full
}

static FILE *lookup_handle(uint32_t h) {
    if (h == 0 || h >= MAX_OPEN_FILES) return NULL;
    return g_files[h];
}
#endif
static void free_handle(uint32_t h) {
    if (h == 0 || h >= MAX_OPEN_FILES) return;
    g_files[h] = NULL;
#ifdef __EMSCRIPTEN__
    g_em_handle[h] = 0;
    g_em_pos[h] = 0;
#endif
}


// These should map to 
// uint32_t (*amy_external_fopen_hook)(char * filename, char * mode) = NULL;
// uint32_t (*amy_external_fwrite_hook)(uint32_t fptr, uint8_t * bytes, uint32_t len) = NULL;
// uint32_t (*amy_external_fread_hook)(uint32_t fptr, uint8_t * bytes, uint32_t len) = NULL;
// void (*amy_external_fclose_hook)(uint32_t fptr) = NULL;

uint32_t posix_external_fopen_hook(char * filename, const char *mode) {
#ifdef __EMSCRIPTEN__
    uint32_t js_handle = EM_ASM_INT({
        if (typeof amy_shared_open === 'function') {
            return amy_shared_open(UTF8ToString($0), UTF8ToString($1));
        }
        return 0;
    }, filename, mode);
    if (js_handle == 0) {
        return HANDLE_INVALID;
    }
    for (uint32_t i = 1; i < MAX_OPEN_FILES; i++) {
        if (g_em_handle[i] == 0) {
            g_em_handle[i] = js_handle;
            g_em_pos[i] = 0;
            return i;
        }
    }
    EM_ASM({
        if (typeof amy_shared_close === 'function') {
            amy_shared_close($0);
        }
    }, js_handle);
    return HANDLE_INVALID;
#else
    FILE *f = fopen(filename, mode);
    if (!f) {
        return HANDLE_INVALID;
    }
    uint32_t h = alloc_handle(f);
    if (h == HANDLE_INVALID) {
        fclose(f);
        return HANDLE_INVALID;
    }
    return h;
#endif
}

uint32_t posix_external_fread_hook(uint32_t h, uint8_t *buf, uint32_t len) {
#ifdef __EMSCRIPTEN__
    if (h == 0 || h >= MAX_OPEN_FILES) {
        return 0;
    }
    uint32_t js_handle = g_em_handle[h];
    if (js_handle == 0) {
        return 0;
    }
    uint32_t pos = g_em_pos[h];
    uint32_t r = EM_ASM_INT({
        if (typeof amy_shared_read === 'function') {
            return amy_shared_read($0, $1, $2, $3);
        }
        return 0;
    }, js_handle, pos, buf, len);
    g_em_pos[h] += r;
    return r;
#else
    FILE *f = lookup_handle(h);
    if (!f) {
        return 0;
    }
    uint32_t r = fread(buf, 1, len, f);
    return r;
#endif
}
void posix_external_fseek_hook(uint32_t h, uint32_t pos) {
#ifdef __EMSCRIPTEN__
    if (h == 0 || h >= MAX_OPEN_FILES) {
        return;
    }
    g_em_pos[h] = pos;
    return;
#else
    FILE *f = lookup_handle(h);
    if (!f) {
        return;
    }
    fseek(f, pos, SEEK_SET);
#endif
}

uint32_t posix_external_fwrite_hook(uint32_t h, uint8_t *buf, uint32_t n) {
#ifdef __EMSCRIPTEN__
    if (h == 0 || h >= MAX_OPEN_FILES || g_em_handle[h] == 0) {
        return 0;
    }
    uint32_t written = EM_ASM_INT({
        if (typeof amy_shared_write === 'function') {
            return amy_shared_write($0, $1, $2);
        }
        return 0;
    }, g_em_handle[h], buf, n);
    g_em_pos[h] += written;
    return written;
#else
    FILE *f = lookup_handle(h);
    if (!f) {
        return 0;
    }
    uint32_t w = fwrite(buf, 1, n, f);
    return w;
#endif
}

void posix_external_fclose_hook(uint32_t h) {
#ifdef __EMSCRIPTEN__
    if (h == 0 || h >= MAX_OPEN_FILES) {
        return;
    }
    uint32_t js_handle = g_em_handle[h];
    if (js_handle != 0) {
        EM_ASM({
            if (typeof amy_shared_close === 'function') {
                amy_shared_close($0);
            }
        }, js_handle);
        free_handle(h);
    }
#else
    FILE *f = lookup_handle(h);
    if (f) {
        fclose(f);
        free_handle(h);
    }
#endif
}

#endif



// We keep one decbuf around and reuse it during transfer
b64_buffer_t decbuf;

// signals to AMY that i'm now receiving a transfer of length (bytes!) into allocated storage
void start_receiving_transfer(uint32_t length, uint8_t * storage) {
    amy_global.transfer_flag = AMY_TRANSFER_TYPE_AUDIO;
    amy_global.transfer_storage = storage;
    amy_global.transfer_length_bytes = length;
    amy_global.transfer_stored_bytes = 0;
    amy_global.transfer_file_handle = 0;
    amy_global.transfer_filename[0] = '\0';
    b64_buf_malloc(&decbuf);
}

void start_receiving_sample(uint32_t frames, uint8_t bus, int16_t *storage) {
    amy_global.transfer_flag = AMY_TRANSFER_TYPE_SAMPLE;
    amy_global.transfer_storage = (uint8_t *)storage;
    amy_global.transfer_length_bytes = frames*sizeof(int16_t)*AMY_NCHANS;
    amy_global.transfer_stored_bytes = 0;
    amy_global.transfer_file_handle = bus; // use file handle to store bus number
    amy_global.transfer_filename[0] = '\0';
}

void stop_receiving_sample() {
    amy_global.transfer_file_handle = 0;
    amy_global.transfer_flag = AMY_TRANSFER_TYPE_NONE;
    amy_global.transfer_stored_bytes = 0;
    amy_global.transfer_length_bytes = 0;
}

// signals to AMY that i'm now receiving a file transfer of length (bytes!) to filename
void start_receiving_file_transfer(uint32_t length, const char *filename) {

    if (filename == NULL || filename[0] == '\0') {
        return;
    }
    if (amy_global.config.amy_external_fopen_hook == NULL || amy_global.config.amy_external_fwrite_hook == NULL || amy_global.config.amy_external_fclose_hook == NULL) {

        fprintf(stderr, "file transfer hooks not enabled on platform\n");
        return;
    }
        uint32_t handle = amy_global.config.amy_external_fopen_hook((char *)filename, "wb");
    if (handle == HANDLE_INVALID) {
        fprintf(stderr, "could not open file for transfer: %s\n", filename);
        return;
    }
    amy_global.transfer_flag = AMY_TRANSFER_TYPE_FILE;
    amy_global.transfer_storage = NULL;
    amy_global.transfer_length_bytes = length;
    amy_global.transfer_stored_bytes = 0;
    amy_global.transfer_file_handle = handle;
    strncpy(amy_global.transfer_filename, filename, sizeof(amy_global.transfer_filename) - 1);
    amy_global.transfer_filename[sizeof(amy_global.transfer_filename) - 1] = '\0';
    b64_buf_malloc(&decbuf);
}

// takes a wire message and adds it to storage after decoding it. stops transfer when it's done
void parse_transfer_message(char * message, uint16_t len) {
    size_t decoded = 0;
    uint8_t * block = b64_decode_ex (message, len, &decbuf, &decoded);
    if (amy_global.transfer_flag == AMY_TRANSFER_TYPE_FILE) {
        if (amy_global.config.amy_external_fwrite_hook != NULL && amy_global.transfer_file_handle != HANDLE_INVALID) {
            uint32_t wrote = amy_global.config.amy_external_fwrite_hook(amy_global.transfer_file_handle, block, (uint32_t)decoded);
            amy_global.transfer_stored_bytes += wrote;
        }
    } else {
        for (uint16_t i = 0; i < decoded; i++) {
            amy_global.transfer_storage[amy_global.transfer_stored_bytes++] = block[i];
        }
    }
    if (amy_global.transfer_stored_bytes >= amy_global.transfer_length_bytes) { // we're done
        if (amy_global.transfer_flag == AMY_TRANSFER_TYPE_FILE) {
            if (amy_global.config.amy_external_fclose_hook != NULL && amy_global.transfer_file_handle != HANDLE_INVALID) {
                amy_global.config.amy_external_fclose_hook(amy_global.transfer_file_handle);
            }
            // Pair the sequencer_midi_stop() that zT's start handler fired in
            // amy_parse_transfer_layer_message(). Do it BEFORE the done hook so
            // the hook can observe a running sequencer if it needs to, and so
            // any platform that uses zT without a platform-specific restart in
            // its file_transfer_done_hook doesn't leave playback wedged.
            sequencer_midi_start();
            if (amy_global.config.amy_external_file_transfer_done_hook != NULL) {
                amy_global.config.amy_external_file_transfer_done_hook(amy_global.transfer_filename);
            }
            amy_global.transfer_file_handle = 0;
            amy_global.transfer_filename[0] = '\0';
        }
        amy_global.transfer_flag = AMY_TRANSFER_TYPE_NONE;
        free(decbuf.ptr);
    }
}

int b64_buf_malloc(b64_buffer_t * buf)
{
    buf->ptr = malloc(B64_BUFFER_SIZE);
    if (!buf->ptr) return -1;

    buf->bufc = 1;

    return 0;
}

// We don't do encoding in AMY, we rely on python for that, but we'll leave it here if you want it in C
// Just like decode you pass it an allocated encbuf. 
char * b64_encode (const unsigned char *src, b64_buffer_t * encbuf, size_t len) {
    int i = 0;
    int j = 0;
    size_t size = 0;
    unsigned char buf[4];
    unsigned char tmp[3];


    // parse until end of source
    while (len--) {
        // read up to 3 bytes at a time into `tmp'
        tmp[i++] = *(src++);

        // if 3 bytes read then encode into `buf'
        if (3 == i) {
            buf[0] = (tmp[0] & 0xfc) >> 2;
            buf[1] = ((tmp[0] & 0x03) << 4) + ((tmp[1] & 0xf0) >> 4);
            buf[2] = ((tmp[1] & 0x0f) << 2) + ((tmp[2] & 0xc0) >> 6);
            buf[3] = tmp[2] & 0x3f;

            // allocate 4 new byts for `enc` and
            // then translate each encoded buffer
            // part by index from the base 64 index table
            // into `encbuf.ptr' unsigned char array

            for (i = 0; i < 4; ++i) {
                encbuf->ptr[size++] = b64_table[buf[i]];
            }

            // reset index
            i = 0;
        }
    }

    // remainder
    if (i > 0) {
        // fill `tmp' with `\0' at most 3 times
        for (j = i; j < 3; ++j) {
            tmp[j] = '\0';
        }

        // perform same codec as above
        buf[0] = (tmp[0] & 0xfc) >> 2;
        buf[1] = ((tmp[0] & 0x03) << 4) + ((tmp[1] & 0xf0) >> 4);
        buf[2] = ((tmp[1] & 0x0f) << 2) + ((tmp[2] & 0xc0) >> 6);
        buf[3] = tmp[2] & 0x3f;

        // perform same write to `encbuf->ptr` with new allocation
        for (j = 0; (j < i + 1); ++j) {
            encbuf->ptr[size++] = b64_table[buf[j]];
        }

        // while there is still a remainder
        // append `=' to `encbuf.ptr'
        while ((i++ < 3)) {
            encbuf->ptr[size++] = '=';
        }
    }

    // Make sure we have enough space to add '\0' character at end.
    encbuf->ptr[size] = '\0';

    return encbuf->ptr;
}



unsigned char *
b64_decode_ex (const char *src, size_t len, b64_buffer_t * decbuf, size_t *decsize) {
    int i = 0;
    int j = 0;
    int l = 0;
    size_t size = 0;
    unsigned char buf[3];
    unsigned char tmp[4];

    // alloc
    //if (b64_buf_malloc(&decbuf) == -1) { return NULL; }

    // parse until end of source
    while (len--) {
        // break if char is `=' or not base64 char
        if ('=' == src[j]) { break; }
        if (!(isalnum((unsigned char)src[j]) || '+' == src[j] || '/' == src[j])) { break; }

        // read up to 4 bytes at a time into `tmp'
        tmp[i++] = src[j++];

        // if 4 bytes read then decode into `buf'
        if (4 == i) {
            // translate values in `tmp' from table
            for (i = 0; i < 4; ++i) {
                // find translation char in `b64_table'
                for (l = 0; l < 64; ++l) {
                    if (tmp[i] == b64_table[l]) {
                        tmp[i] = l;
                        break;
                    }
                }
            }

            // decode
            buf[0] = (tmp[0] << 2) + ((tmp[1] & 0x30) >> 4);
            buf[1] = ((tmp[1] & 0xf) << 4) + ((tmp[2] & 0x3c) >> 2);
            buf[2] = ((tmp[2] & 0x3) << 6) + tmp[3];

            // write decoded buffer to `decbuf.ptr'
            //if (b64_buf_realloc(&decbuf, size + 3) == -1)  return NULL;
            for (i = 0; i < 3; ++i) {
                ((unsigned char*)decbuf->ptr)[size++] = buf[i];
            }

            // reset
            i = 0;
        }
    }

    // remainder
    if (i > 0) {
        // fill `tmp' with `\0' at most 4 times
        for (j = i; j < 4; ++j) {
            tmp[j] = '\0';
        }

        // translate remainder
        for (j = 0; j < 4; ++j) {
            // find translation char in `b64_table'
            for (l = 0; l < 64; ++l) {
                if (tmp[j] == b64_table[l]) {
                    tmp[j] = l;
                    break;
                }
            }
        }

        // decode remainder
        buf[0] = (tmp[0] << 2) + ((tmp[1] & 0x30) >> 4);
        buf[1] = ((tmp[1] & 0xf) << 4) + ((tmp[2] & 0x3c) >> 2);
        buf[2] = ((tmp[2] & 0x3) << 6) + tmp[3];

        // write remainer decoded buffer to `decbuf.ptr'
        //if (b64_buf_realloc(&decbuf, size + (i - 1)) == -1)  return NULL;
        for (j = 0; (j < i - 1); ++j) {
            ((unsigned char*)decbuf->ptr)[size++] = buf[j];
        }
    }

    // Make sure we have enough space to add '\0' character at end.
    //if (b64_buf_realloc(&decbuf, size + 1) == -1)  return NULL;
    ((unsigned char*)decbuf->ptr)[size] = '\0';

    // Return back the size of decoded string if demanded.
    if (decsize != NULL) {
        *decsize = size;
    }

    return (unsigned char*) decbuf->ptr;
}


// ── State dump helpers ──────────────────────────────────────────────────────

// Walk all active instruments and emit one wire-command line per callback.
static void amy_emit_state_lines(void (*cb)(const char *line, int len, void *ctx), void *ctx) {
    char buf[1024];
    char line[1100];
    bool include_fx = true;
    for (int inst = 0; inst < instruments_max_instruments(); inst++) {
        if (instrument_number_exists(inst, NULL)) {
            uint16_t voices[MAX_VOICES_PER_INSTRUMENT];
            int num_voices = instrument_get_num_voices(inst, voices);
            if (num_voices < 1) continue;  // should never happen.
            int oscs_per_voice = instrument_get_oscs_per_voice(inst);
            int n = snprintf(line, sizeof(line), "i%dic255Z", inst);
            cb(line, n, ctx);
            n = snprintf(line, sizeof(line), "i%div%din%dZ", inst, num_voices, oscs_per_voice);
            cb(line, n, ctx);
            void *state = NULL;
            do {
                state = yield_synth_commands((uint8_t)inst, buf, sizeof(buf), include_fx, state);
                if (buf[0] == '\0') continue;
                n = snprintf(line, sizeof(line), "i%d%s", inst, buf);  // Prepends to FX as well, that's OK.
                cb(line, n, ctx);
            } while (state);
            // We include FX only in the first osc.
            include_fx = false;
        }
    }
}

typedef struct { char *buf; int pos; int cap; } _string_ctx;
static void _dump_string_cb(const char *line, int len, void *ctx) {
    _string_ctx *c = (_string_ctx *)ctx;
    if (c->pos + len + 1 > c->cap) return;
    memcpy(c->buf + c->pos, line, len);
    c->pos += len;
    c->buf[c->pos++] = '\n';
}

char *amy_dump_state_to_string(int *out_len) {
    int cap = 16384;
    char *buf = (char *)malloc_caps(cap, amy_global.config.ram_caps_sysex);
    if (!buf) { *out_len = 0; return NULL; }
    _string_ctx ctx = { buf, 0, cap };
    amy_emit_state_lines(_dump_string_cb, &ctx);
    buf[ctx.pos] = '\0';
    *out_len = ctx.pos;
    return buf;
}

// Max base64 chars per sysex chunk. Empirically Chrome's Web MIDI has trouble
// delivering sysex messages above a few KB reliably, so we split large dumps
// into multiple smaller sysex frames. 1024 base64 chars → ~1030-byte sysex
// frame including header/footer, well within the "reliably delivered" band.
#define ZDUMP_CHUNK_B64_MAX 1024

// Raw bytes per streamed chunk. 768 = 3 * 256 → exactly 1024 base64 chars with
// no padding, so non-final chunks can be concatenated on the receiver and
// decoded as one base64 string. Only the final chunk may emit '=' padding
// (which is fine: it's appended last).
#define ZDUMP_STREAM_RAW_CHUNK 768

// Streaming sysex dump state. Lets amy_dump_state_to_sysex and
// amy_dump_file_to_sysex produce arbitrarily-large dumps without holding the
// whole payload in RAM. Each ZDUMP_STREAM_RAW_CHUNK bytes of raw data is
// base64-encoded and emitted as one sysex frame (marker 'C' if more follow,
// 'E' or '0' on the last frame). The effective upper bound is now the
// web-side reassembler's SYSEX_REASM_MAX rather than MAX_SYSEX_BYTES.
typedef struct {
    uint8_t *raw_buf;     // ZDUMP_STREAM_RAW_CHUNK-byte raw accumulator
    int raw_pos;          // current fill of raw_buf
    uint8_t *enc;         // base64 scratch: b64(raw_buf)
    uint8_t *frame;       // sysex frame scratch: F0 00 03 45 <marker> <b64...> F7
    int frames_sent;
    int bytes_sent;       // raw bytes drained so far, for logging
    bool ok;
} _zdump_stream;

static int _zdump_stream_init(_zdump_stream *c) {
    c->raw_buf = NULL;
    c->enc = NULL;
    c->frame = NULL;
    c->raw_pos = 0;
    c->frames_sent = 0;
    c->bytes_sent = 0;
    c->ok = false;
    c->raw_buf = (uint8_t *)malloc_caps(ZDUMP_STREAM_RAW_CHUNK, amy_global.config.ram_caps_sysex);
    c->enc     = (uint8_t *)malloc_caps(ZDUMP_CHUNK_B64_MAX + 4, amy_global.config.ram_caps_sysex);
    c->frame   = (uint8_t *)malloc_caps(ZDUMP_CHUNK_B64_MAX + 6, amy_global.config.ram_caps_sysex);
    if (!c->raw_buf || !c->enc || !c->frame) {
        fprintf(stderr, "zD: stream init malloc failed (raw=%p enc=%p frame=%p)\n",
                (void *)c->raw_buf, (void *)c->enc, (void *)c->frame);
        return -1;
    }
    c->ok = true;
    return 0;
}

static void _zdump_stream_destroy(_zdump_stream *c) {
    if (c->raw_buf) { free(c->raw_buf); c->raw_buf = NULL; }
    if (c->enc)     { free(c->enc);     c->enc = NULL; }
    if (c->frame)   { free(c->frame);   c->frame = NULL; }
    c->ok = false;
}

// Emit one sysex frame carrying b64(raw_buf[0..raw_pos]). If is_last is false
// the marker is 'C' (more frames follow); otherwise it's 'E' when we've
// already sent at least one frame, or '0' for a single-frame dump.
static void _zdump_stream_drain(_zdump_stream *c, bool is_last) {
    if (!c->ok) return;
    uint8_t marker;
    if (is_last) {
        marker = (c->frames_sent == 0) ? '0' : 'E';
    } else {
        marker = 'C';
    }
    b64_buffer_t bb = { (char *)c->enc, 1 };
    b64_encode(c->raw_buf, &bb, (size_t)c->raw_pos);
    int enc_len = (int)strlen((char *)c->enc);
    c->frame[0] = 0xF0;
    c->frame[1] = 0x00;
    c->frame[2] = 0x03;
    c->frame[3] = 0x45;
    c->frame[4] = marker;
    memcpy(c->frame + 5, c->enc, enc_len);
    c->frame[5 + enc_len] = 0xF7;
    midi_out(c->frame, 6 + enc_len);
    c->bytes_sent += c->raw_pos;
    c->frames_sent++;
    c->raw_pos = 0;
}

// Append `len` bytes from `data` to the raw accumulator, draining as 'C'
// frames whenever the accumulator is full and more data arrives. The last
// (possibly partial) chunk is held until _zdump_stream_finish is called.
static void _zdump_stream_write(_zdump_stream *c, const uint8_t *data, int len) {
    if (!c->ok) return;
    while (len > 0) {
        if (c->raw_pos == ZDUMP_STREAM_RAW_CHUNK) {
            _zdump_stream_drain(c, false);
        }
        int space = ZDUMP_STREAM_RAW_CHUNK - c->raw_pos;
        int take = len < space ? len : space;
        memcpy(c->raw_buf + c->raw_pos, data, take);
        c->raw_pos += take;
        data += take;
        len -= take;
    }
}

// Emit any buffered data as the final frame. Always emits at least one frame
// so the receiver sees a well-formed '0' (empty-payload) dump response for
// empty inputs.
static void _zdump_stream_finish(_zdump_stream *c) {
    if (!c->ok) return;
    _zdump_stream_drain(c, true);
}

// amy_emit_state_lines callback: forward each wire-command line (plus '\n')
// into the stream.
static void _zdump_state_line_cb(const char *line, int len, void *ctx) {
    _zdump_stream *c = (_zdump_stream *)ctx;
    _zdump_stream_write(c, (const uint8_t *)line, len);
    uint8_t nl = '\n';
    _zdump_stream_write(c, &nl, 1);
}

void amy_dump_state_to_sysex(void) {
    if (!amy_global.config.midi) return;
    sequencer_midi_stop();
    _zdump_stream stream;
    if (_zdump_stream_init(&stream) != 0) {
        sequencer_midi_start();
        return;
    }
    amy_emit_state_lines(_zdump_state_line_cb, &stream);
    _zdump_stream_finish(&stream);
    _zdump_stream_destroy(&stream);
    sequencer_midi_start();
}

void amy_dump_file_to_sysex(const char *filename) {
    // Only meaningful on platforms with MIDI sysex output (hardware, emscripten).
    // On desktop, midi_out can overflow its stack buffer with large sysex.
    if (!amy_global.config.midi) return;
    sequencer_midi_stop();
    if (!amy_global.config.amy_external_fopen_hook ||
        !amy_global.config.amy_external_fread_hook  ||
        !amy_global.config.amy_external_fclose_hook) {
        fprintf(stderr, "zD: file I/O hooks unavailable\n");
        sequencer_midi_start();
        return;
    }
    uint32_t fh = amy_global.config.amy_external_fopen_hook((char *)filename, "r");
    if (!fh) {
        fprintf(stderr, "zD: could not open '%s'\n", filename);
        sequencer_midi_start();
        return;
    }
    _zdump_stream stream;
    if (_zdump_stream_init(&stream) != 0) {
        amy_global.config.amy_external_fclose_hook(fh);
        sequencer_midi_start();
        return;
    }
    uint8_t *read_buf = (uint8_t *)malloc_caps(ZDUMP_STREAM_RAW_CHUNK, amy_global.config.ram_caps_sysex);
    if (!read_buf) {
        fprintf(stderr, "zD: malloc read_buf(%d) FAILED\n", ZDUMP_STREAM_RAW_CHUNK);
        _zdump_stream_destroy(&stream);
        amy_global.config.amy_external_fclose_hook(fh);
        sequencer_midi_start();
        return;
    }
    while (1) {
        uint32_t n = amy_global.config.amy_external_fread_hook(fh, read_buf, ZDUMP_STREAM_RAW_CHUNK);
        if (n == 0) break;
        _zdump_stream_write(&stream, read_buf, (int)n);
    }
    free(read_buf);
    _zdump_stream_finish(&stream);
    _zdump_stream_destroy(&stream);
    amy_global.config.amy_external_fclose_hook(fh);
    sequencer_midi_start();
}

// ── End state dump helpers ──────────────────────────────────────────────────

void transfer_init() {
    #ifdef __EMSCRIPTEN__

    #endif

 #if defined(_POSIX_VERSION) 
    amy_global.config.amy_external_fopen_hook = posix_external_fopen_hook;
    amy_global.config.amy_external_fread_hook = posix_external_fread_hook;
    amy_global.config.amy_external_fwrite_hook = posix_external_fwrite_hook;
    amy_global.config.amy_external_fclose_hook = posix_external_fclose_hook;
    amy_global.config.amy_external_fseek_hook = posix_external_fseek_hook;
#endif
}

static uint16_t read_u16_le(const uint8_t *buf) {
    return (uint16_t)buf[0] | ((uint16_t)buf[1] << 8);
}

static uint32_t read_u32_le(const uint8_t *buf) {
    return (uint32_t)buf[0] | ((uint32_t)buf[1] << 8) |
                  ((uint32_t)buf[2] << 16) | ((uint32_t)buf[3] << 24);
}

static int read_exact(uint32_t handle, uint8_t *buf, uint32_t len) {
    if (amy_global.config.amy_external_fread_hook == NULL) {
        return 0;
    }
    uint32_t offset = 0;
    while (offset < len) {
        uint32_t got = amy_global.config.amy_external_fread_hook(handle, buf + offset, len - offset);
        if (got == 0) {
            return 0;
        }
        offset += got;
    }
    return 1;
}

static int skip_bytes(uint32_t handle, uint32_t len) {
    uint8_t scratch[64];
    while (len > 0) {
        uint32_t chunk = len > sizeof(scratch) ? sizeof(scratch) : len;
        if (!read_exact(handle, scratch, chunk)) {
            return 0;
        }
        len -= chunk;
    }
    return 1;
}

int wave_parse_header(uint32_t handle, wave_info_t *info, uint32_t *data_bytes) {
    if (info == NULL || data_bytes == NULL) {
        return 0;
    }
    uint8_t riff_header[12];
    if (!read_exact(handle, riff_header, sizeof(riff_header))) {
        return 0;
    }
    if (memcmp(riff_header, "RIFF", 4) != 0 || memcmp(riff_header + 8, "WAVE", 4) != 0) {
        return 0;
    }
    uint8_t fmt_found = 0;
    wave_info_t tmp_info = {0};
    while (1) {
        uint8_t chunk_header[8];
        if (!read_exact(handle, chunk_header, sizeof(chunk_header))) {
            return 0;
        }
        uint32_t chunk_size = read_u32_le(chunk_header + 4);
        if (memcmp(chunk_header, "fmt ", 4) == 0) {
            if (chunk_size < 16) {
                return 0;
            }
            uint8_t fmt_chunk[16];
            if (!read_exact(handle, fmt_chunk, sizeof(fmt_chunk))) {
                return 0;
            }
            uint16_t audio_format = read_u16_le(fmt_chunk);
            tmp_info.channels = read_u16_le(fmt_chunk + 2);
            tmp_info.sample_rate = read_u32_le(fmt_chunk + 4);
            if (audio_format != 1 || tmp_info.channels == 0 || tmp_info.channels > 2) {
                return 0;
            }
            if (!skip_bytes(handle, chunk_size - sizeof(fmt_chunk))) {
                return 0;
            }
            if (chunk_size & 1) {
                if (!skip_bytes(handle, 1)) {
                    return 0;
                }
            }
            fmt_found = 1;
            continue;
        }
        if (memcmp(chunk_header, "data", 4) == 0) {
            if (!fmt_found) {
                return 0;
            }
            *info = tmp_info;
            *data_bytes = chunk_size;
            return 1;
        }
        if (!skip_bytes(handle, chunk_size)) {
            return 0;
        }
        if (chunk_size & 1) {
            if (!skip_bytes(handle, 1)) {
                return 0;
            }
        }
    }
    return 0;
}
uint32_t wave_read_pcm_frames_s16(uint32_t handle, uint16_t channels, 
        uint32_t *bytes_remaining, int16_t *dest, uint32_t max_frames) {
    if (dest == NULL || bytes_remaining == 0 || channels == 0 || channels > 2) {
        return 0;
    }
    uint32_t bytes_per_frame = channels * 2;
    if (*bytes_remaining < bytes_per_frame) {
        return 0;
    }
    uint32_t max_bytes = max_frames * bytes_per_frame;
    if (max_bytes > *bytes_remaining) {
        max_bytes = *bytes_remaining - (*bytes_remaining % bytes_per_frame);
    }
    uint32_t frames_to_read = max_bytes / bytes_per_frame;
    if (frames_to_read == 0) {
        return 0;
    }
    uint8_t raw_buf[(AMY_BLOCK_SIZE * PCM_FILE_BUFFER_MULT + 1) * 4];
    if (max_bytes > sizeof(raw_buf)) {
        max_bytes = sizeof(raw_buf) - (sizeof(raw_buf) % bytes_per_frame);
        frames_to_read = max_bytes / bytes_per_frame;
    }
    if (amy_global.config.amy_external_fread_hook == NULL) {
        return 0;
    }
    uint32_t got = amy_global.config.amy_external_fread_hook(handle, raw_buf, max_bytes);
    got -= got % bytes_per_frame;
    if (got == 0) {
        return 0;
    }
    frames_to_read = got / bytes_per_frame;
    *bytes_remaining -= got;
    if (channels == 1) {
        for (uint32_t i = 0; i < frames_to_read; i++) {
            dest[i] = (int16_t)read_u16_le(raw_buf + i * 2);
        }
    } else {
        for (uint32_t i = 0; i < frames_to_read; i++) {
            dest[i * 2] = (int16_t)read_u16_le(raw_buf + i * 4);
            dest[i * 2 + 1] = (int16_t)read_u16_le(raw_buf + i * 4 + 2);
        }
    }
    return frames_to_read;
}
