// Copyright 2020 Joe Drago. All rights reserved.
// SPDX-License-Identifier: BSD-2-Clause

// #define WIN32_MEMORY_LEAK_DETECTION
#ifdef WIN32_MEMORY_LEAK_DETECTION
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#endif

#include "avif/avif.h"

#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#if defined(_WIN32)

#include <windows.h>

typedef struct NextFilenameData
{
    int didFirstFile;
    HANDLE handle;
    WIN32_FIND_DATA wfd;
} NextFilenameData;

static const char * nextFilename(const char * parentDir, const char * extension, NextFilenameData * nfd)
{
    for (;;) {
        if (nfd->didFirstFile) {
            if (FindNextFile(nfd->handle, &nfd->wfd) == 0) {
                // No more files
                break;
            }
        } else {
            char filenameBuffer[2048];
            snprintf(filenameBuffer, sizeof(filenameBuffer), "%s\\*", parentDir);
            filenameBuffer[sizeof(filenameBuffer) - 1] = 0;
            nfd->handle = FindFirstFile(filenameBuffer, &nfd->wfd);
            if (nfd->handle == INVALID_HANDLE_VALUE) {
                return NULL;
            }
            nfd->didFirstFile = 1;
        }

        // If we get here, we should have a valid wfd
        const char * dot = strrchr(nfd->wfd.cFileName, '.');
        if (dot) {
            ++dot;
            if (!strcmp(dot, extension)) {
                return nfd->wfd.cFileName;
            }
        }
    }

    FindClose(nfd->handle);
    nfd->handle = INVALID_HANDLE_VALUE;
    nfd->didFirstFile = 0;
    return NULL;
}

#else
#include <dirent.h>
typedef struct NextFilenameData
{
    DIR * dir;
} NextFilenameData;

static const char * nextFilename(const char * parentDir, const char * extension, NextFilenameData * nfd)
{
    if (!nfd->dir) {
        nfd->dir = opendir(parentDir);
        if (!nfd->dir) {
            return NULL;
        }
    }

    struct dirent * entry;
    while ((entry = readdir(nfd->dir)) != NULL) {
        const char * dot = strrchr(entry->d_name, '.');
        if (dot) {
            ++dot;
            if (!strcmp(dot, extension)) {
                return entry->d_name;
            }
        }
    }

    closedir(nfd->dir);
    nfd->dir = NULL;
    return NULL;
}
#endif

typedef struct avifIOTestReader
{
    avifIO io;
    avifROData rodata;
    size_t availableBytes;
} avifIOTestReader;

static avifResult avifIOTestReaderRead(struct avifIO * io, uint32_t readFlags, uint64_t offset, size_t size, avifROData * out)
{
    // printf("avifIOTestReaderRead offset %" PRIu64 " size %zu\n", offset, size);

    if (readFlags != 0) {
        // Unsupported readFlags
        return AVIF_RESULT_IO_ERROR;
    }

    avifIOTestReader * reader = (avifIOTestReader *)io;

    // Sanitize/clamp incoming request
    if (offset > reader->rodata.size) {
        // The offset is past the end of the buffer.
        return AVIF_RESULT_IO_ERROR;
    }
    if (offset == reader->rodata.size) {
        // The parser is *exactly* at EOF: return a 0-size pointer to any valid buffer
        offset = 0;
        size = 0;
    }
    uint64_t availableSize = reader->rodata.size - offset;
    if (size > availableSize) {
        size = (size_t)availableSize;
    }

    if (offset > reader->availableBytes) {
        return AVIF_RESULT_WAITING_ON_IO;
    }
    if (size > (reader->availableBytes - offset)) {
        return AVIF_RESULT_WAITING_ON_IO;
    }

    out->data = reader->rodata.data + offset;
    out->size = size;
    return AVIF_RESULT_OK;
}

static void avifIOTestReaderDestroy(struct avifIO * io)
{
    avifFree(io);
}

static avifIOTestReader * avifIOCreateTestReader(const uint8_t * data, size_t size)
{
    avifIOTestReader * reader = avifAlloc(sizeof(avifIOTestReader));
    memset(reader, 0, sizeof(avifIOTestReader));
    reader->io.destroy = avifIOTestReaderDestroy;
    reader->io.read = avifIOTestReaderRead;
    reader->io.sizeHint = size;
    reader->io.persistent = AVIF_TRUE;
    reader->rodata.data = data;
    reader->rodata.size = size;
    return reader;
}

#define FILENAME_MAX_LENGTH 2047

