// 1B Music — decodificador nativo con FFmpeg (libavformat + libavcodec + libswresample, LGPL 2.1+).
// Abre un fichero (descriptor, con lectura posicional) o un flujo secuencial (callback a Java), elige la pista de audio,
// decodifica y entrega PCM intercalado: s16, s32 o float (según el códec), a la frecuencia del códec salvo DSD
// (→ ≤ 96 kHz) y > 192 kHz. Búsqueda exacta a la muestra: av_seek_frame hacia atrás + descartar hasta el objetivo.
// Recorte gapless (pre-skip/relleno de Opus/Vorbis por granule, etc.): lo aplica libavcodec (AV_PKT_DATA_SKIP_SAMPLES).
// Uso desde Kotlin: com.onebytesystems.music.android.playback.ffmpeg.NativeAudio. Un contexto = un hilo a la vez.

#include <jni.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <android/log.h>

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/channel_layout.h>
#include <libavutil/opt.h>
#include <libswresample/swresample.h>

#define TAG "OneBAudio"
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)

#define IO_BUF 65536
#define MAX_BAD_PACKETS 200

enum { OUT_S16 = 0, OUT_S32 = 1, OUT_FLT = 2 };

typedef struct {
    // E/S
    int fd;              // modo fichero (≥ 0) o −1 (flujo)
    int64_t fileSize;
    int64_t pos;         // posición lógica de lectura (modo fichero)
    int64_t consumed;    // bytes leídos del origen (para el ritmo de carga en Kotlin)
    JNIEnv *env;         // modo flujo: válido solo durante la llamada nativa en curso
    jobject reader;      // NativeAudio.StreamReader (referencia global)
    jmethodID readMid;
    jbyteArray jbuf;     // búfer Java para el callback (global)
    int javaError;       // hubo excepción Java en el callback
    AVIOContext *avio;
    AVFormatContext *fmt;
    // decodificación
    int si;
    AVStream *st;
    AVCodecContext *dec;
    AVPacket *pkt;
    AVFrame *frame;
    SwrContext *swr;
    int swrInFmt, swrInRate;
    AVChannelLayout swrInLayout;
    int outFmt, outRate, outCh, outBps;   // bps = bytes por frame (todas las muestras de un instante)
    AVChannelLayout outLayout;
    int64_t base;        // start_time del flujo (unidades del flujo) que corresponde a la muestra 0
    // PCM pendiente de entregar
    uint8_t *buf;
    int bufCap, bufLen, bufPos;
    int64_t bufStart;    // posición (frames de salida) de buf[0]; −1 desconocida
    int64_t nextPos;     // posición tras el búfer actual
    int64_t inPos;       // posición (frecuencia de entrada) del siguiente frame por continuidad; −1 desconocida
    int slowSeek;        // búsqueda decodificando desde el principio (Musepack: el índice de FFmpeg está roto)
    int64_t seqIn;       // muestras de entrada desde la apertura sin búsquedas (calibración); −1 tras buscar
    int64_t ptsOffset;   // posición real − posición según la marca del frame (retardo del decodificador, p. ej. WMA)
    int calibrated;
    int64_t lastPts;     // marca del frame anterior (varios frames de un mismo paquete la repiten: APE, WMA)
    int64_t prerollUs;   // margen antes del objetivo al buscar (códecs con pérdida: solapamiento de tramas)
    int64_t seekTarget;  // tras búsqueda: frame de salida objetivo (−1 = nada que descartar)
    int demuxEof, decoderEof, swrFlushed, badPackets, drained;
    char err[256];
} Ctx;

// ---------------------------------------------------------------------------------------------- E/S

static int file_read(void *opaque, uint8_t *b, int size) {
    Ctx *c = opaque;
    if (c->pos >= c->fileSize) return AVERROR_EOF;
    ssize_t n;
    do { n = pread(c->fd, b, size, c->pos); } while (n < 0 && errno == EINTR);
    if (n < 0) return AVERROR(errno);
    if (n == 0) return AVERROR_EOF;
    c->pos += n;
    c->consumed += n;
    return (int) n;
}

