/*!
 * \file
 * \brief   Main application file for PDS HW3 (CUDA)
 *
 * \author
 *    Christos Choutouridis AEM:8997
 *    <cchoutou@ece.auth.gr>
 */

#include <exception>
#include <stdexcept>
#include <iostream>
#include <algorithm>
#include <random>
#include <cuda_runtime.h>

#include "utils.hpp"
#include "config.h"
#include "bitonicsort.hpp"


// Global session data
Data_t          Data;
config_t        config;
Log             logger;
cudaDeviceProp 	device;

// Mersenne seeded from hw if possible. range: [type_min, type_max]
std::random_device  rd;
std::mt19937        gen(rd());

//! Performance timers for each one of the "costly" functions
Timing Timer_total, Timer_memory, Timer_sorting;


//! Init timing objects for extra rounds
void measurements_init() {
    if (config.perf > 1) {
        Timer_total.init(config.perf);
        Timer_memory.init(config.perf);
        Timer_sorting.init(config.perf);
    }
}

//! iterate ot the next round of measurements for all measurement objects
void measurements_next() {
    if (config.perf > 1) {
        Timer_total.next();
        Timer_memory.next();
        Timer_sorting.next();
    }
}

/*!
 * A small command line argument parser
 * \return  The status of the operation
 */
bool get_options(int argc, char* argv[]){
    bool status =true;

    // iterate over the passed arguments
    for (int i=1 ; i<argc ; ++i) {
        std::string arg(argv[i]);     // get current argument

        if (arg == "-q" || arg == "--array-size") {
            if (i+1 < argc) {
                config.arraySize = (ArraySize_t)1 << atoi(argv[++i]);
            }
            else {
                status = false;
            }
        }
        else if (arg == "-b" || arg == "--block-size") {
            if (i+1 < argc) {
                config.blockSize = atoi(argv[++i]);
            }
            else {
                status = false;
            }
        }
        else if (arg == "--validation") {
            config.validation = true;
        }
        else if (arg == "--perf") {
            if (i+1 < argc) {
                config.perf = atoi(argv[++i]);
            }
            else {
                status = false;
            }
        }
        else if (arg == "-v" || arg == "--verbose") {
            config.verbose = true;
        }
        else if (arg == "--version") {
            std::cout << STR(TARGET) << " - A GPU accelerated bitonic sort utility (V" << STR(CODE_VERSION)<< ") \n";
            std::cout << "version: " << version << "\n\n";
            exit(0);
        }
        else if (arg == "-h" || arg == "--help") {
            std::cout << STR(TARGET) << " - A GPU accelerated bitonic sort utility (V" << STR(CODE_VERSION)<< ") \n\n";
            std::cout << "  " << STR(TARGET) << " -q <N> -b <N> [--validation] [--perf <N>] [-v]\n";
            std::cout << "  " << STR(TARGET) << " -h\n";
            std::cout << "  " << STR(TARGET) << " --version\n";
            std::cout << '\n';
            std::cout << "Options:\n\n";
            std::cout << "   -q | --array-size <N>\n";
            std::cout << "      Selects the array size according to size = 2^N\n";
            std::cout << "      [Size must be larger than 2 * blockSize]\n";
            std::cout << "      [Default is 2^16]\n\n";
            std::cout << "   -b | --block-size <N>\n";
            std::cout << "      Selects the number of CUDA threads per block\n";
            std::cout << "      [Size has to be multiple of device's warp size (usually 32)\n";
            std::cout << "      [Default is the maximum device supported number. For ex: (GTX 1650) block-size=1024]\n\n";
            std::cout << "   --validation\n";
            std::cout << "      Request a full validation at the end\n\n";
            std::cout << "   --perf <N> \n";
            std::cout << "      Enable performance timing measurements and prints, and repeat\n";
            std::cout << "      the sorting <N> times.\n\n";
            std::cout << "   -v | --verbose\n";
            std::cout << "      Request a more verbose output to stdout.\n\n";
            std::cout << "   -h | --help\n";
            std::cout << "      Prints this and exit.\n\n";
            std::cout << "   --version\n";
            std::cout << "      Prints version and exit.\n\n";
            std::cout << "Examples:\n\n";
            std::cout << "  " << STR(TARGET) << " -q 24\n";
            std::cout << "      Runs bitonic sort on an 2^24 points array, using GPU acceleration\n\n";
            std::cout << "  " << STR(TARGET) << " --validation --perf 5 -b 512 -q 26\n";
            std::cout << "      Runs bitonic sort on an 2^26 points array 5 times, using GPU acceleration with\n";
            std::cout << "      512 threads per block, performs a validation check at the end and prints the time\n";
            std::cout << "      of the median.\n\n";

            exit(0);
        }
        else {   // parse error
            std::cout << "Invocation error. Try -h for details.\n";
            status = false;
        }
    }

    // Check configuration requirements
    if (config.blockSize % device.warpSize)
    	throw std::runtime_error("[Config] - Number of threads per block is not an exact multiple of warp size");
    if (config.arraySize < 2*config.blockSize)
    	throw std::runtime_error("[Config] - Unsupported array size (smaller than "
                                 + std::to_string(SizeToThreadsRatio*config.blockSize) + ")");
    if (device.totalGlobalMem < config.arraySize * sizeof(Value_t))
    	throw std::runtime_error("[CUDA] - Unsupported array size: "
                                 + std::to_string(config.arraySize * sizeof(Value_t))
                                 + " (larger than GPU's: " + std::to_string(device.totalGlobalMem) + ")");

    return status;
}

