// Copyright (C) 2015 Deltares
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA

/**
 * @file rtcToolsBMI.cpp
 * @brief BMI bindings for RTC-Tools
 * @author Jorn Baayen
 * @version 0.1
 * @date 2015
 */

#include <iterator>
#include <stdexcept>
#include <sstream>
#include <fstream>
#include <limits>
#include <string>
#include <boost/filesystem.hpp>
#include <boost/property_tree/json_parser.hpp>

// These are defines in enums by RTC-Tools
#undef ABSOLUTE
#undef RELATIVE

#include "rtcToolsRuntime.h"
#include "piDiagInterface.h"

#if defined _WIN32
#define dllexp extern "C" __declspec(dllexport)

// Undefine min and max to avoid conflict with std::numeric_limits::{min,max}.
// See https://social.msdn.microsoft.com/Forums/vstudio/en-US/e94865f7-84cd-4912-8339-6ee95fb58825/numericlimitsdoublemax-not-recognized?forum=vcgeneral
#undef min
#undef max

#else
#define dllexp extern "C"
#endif

#define MAXSTRLEN 1024

static rtcToolsRuntime *runtime = NULL;
static double *cache = NULL;
static int step;
static double dt;
static double tStart;
static double tEnd;

// BMI interface

dllexp void initialize(const char *moduleDir)
{
	// Parse module configuration file
	// We do this 3Di-style, that is, with a "settings.json" in the given folder.
	boost::filesystem::path modulePath(moduleDir);

	boost::filesystem::path settingsPath(modulePath / "settings.json");
	std::ifstream f(settingsPath.string().c_str());

	std::string schemaDir;
	std::string    xmlDir;

	if ( boost::filesystem::exists(modulePath / "settings.json") ) {
    
		boost::property_tree::ptree pt;
		boost::property_tree::read_json(f, pt);

		// Look up paths from settings
		schemaDir = pt.get<std::string>("schemaDir");
		   xmlDir = pt.get<std::string>("xmlDir"   );

	} else {

		// Default setting if settings file does not exists
		schemaDir = "./bin";
		   xmlDir = ".";
	}

    // Resolve paths relative to module folder
    boost::filesystem::path schemaPath(schemaDir);
    boost::filesystem::path xmlPath(xmlDir);

    schemaPath = boost::filesystem::absolute(schemaPath, modulePath);
    xmlPath    = boost::filesystem::absolute(xmlPath   , modulePath);

	// Create RTC-Tools runtime object
	runtime = new rtcToolsRuntime(schemaPath.string().c_str(), xmlPath.string().c_str());

	// Cleanup
	f.close();

	// Check mode
	switch (runtime->getRuntimeSettings()->modeInfo[0].mode) {
	case SIMULATE:
	case OPTIMIZE:
		// Supported modes
		break;

	default:
		// Unsupported mode
		std::stringstream ss;
		ss << "BMI initialize(...) - error: Unsupported mode " << runtime->getRuntimeSettings()->modeInfo[0].mode << "!";
		std::string s = ss.str();
	    piDiagInterface::addLine(1, s);
		throw runtime_error(s);
	}

	// Allocate value cache
	cache = new double[runtime->getTimeSeriesModel()->getTimeSeriesTensor()->getScalarIDMap().size()];

	// start time, end time and step size in seconds
	tStart = ((double) runtime->getTimeSeriesModel()->getTimeSeriesTensor()->getStartTime()) / 1000.0;
	tEnd = ((double) runtime->getTimeSeriesModel()->getTimeSeriesTensor()->getEndTime()) / 1000.0;
	dt = ((double)runtime->getTimeSeriesModel()->getTimeSeriesTensor()->getDT()) / 1000.0;

	// Initial step
	step = 0;
}

dllexp int finalize()
{
	// Flush diagnostics
	piDiagInterface::write();

	// Release memory
	runtime->finish(step);
	delete runtime;

	delete[] cache;

	// Done
	return 0;
}

dllexp int update(double dt_ext)
{
	// The time increment is determined by RTC-Tools
	if (dt_ext>0 && dt_ext!=dt) {
		stringstream ss;
		ss << "BMI update(double dt) - error: External " << dt_ext << " and internal " << dt << " time steps are inconsistent.";
        piDiagInterface::addLine(1, ss.str());
        throw runtime_error(ss.str());
	}

	// Write output
	// We perform this task here, rather than after execute().  In this way, all set values are logged, regardless of whether they
	// are set implicitly or explicitly:
	// Step n - 1:  Set values at n - 1, n    .  Log values at n - 1.  Compute values at n    .
	// Step n    :  Set values at n    , n + 1.  Log values at n    .  Compute values at n + 1.
	// Step n + 1:  Set values at n + 1, n + 2.  Log values at n + 1.  Compute values at n + 2.
	runtime->writeOutput(step, false);

	// Increment current time step
	step++;

	// Ensure new step is valid
	if (step >= runtime->getTimeSeriesModel()->getTimeSeriesTensor()->getNTimeStep()) {
		const string msg = "BMI update(dt) - error: Trying to step beyond end of data.";
        piDiagInterface::addLine(1, msg);
        throw runtime_error(msg);
	}

	// Perform mode-dependent update action
	runtime->execute(step);
	
	// Done
	return 0;
}

