//
// Created by claunia on 20/10/21.
//

#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include "3rdparty/flac/include/FLAC/metadata.h"
#include "3rdparty/flac/include/FLAC/stream_decoder.h"
#include "3rdparty/flac/include/FLAC/stream_encoder.h"
#include "library.h"
#include "flac.h"

static FLAC__StreamDecoderReadStatus read_callback(const FLAC__StreamDecoder *decoder,
                                                   FLAC__byte                 buffer[],
                                                   size_t *                   bytes,
                                                   void *                     client_data);

static FLAC__StreamDecoderWriteStatus write_callback(const FLAC__StreamDecoder *decoder,
                                                     const FLAC__Frame *        frame,
                                                     const FLAC__int32 *const   buffer[],
                                                     void *                     client_data);

static void error_callback(const FLAC__StreamDecoder *    decoder,
                           FLAC__StreamDecoderErrorStatus status,
                           void *                         client_data);

AARU_EXPORT size_t AARU_CALL AARU_flac_decode_redbook_buffer(uint8_t *      dst_buffer,
                                                             size_t         dst_size,
                                                             const uint8_t *src_buffer,
                                                             size_t         src_size)
{
    FLAC__StreamDecoder *         decoder;
    FLAC__StreamDecoderInitStatus init_status;
    aaru_flac_ctx *               ctx = (aaru_flac_ctx *)malloc(sizeof(aaru_flac_ctx));
    size_t                        ret_size;

    memset(ctx, 0, sizeof(aaru_flac_ctx));

    ctx->src_buffer = src_buffer;
    ctx->src_len    = src_size;
    ctx->src_pos    = 0;
    ctx->dst_buffer = dst_buffer;
    ctx->dst_len    = dst_size;
    ctx->dst_pos    = 0;
    ctx->error      = 0;

    decoder = FLAC__stream_decoder_new();

    if(!decoder)
    {
        free(ctx);
        return -1;
    }

    FLAC__stream_decoder_set_md5_checking(decoder, false);

    init_status = FLAC__stream_decoder_init_stream(decoder, read_callback, // 1
                                                   NULL,                   // 2 seek
                                                   NULL,                   // 3 tell
                                                   NULL,                   // 4 length
                                                   NULL,                   // 5 eof
                                                   write_callback,         // 6 write
                                                   NULL,                   // 7 metadata
                                                   error_callback,         // 8 error
                                                   ctx                     // 9 client_data
    );

    if(init_status != FLAC__STREAM_DECODER_INIT_STATUS_OK)
    {
        free(ctx);
        return -1;
    }

    // TODO: Return error somehow
    FLAC__stream_decoder_process_until_end_of_stream(decoder);

    FLAC__stream_decoder_delete(decoder);

    ret_size = ctx->dst_pos;

    free(ctx);

    return ret_size;
}

static FLAC__StreamDecoderReadStatus read_callback(const FLAC__StreamDecoder *decoder,
                                                   FLAC__byte                 buffer[],
                                                   size_t *                   bytes,
                                                   void *                     client_data)
{
    aaru_flac_ctx *ctx = (aaru_flac_ctx *)client_data;

    if(ctx->src_len - ctx->src_pos < *bytes) *bytes = ctx->src_len - ctx->src_pos;

    if(*bytes == 0) return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM;

    memcpy(buffer, ctx->src_buffer + ctx->src_pos, *bytes);
    ctx->src_pos += *bytes;

    return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE;
}

static FLAC__StreamDecoderWriteStatus write_callback(const FLAC__StreamDecoder *decoder,
                                                     const FLAC__Frame *        frame,
                                                     const FLAC__int32 *const   buffer[],
                                                     void *                     client_data)
{
    aaru_flac_ctx *ctx = (aaru_flac_ctx *)client_data;
    size_t         i;
    uint16_t *     buffer16 = (uint16_t *)(ctx->dst_buffer + ctx->dst_pos);

    // Why FLAC does not interleave the channels as PCM do, oh the mistery, we could use memcpy instead of looping
    for(i = 0; i < frame->header.blocksize && ctx->dst_pos < ctx->dst_len; i++)
    {
        // Left channel
        *(buffer16++) = (FLAC__int16)buffer[0][i];
        // Right channel
        *(buffer16++) = (FLAC__int16)buffer[1][i];

        ctx->dst_pos += 4;

        /* TODO: Big-endian (use bswap?)
        // Left channel
        ctx->dst_buffer[ctx->dst_pos++] = (FLAC__uint16)(FLAC__int16)buffer[0][i];
        ctx->dst_buffer[ctx->dst_pos++] = (FLAC__uint16)(FLAC__int16)buffer[0][i] >> 8;
        // Right channel
        ctx->dst_buffer[ctx->dst_pos++] = (FLAC__uint16)(FLAC__int16)buffer[1][i];
        ctx->dst_buffer[ctx->dst_pos++] = (FLAC__uint16)(FLAC__int16)buffer[1][i] >> 8;
        */
    }

    return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
}

static void error_callback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data)
{
    aaru_flac_ctx *ctx = (aaru_flac_ctx *)client_data;

    fprintf(stderr, "Got error callback: %s\n", FLAC__StreamDecoderErrorStatusString[status]);

    ctx->error = 1;
}

static FLAC__StreamEncoderWriteStatus encoder_write_callback(const FLAC__StreamEncoder *encoder,
                                                             const FLAC__byte           buffer[],
                                                             size_t                     bytes,
                                                             uint32_t                   samples,
                                                             uint32_t                   current_frame,
                                                             void *                     client_data);

