// 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 "FLAC/stream_decoder.h"
#include "Library.h"
#include "flac_error.h"
#include "net_avadeaux_klipspringer_codec_FlacDecoder.h" // generated by javac -h

// A pointer to this struct is cast to an integer and used for interaction with the Java side. Also
// passed to FLAC callbacks.
typedef struct {
    // Members set in create.
    FLAC__StreamDecoder *decoder;               // FLAC interface
    jobject this;                               // FlacDecoder object
    jmethodID metadataMid;                      // this metadata callback method
    jmethodID writeMid;                         // this write callback method
    FILE *file;                                 // set in create, kept open until delete
    bool ogg;                                   // true if Ogg FLAC

    void *restrict buffer;                      // allocated in first metadata_cb
    JNIEnv *env;                                // for passing handle through to FLAC callbacks
} 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->decoder) { FLAC__stream_decoder_delete(drec->decoder); }
        if (drec->this) { (*env)->DeleteGlobalRef(env, drec->this); }
        if (drec->file) { fclose(drec->file); }
        if (drec->buffer) { free(drec->buffer); }
        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;
}

// Calculates frame size from number of bits per sample. There is a choice for what to do when 16 <
// bips < 25, either use 3 bytes or pad to 4 bytes. Both have advantages, but we have to pick one.
static unsigned
calc_ss(unsigned bips) {
    return (bips+7)/8;                  // 3 byte samples are ok
    // return 1 << (bips-1)/8 - bips/25;   // pad 3 to 4 bytes
}

// -------------------------------------------------------------------------------------------------
// FLAC callback functions

// Allocates buffer and sends metadata to Java side, unless this has already been done.
static void
metadata_cb(const FLAC__StreamDecoder *decoder,
            const FLAC__StreamMetadata *metadata,
            void *handle)
{
    DecoderRecord *drec = (DecoderRecord *) handle;
    JNIEnv *env = drec->env;
    if ((*env)->ExceptionCheck(env)) { return; }

    if (metadata->type == FLAC__METADATA_TYPE_STREAMINFO) {
        uint32_t rate = metadata->data.stream_info.sample_rate;
        uint32_t bips = metadata->data.stream_info.bits_per_sample;
        uint32_t channels = metadata->data.stream_info.channels;
        FLAC__uint64 samples = metadata->data.stream_info.total_samples;

        if (drec->buffer == NULL) {             // first time?
            unsigned ss = calc_ss(bips);
            drec->buffer = malloc(metadata->data.stream_info.max_blocksize * channels * ss);
            if (drec->buffer == NULL) {
                raiseError(env, "Failed to allocate PCM data buffer");
                return;
            }

            // Send metadata to Java.
            (*env)->CallVoidMethod(env, drec->this, drec->metadataMid, rate, bips, ss, channels, platform_order(), samples);
        }
    } else if (metadata->type == FLAC__METADATA_TYPE_PICTURE) {
        picture(env, drec->this,
                metadata->data.picture.mime_type,
                FLAC__StreamMetadata_Picture_TypeString[metadata->data.picture.type],
                (char *) metadata->data.picture.description,
                metadata->data.picture.data, metadata->data.picture.data_length);
    }
}

// Sends block to Java side as a ByteBuffer.
static FLAC__StreamDecoderWriteStatus
write_cb(const FLAC__StreamDecoder *decoder,
         const FLAC__Frame *frame,
         const FLAC__int32 * const buffer[],
         void *handle)
{
    DecoderRecord *drec = (DecoderRecord *) handle;
    JNIEnv *env = drec->env;
    if ((*env)->ExceptionCheck(env)) { return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; }

    unsigned ss = calc_ss(frame->header.bits_per_sample);
    unsigned channels = frame->header.channels;
    unsigned blocksz = frame->header.blocksize;

    // Intermix the channels in the buffer, make compiler optimize for important cases.

    int8_t *w = drec->buffer;

#   define COPY_LOOP {                                          \
        unsigned bo = platform_bigend() ? 4-ss : 0;             \
        for (int i = 0; i < blocksz; i++) {                     \
            for (int j = 0; j < channels; j++) {                \
                memcpy(w, (int8_t *) (buffer[j]+i) + bo, ss);   \
                w += ss;                                        \
            }                                                   \
        }                                                       \
    }

