// Copyright 2014-2025 Jesper Larsson
//
// This file is part of Klipspringer, <https://klipspringer.avadeaux.net/>
//
// Klipspringer 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 3 of the
// License, or (at your option) any later version.
//
// Klipspringer 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 Klipspringer. If
// not, see <https://www.gnu.org/licenses/>.

#include <stdlib.h>
#include <string.h>
#include <sys/errno.h>
#include <sys/stat.h>
#include "opusfile.h"
#include "opus_error.h"
#include "Library.h"
#include "net_avadeaux_klipspringer_codec_OpusDecoder.h" // generated by javac -h

#define PCM_BUFFER_FRAMES 8192                  // opus doc recommends at least 5760

// A pointer to this struct is cast to an integer and used for interaction with the Java side.
typedef struct {
    OggOpusFile *of;                            // Opusfile interface
    jmethodID metadataMid;                      // metadata callback method
    jmethodID writeMid;                         // write callback method
    FILE *file;                                 // set in create, kept open until free
} DecoderRecord;

// -------------------------------------------------------------------------------------------------
// Callback function stuff. We dare not cast stdio functions for seek and tell directly to the
// opusfile types, in case the size of the offset type is different.

int file_read(void *f, unsigned char *p, int n) {
    return fread(p, 1, n, (FILE *) f);
}

static int file_seek(void *f, opus_int64 offset, int whence) {
    return fseeko((FILE *) f, offset, whence);
}

static opus_int64 file_tell(void *f) {
    return ftello(f);
}

OpusFileCallbacks file_callbacks = { file_read, file_seek, file_tell, NULL };

// -------------------------------------------------------------------------------------------------
// Helper functions used on errors and by free. The policy is that drec is freed on error in create,
// but in other cases the caller should catch IOException and call free.

// Closes and frees everything.
static void
free_drec(JNIEnv *env, DecoderRecord *drec) {
    if (drec != NULL) {
        if (drec->of) { op_free(drec->of); }
        if (drec->file) { fclose(drec->file); }
        free(drec);
    }
}

// Throws Error unless already thrown, frees drec, and returns 0.
static jlong
fail(JNIEnv *env, const char *message, DecoderRecord *drec) {
    raiseError(env, message);
    free_drec(env, drec);
    return 0;
}

// -------------------------------------------------------------------------------------------------
// Native OpusDecoder methods

// Helper for create and rewind. Returns 0 on success, throws and returns -1 on failure.
static int
init(JNIEnv *env, DecoderRecord *drec) {
    int err;

    drec->of = op_open_callbacks(drec->file, &file_callbacks, NULL, 0, &err);
    if (drec->of == NULL) {
        raiseErrorCodeException(env, err, opusfile_strerror(err));
        return -1;
    }
    op_set_dither_enabled(drec->of, true);
    return 0;
}

#define METHOD(name) JNICALL Java_net_avadeaux_klipspringer_codec_OpusDecoder_ ## name

// Opens file, creates and initializes decoder.
JNIEXPORT jlong
METHOD(create) (JNIEnv *env,
                jobject jthis,
                jbyteArray jfnam)
{
    if (sizeof(opus_int16) != 2) {
        raiseIOException(env, "Integer size mismatch for Opus decoding");
        return 0;
    }
    DecoderRecord *drec = malloc(sizeof *drec);
    if (drec == NULL) { return fail(env, "Failed to allocate decoder record", drec); }

    // Null pointers so that free_drec can handle partially initialized record.
    drec->of = NULL;
    drec->file = NULL;

    jclass ecls = (*env)->GetObjectClass(env, jthis);
    if (ecls == NULL) { return fail(env, "Failed to get encoder class", drec); }
    if ((drec->metadataMid = (*env)->GetMethodID(env, ecls, "metadata", "(IIIILjava/nio/ByteOrder;J)V")) == NULL) {
        return fail(env, "Failed to get metadata method ID", drec);
    }
    if ((drec->writeMid = (*env)->GetMethodID(env, ecls, "write", "(Ljava/nio/ByteBuffer;)Z")) == NULL) {
        return fail(env, "Failed to get write method ID", drec);
    }

    jbyte *fnam = (*env)->GetByteArrayElements(env, jfnam, NULL);
    if (fnam == NULL) { return fail(env, "Failed to allocate filename string", drec); }
    drec->file = fopen((char *) fnam, "r");
    (*env)->ReleaseByteArrayElements(env, jfnam, fnam, JNI_ABORT);
    if (drec->file == NULL) { return fail(env, raiseFileNotFound(env, strerror(errno)), drec); }

    if (init(env, drec)) { free_drec(env, drec); return 0; }

    return (intptr_t) drec;
}