AARU_EXPORT size_t AARU_CALL AARU_flac_encode_redbook_buffer(uint8_t *      dst_buffer,
                                                             size_t         dst_size,
                                                             const uint8_t *src_buffer,
                                                             size_t         src_size,
                                                             uint32_t       blocksize,
                                                             int32_t        do_mid_side_stereo,
                                                             int32_t        loose_mid_side_stereo,
                                                             const char *   apodization,
                                                             uint32_t       max_lpc_order,
                                                             uint32_t       qlp_coeff_precision,
                                                             int32_t        do_qlp_coeff_prec_search,
                                                             int32_t        do_exhaustive_model_search,
                                                             uint32_t       min_residual_partition_order,
                                                             uint32_t       max_residual_partition_order,
                                                             const char *   application_id,
                                                             uint32_t       application_id_len)
{
    FLAC__StreamEncoder *         encoder;
    aaru_flac_ctx *               ctx = (aaru_flac_ctx *)malloc(sizeof(aaru_flac_ctx));
    FLAC__StreamEncoderInitStatus init_status;
    size_t                        ret_size;
    FLAC__int32 *                 pcm;
    int                           i;
    int16_t *                     buffer16 = (int16_t *)src_buffer;
    FLAC__StreamMetadata *        metadata[1];

    memset(ctx, 0, sizeof(aaru_flac_ctx));

    ctx->src_buffer = src_buffer;
    ctx->src_len    = src_size;
    ctx->src_pos    = 0;
    ctx->dst_buffer = dst_buffer;
    ctx->dst_len    = dst_size;
    ctx->dst_pos    = 0;
    ctx->error      = 0;

    encoder = FLAC__stream_encoder_new();

    if(!encoder)
    {
        free(ctx);
        return -1;
    }

    // TODO: Error detection here
    FLAC__stream_encoder_set_verify(encoder, false);
    FLAC__stream_encoder_set_streamable_subset(encoder, false);
    FLAC__stream_encoder_set_channels(encoder, 2);
    FLAC__stream_encoder_set_bits_per_sample(encoder, 16);
    FLAC__stream_encoder_set_sample_rate(encoder, 44100);
    FLAC__stream_encoder_set_blocksize(encoder, blocksize);
    // true compresses more
    FLAC__stream_encoder_set_do_mid_side_stereo(encoder, do_mid_side_stereo);
    // false compresses more
    FLAC__stream_encoder_set_loose_mid_side_stereo(encoder, loose_mid_side_stereo);
    // Apodization
    FLAC__stream_encoder_set_apodization(encoder, apodization);
    FLAC__stream_encoder_set_max_lpc_order(encoder, max_lpc_order);
    FLAC__stream_encoder_set_qlp_coeff_precision(encoder, qlp_coeff_precision);
    FLAC__stream_encoder_set_do_qlp_coeff_prec_search(encoder, do_qlp_coeff_prec_search);
    FLAC__stream_encoder_set_do_exhaustive_model_search(encoder, do_exhaustive_model_search);
    FLAC__stream_encoder_set_min_residual_partition_order(encoder, min_residual_partition_order);
    FLAC__stream_encoder_set_max_residual_partition_order(encoder, max_residual_partition_order);
    FLAC__stream_encoder_set_total_samples_estimate(encoder, src_size / 4);

    /* TODO: This is ignored by FLAC, need to replace it
    if((metadata[0] = FLAC__metadata_object_new(FLAC__METADATA_TYPE_VORBIS_COMMENT)) != NULL)
    {
        memset(&vorbis_entry, 0, sizeof(FLAC__StreamMetadata_VorbisComment_Entry));
        vorbis_entry.entry = (unsigned char *)"Aaru.Compression.Native";
        vorbis_entry.length = strlen("Aaru.Compression.Native");

        FLAC__metadata_object_vorbiscomment_set_vendor_string(metadata[0], vorbis_entry, true);
    }
    */

    if(application_id_len > 0 && application_id != NULL)
        if((metadata[0] = FLAC__metadata_object_new(FLAC__METADATA_TYPE_APPLICATION)) != NULL)
            FLAC__metadata_object_application_set_data(metadata[0], (unsigned char *)application_id, application_id_len,
                                                       true);

    FLAC__stream_encoder_set_metadata(encoder, metadata, 1);

    init_status = FLAC__stream_encoder_init_stream(encoder, encoder_write_callback, NULL, NULL, NULL, ctx);

    if(init_status != FLAC__STREAM_ENCODER_INIT_STATUS_OK)
    {
        free(ctx);
        return -1;
    }

    pcm = malloc((src_size / 2) * sizeof(FLAC__int32));

    for(i = 0; i < src_size / 2; i++) pcm[i] = (FLAC__int32)*(buffer16++);

    FLAC__stream_encoder_process_interleaved(encoder, pcm, src_size / 4);

    FLAC__stream_encoder_finish(encoder);

    FLAC__stream_encoder_delete(encoder);

    ret_size = ctx->dst_pos;

    free(ctx);
    free(pcm);
    FLAC__metadata_object_delete(metadata[0]);

    return ret_size;
}

static FLAC__StreamEncoderWriteStatus encoder_write_callback(const FLAC__StreamEncoder *encoder,
                                                             const FLAC__byte           buffer[],
                                                             size_t                     bytes,
                                                             uint32_t                   samples,
                                                             uint32_t                   current_frame,
                                                             void *                     client_data)
{
    aaru_flac_ctx *ctx = (aaru_flac_ctx *)client_data;

    if(bytes > ctx->dst_len - ctx->dst_pos) bytes = ctx->dst_len - ctx->dst_pos;

    memcpy(ctx->dst_buffer + ctx->dst_pos, buffer, bytes);

    ctx->dst_pos += bytes;

    return FLAC__STREAM_ENCODER_WRITE_STATUS_OK;
}