#   define SS_CASES                                             \
    switch (ss) {                                               \
    case 1: COPY_LOOP break;                                    \
    case 2: COPY_LOOP break;                                    \
    case 4: COPY_LOOP break;                                    \
    default: COPY_LOOP;                                         \
    }

    switch (channels) {
    case 1: SS_CASES break;
    case 2: SS_CASES break;
    default: SS_CASES;
    }

    // Wrap the buffer as a DirectByteBuffer.
    jobject bb = wrapByteBuffer(env, drec->buffer, blocksz*channels*ss, platform_bigend());
    if (bb == NULL) { return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; }

    // Call Java side.
    return (*env)->CallBooleanMethod(env, drec->this, drec->writeMid, bb) ?
        FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE :
        FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
}

// Sets exception to be thrown.
static void
error_cb(const FLAC__StreamDecoder *decoder,
         FLAC__StreamDecoderErrorStatus status,
         void *handle)
{
    raiseErrorCodeException(((DecoderRecord *) handle)->env, ERRORSTAT_OFF+status, (char *) FLAC__StreamDecoderErrorStatusString[status]);
}

// Standard callback function more or less copied from FLAC docs.
static FLAC__StreamDecoderReadStatus
read_cb(const FLAC__StreamDecoder *decoder,
        FLAC__byte buffer[],
        size_t *bytes,
        void *handle)
{
    DecoderRecord *drec = (DecoderRecord *) handle;
    JNIEnv *env = drec->env;
    if ((*env)->ExceptionCheck(env)) { return FLAC__STREAM_DECODER_READ_STATUS_ABORT; }

    if (*bytes == 0) { return FLAC__STREAM_DECODER_READ_STATUS_ABORT; } // example in docs does this
    *bytes = fread(buffer, sizeof(FLAC__byte), *bytes, drec->file);
    if (ferror(drec->file)) {
        raiseIOException(env, "Error reading FLAC file");
        return FLAC__STREAM_DECODER_READ_STATUS_ABORT;
    }
    return *bytes == 0 ?
        FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM :
        FLAC__STREAM_DECODER_READ_STATUS_CONTINUE;
}

// Standard callback function more or less copied from FLAC docs.
static FLAC__StreamDecoderSeekStatus
seek_cb(const FLAC__StreamDecoder *decoder,
        FLAC__uint64 absolute_byte_offset,
        void *handle)
{
    DecoderRecord *drec = (DecoderRecord *) handle;
    JNIEnv *env = drec->env;
    if ((*env)->ExceptionCheck(env)) { return FLAC__STREAM_DECODER_SEEK_STATUS_ERROR; }

    if (fseeko(drec->file, (off_t) absolute_byte_offset, SEEK_SET) < 0) {
        raiseErrorCodeException(env, ERRNO_OFF+errno, "Error seeking in FLAC file");
        return FLAC__STREAM_DECODER_SEEK_STATUS_ERROR;
    }
    return FLAC__STREAM_DECODER_SEEK_STATUS_OK;
}

// Standard callback function more or less copied from FLAC docs.
static FLAC__StreamDecoderTellStatus
tell_cb(const FLAC__StreamDecoder *decoder,
        FLAC__uint64 *absolute_byte_offset,
        void *handle)
{
    DecoderRecord *drec = (DecoderRecord *) handle;
    JNIEnv *env = drec->env;
    if ((*env)->ExceptionCheck(env)) { return FLAC__STREAM_DECODER_TELL_STATUS_ERROR; }

    off_t pos;
    if ((pos = ftello(drec->file)) < 0) {
        raiseErrorCodeException(env, ERRNO_OFF+errno, "Error getting position in FLAC file");
        return FLAC__STREAM_DECODER_TELL_STATUS_ERROR;
    }
    *absolute_byte_offset = (FLAC__uint64) pos;
    return FLAC__STREAM_DECODER_TELL_STATUS_OK;
}

// Standard callback function more or less copied from FLAC docs.
static FLAC__StreamDecoderLengthStatus
length_cb(const FLAC__StreamDecoder *decoder,
          FLAC__uint64 *stream_length,
          void *handle)
{
    struct stat filestats;
    DecoderRecord *drec = (DecoderRecord *) handle;
    JNIEnv *env = drec->env;
    if ((*env)->ExceptionCheck(env)) { return FLAC__STREAM_DECODER_LENGTH_STATUS_ERROR; }

    if (fstat(fileno(drec->file), &filestats) != 0) {
        raiseErrorCodeException(env, ERRNO_OFF+errno, "Error getting file stats of FLAC file");
        return FLAC__STREAM_DECODER_LENGTH_STATUS_ERROR;
    }
    *stream_length = (FLAC__uint64) filestats.st_size;
    return FLAC__STREAM_DECODER_LENGTH_STATUS_OK;
}