static int64_t file_seek(void *opaque, int64_t offset, int whence) {
    Ctx *c = opaque;
    switch (whence & ~AVSEEK_FORCE) {
        case AVSEEK_SIZE: return c->fileSize;
        case SEEK_SET: c->pos = offset; break;
        case SEEK_CUR: c->pos += offset; break;
        case SEEK_END: c->pos = c->fileSize + offset; break;
        default: return AVERROR(EINVAL);
    }
    return c->pos;
}

static int stream_read(void *opaque, uint8_t *b, int size) {
    Ctx *c = opaque;
    JNIEnv *env = c->env;
    if (!env || c->javaError) return AVERROR(EIO);
    jsize cap = (*env)->GetArrayLength(env, c->jbuf);
    if (size > cap) size = cap;
    jint n = (*env)->CallIntMethod(env, c->reader, c->readMid, c->jbuf, 0, size);
    if ((*env)->ExceptionCheck(env)) { c->javaError = 1; return AVERROR(EIO); }
    if (n < 0) return AVERROR_EOF;
    if (n == 0) return AVERROR(EAGAIN);
    (*env)->GetByteArrayRegion(env, c->jbuf, 0, n, (jbyte *) b);
    c->consumed += n;
    return n;
}

// ---------------------------------------------------------------------------------------------- utilidades

static void set_err(Ctx *c, const char *what, int e) {
    char s[128];
    av_strerror(e, s, sizeof s);
    snprintf(c->err, sizeof c->err, "%s: %s", what, s);
}

static int is_dsd(enum AVCodecID id) {
    return id == AV_CODEC_ID_DSD_LSBF || id == AV_CODEC_ID_DSD_MSBF || id == AV_CODEC_ID_DSD_LSBF_PLANAR ||
           id == AV_CODEC_ID_DSD_MSBF_PLANAR || id == AV_CODEC_ID_DST;
}

static int pick_out_fmt(enum AVSampleFormat f) {
    switch (av_get_packed_sample_fmt(f)) {
        case AV_SAMPLE_FMT_U8:
        case AV_SAMPLE_FMT_S16: return OUT_S16;
        case AV_SAMPLE_FMT_S32:
        case AV_SAMPLE_FMT_S64: return OUT_S32;
        default: return OUT_FLT;
    }
}

static enum AVSampleFormat av_fmt_of(int o) {
    return o == OUT_S16 ? AV_SAMPLE_FMT_S16 : o == OUT_S32 ? AV_SAMPLE_FMT_S32 : AV_SAMPLE_FMT_FLT;
}

/** (Re)configura el conversor para la entrada del frame actual (la salida se fija con el primer frame). */
static int setup_swr(Ctx *c, AVFrame *f) {
    if (c->swr && c->swrInFmt == f->format && c->swrInRate == f->sample_rate &&
        !av_channel_layout_compare(&c->swrInLayout, &f->ch_layout))
        return 0;
    swr_free(&c->swr);
    AVChannelLayout in = {0};
    if (f->ch_layout.order == AV_CHANNEL_ORDER_UNSPEC || !av_channel_layout_check(&f->ch_layout))
        av_channel_layout_default(&in, f->ch_layout.nb_channels);
    else
        av_channel_layout_copy(&in, &f->ch_layout);
    int r = swr_alloc_set_opts2(&c->swr, &c->outLayout, av_fmt_of(c->outFmt), c->outRate,
                                &in, f->format, f->sample_rate, 0, NULL);
    av_channel_layout_uninit(&in);
    if (r < 0) { set_err(c, "swr_alloc", r); return r; }
    if (c->outRate != f->sample_rate) {
        av_opt_set_int(c->swr, "filter_size", 32, 0);
        av_opt_set_int(c->swr, "phase_shift", 10, 0);
    }
    if ((r = swr_init(c->swr)) < 0) { set_err(c, "swr_init", r); return r; }
    c->swrInFmt = f->format;
    c->swrInRate = f->sample_rate;
    av_channel_layout_uninit(&c->swrInLayout);
    av_channel_layout_copy(&c->swrInLayout, &f->ch_layout);
    return 0;
}