dllexp void get_var_count(int *count)
{
	// Return count of all variables
	*count = (int) runtime->getTimeSeriesModel()->getTimeSeriesTensor()->getScalarIDMap().size();
}

dllexp void get_var_name(int n, char *name)
{
	// Retrieve name of n'th variable
	string id;

	if (n<runtime->getTimeSeriesModel()->getTimeSeriesTensor()->getNSeries()) {
		id = runtime->getTimeSeriesModel()->getTimeSeriesTensor()->getSeriesIDs()[n];
	} else {
		const string msg = "BMI get_var_name(int n, char *name) - error: Index n out of range.";
        piDiagInterface::addLine(1, msg);
        throw runtime_error(msg);
	}

	strncpy(name, id.c_str(), MAXSTRLEN);
}

dllexp void get_var_type(const char *name, char *type)
{
	// All variables are doubles
	strcpy(type, "double");
}

dllexp void get_var_rank(const char *name, int *rank)
{
	// All variables are scalar (0-rank)
	*rank = 0;
}

dllexp void get_var_shape(const char *name, int *shape)
{
	// Scalar variables have no shape
}

dllexp void get_start_time(double *start_time)
{
	switch (runtime->getRuntimeSettings()->modeInfo[0].mode) {

	case SIMULATE:
	case OPTIMIZE:
		// Return start time
		*start_time = tStart;

		break;

	default:
		// Do nothing
		break;

	}
}

dllexp void get_end_time(double *end_time)
{
	switch (runtime->getRuntimeSettings()->modeInfo[0].mode) {

	case SIMULATE:
	case OPTIMIZE:
		// Return start time
		*end_time = tEnd;

		break;

	default:
		// Do nothing
		break;
	
	}
}

dllexp void get_current_time(double *current_time)
{
	*current_time = tStart + step * dt;
}

dllexp void get_time_step(double *time_step)
{
	// Return time step size
	*time_step = dt;
}

dllexp void get_var(const char *name, char **data_ptr)
{
	// Cache latest variable value
	try {
		int index = runtime->getTimeSeriesModel()->getTimeSeriesTensor()->getScalarIndex(name);
		cache[index] = runtime->getTimeSeriesModel()->getTimeSeriesTensor()->getValue(0, step, index);

		// Return pointer to cache
		*data_ptr = (char *) &(cache[index]);

	} catch (exception e) {
		const string msg = "BMI get_var(const char *name, char **data_ptr) - error: Variable does not exist.";
        piDiagInterface::addLine(1, msg);
        throw runtime_error(msg);
	}
}

dllexp void set_var(const char *name, const char *data)
{
	// Define the time step at which to set the variable 
	int target_step = step;

	// Parse variable name
	size_t name_len = strlen(name);
	char *parsed_name = (char *) alloca(name_len + 1);
	if (name[name_len - 1] == '*') {
		// n               n+1
		// o----------------o   D-FLOW
		//              .
		//          .
		//      .
		//  .
		// o----------------o   RTC
		// 
		// In the above scenario, the flow values at step n+1 are passed to RTC at RTC timestep n. 
		// In this case, the variable name can be annotated with an asterisk (*), which causes it to be
		// stored in the slot for RTC timestep n + 1.  RTC should then be configured to relate this
		// variable implicitly to the output variable.
		strncpy(parsed_name, name, name_len - 1);
		parsed_name[name_len - 1] = '\0';

		target_step++;

		// Check step
		if (target_step >= runtime->getTimeSeriesModel()->getTimeSeriesTensor()->getNTimeStep()) {
			std::stringstream ss;
			ss << "BMI set_var(...) - error: Target time step " << target_step << " beyond end of available data!";
			std::string s = ss.str();
	        piDiagInterface::addLine(1, s);
	        throw runtime_error(s);
		}

	} else {
		strncpy(parsed_name, name, name_len);
		parsed_name[name_len] = '\0';
	}

	// Set variable value
	int index = runtime->getTimeSeriesModel()->getTimeSeriesTensor()->getScalarIndex(parsed_name);

	double value = *((double *) data);

	runtime->getTimeSeriesModel()->getTimeSeriesTensor()->setValue(0, target_step, index, value);
}


// EXTENSIONS (these are NON BMI functions, but also available via the dll interface)
dllexp void set_parameters(int n, string* id, double* input)
{
	// modifies parameters of the model in the model schematization
	runtime->setParameters(n, id, input);
}

dllexp void simulate(double* J)
{
	// model simulation over the whole period
	runtime->execute();
	*J = runtime->getJ();
}