/**
 * \file    utils.hpp
 * \brief   Utilities header
 *
 * \author
 *    Christos Choutouridis AEM:8997
 *    <cchoutou@ece.auth.gr>
 */
#ifndef UTILS_HPP_
#define UTILS_HPP_

#include <iostream>
#include <chrono>
#include <unistd.h>
#include <hdf5.h>

#include "matrix.hpp"
#include "config.h"

/*!
 * A Logger for entire program.
 */
struct Log {
   struct Endl {} endl;    //!< a tag object to to use it as a new line request.

   //! We provide logging via << operator
   template<typename T>
   Log& operator<< (T&& t) {
      if (session.verbose) {
         if (line_) {
            std::cout << "[Log]: " << t;
            line_ = false;
         }
         else
            std::cout << t;
      }
      return *this;
   }
   // overload for special end line handling
   Log& operator<< (Endl e) { (void)e;
      if (session.verbose) {
         std::cout << '\n';
         line_ = true;
      }
      return *this;
   }
private:
   bool line_ {true};
};

extern Log logger;

/*!
 * A small timing utility based on chrono.
 */
struct Timing{
   using Tpoint = std::chrono::steady_clock::time_point;
   using microseconds = std::chrono::microseconds;
   using milliseconds = std::chrono::milliseconds;
   using seconds = std::chrono::seconds;

   //! tool to mark the starting point
   Tpoint start () noexcept { return start_ = std::chrono::steady_clock::now(); }
   //! tool to mark the ending point
   Tpoint stop ()  noexcept { return stop_ = std::chrono::steady_clock::now(); }

   auto dt () noexcept {
      return std::chrono::duration_cast<std::chrono::microseconds>(stop_ - start_).count();
   }
   //! tool to print the time interval
   void print_dt (const char* what) noexcept {
      if (session.timing) {
         auto t = stop_ - start_;
         if       (std::chrono::duration_cast<microseconds>(t).count() < 10000)
            std::cout << "[Timing]: " << what << ": " << std::to_string(std::chrono::duration_cast<microseconds>(t).count()) << " [usec]\n";
         else if (std::chrono::duration_cast<milliseconds>(t).count() < 10000)
            std::cout << "[Timing]: " << what << ": " << std::to_string(std::chrono::duration_cast<milliseconds>(t).count()) << " [msec]\n";
         else
            std::cout << "[Timing]: " << what << ": " << std::to_string(std::chrono::duration_cast<seconds>(t).count()) << " [sec]\n";
      }
   }
private:
   Tpoint start_;
   Tpoint stop_;
};



struct Mtx {

   template<typename MatrixType, HDF5_type Type>
   static void load(const std::string& filename, const std::string& dataset, MatrixType& matrix) {

      hid_t file_id{}, dataset_id{}, dataspace_id{};
      herr_t read_st;
      do {
         // Open file
         logger << "Load HDF5 file: " << filename << " Dataset: " << dataset << "...";
         if ((file_id = H5Fopen(filename.c_str(), H5F_ACC_RDONLY, H5P_DEFAULT)) < 0)
            break;

         // Open dataset
         if ((dataset_id = H5Dopen2(file_id, dataset.c_str(), H5P_DEFAULT)) < 0)
            break;

         // Get dataspace and allocate memory for read buffer
         if ((dataspace_id = H5Dget_space(dataset_id)) < 0)
            break;
         hsize_t dims[2];
         H5Sget_simple_extent_dims(dataspace_id, dims, NULL);
         matrix.resize(dims[0], dims[1]);

         // Read the dataset
         // ToDo: Come up with a better way to do this
         if constexpr (Type == HDF5_type::DOUBLE) {
            if ((read_st = H5Dread(dataset_id, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, matrix.data())) < 0)
               break;
         }
         else if (Type == HDF5_type::FLOAT) {
            if ((read_st = H5Dread(dataset_id, H5T_NATIVE_FLOAT, H5S_ALL, H5S_ALL, H5P_DEFAULT, matrix.data())) < 0)
               break;
         }
         else if (Type == HDF5_type::UINT) {
            if ((read_st = H5Dread(dataset_id, H5T_NATIVE_UINT, H5S_ALL, H5S_ALL, H5P_DEFAULT, matrix.data())) < 0)
               break;
         }
         else if (Type == HDF5_type::INT) {
            if ((read_st = H5Dread(dataset_id, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, matrix.data())) < 0)
               break;
         }
         // Done
         H5Dclose(dataset_id);
         H5Sclose(dataspace_id);
         H5Fclose(file_id);
         logger << " Done" << logger.endl;
         return;
      } while (0);

      // Error: close everything (if possible) and return false
      H5Dclose(dataset_id);
      H5Sclose(dataspace_id);
      H5Fclose(file_id);
      throw std::runtime_error("Cannot store to " + filename + " dataset:" + dataset + '\n');
   }