static int ensure_buf(Ctx *c, int frames) {
    int need = frames * c->outBps;
    if (need <= c->bufCap) return 0;
    uint8_t *nb = av_realloc(c->buf, need);
    if (!nb) return AVERROR(ENOMEM);
    c->buf = nb;
    c->bufCap = need;
    return 0;
}

/** Quita [n] muestras del principio de [f] (sin copiar: mueve los punteros). */
static void trim_front(AVFrame *f, int n) {
    int bps = av_get_bytes_per_sample(f->format);
    int ch = f->ch_layout.nb_channels;
    if (av_sample_fmt_is_planar(f->format)) for (int i = 0; i < ch; i++) f->extended_data[i] += (size_t) n * bps;
    else f->extended_data[0] += (size_t) n * bps * ch;
    f->nb_samples -= n;
}

/**
 * Convierte [f] (o vacía el conversor si f == NULL) al búfer pendiente. Tras una búsqueda, recorta el frame A LA
 * FRECUENCIA DE ENTRADA hasta el objetivo (así el remuestreo de DSD queda en la misma rejilla que sin buscar).
 * Posiciones: por continuidad (las marcas de tiempo de algunos contenedores bailan); la marca solo tras buscar.
 */
static int convert(Ctx *c, AVFrame *f) {
    int r;
    if (f) {
        if ((r = setup_swr(c, f)) < 0) return r;
        int64_t pts = f->best_effort_timestamp != AV_NOPTS_VALUE ? f->best_effort_timestamp : f->pts;
        int64_t ptsIn = pts == AV_NOPTS_VALUE ? -1
            : av_rescale_q(pts - c->base, c->st->time_base, (AVRational){1, f->sample_rate});
        // Calibración con la decodificación secuencial: desfase constante entre marca y contenido (desde el 2.º frame).
        if (c->seqIn >= 0) {
            if (!c->calibrated && c->seqIn > 0 && pts != AV_NOPTS_VALUE && pts != c->lastPts) {
                c->ptsOffset = c->seqIn - ptsIn;
                c->calibrated = 1;
            }
            c->seqIn += f->nb_samples;
        }
        c->lastPts = pts;
        int64_t here = c->inPos;
        if (c->inPos >= 0) c->inPos += f->nb_samples;
        if (c->seekTarget >= 0) {
            int64_t startOut = c->seekTarget; // sin marca ni continuidad: se asume que se cayó en el objetivo
            if (here >= 0 || pts != AV_NOPTS_VALUE) {
                int64_t fin = here >= 0 ? here : ptsIn + c->ptsOffset;
                // IMA4 de AIFC/CAF: bloques de 64 muestras; la marca tras buscar sale redondeada (±1) por FFmpeg.
                if (here < 0 && c->dec->codec_id == AV_CODEC_ID_ADPCM_IMA_QT) fin = (fin + 32) / 64 * 64;
                int64_t tin = av_rescale_rnd(c->seekTarget, f->sample_rate, c->outRate, AV_ROUND_DOWN);
                int64_t skip = tin - fin;
                if (skip >= f->nb_samples) { // frame entero antes del objetivo; los siguientes, por continuidad
                    c->inPos = fin + f->nb_samples;    // (los sub-frames de un paquete repiten su marca: APE)
                    return 0;
                }
                if (skip > 0) trim_front(f, (int) skip);
                else if (skip < 0) startOut = av_rescale(fin, c->outRate, f->sample_rate); // se cayó después
            }
            c->seekTarget = -1;
            c->bufLen = c->bufPos = 0;
            c->nextPos = startOut;
        }
    } else if (!c->swr) {
        return 0;
    }
    int max = f ? (int) swr_get_out_samples(c->swr, f->nb_samples) : (int) swr_get_out_samples(c->swr, 0);
    if (max <= 0) return 0;
    // Lo pendiente se conserva (al abrir se decodifican dos frames para calibrar).
    int keep = c->bufLen - c->bufPos;
    if (keep > 0 && c->bufPos > 0) memmove(c->buf, c->buf + c->bufPos, keep);
    if (keep > 0) c->bufStart += c->bufPos / c->outBps;
    if (keep < 0) keep = 0;
    if ((r = ensure_buf(c, max + keep / c->outBps)) < 0) return r;
    uint8_t *out[1] = {c->buf + keep};
    int n = swr_convert(c->swr, out, max, f ? (const uint8_t **) f->extended_data : NULL, f ? f->nb_samples : 0);
    if (n < 0) { set_err(c, "swr_convert", n); return n; }
    c->bufPos = 0;
    c->bufLen = keep + n * c->outBps;
    if (c->nextPos < 0) c->nextPos = 0;
    if (keep == 0) c->bufStart = c->nextPos;
    c->nextPos += n;
    return 0;
}

