// 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 "vorbis/codec.h"
#include "vorbis/vorbisfile.h"
#include "vorbis_error.h"
#include "Library.h"
#include "net_avadeaux_klipspringer_codec_VorbisDecoder.h" // generated by javac -h

#define VORBIS_BYPS 2
#define VORBIS_BIGEND 0
#define VORBIS_SIGNED 1

// A pointer to this struct is cast to an integer and used for interaction with the Java side.
typedef struct {
    OggVorbis_File vf;                          // Vorbisfile interface
    bool vf_open;                               // vf has been opened and not cleared
    jobject this;                               // VorbisDecoder object
    jmethodID metadataMid;                      // this metadata callback method
    jmethodID writeMid;                         // this write callback method
    FILE *file;                                 // set in create, kept open until delete
} DecoderRecord;

// -------------------------------------------------------------------------------------------------
// 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->vf_open) { ov_clear(&drec->vf); }
        if (drec->this) { (*env)->DeleteGlobalRef(env, drec->this); }
        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 VorbisDecoder 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 = ov_open_callbacks(drec->file, &drec->vf, NULL, 0, OV_CALLBACKS_NOCLOSE);
    if (err < 0) {
        raiseErrorCodeException(env, err, vorbis_strerror(err));
        return -1;
    }
    drec->vf_open = true;
    return 0;
}

#define METHOD(name) JNICALL Java_net_avadeaux_klipspringer_codec_VorbisDecoder_ ## name

// Opens file, creates and initializes decoder.
JNIEXPORT jlong
METHOD(create) (JNIEnv *env,
                jobject jthis,
                jbyteArray jfnam)
{
    DecoderRecord *drec = malloc(sizeof *drec);
    if (drec == NULL) { return fail(env, "Failed to allocate decoder record", drec); }
    drec->vf_open = false;

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

    if ((drec->this = (*env)->NewGlobalRef(env, jthis)) == NULL) {
        return fail(env, "Failed to get global decoder reference", drec);
    }

    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 vf not cleared. 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;

    ov_clear(&drec->vf);
    drec->vf_open = false;

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

// Calls free_drec to close file and clear vf.
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 = ov_pcm_seek(&drec->vf, jpos);
    if (err) { raiseErrorCodeException(env, err, vorbis_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;

    vorbis_info *vi = ov_info(&drec->vf, -1);
    ogg_int64_t samples = ov_pcm_total(&drec->vf, -1);
    (*env)->CallVoidMethod(env, drec->this, drec->metadataMid, vi->rate, VORBIS_BYPS*8, VORBIS_BYPS, vi->channels, VORBIS_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;

    char buffer[4096];
    jobject bb = wrapByteBuffer(env, buffer, sizeof buffer, VORBIS_BIGEND);
    if (bb == NULL) { return JNI_FALSE; }
    int section;
    while (true) {
        long n = ov_read(&drec->vf, buffer, sizeof buffer, VORBIS_BIGEND, VORBIS_BYPS, VORBIS_SIGNED, &section);
        if (n == 0) { return JNI_TRUE; }
        if (n < 0) {
            raiseErrorCodeException(env, n, vorbis_strerror(n));
            return JNI_FALSE;
        }
        if (!byteBufferClear(env, bb)
            || !byteBufferLimit(env, bb, n)
            || !(*env)->CallBooleanMethod(env, jthis, drec->writeMid, bb)) { fflush(stdout);return JNI_FALSE; }
    }
}