// Expects file to be still open and of either NULL or not freed. Rewinds file, clears and
// reinitializes vf, and leaves the decoder ready for the next nativeDecodeAll.
JNIEXPORT void
METHOD (rewind) (JNIEnv *env,
                 jobject jthis,
                 jlong jdrec)
{
    DecoderRecord *drec = (DecoderRecord *) (intptr_t) jdrec;

    if (drec->of) {
        op_free(drec->of);
        drec->of = NULL;
    }

    errno = 0;
    rewind(drec->file);
    if (errno != 0) {
        raiseErrorCodeException(env, errno, strerror(errno));
        return;
    }
    init(env, drec);
}

// Calls free_drec to close file and free of.
JNIEXPORT void
METHOD (free) (JNIEnv *env,
               jobject jthis,
               jlong jdrec)
{
    free_drec(env, (DecoderRecord *) (intptr_t) jdrec);
}

// Absolute seek.
JNIEXPORT void
METHOD(nativeSeek) (JNIEnv *env,
                    jobject jthis,
                    jlong jdrec,
                    jlong jpos)
{
    DecoderRecord *drec = (DecoderRecord *) (intptr_t) jdrec;
    int err = op_pcm_seek(drec->of, jpos);
    if (err) { raiseErrorCodeException(env, err, opusfile_strerror(err)); }
}

// Called as the first thing after create to send metadata to the java side.
JNIEXPORT void
METHOD(nativeDecodeMetadata) (JNIEnv *env,
                              jobject jthis,
                              jlong jdrec)
{
    DecoderRecord *drec = (DecoderRecord *) (intptr_t) jdrec;

    ogg_int64_t samples = op_pcm_total(drec->of, -1);
    (*env)->CallVoidMethod(env, jthis, drec->metadataMid, 48000, 16, 2, op_channel_count(drec->of, -1), platform_bigend(), samples);
}

// Expected to be called after processing metadata.
JNIEXPORT jboolean
METHOD(nativeDecodeAll) (JNIEnv *env,
                         jobject jthis,
                         jlong jdrec)
{
    DecoderRecord *drec = (DecoderRecord *) (intptr_t) jdrec;

    int channels = op_channel_count(drec->of, -1);
    int buf_bytes = PCM_BUFFER_FRAMES * channels * 2;
    opus_int16 *buf = malloc(buf_bytes);
    if (buf == NULL) {
        raiseIOException(env, "cannot allocate decoding buffer");
        return JNI_FALSE;
    }
    jobject bb = wrapByteBuffer(env, buf, buf_bytes, platform_bigend());
    jboolean ok = bb != NULL;
    while (ok) {
        long n = op_read(drec->of, buf, PCM_BUFFER_FRAMES * channels, NULL);
        if (n == 0) { break; }
        if (n < 0) {
            raiseErrorCodeException(env, n, opusfile_strerror(n));
            ok = JNI_FALSE;
        }
        if (!byteBufferClear(env, bb)
            || !byteBufferLimit(env, bb, n * channels * 2)
            || !(*env)->CallBooleanMethod(env, jthis, drec->writeMid, bb)) {
            ok = JNI_FALSE;
        }
    }
    free(buf);
    return ok;
}