/** Siguiente frame decodificado en c->frame: 0, AVERROR_EOF (decodificador terminado) o error. */
static int next_frame(Ctx *c) {
    for (;;) {
        int r = avcodec_receive_frame(c->dec, c->frame);
        if (r >= 0) return 0;
        if (r == AVERROR_EOF) return r;
        if (r != AVERROR(EAGAIN)) { set_err(c, "decode", r); return r; }
        if (c->demuxEof) {
            if (avcodec_send_packet(c->dec, NULL) == AVERROR_EOF && c->drained++ > 2) return AVERROR_EOF;
            continue;
        }
        r = av_read_frame(c->fmt, c->pkt);
        if (r < 0) {
            if (c->javaError) return AVERROR(EIO);
            if (r != AVERROR_EOF && !avio_feof(c->fmt->pb)) LOGW("read_frame: %d, treated as end", r);
            c->demuxEof = 1;
            avcodec_send_packet(c->dec, NULL);
            continue;
        }
        if (c->pkt->stream_index == c->si) {
            r = avcodec_send_packet(c->dec, c->pkt);
            if (r < 0 && r != AVERROR(EAGAIN)) {
                if (++c->badPackets > MAX_BAD_PACKETS) { av_packet_unref(c->pkt); set_err(c, "too many bad packets", r); return r; }
            } else c->badPackets = 0;
        }
        av_packet_unref(c->pkt);
    }
}

/** Rellena el búfer pendiente con el siguiente trozo. 0 = hay datos, AVERROR_EOF = fin, <0 error. */
static int fill(Ctx *c) {
    for (;;) {
        if (c->bufPos < c->bufLen) return 0;
        if (c->decoderEof) {
            if (!c->swrFlushed) {
                c->swrFlushed = 1;
                int r = convert(c, NULL);
                if (r < 0) return r;
                continue;
            }
            return AVERROR_EOF;
        }
        int r = next_frame(c);
        if (r == AVERROR_EOF) { c->decoderEof = 1; continue; }
        if (r < 0) return r;
        r = convert(c, c->frame);
        av_frame_unref(c->frame);
        if (r < 0) return r;
    }
}

