/*
 * This file provides various general utility functions which are used throughout ATC.
 */

#pragma once

#include <cassert>
#include <cstdint>
#include <chrono>
#include <vector>
#include <iostream>
#include <cmath>
#include <Eigen/Core>
#include <functional>
#include <stdexcept>
#include <map>
#include <numeric>

#include "Options.hpp"
#include "DataType.hpp"

namespace atc {
	
	// Basic utilities
	
	// Computes the product of all values in the given std::vector .
	inline size_t product(const std::vector<size_t>& values) {
		return std::accumulate(values.begin(), values.end(), size_t(1), std::multiplies<size_t>()); // Just using "1" leads to overflow issues with very large datasets
	}
	
	/*
	 * Returns a time_point representing the current time.
	 * Simply short-hand for the verbose std::chrono call
	 */
	inline std::chrono::high_resolution_clock::time_point time_now() {
		return std::chrono::high_resolution_clock::now();
	}
	
	/*
	 * Returns the time passed since the given time_point in seconds (resolution: nanoseconds).
	 * Simply short-hand for some verbose chrono work.
	 */
	inline double elapsed_time(const std::chrono::high_resolution_clock::time_point& time_point) {
		return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - time_point).count()*1.0e-9;
	}
	
	/*
	 * Generic min/max functions using templates.
	 * Allows comparison in between different numerical types, but always returns a value of the first type,
	 * so not always the right method to use in practice.
	 */
	template<typename T1, typename T2>
	T1 min(T1 a, T2 b) {
		return (a < b ? a : b);
	}
	template<typename T1, typename T2>
	T1 max(T1 a, T2 b) {
		return (a > b ? a : b);
	}
	
	// Vector utilities
	
	// Forces the STL to clear the vector
	template<typename T>
	void force_clear(std::vector<T>& vector) {
		std::vector<T>().swap(vector);
	}
	
	// Prints a string representation of the given vector. If truncate is not 0, only the first truncate elements of the vector are printed.
	template<typename T>
	void print_vector(const std::vector<T>& vector, size_t truncate = 0) {
		std::cout << "[";
		for (size_t i = 0; i < (truncate ? std::min(truncate, vector.size()) : vector.size()); i++) {
			std::cout << (std::is_same<T, uint8_t>::value ? uint64_t(vector[i]) : vector[i]);
			if (i < vector.size() - 1)
				std::cout << ", ";
		}
		std::cout << "]" << std::endl;
	}
	
	// Computes the squared Frobenius/Euclidean norm of the given vector.
	template<typename ftype>
	double squared_norm(const std::vector<ftype>& vector) {
		return Eigen::Map<const Eigen::Matrix<ftype, Eigen::Dynamic, 1>>(vector.data(), vector.size()).squaredNorm();
	}
	
	// Computes the Frobenius/Euclidean norm of the given vector.
	template<typename ftype>
	double norm(const std::vector<ftype>& vector) {
		return sqrt(squared_norm(vector));
	}
	
	// 
	
	/*
	 * Performs an elementwise binary operation in between two vectors of the same size and returns the result (a vector of the same size).
	 * Valarrays do not work for the following functionality because they require T1 == T2, even if op(T1, T2) is defined.
	 */
	template<typename T1, typename T2, typename F>
	std::vector<T1> binary_op_vectors(const std::vector<T1>& v1, const std::vector<T2>& v2, const F& op) {
		assert(v1.size() == v2.size());
		std::vector<T1> res(v1.size());
		for (size_t i = 0; i < v1.size(); i++)
			res[i] = op(v1[i], v2[i]);
		return res;
	}
	
	// Short-hand for binary_op_vectors where the operation is the division operator.
	template<typename T1, typename T2>
	std::vector<T1> divide_vectors(const std::vector<T1>& v1, const std::vector<T2>& v2) {
		return binary_op_vectors(v1, v2, [] (const T1& x, const T2& y) {return x/y;});
	}
	
	// Single map that hardcodes the necessary atc::DataType conversions.
	static const std::map<DataType, uint8_t> data_type_to_id_map = {
		{DataType::uint8, 0},
		{DataType::uint16, 1},
		{DataType::uint32, 2},
		{DataType::uint64, 3},
		{DataType::int8, 4},
		{DataType::int16, 5},
		{DataType::int32, 6},
		{DataType::int64, 7},
		{DataType::float32, 8},
		{DataType::float64, 9},
	};
	
	// This function automatically inverts the map above, preventing the need of hardcoding each conversion twice.
	static std::map<uint8_t, DataType> create_id_to_dtype_map() {
		std::map<uint8_t, DataType> inverse_map;
		for (auto it = data_type_to_id_map.begin(); it != data_type_to_id_map.end(); it++)
			inverse_map[it->second] = it->first;
		return inverse_map;
	}
	
	// We call the function above only once during initialization and store the result in a variable to prevent recomputation.
	static const std::map<uint8_t, DataType> id_to_data_type_map = create_id_to_dtype_map();
	
	// Converts an atc::DataType value to the corresponding integer data type ID
	inline uint8_t data_type_to_id(DataType data_type) {
		return data_type_to_id_map.at(data_type);
	}
	
	// Converts an integer data type ID to the corresponding atc::DataType value
	inline DataType id_to_data_type(uint8_t data_type_id) {
		return id_to_data_type_map.at(data_type_id);
	}
	
	// Computes the bounds of a given amount of blocks over a given range for quantization/dequantization.
	inline std::vector<size_t> calculate_block_bounds(size_t full_range, size_t blocks) {
		std::vector<size_t> block_bounds(blocks + 1);
		for (size_t i = 0; i < blocks; i++)
			block_bounds[i] = (i*full_range)/blocks;
		block_bounds[blocks] = full_range;
		return block_bounds;
	}
	
}
