#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <map>
#include <limits>
#include <algorithm>

#define BOOST_TEST_MODULE TestTuningXXXMBE
#include <boost/test/unit_test.hpp>

struct Column : public std::vector<std::string> {
		Column(const std::string& name_, const int size = 0, const int num_rows_estimate = 1000) : 
				std::vector<std::string>(size), name(name_)
		{
				this->reserve(num_rows_estimate);
		}

		// Return vector of double values, invalid elements set to NaN
		std::vector<double> dvalues() const {
				std::vector<double> vec;
				vec.reserve(this->size());
				
				const auto& conv_func = [](const std::string& strval) {
						double dval;
						try {
								dval = std::stod(strval);
						} catch (std::invalid_argument& exc) {
								dval = std::numeric_limits<double>::quiet_NaN();
						}
						return dval;
				};

				std::transform(this->cbegin(), this->cend(), std::back_inserter(vec), conv_func);
				return vec;
		}

		// Return vector of double values values, invalid elements set to std::numeric_limits<int>::min()
		std::vector<int> ivalues() const {
				std::vector<int> vec;
				vec.reserve(this->size());
				
				const auto& conv_func = [](const std::string& strval) {
						int ival;
						try {
								ival = std::stoi(strval);
						} catch (std::invalid_argument& exc) {
								ival = std::numeric_limits<int>::min();
						}
						return ival;
				};

				std::transform(this->cbegin(), this->cend(), std::back_inserter(vec), conv_func);
				return vec;
		}

		std::string name;
};


struct ColumnData {
		ColumnData(const std::string& file_name, const int num_columns_estimate=20) {
				raw_columns.reserve(num_columns_estimate);
				load_file(file_name);
		}

		void load_file(const std::string& file_name) {
				// Open file and read first line with column names
				std::ifstream ifs(file_name);
				std::string line, colname;

				std::getline(ifs, line);
				std::istringstream iss(line);
				while (iss >> colname) {
						column_names.push_back(colname);
						raw_columns.emplace_back(colname);
						columns[colname] = &(raw_columns.back());
				}
				const int num_columns = column_names.size();

				// Read remaining lines into std::string vectors
				int lineno = 1;
				while (std::getline(ifs, line)) {
						iss.str(line); iss.clear();
						int i=0;
						while (iss >> colname && i < num_columns) {
								raw_columns[i].push_back(colname);
								++i;
						}
						if (i >= num_columns && iss >> colname) {
								std::cout << "Warning: Ignoring extra column(s) on line " << lineno << std::endl;
						}
						++lineno;
				}
		}

		// Get data vectors of different types
		std::vector<double> get_dvector(const std::string& colname) const { return columns.at(colname)->dvalues(); }
		std::vector<int> get_ivector(const std::string& colname) const { return columns.at(colname)->ivalues(); }
		// Default is to return double values
		std::vector<double> operator[](const std::string& colname) const { return columns.at(colname)->dvalues(); }
		
		std::vector<std::string> column_names;
		std::vector<Column> raw_columns;
		std::map<std::string, Column*> columns;
};

BOOST_AUTO_TEST_CASE(CheckMassBalanceWithinXXXMBE)
{
		//std::string case_name(boost::unit_test::framework::master_test_suite().argv[1]);
		std::string case_name("01_TUNING_XXXMBE");
		std::string file_name = case_name + ".INFOITER";

		ColumnData data(file_name);
		auto rstep = data.get_ivector("ReportStep");
		auto tstep = data.get_ivector("TimeStep");
		auto mbo = data["MB_Oil"];
		auto mbw = data["MB_Water"];
		auto mbg = data["MB_Gas"];

		const int num_reports = 1 + *std::max_element(rstep.begin(), rstep.end());
		std::vector<double> max_mb;
		max_mb.reserve(num_reports);


		// Find the maximum mass balance error at each converged time step for each report step.. 
		const int nrows = rstep.size();
		int rcur = 0;
		int tcur = 0;
		double max_mb_step = std::numeric_limits<double>::min();
		for (int i=0; i<(nrows-1); ++i) {
				if (tcur != tstep[i+1] || rcur != rstep[i+1]) {
						max_mb_step = std::max({mbo[i], mbw[i], mbg[i], max_mb_step});
						tcur = tstep[i+1];
				}
				if (rcur != rstep[i+1]) {
						max_mb.push_back(max_mb_step);
						max_mb_step = std::numeric_limits<double>::min();
						rcur = rstep[i+1];
				}			
		}
		max_mb.push_back( std::max({mbo.back(), mbw.back(), mbg.back(), max_mb_step}) );

		BOOST_TEST_MESSAGE("---------------------------------------------------------------------------");
		BOOST_TEST_MESSAGE("Found the following converged max mass balance error (per report step):");
		for (auto& val : max_mb)
				BOOST_TEST_MESSAGE(val);
		BOOST_TEST_MESSAGE("---------------------------------------------------------------------------");


		BOOST_CHECK_MESSAGE( max_mb[0] < 1.0e-6, "max_mb[0] (= " << max_mb[0] << ") is not strictly less than 1.0e-6" );
		BOOST_CHECK_MESSAGE( max_mb[1] < 1.0e-8, "max_mb[1] (= " << max_mb[1] << ") is not strictly less than 1.0e-8"  );
		BOOST_CHECK_MESSAGE( max_mb[2] < 1.0e-10, "max_mb[2] (= " << max_mb[1] << ") is not strictly less than 1.0e-10"  );
  
}