static void ctx_free(JNIEnv *env, Ctx *c) {
    if (!c) return;
    c->env = env;
    swr_free(&c->swr);
    av_channel_layout_uninit(&c->swrInLayout);
    av_channel_layout_uninit(&c->outLayout);
    avcodec_free_context(&c->dec);
    av_packet_free(&c->pkt);
    av_frame_free(&c->frame);
    if (c->fmt) avformat_close_input(&c->fmt);
    if (c->avio) { av_freep(&c->avio->buffer); avio_context_free(&c->avio); }
    if (c->fd >= 0) close(c->fd);
    if (c->reader) (*env)->DeleteGlobalRef(env, c->reader);
    if (c->jbuf) (*env)->DeleteGlobalRef(env, c->jbuf);
    av_free(c->buf);
    free(c);
}

/** Abre formato + decodificador y decodifica el primer trozo para conocer el formato de salida real. */
static int open_common(Ctx *c, const char *hint) {
    int r;
    unsigned char *iob = av_malloc(IO_BUF);
    if (!iob) return AVERROR(ENOMEM);
    int seekable = c->fd >= 0;
    c->avio = avio_alloc_context(iob, IO_BUF, 0, c, seekable ? file_read : stream_read, NULL, seekable ? file_seek : NULL);
    if (!c->avio) { av_free(iob); return AVERROR(ENOMEM); }
    c->avio->seekable = seekable ? AVIO_SEEKABLE_NORMAL : 0;
    c->fmt = avformat_alloc_context();
    if (!c->fmt) return AVERROR(ENOMEM);
    c->fmt->pb = c->avio;
    c->fmt->flags |= AVFMT_FLAG_CUSTOM_IO;
    const AVInputFormat *ifmt = hint && *hint ? av_find_input_format(hint) : NULL;
    if ((r = avformat_open_input(&c->fmt, NULL, ifmt, NULL)) < 0) { c->fmt = NULL; set_err(c, "open_input", r); return r; }
    if ((r = avformat_find_stream_info(c->fmt, NULL)) < 0) { set_err(c, "find_stream_info", r); return r; }
    const AVCodec *codec = NULL;
    c->si = av_find_best_stream(c->fmt, AVMEDIA_TYPE_AUDIO, -1, -1, &codec, 0);
    if (c->si < 0 || !codec) { set_err(c, "no audio stream/decoder", c->si < 0 ? c->si : AVERROR_DECODER_NOT_FOUND); return AVERROR_DECODER_NOT_FOUND; }
    for (unsigned i = 0; i < c->fmt->nb_streams; i++) if ((int) i != c->si) c->fmt->streams[i]->discard = AVDISCARD_ALL;
    c->st = c->fmt->streams[c->si];
    c->base = (c->st->start_time != AV_NOPTS_VALUE && c->st->start_time > 0) ? c->st->start_time : 0;
    c->dec = avcodec_alloc_context3(codec);
    if (!c->dec) return AVERROR(ENOMEM);
    if ((r = avcodec_parameters_to_context(c->dec, c->st->codecpar)) < 0) { set_err(c, "params", r); return r; }
    c->dec->pkt_timebase = c->st->time_base;
    if ((r = avcodec_open2(c->dec, codec, NULL)) < 0) { set_err(c, "codec_open", r); return r; }
    c->pkt = av_packet_alloc();
    c->frame = av_frame_alloc();
    if (!c->pkt || !c->frame) return AVERROR(ENOMEM);

    // Primer frame → formato de salida.
    r = next_frame(c);
    if (r < 0) { if (r == AVERROR_EOF) set_err(c, "no audio", r); return r; }
    AVFrame *f = c->frame;
    c->outFmt = pick_out_fmt(f->format);
    int rate = f->sample_rate;
    if (is_dsd(codec->id) || rate > 192000) {
        while (rate > 96000 && rate % 2 == 0) rate /= 2;
        c->outFmt = OUT_FLT;
    }
    c->outRate = rate;
    int ch = f->ch_layout.nb_channels;
    if (ch > 8) av_channel_layout_default(&c->outLayout, 2);
    else if (f->ch_layout.order == AV_CHANNEL_ORDER_NATIVE) av_channel_layout_copy(&c->outLayout, &f->ch_layout);
    else av_channel_layout_default(&c->outLayout, ch);
    c->outCh = c->outLayout.nb_channels;
    c->outBps = c->outCh * av_get_bytes_per_sample(av_fmt_of(c->outFmt));
    c->bufStart = -1;
    c->nextPos = 0;
    c->inPos = -1; // la continuidad de entrada solo se usa en la búsqueda lenta
    c->seqIn = 0;
    c->lastPts = AV_NOPTS_VALUE;
    c->seekTarget = -1;
    const AVCodecDescriptor *desc = avcodec_descriptor_get(codec->id);
    int64_t codecPre = c->st->codecpar->seek_preroll > 0 && c->st->codecpar->sample_rate > 0
        ? av_rescale(c->st->codecpar->seek_preroll, 1000000, c->st->codecpar->sample_rate) : 0;
    c->prerollUs = desc && (desc->props & AV_CODEC_PROP_LOSSY) ? FFMAX(codecPre, 250000) : codecPre;
    c->slowSeek = codec->id == AV_CODEC_ID_MUSEPACK8 || codec->id == AV_CODEC_ID_MUSEPACK7;
    r = convert(c, f);
    av_frame_unref(f);
    if (r < 0) return r;
    // Unos frames más (≤ 1 s) para calibrar el desfase marca/contenido antes de una búsqueda inmediata (reanudar).
    for (int i = 0; i < 16 && !c->calibrated && c->seqIn < c->dec->sample_rate; i++) {
        r = next_frame(c);
        if (r == AVERROR_EOF) { c->decoderEof = 1; return 0; }
        if (r < 0) return r;
        r = convert(c, c->frame);
        av_frame_unref(c->frame);
        if (r < 0) return r;
    }
    return 0;
}

