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

#include "wavpack.h"

#ifdef __cplusplus
using namespace std;
#endif

#define BUF_SAMPLES 1024

typedef struct {
    unsigned char ungetc_char, ungetc_flag;
    unsigned char *sptr, *dptr, *eptr;
    int64_t total_bytes_read;
} WavpackRawContext;

static int32_t raw_read_bytes (void *id, void *data, int32_t bcount)
{
    WavpackRawContext *rcxt = (WavpackRawContext *) id;
    unsigned char *outptr = (unsigned char *) data;

    while (bcount) {
        if (rcxt->ungetc_flag) {
            *outptr++ = rcxt->ungetc_char;
            rcxt->ungetc_flag = 0;
            bcount--;
        }
        else {
            size_t bytes_to_copy = rcxt->eptr - rcxt->dptr;

            if (!bytes_to_copy)
                break;

            if (bytes_to_copy > bcount)
                bytes_to_copy = bcount;

            memcpy (outptr, rcxt->dptr, bytes_to_copy);
            rcxt->total_bytes_read += bytes_to_copy;
            rcxt->dptr += bytes_to_copy;
            outptr += bytes_to_copy;
            bcount -= bytes_to_copy;
        }
    }

    return (int32_t)(outptr - (unsigned char *) data);
}

static int32_t raw_write_bytes (void *id, void *data, int32_t bcount)
{
    return data ? bcount : 0;
}

static int64_t raw_get_pos (void *id)
{
    WavpackRawContext *rcxt = (WavpackRawContext *) id;
    return rcxt->dptr - rcxt->sptr;
}

static int raw_set_pos_abs (void *id, int64_t pos)
{
    WavpackRawContext *rcxt = (WavpackRawContext *) id;

    if (rcxt->sptr + pos < rcxt->sptr || rcxt->sptr + pos > rcxt->eptr)
        return 1;

    rcxt->dptr = rcxt->sptr + pos;
    return 0;
}

static int raw_set_pos_rel (void *id, int64_t delta, int mode)
{
    WavpackRawContext *rcxt = (WavpackRawContext *) id;
    unsigned char *ref = NULL;

    if (mode == SEEK_SET)
        ref = rcxt->sptr;
    else if (mode == SEEK_CUR)
        ref = rcxt->dptr;
    else if (mode == SEEK_END)
        ref = rcxt->eptr;

    if (ref + delta < rcxt->sptr || ref + delta > rcxt->eptr)
        return 1;

    rcxt->dptr = ref + delta;
    return 0;
}

static int raw_push_back_byte (void *id, int c)
{
    WavpackRawContext *rcxt = (WavpackRawContext *) id;
    rcxt->ungetc_char = c;
    rcxt->ungetc_flag = 1;
    return c;
}

static int64_t raw_get_length (void *id)
{
    WavpackRawContext *rcxt = (WavpackRawContext *) id;
    return rcxt->eptr - rcxt->sptr;
}

static int raw_can_seek (void *id)
{
    return 1;
}

static int raw_close_stream (void *id)
{
    return 0;
}

static WavpackStreamReader64 raw_reader = {
    raw_read_bytes, raw_write_bytes, raw_get_pos, raw_set_pos_abs, raw_set_pos_rel,
    raw_push_back_byte, raw_get_length, raw_can_seek, NULL, raw_close_stream
};

static long long debug_log_mask = -1;