// Standard callback function more or less copied from FLAC docs.
static FLAC__bool
eof_cb(const FLAC__StreamDecoder *decoder, void *handle) {
    DecoderRecord *drec = (DecoderRecord *) handle;
    return feof(drec->file) ? true : false;
}

// -------------------------------------------------------------------------------------------------
// Native FlacDecoder methods

// Helper for create and rewind. Returns 0 on success, throws and returns -1 on failure.
static int
init(JNIEnv *env, DecoderRecord *drec, bool pictures) {
    if (pictures) { FLAC__stream_decoder_set_metadata_respond(drec->decoder, FLAC__METADATA_TYPE_PICTURE); }
    else          { FLAC__stream_decoder_set_metadata_ignore(drec->decoder, FLAC__METADATA_TYPE_PICTURE); }
    FLAC__StreamDecoderInitStatus status = (drec->ogg ? FLAC__stream_decoder_init_ogg_stream : FLAC__stream_decoder_init_stream)
        (drec->decoder,
         read_cb, seek_cb, tell_cb, length_cb, eof_cb,
         write_cb, metadata_cb, error_cb,
         drec);
    if (status != FLAC__STREAM_DECODER_INIT_STATUS_OK) {
        raiseErrorCodeException(env, INITSTAT_OFF+status, (char *) FLAC__StreamDecoderInitStatusString[status]);
        return -1;
    } else {
        return 0;
    }
}

#define METHOD(name) JNICALL Java_net_avadeaux_klipspringer_codec_FlacDecoder_ ## name

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

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

    if ((drec->decoder = FLAC__stream_decoder_new()) == NULL) {
        return fail(env, "Failed to create FLAC decoder", drec);
    }
    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, accepts_picture(env, jthis))) { free_drec(env, drec); return 0; }
    return (intptr_t) drec;
}

// Expects file to be still open and decoder to be in uninitialized state (after
// FLAC__stream_decoder_finish was called). Rewinds file, initializes decoder, and processes
// metadata (which does not send it on to the Java side), 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 (FLAC__stream_decoder_get_state(drec->decoder) != FLAC__STREAM_DECODER_UNINITIALIZED) {
        FLAC__stream_decoder_finish(drec->decoder);
    }
    errno = 0;
    rewind(drec->file);
    if (errno != 0) {
        raiseErrorCodeException(env, ERRNO_OFF+errno, strerror(errno));
    } else if (init(env, drec, false) == 0 && !FLAC__stream_decoder_process_until_end_of_metadata(drec->decoder)) {
        raiseErrorCodeException(env, ERRNO_OFF+errno, "Failed to process metadata in rewind");
    }
}

// Calls free_drec to close file and delete decoder.
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;
    drec->env = env;
    if (!FLAC__stream_decoder_seek_absolute(drec->decoder, jpos)) {
        raiseIOException(env, "Failed to seek in FLAC file (possible invalid position for file)");
    }
}

// Expected to be 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;
    drec->env = env;
    if (!FLAC__stream_decoder_process_until_end_of_metadata(drec->decoder)) {
        raiseIOException(env, "Failed to decode metadata");
    }
}

// Expected to be called after processing metadata.
JNIEXPORT jboolean
METHOD(nativeDecodeAll) (JNIEnv *env,
                         jobject jthis,
                         jlong jdrec)
{
    DecoderRecord *drec = (DecoderRecord *) (intptr_t) jdrec;
    drec->env = env;
    jboolean r = JNI_TRUE;
    if (!FLAC__stream_decoder_process_until_end_of_stream(drec->decoder)) {
        FLAC__StreamDecoderState state = FLAC__stream_decoder_get_state(drec->decoder);
        if (state != FLAC__STREAM_DECODER_ABORTED) {
            raiseErrorCodeException(env, DECSTAT_OFF+state, (char *) FLAC__StreamDecoderStateString[state]);
        }
        r = JNI_FALSE;
    }
    FLAC__stream_decoder_finish(drec->decoder);
    return r;
}