// ---------------------------------------------------------------------------------------------- JNI

#define JFN(name) Java_com_onebytesystems_music_android_playback_ffmpeg_NativeAudio_##name

static void throw_io(JNIEnv *env, const char *msg) {
    if ((*env)->ExceptionCheck(env)) return;
    jclass ex = (*env)->FindClass(env, "java/io/IOException");
    if (ex) (*env)->ThrowNew(env, ex, msg);
}

JNIEXPORT jlong JNICALL JFN(nativeOpenFd)(JNIEnv *env, jclass cls, jint fd, jlong size, jstring jhint) {
    (void) cls;
    Ctx *c = calloc(1, sizeof(Ctx));
    if (!c) { close(fd); throw_io(env, "out of memory"); return 0; }
    c->fd = fd;
    c->fileSize = size >= 0 ? size : lseek(fd, 0, SEEK_END);
    c->env = env;
    const char *hint = jhint ? (*env)->GetStringUTFChars(env, jhint, NULL) : NULL;
    int r = open_common(c, hint);
    if (hint) (*env)->ReleaseStringUTFChars(env, jhint, hint);
    if (r < 0) {
        char msg[300];
        snprintf(msg, sizeof msg, "ffmpeg: %s", c->err[0] ? c->err : "open failed");
        ctx_free(env, c);
        throw_io(env, msg);
        return 0;
    }
    return (jlong) (intptr_t) c;
}