#ifdef __cplusplus
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
#else
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
#endif
{
    static long long times_called, opens, seeks, tag_writes, samples_decoded, text_tags, binary_tags;
    int flags = OPEN_TAGS | OPEN_EDIT_TAGS | OPEN_WRAPPER | OPEN_DSD_AS_PCM | OPEN_NO_CHECKSUM | OPEN_NORMALIZE;
    WavpackRawContext raw_wv;
    WavpackContext *wpc;
    char error [80];
    int num_chans, bps, mode, qmode;
    int64_t total_samples;
    int retval = 0;

    times_called++;

    WavpackGetLibraryVersionString ();
    WavpackGetLibraryVersion ();

    memset (&raw_wv, 0, sizeof (WavpackRawContext));
    raw_wv.dptr = raw_wv.sptr = (unsigned char *) data;
    raw_wv.eptr = raw_wv.dptr + size;
    wpc = WavpackOpenFileInputEx64 (&raw_reader, &raw_wv, NULL, error, flags, 15);

    if (!wpc) {
        retval = 1;
        goto exit;
    }

    opens++;
    num_chans = WavpackGetNumChannels (wpc);
    total_samples = WavpackGetNumSamples64 (wpc);
    bps = WavpackGetBytesPerSample (wpc);
    qmode = WavpackGetQualifyMode (wpc);
    mode = WavpackGetMode (wpc);

    // call some other APIs for coverage
    WavpackGetErrorMessage (wpc);
    WavpackGetSampleIndex64 (wpc);
    WavpackGetSampleIndex (wpc);
    WavpackGetNumSamples (wpc);
    WavpackGetNumErrors (wpc);
    WavpackLossyBlocks (wpc);
    WavpackGetProgress (wpc);
    WavpackGetRatio (wpc);
    WavpackGetAverageBitrate (wpc, 1);
    WavpackGetInstantBitrate (wpc);
    WavpackGetNativeSampleRate (wpc);
    WavpackGetSampleRate (wpc);
    WavpackGetChannelMask (wpc);
    WavpackGetFloatNormExp (wpc);
    WavpackGetBitsPerSample (wpc);
    WavpackGetBytesPerSample (wpc);
    WavpackGetReducedChannels (wpc);
    WavpackGetVersion (wpc);
    WavpackGetFileFormat (wpc);
    WavpackGetFileExtension (wpc);

    if (WavpackGetWrapperBytes (wpc))
        WavpackGetWrapperData (wpc);

    if (num_chans) {
        unsigned char identities [num_chans + 1];
        WavpackGetChannelIdentities (wpc, identities);

        if (WavpackGetChannelLayout (wpc, NULL) & 0xff) {
            unsigned char reordering [WavpackGetChannelLayout (wpc, NULL) & 0xff];
            WavpackGetChannelLayout (wpc, reordering);
        }
    }

    // Get all the metadata tags (text & binary)
    if (mode & MODE_VALID_TAG) {
        int num_binary_items = WavpackGetNumBinaryTagItems (wpc);
        int num_items = WavpackGetNumTagItems (wpc), i;

        for (i = 0; i < num_items; ++i) {
            int item_len, value_len, j;
            char *item, *value;

            item_len = WavpackGetTagItemIndexed (wpc, i, NULL, 0);
            item = (char *) malloc (item_len + 1);
            WavpackGetTagItemIndexed (wpc, i, item, item_len + 1);
            value_len = WavpackGetTagItem (wpc, item, NULL, 0);
            value = (char *) malloc (value_len + 1);
            WavpackGetTagItem (wpc, item, value, value_len + 1);
            text_tags++;
            free (value);
            free (item);
        }

        for (i = 0; i < num_binary_items; ++i) {
            int item_len, value_len;
            char *item, *value;

            item_len = WavpackGetBinaryTagItemIndexed (wpc, i, NULL, 0);
            item = (char *) malloc (item_len + 1);
            WavpackGetBinaryTagItemIndexed (wpc, i, item, item_len + 1);
            value_len = WavpackGetBinaryTagItem (wpc, item, NULL, 0);
            value = (char *) malloc (value_len);
            WavpackGetBinaryTagItem (wpc, item, value, value_len);
            binary_tags++;
            free (value);
            free (item);
        }

        WavpackAppendTagItem (wpc, "Artist", "The Googlers", strlen ("The Googlers"));
        WavpackAppendTagItem (wpc, "Title", "Fuzz Me All Night Long", strlen ("Fuzz Me All Night Long"));
        WavpackAppendTagItem (wpc, "Album", "Meet The Googlers", strlen ("Meet The Googlers"));
        WavpackAppendBinaryTagItem (wpc, "Cover Art (Front)", (const char *) data, size < 4096 ? size : 4096);
    }

    // Decode all
    if (num_chans && num_chans <= 256) {
        int32_t decoded_samples [BUF_SAMPLES * num_chans];
        unsigned char md5sum [16];
        int unpack_result;

        do {
            unpack_result = WavpackUnpackSamples (wpc, decoded_samples, BUF_SAMPLES);
            samples_decoded += unpack_result;
        } while (unpack_result);

        WavpackGetMD5Sum (wpc, md5sum);
    }

    // Seek to 1/3 of the way in plus 1000 samples (definitely not a block boundary)
    if (WavpackSeekSample64 (wpc, total_samples / 3 + 1000)) {
        ++seeks;

        // if we're still okay, try to write out the modified tags
        if (WavpackWriteTag (wpc))
            ++tag_writes;
    }

    WavpackCloseFile (wpc);

exit:
    if (!(times_called & debug_log_mask))
        printf ("LLVMFuzzerTestOneInput(): %lld calls, %lld opens, %lld seeks, %lld tag writes, %lld samples, %lld text & %lld binary tags\n",
            times_called, opens, seeks, tag_writes, samples_decoded, text_tags, binary_tags);

    return retval;
}

#ifdef STAND_ALONE_LENGTH   // max file length for stand-alone testing (sans fuzz)

int main (int argc, char **argv)
{
    unsigned char *buffer = (unsigned char *) malloc (STAND_ALONE_LENGTH);
    int index;

    // debug_log_mask = 0;

    for (index = 1; index < argc; ++index) {
        const char *filename = argv [index];
        FILE *infile = fopen (filename, "rb");
        int bytes_read;

        if (!infile) {
            fprintf (stderr, "can't open file %s!\n", filename);
            continue;
        }

        bytes_read = fread (buffer, 1, STAND_ALONE_LENGTH, infile);
        printf ("read %d bytes from file %s\n", bytes_read, filename);

        if (bytes_read == STAND_ALONE_LENGTH)
            printf ("warning: at maximum length, perhaps truncated!\n");

        fclose (infile);
        LLVMFuzzerTestOneInput(buffer, bytes_read);
    }

    free (buffer);

    return 0;
}

#endif