   template<typename MatrixType, HDF5_type Type>
   static void store(const std::string& filename, const std::string& dataset, MatrixType& matrix) {

      hid_t file_id{}, dataset_id{}, dataspace_id{};
      herr_t write_st;
      do {
         // Try to open the file in read-write mode
         logger << "Store HDF5 file: " << filename << " Dataset: " << dataset << "...";
         if (access(session.outMtxFile.c_str(), F_OK) == 0){
            if ((file_id = H5Fopen(filename.c_str(), H5F_ACC_RDWR, H5P_DEFAULT)) < 0)
               break;
         }
         else {
            if ((file_id = H5Fcreate(filename.c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT)) < 0)
               break;
         }

         // Create the dataspace for the dataset
         hsize_t dims[] = { matrix.rows(), matrix.columns() };
         if ((dataspace_id = H5Screate_simple(2, dims, NULL)) < 0)
            break;

         // ToDo: Come up with a better way to do this
         if constexpr (Type == HDF5_type::DOUBLE) {
            // Create the dataset with default properties
            if ((dataset_id = H5Dcreate2(
                  file_id, dataset.c_str(), H5T_NATIVE_DOUBLE, dataspace_id, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT)) < 0)
               break;
            // Write the data to the dataset
            if ((write_st = H5Dwrite(dataset_id, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, matrix.data())) <0 )
               break;
         }
         else if (Type == HDF5_type::FLOAT) {
            // Create the dataset with default properties
            if ((dataset_id = H5Dcreate2(
                  file_id, dataset.c_str(), H5T_NATIVE_FLOAT, dataspace_id, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT)) < 0)
               break;
            // Write the data to the dataset
            if ((write_st = H5Dwrite(dataset_id, H5T_NATIVE_FLOAT, H5S_ALL, H5S_ALL, H5P_DEFAULT, matrix.data())) <0 )
               break;
         }
         else if (Type == HDF5_type::UINT) {
            // Create the dataset with default properties
            if ((dataset_id = H5Dcreate2(
                  file_id, dataset.c_str(), H5T_NATIVE_UINT, dataspace_id, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT)) < 0)
               break;
            // Write the data to the dataset
            if ((write_st = H5Dwrite(dataset_id, H5T_NATIVE_UINT, H5S_ALL, H5S_ALL, H5P_DEFAULT, matrix.data())) <0 )
               break;
         }
         else if (Type == HDF5_type::INT) {
            // Create the dataset with default properties
            if ((dataset_id = H5Dcreate2(
                  file_id, dataset.c_str(), H5T_NATIVE_INT, dataspace_id, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT)) < 0)
               break;
            // Write the data to the dataset
            if ((write_st = H5Dwrite(dataset_id, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, matrix.data())) <0 )
               break;
         }
         // Close the dataset, dataspace, and file
         H5Dclose(dataset_id);
         H5Sclose(dataspace_id);
         H5Fclose(file_id);
         logger << " Done" << logger.endl;
         return;
      } while (0);

      // Error: close everything (if possible) and return false
      H5Dclose(dataset_id);
      H5Sclose(dataspace_id);
      H5Fclose(file_id);
      throw std::runtime_error("Cannot store " + filename + " with dataset:" + dataset +'\n');
   }
};



#endif /* UTILS_HPP_ */