JNIEXPORT jlong JNICALL JFN(nativeOpenStream)(JNIEnv *env, jclass cls, jobject reader, jstring jhint) {
    (void) cls;
    Ctx *c = calloc(1, sizeof(Ctx));
    if (!c) { throw_io(env, "out of memory"); return 0; }
    c->fd = -1;
    c->fileSize = -1;
    c->env = env;
    c->reader = (*env)->NewGlobalRef(env, reader);
    jclass rc = (*env)->GetObjectClass(env, reader);
    c->readMid = (*env)->GetMethodID(env, rc, "read", "([BII)I");
    jbyteArray b = (*env)->NewByteArray(env, IO_BUF);
    c->jbuf = (*env)->NewGlobalRef(env, b);
    (*env)->DeleteLocalRef(env, b);
    const char *hint = jhint ? (*env)->GetStringUTFChars(env, jhint, NULL) : NULL;
    int r = open_common(c, hint);
    if (hint) (*env)->ReleaseStringUTFChars(env, jhint, hint);
    if (r < 0) {
        char msg[300];
        snprintf(msg, sizeof msg, "ffmpeg: %s", c->err[0] ? c->err : "open failed");
        ctx_free(env, c);
        throw_io(env, msg); // no-op si ya hay una excepción pendiente del callback
        return 0;
    }
    return (jlong) (intptr_t) c;
}

/**
 * Información: [0] frecuencia de salida, [1] canales, [2] formato (0 s16, 1 s32, 2 float), [3] duración µs (−1),
 * [4] bitrate (bit/s, 0), [5] bits por muestra del original (0), [6] seekable (0/1), [7] frecuencia original.
 */
JNIEXPORT void JNICALL JFN(nativeInfo)(JNIEnv *env, jclass cls, jlong h, jlongArray out) {
    (void) cls;
    Ctx *c = (Ctx *) (intptr_t) h;
    jlong v[8];
    v[0] = c->outRate;
    v[1] = c->outCh;
    v[2] = c->outFmt;
    int64_t d = -1;
    if (c->st->duration != AV_NOPTS_VALUE && c->st->duration > 0) d = av_rescale_q(c->st->duration, c->st->time_base, AV_TIME_BASE_Q);
    else if (c->fmt->duration != AV_NOPTS_VALUE && c->fmt->duration > 0) d = c->fmt->duration;
    v[3] = d;
    v[4] = c->st->codecpar->bit_rate > 0 ? c->st->codecpar->bit_rate : (c->fmt->bit_rate > 0 ? c->fmt->bit_rate : 0);
    int bits = c->st->codecpar->bits_per_raw_sample;
    if (bits <= 0) bits = c->st->codecpar->bits_per_coded_sample;
    v[5] = bits > 0 ? bits : 0;
    v[6] = c->fd >= 0 && d > 0;
    v[7] = c->st->codecpar->sample_rate;
    (*env)->SetLongArrayRegion(env, out, 0, 8, v);
}

JNIEXPORT jstring JNICALL JFN(nativeCodec)(JNIEnv *env, jclass cls, jlong h) {
    (void) cls;
    Ctx *c = (Ctx *) (intptr_t) h;
    return (*env)->NewStringUTF(env, avcodec_get_name(c->st->codecpar->codec_id));
}

/** Copia hasta [len] bytes de PCM (frames completos) a dst[off]. Devuelve bytes, −1 al final. Posición: nativePosition. */
JNIEXPORT jint JNICALL JFN(nativeRead)(JNIEnv *env, jclass cls, jlong h, jbyteArray dst, jint off, jint len) {
    (void) cls;
    Ctx *c = (Ctx *) (intptr_t) h;
    c->env = env;
    int r = fill(c);
    c->env = NULL;
    if (r == AVERROR_EOF) return -1;
    if (r < 0) {
        // Siempre una excepción (si ya hay una pendiente del callback de Java, esa): devolver 0 sin lanzar dejaba a
        // Media3 llamando a read() para siempre (bucle al 100 % de CPU, reproductor en BUFFERING sin error).
        char msg[300];
        snprintf(msg, sizeof msg, "ffmpeg: %s", c->javaError ? "stream source failed" : c->err[0] ? c->err : "decode failed");
        throw_io(env, msg);
        return -1;
    }
    int n = c->bufLen - c->bufPos;
    if (n > len) n = len - len % c->outBps;
    (*env)->SetByteArrayRegion(env, dst, off, n, (const jbyte *) c->buf + c->bufPos);
    c->bufPos += n;
    return n;
}

