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

#include <exception>
#include <iostream>
#include <algorithm>
#include <random>

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


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

// 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;


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

//! iterate ot the next round of measurements for all measurement objects
void measurements_next() {
    if (config.perf > 1) {
        Timer_total.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 = 1 << 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 == "--ndebug") {
            config.ndebug = true;
        }
        else if (arg == "-v" || arg == "--verbose") {
            config.verbose = true;
        }
        else if (arg == "--version") {
            std::cout << "bitonic - A GPU accelerated sort utility\n";
            std::cout << "version: " << version << "\n\n";
            exit(0);
        }
        else if (arg == "-h" || arg == "--help") {
            std::cout << "distbitonic - A distributed sort utility\n\n";
            std::cout << "  distbitonic -q <N> [--validation] [--perf <N>] [--ndebug] [-v]\n";
            std::cout << "  distbitonic -h\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\n";
            std::cout << "   --validation\n";
            std::cout << "      Request a full validation at the end, performed by process rank 0\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 << "   --ndebug\n";
            std::cout << "      Skip debug breakpoint when on debug build.\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 << "   bitonic -q 24\n";
            std::cout << "      Runs bitonic with GPU acceleration with 2^24 array points\n\n";

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

    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) {
    // try to read command line
    if (!get_options(*argc, *argv))
        exit(1);

    // Prepare vector and timing data
    Data.resize(config.arraySize);
    measurements_init();
}

#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);

    for (size_t it = 0 ; it < config.perf ; ++it) {
        // Initialize local data
        logger << "Initialize local array of " << config.arraySize << " elements" << logger.endl;
        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); });
        // Run distributed sort
        logger << "Starting distributed 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");
    }
    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[32m [FAILED] \x1B[0m\n");
        if (Data.size() < 128) {
        	std::cout << "Data: ";
        	for (auto& v : Data) {
        		std::cout << (int)v << ", ";
        	}
        	std::cout << '\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