/*!
 * A simple validator for the entire distributed process
 *
 * @tparam DataT    A buffer type with random access iterator.
 *
 * @param data      [DataT] The data
 * @return          [bool]  True if sorted in ascending order
 */
template<typename DataT>
bool validator(DataT& data) {
    return std::is_sorted(data.begin(), data.end());
}

/*!
 * Initializes the environment, must called from each process
 *
 * @param argc  [int*]      POINTER to main's argc argument
 * @param argv  [char***]   POINTER to main's argv argument
 */
void init(int* argc, char*** argv) {

    // Get device configuration
    if (cudaGetDeviceProperties(&device, 0) == cudaSuccess){
        config.blockSize = static_cast<size_t>(device.maxThreadsPerBlock);

        // try to read command line
        if (!get_options(*argc, *argv))
            exit(1);

        // Prepare vector and timing data
        Data.resize(config.arraySize);
        measurements_init();
    }
    else {
        // try to read command line for -h or --version
        get_options(*argc, *argv);
        throw std::runtime_error("[CUDA] - Can not read GPU");
    }
}

#if !defined TESTING
/*!
 * @return Returns 0, but.... we may throw or exit(0) / exit(1)
 */
int main(int argc, char* argv[]) try {

    // Init everything
    init(&argc, &argv);

    logger << "Code version:   " << 'V' << STR(CODE_VERSION) << logger.endl;
    logger << "Array size:     " << config.arraySize << " (Q=" << static_cast<size_t>(log2(config.arraySize))<< ")" << logger.endl;
    logger << "Repeated sorts: " << config.perf << logger.endl;
    logger << "GPU:            " << device.name << logger.endl;
    logger << "Block size:     " << config.blockSize << logger.endl;

    for (size_t it = 0 ; it < config.perf ; ++it) {
        // Initialize local data
        logger << "Initialize array ... ";
        std::uniform_int_distribution<Value_t > dis(
                std::numeric_limits<Value_t>::min(),
                std::numeric_limits<Value_t>::max()
        );
        std::generate(Data.begin(), Data.end(), [&]() { return dis(gen); });
        logger << " Done." << logger.endl;

        // Run distributed sort
        logger << "Start sorting    ... ";
        Timer_total.start();
        bitonicSort(Data);
        Timer_total.stop();
        measurements_next();
        logger << " Done." << logger.endl;
    }

    // Print-outs and validation
    if (config.perf > 1) {
        Timing::print_duration(Timer_total.median(),  "Total   ");
        Timing::print_duration(Timer_memory.median(), "Mem-xch ");
        Timing::print_duration(Timer_sorting.median(),"Sorting ");
    }
    if (config.validation) {
        // If requested, we have the chance to fail!
        std::cout << "[Validation] Results validation ...";
        bool val = validator(Data);
        std::cout << ((val) ? "\x1B[32m [PASSED] \x1B[0m\n" : " \x1B[31m [FAILED] \x1B[0m\n");
    }
    return 0;
}
catch (std::exception& e) {
    //we probably pollute the user's screen. Comment `cerr << ...` if you don't like it.
    std::cerr << "Error: " << e.what() << '\n';
    exit(1);
}

#else

#include <gtest/gtest.h>
#include <exception>

/*!
 * The testing version of our program
 */
GTEST_API_ int main(int argc, char **argv) try {
   testing::InitGoogleTest(&argc, argv);
   return RUN_ALL_TESTS();
}
catch (std::exception& e) {
    std::cout << "Exception: " << e.what() << '\n';
}

#endif
