/* Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *  * Neither the name of NVIDIA CORPORATION nor the names of its
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

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

#include "commonDefs.hpp"
#define CU_INIT_UUID
#include <cmath>

#define UNITS_Time "ms"
#define UNITS_BW   "MB/s"
#define KB_str     "KB"
#define MB_str     "MB"

struct resultsData
{
    char                resultsName[64];
    struct testResults *results;
    // this has MEMALLOC_TYPE_COUNT * results->numSizesToTest *
    // results->numMeasurements elements
    double            **runTimesInMs[MEMALLOC_TYPE_COUNT];
    double             *averageRunTimesInMs[MEMALLOC_TYPE_COUNT];
    double             *stdDevRunTimesInMs[MEMALLOC_TYPE_COUNT];
    double             *stdDevBandwidthInMBps[MEMALLOC_TYPE_COUNT];
    bool                printOnlyInVerbose;
    bool                reportAsBandwidth;
    struct resultsData *next;
};

struct testResults
{
    char                testName[64];
    unsigned int        numMeasurements;
    unsigned long      *sizesToTest;
    unsigned int        numSizesToTest;
    struct resultsData *resultsDataHead;
    struct resultsData *resultsDataTail;
};

unsigned int findNumSizesToTest(unsigned int minSize, unsigned int maxSize, unsigned int multiplier)
{
    unsigned int numSizesToTest = 0;
    while (minSize <= maxSize) {
        numSizesToTest++;
        minSize *= multiplier;
    }
    return numSizesToTest;
}

int compareDoubles(const void *ptr1, const void *ptr2) { return (*(double *)ptr1 > *(double *)ptr2) ? 1 : -1; }

static inline double getTimeOrBandwidth(double runTimeInMs, unsigned long size, bool getBandwidth)
{
    return (getBandwidth) ? (1000 * (size / runTimeInMs)) / ONE_MB : runTimeInMs;
}

void createAndInitTestResults(struct testResults **ptrResults,
                              const char          *testName,
                              unsigned int         numMeasurements,
                              unsigned int         numSizesToTest)
{
    unsigned int        i;
    struct testResults *results;
    results = (struct testResults *)malloc(sizeof(struct testResults));
    memset(results, 0, sizeof(struct testResults));
    strcpy(results->testName, testName);
    results->numMeasurements = numMeasurements;
    results->numSizesToTest  = numSizesToTest;
    results->sizesToTest     = (unsigned long *)malloc(numSizesToTest * sizeof(unsigned long));
    results->resultsDataHead = NULL;
    results->resultsDataTail = NULL;

    *ptrResults = results;
}

unsigned long *getPtrSizesToTest(struct testResults *results) { return results->sizesToTest; }

void createResultDataAndAddToTestResults(struct resultsData **ptrData,
                                         struct testResults  *results,
                                         const char          *resultsName,
                                         bool                 printOnlyInVerbose,
                                         bool                 reportAsBandwidth)
{
    unsigned int        i, j;
    struct resultsData *data;
    data = (struct resultsData *)malloc(sizeof(struct resultsData));
    memset(data, 0, sizeof(struct resultsData));
    strcpy(data->resultsName, resultsName);
    data->results = results;
    for (i = 0; i < MEMALLOC_TYPE_COUNT; i++) {
        data->runTimesInMs[i] = (double **)malloc(results->numSizesToTest * sizeof(double *));
        for (j = 0; j < results->numSizesToTest; j++) {
            data->runTimesInMs[i][j] = (double *)malloc(results->numMeasurements * sizeof(double));
        }
        data->averageRunTimesInMs[i]   = (double *)malloc(results->numSizesToTest * sizeof(double));
        data->stdDevRunTimesInMs[i]    = (double *)malloc(results->numSizesToTest * sizeof(double));
        data->stdDevBandwidthInMBps[i] = (double *)malloc(results->numSizesToTest * sizeof(double));
    }
    data->printOnlyInVerbose = printOnlyInVerbose;
    data->reportAsBandwidth  = reportAsBandwidth;
    data->next               = NULL;
    *ptrData                 = data;
    if (results->resultsDataHead == NULL) {
        results->resultsDataHead = data;
        results->resultsDataTail = data;
    }
    else {
        results->resultsDataTail->next = data;
        results->resultsDataTail       = data;
    }
}

double *getPtrRunTimesInMs(struct resultsData *data, int allocType, int sizeIndex)
{
    return data->runTimesInMs[allocType][sizeIndex];
}

void freeTestResultsAndAllResultsData(struct testResults *results)
{
    struct resultsData *data, *dataToFree;
    unsigned int        i, j;
    for (data = results->resultsDataHead; data != NULL;) {
        for (i = 0; i < MEMALLOC_TYPE_COUNT; i++) {
            for (j = 0; j < results->numSizesToTest; j++) {
                free(data->runTimesInMs[i][j]);
            }
            free(data->runTimesInMs[i]);
            free(data->averageRunTimesInMs[i]);
            free(data->stdDevRunTimesInMs[i]);
            free(data->stdDevBandwidthInMBps[i]);
        }
        dataToFree = data;
        data       = data->next;
        free(dataToFree);
    }
    free(results->sizesToTest);
    free(results);
}

void calculateAverageAndStdDev(double *pAverage, double *pStdDev, double *allResults, unsigned int count)
{
    unsigned int i;
    double       average = 0.0;
    double       stdDev  = 0.0;
    for (i = 0; i < count; i++) {
        average += allResults[i];
    }
    average /= count;
    for (i = 0; i < count; i++) {
        stdDev += (allResults[i] - average) * (allResults[i] - average);
    }
    stdDev /= count;
    stdDev    = sqrt(stdDev);
    *pAverage = average;
    *pStdDev  = (average == 0.0) ? 0.0 : ((100.0 * stdDev) / average);
}

void calculateStdDevBandwidth(double *pStdDev, double *allResults, unsigned int count, unsigned long size)
{
    unsigned int i;
    double       bandwidth;
    double       average = 0.0;
    double       stdDev  = 0.0;
    for (i = 0; i < count; i++) {
        bandwidth = (1000 * (size / allResults[i])) / ONE_MB;
        average += bandwidth;
    }
    average /= count;
    for (i = 0; i < count; i++) {
        bandwidth = (1000 * (size / allResults[i])) / ONE_MB;
        stdDev += (bandwidth - average) * (bandwidth - average);
    }
    stdDev /= count;
    stdDev   = sqrt(stdDev);
    *pStdDev = (average == 0.0) ? 0.0 : ((100.0 * stdDev) / average);
}

void printTimesInTableFormat(struct testResults *results, struct resultsData *data, bool printAverage, bool printStdDev)
{
    unsigned int i, j;
    bool         printStdDevBandwidth = printStdDev && data->reportAsBandwidth;
    printf("Size_KB");
    for (i = 0; i < MEMALLOC_TYPE_COUNT; i++) {
        printf("\t%7s", memAllocTypeShortStr[i]);
    }
    printf("\n");
    for (j = 0; j < results->numSizesToTest; j++) {
        printf("%lu", results->sizesToTest[j] / ONE_KB);
        for (i = 0; i < MEMALLOC_TYPE_COUNT; i++) {
            printf(data->reportAsBandwidth ? "\t%7.2lf" : "\t%7.3lf",
                   printStdDevBandwidth ? data->stdDevBandwidthInMBps[i][j]
                                        : getTimeOrBandwidth(printAverage ? data->averageRunTimesInMs[i][j]
                                                                          : data->stdDevRunTimesInMs[i][j],
                                                             results->sizesToTest[j],
                                                             data->reportAsBandwidth));
        }
        printf("\n");
    }
}

void printAllResultsInVerboseMode(struct testResults *results, struct resultsData *data)
{
    unsigned int i, j, k;
    for (i = 0; i < MEMALLOC_TYPE_COUNT; i++) {
        printf("Verbose mode, printing all results for %s\n", memAllocTypeStr[i]);
        printf("Instance");
        for (j = 0; j < results->numSizesToTest; j++) {
            printf("\t%lu", results->sizesToTest[j] / ONE_KB);
        }
        printf("\n");
        for (k = 0; k < results->numMeasurements; k++) {
            printf("%u", k);
            for (j = 0; j < results->numSizesToTest; j++) {
                printf(
                    data->reportAsBandwidth ? "\t%7.2lf" : "\t%7.3lf",
                    getTimeOrBandwidth(data->runTimesInMs[i][j][k], results->sizesToTest[j], data->reportAsBandwidth));
            }
            printf("\n");
        }
    }
}

void printResults(struct testResults *results, bool print_launch_transfer_results, bool print_std_deviation)
{
    char                vulcanPrint[256];
    char                resultNameNoSpaces[64];
    unsigned int        i, j, k;
    struct resultsData *resultsIter;
    bool                sizeGreaterThan1MB;
    for (resultsIter = results->resultsDataHead; resultsIter != NULL; resultsIter = resultsIter->next) {
        if (!verboseResults && resultsIter->printOnlyInVerbose) {
            continue;
        }
        if (!print_launch_transfer_results) {
            if (!(strcmp(resultsIter->resultsName, "Overall Time") == 0)) {
                continue;
            }
        }
        // regular print
        printf("\n%s For %s ", resultsIter->resultsName, results->testName);
        printf("\n");
        for (j = 0; j < results->numSizesToTest; j++) {
            for (i = 0; i < MEMALLOC_TYPE_COUNT; i++) {
                calculateAverageAndStdDev(&resultsIter->averageRunTimesInMs[i][j],
                                          &resultsIter->stdDevRunTimesInMs[i][j],
                                          resultsIter->runTimesInMs[i][j],
                                          results->numMeasurements);
                if (resultsIter->reportAsBandwidth) {
                    calculateStdDevBandwidth(&resultsIter->stdDevBandwidthInMBps[i][j],
                                             resultsIter->runTimesInMs[i][j],
                                             results->numMeasurements,
                                             results->sizesToTest[j]);
                }
            }
        }
        printf("\nPrinting Average of %u measurements in (%s)\n",
               results->numMeasurements,
               resultsIter->reportAsBandwidth ? UNITS_BW : UNITS_Time);
        printTimesInTableFormat(results, resultsIter, true, false);
        if (print_std_deviation) {
            printf("\nPrinting Standard Deviation as %% of average of %u measurements\n", results->numMeasurements);
            printTimesInTableFormat(results, resultsIter, false, true);
        }
        if (verboseResults) {
            printAllResultsInVerboseMode(results, resultsIter);
        }
    }
}