static int runIOTests(const char * dataDir)
{
    printf("AVIF Test Suite: Running IO Tests...\n");

    static const char * ioSuffix = "/io/";

    char ioDir[FILENAME_MAX_LENGTH + 1];
    size_t dataDirLen = strlen(dataDir);
    size_t ioSuffixLen = strlen(ioSuffix);

    if ((dataDirLen + ioSuffixLen) > FILENAME_MAX_LENGTH) {
        printf("Path too long: %s\n", dataDir);
        return 1;
    }
    strcpy(ioDir, dataDir);
    strcat(ioDir, ioSuffix);
    size_t ioDirLen = strlen(ioDir);

    int retCode = 0;

    NextFilenameData nfd;
    memset(&nfd, 0, sizeof(nfd));
    avifRWData fileBuffer = AVIF_DATA_EMPTY;
    const char * filename = nextFilename(ioDir, "avif", &nfd);
    for (; filename != NULL; filename = nextFilename(ioDir, "avif", &nfd)) {
        char fullFilename[FILENAME_MAX_LENGTH + 1];
        size_t filenameLen = strlen(filename);
        if ((ioDirLen + filenameLen) > FILENAME_MAX_LENGTH) {
            printf("Path too long: %s\n", filename);
            retCode = 1;
            break;
        }
        strcpy(fullFilename, ioDir);
        strcat(fullFilename, filename);

        FILE * f = fopen(fullFilename, "rb");
        if (!f) {
            printf("Can't open for read: %s\n", filename);
            retCode = 1;
            break;
        }
        fseek(f, 0, SEEK_END);
        size_t fileSize = ftell(f);
        fseek(f, 0, SEEK_SET);
        if (avifRWDataRealloc(&fileBuffer, fileSize) != AVIF_RESULT_OK) {
            printf("Out of memory when allocating buffer to read file: %s\n", filename);
            fclose(f);
            retCode = 1;
            break;
        }
        if (fread(fileBuffer.data, 1, fileSize, f) != fileSize) {
            printf("Can't read entire file: %s\n", filename);
            fclose(f);
            retCode = 1;
            break;
        }
        fclose(f);

        avifDecoder * decoder = avifDecoderCreate();
        avifIOTestReader * io = avifIOCreateTestReader(fileBuffer.data, fileBuffer.size);
        avifDecoderSetIO(decoder, (avifIO *)io);

        for (int pass = 0; pass < 4; ++pass) {
            io->io.persistent = ((pass % 2) == 0);
            decoder->ignoreExif = decoder->ignoreXMP = (pass < 2);

            // Slowly pretend to have streamed-in / downloaded more and more bytes
            avifResult parseResult = AVIF_RESULT_UNKNOWN_ERROR;
            for (io->availableBytes = 0; io->availableBytes <= io->io.sizeHint; ++io->availableBytes) {
                parseResult = avifDecoderParse(decoder);
                if (parseResult == AVIF_RESULT_WAITING_ON_IO) {
                    continue;
                }
                if (parseResult != AVIF_RESULT_OK) {
                    retCode = 1;
                }

                printf("File: [%s @ %zu / %" PRIu64 " bytes, %s, %s] parse returned: %s\n",
                       filename,
                       io->availableBytes,
                       io->io.sizeHint,
                       io->io.persistent ? "Persistent" : "NonPersistent",
                       decoder->ignoreExif ? "IgnoreMetadata" : "Metadata",
                       avifResultToString(parseResult));
                break;
            }

            if (parseResult == AVIF_RESULT_OK) {
                for (; io->availableBytes <= io->io.sizeHint; ++io->availableBytes) {
                    avifExtent extent;
                    avifResult extentResult = avifDecoderNthImageMaxExtent(decoder, 0, &extent);
                    if (extentResult != AVIF_RESULT_OK) {
                        retCode = 1;

                        printf("File: [%s @ %zu / %" PRIu64 " bytes, %s, %s] maxExtent returned: %s\n",
                               filename,
                               io->availableBytes,
                               io->io.sizeHint,
                               io->io.persistent ? "Persistent" : "NonPersistent",
                               decoder->ignoreExif ? "IgnoreMetadata" : "Metadata",
                               avifResultToString(extentResult));
                    } else {
                        avifResult nextImageResult = avifDecoderNextImage(decoder);
                        if (nextImageResult == AVIF_RESULT_WAITING_ON_IO) {
                            continue;
                        }
                        if (nextImageResult != AVIF_RESULT_OK) {
                            retCode = 1;
                        }

                        printf("File: [%s @ %zu / %" PRIu64 " bytes, %s, %s] nextImage [MaxExtent off %" PRIu64 ", size %zu] returned: %s\n",
                               filename,
                               io->availableBytes,
                               io->io.sizeHint,
                               io->io.persistent ? "Persistent" : "NonPersistent",
                               decoder->ignoreExif ? "IgnoreMetadata" : "Metadata",
                               extent.offset,
                               extent.size,
                               avifResultToString(nextImageResult));
                    }
                    break;
                }
            }
        }

        avifDecoderDestroy(decoder);
    }

    avifRWDataFree(&fileBuffer);
    return retCode;
}

static void syntax(void)
{
    fprintf(stderr, "Syntax: aviftest dataDir\n");
}

int main(int argc, char * argv[])
{
    const char * dataDir = NULL;

#ifdef WIN32_MEMORY_LEAK_DETECTION
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
    // _CrtSetBreakAlloc(2906);
#endif

    // Parse cmdline
    for (int i = 1; i < argc; ++i) {
        char * arg = argv[i];
        if (!strcmp(arg, "--io-only")) {
            fprintf(stderr, "WARNING: --io-only is deprecated; ignoring.\n");
        } else if (dataDir == NULL) {
            dataDir = arg;
        } else {
            fprintf(stderr, "Too many positional arguments: %s\n", arg);
            syntax();
            return 1;
        }
    }

    // Verify all required args were set
    if (dataDir == NULL) {
        fprintf(stderr, "dataDir is required, bailing out.\n");
        syntax();
        return 1;
    }

    setbuf(stdout, NULL);

    char codecVersions[256];
    avifCodecVersions(codecVersions);
    printf("Codec Versions: %s\n", codecVersions);
    printf("Test Data Dir : %s\n", dataDir);

    int retCode = runIOTests(dataDir);
    if (retCode == 0) {
        printf("AVIF Test Suite: Complete.\n");
    } else {
        printf("AVIF Test Suite: Failed.\n");
    }
    return retCode;
}