/** Posición (frames de salida) del siguiente frame que entregará nativeRead (−1 si desconocida). */
JNIEXPORT jlong JNICALL JFN(nativePosition)(JNIEnv *env, jclass cls, jlong h) {
    (void) env; (void) cls;
    Ctx *c = (Ctx *) (intptr_t) h;
    if (c->bufStart < 0) return -1;
    return c->bufStart + (c->bufPos / (c->outBps ? c->outBps : 1));
}

/** Bytes leídos del origen hasta ahora. */
JNIEXPORT jlong JNICALL JFN(nativeConsumed)(JNIEnv *env, jclass cls, jlong h) {
    (void) env; (void) cls;
    return ((Ctx *) (intptr_t) h)->consumed;
}

/** Búsqueda «lenta»: vuelve al principio y se decodifica (descartando) hasta el objetivo, contando muestras. */
static int slow_seek(Ctx *c) {
    int r = avformat_seek_file(c->fmt, c->si, INT64_MIN, 0, 0, 0);
    if (r < 0) r = av_seek_frame(c->fmt, c->si, 0, AVSEEK_FLAG_BACKWARD);
    if (r < 0) return r;
    avcodec_flush_buffers(c->dec);
    c->inPos = 0;
    return 0;
}

/** Busca exactamente a timeUs (frames de salida). Devuelve la posición real (frames) o lanza IOException. */
JNIEXPORT jlong JNICALL JFN(nativeSeek)(JNIEnv *env, jclass cls, jlong h, jlong timeUs) {
    (void) cls;
    Ctx *c = (Ctx *) (intptr_t) h;
    if (c->fd < 0) { throw_io(env, "not seekable"); return -1; }
    c->env = env;
    if (timeUs < 0) timeUs = 0;
    int64_t target = av_rescale_rnd(timeUs, c->outRate, 1000000, AV_ROUND_DOWN);
    int r;
    c->demuxEof = c->decoderEof = c->swrFlushed = c->drained = 0;
    c->seqIn = -1;
    if (c->slowSeek) {
        r = slow_seek(c);
    } else {
        // Se cae antes (pre-roll del códec / solapamiento) y se descarta hasta el objetivo.
        int64_t t = timeUs - c->prerollUs;
        if (t < 0) t = 0;
        int64_t ts = av_rescale_q(t, AV_TIME_BASE_Q, c->st->time_base) + c->base;
        r = avformat_seek_file(c->fmt, c->si, INT64_MIN, ts, ts, 0);
        if (r < 0) r = av_seek_frame(c->fmt, c->si, ts, AVSEEK_FLAG_BACKWARD);
        if (r < 0) r = av_seek_frame(c->fmt, c->si, ts, AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_ANY);
        if (r < 0) r = slow_seek(c);
        else { avcodec_flush_buffers(c->dec); c->inPos = -1; }
    }
    if (r < 0) { set_err(c, "seek", r); c->env = NULL; throw_io(env, c->err); return -1; }
    if (c->swr) { swr_free(&c->swr); c->swrInFmt = -1; }
    c->bufLen = c->bufPos = 0;
    c->bufStart = -1;
    c->nextPos = -1;
    c->seekTarget = target;
    r = fill(c);
    c->env = NULL;
    if (r == AVERROR_EOF) return target;
    if (r < 0) { throw_io(env, c->javaError ? "ffmpeg: stream source failed" : c->err); return -1; }
    return c->bufStart + c->bufPos / c->outBps;
}

JNIEXPORT void JNICALL JFN(nativeClose)(JNIEnv *env, jclass cls, jlong h) {
    (void) cls;
    ctx_free(env, (Ctx *) (intptr_t) h);
}

JNIEXPORT jstring JNICALL JFN(nativeVersion)(JNIEnv *env, jclass cls) {
    (void) cls;
    return (*env)->NewStringUTF(env, av_version_info());
}
