// This file is part of necsim project which is released under MIT license.
// See file **LICENSE.txt** or visit https://opensource.org/licenses/MIT) for full license details
/**
 * @author Samuel Thompson
 * @file Map.h
 * @brief Contains Map for importing .tif files and obtaining a variety of information from them.
 *
 * @copyright <a href="https://opensource.org/licenses/MIT"> MIT Licence.</a>
 */

#ifndef MAP_H
#define MAP_H
#ifdef with_gdal

#include <string>
#include <cstring>
#include <sstream>
#include "Logger.h"
#include <gdal_priv.h>
#include <cpl_conv.h> // for CPLMalloc()
#include <sstream>
#include "Matrix.h"
#include "Logger.h"
#include "custom_exceptions.h"
#include "cpl_custom_handler.h"

using namespace std;
#ifdef DEBUG
#include "custom_exceptions.h"
#endif // DEBUG

/**
 * @brief Read a a tif file to a matrix and obtain spatial metadata.
 * @tparam T The type of the Matrix to create.
 */
template<class T>
class Map : public virtual Matrix<T>
{
protected:
	GDALDataset *po_dataset;
	GDALRasterBand *po_band;
	unsigned long block_x_size, block_y_size;
	double no_data_value;
	string file_name;
	GDALDataType gdal_data_type;
	// Contains the error object to check for any gdal issues
	CPLErr cpl_error;
	double upper_left_x, upper_left_y, x_res, y_res;
	using Matrix<T>::matrix;
	using Matrix<T>::num_cols;
	using Matrix<T>::num_rows;
public:
	using Matrix<T>::setSize;
	using Matrix<T>::getCols;
	using Matrix<T>::getRows;
	using Matrix<T>::operator*;
	using Matrix<T>::operator+;
	using Matrix<T>::operator-;
	using Matrix<T>::operator-=;
	using Matrix<T>::operator+=;
	using Matrix<T>::operator[];

	Map() : Matrix<T>(0, 0), po_dataset(nullptr), po_band(nullptr), block_x_size(0), block_y_size(0),
			no_data_value(0.0), file_name(""), gdal_data_type(GDT_Unknown), cpl_error(CE_None), upper_left_x(0.0),
			upper_left_y(0.0), x_res(0.0), y_res(0.0)
	{
		GDALAllRegister();
		CPLSetErrorHandler(cplNecsimCustomErrorHandler);
	}

	~Map() override
	{
		close();
		CPLSetErrorHandler(null);
	}

	/**
	 * @brief Opens the provided filename to the poDataset object.
	 * @param filename file to open in read-only mode.
	 */
	void open(const string &filename_in)
	{
		if(!po_dataset)
		{
			file_name = filename_in;
			po_dataset = (GDALDataset *) GDALOpen(file_name.c_str(), GA_ReadOnly);
		}
		else
		{
			throw FatalException("File already open at " + file_name);
		}
		if(!po_dataset)
		{
			string s = "File " + file_name + " not found.";
			throw runtime_error(s);
		}
	}

	/**
	 * @brief Overloaded open for using the preset file name.
	 */
	void open()
	{
		open(file_name);
	}

	/**
	 * @brief Checks if the connection to the map file has already been opened.
	 *
	 * All this does is check if poDataset is a null pointer.
	 * @return true if poDataset is a null pointer.
	 */
	bool isOpen()
	{
		return po_dataset != nullptr;
	}

	/**
	 * @brief Destroys the connection to the dataset.
	 */
	void close()
	{
		if(po_dataset)
		{
			GDALClose(po_dataset);
//			if(po_dataset)
//			{
//				throw FatalException("po_dataset not nullptr after closing, please report this bug.");
//			}
			po_dataset = nullptr;
			po_band = nullptr;
		}
	}

	/**
	 * @brief Sets the raster band to the first raster.
	 */
	void getRasterBand()
	{
		if(po_dataset->GetRasterCount() < 1)
		{
			stringstream ss;
			ss << "No raster band detected in " << file_name << endl;
			throw FatalException(ss.str());
		}
		po_band = po_dataset->GetRasterBand(1);
		if(po_band == nullptr)
		{
			stringstream ss;
			ss << "Could not open po_band in " << file_name << ": returned a nullptr" << endl;
			throw FatalException(ss.str());
		}
	}

	/**
	 * @brief Obtains the x and y dimensions from the tif file for reading in blocks.
	 */
	void getBlockSizes()
	{
		block_x_size = static_cast<unsigned long>(po_dataset->GetRasterXSize());
		block_y_size = static_cast<unsigned long>(po_dataset->GetRasterYSize());
	}

	/**
	 * @brief Sets the no data, data type and data type name values from the tif file.
	 */
	void getMetaData()
	{
#ifdef DEBUG
		if(po_dataset == nullptr)
		{
			throw FatalException("po_dataset is nullptr. This is likely a problem with gdal. Please report this bug.");
		}
		if(po_band == nullptr)
		{
			throw FatalException("po_band is nullptr. This is likely a problem with gdal. Please report this bug.");
		}
#endif // DEBUG
		try
		{
			int pbSuccess;
			no_data_value = po_band->GetNoDataValue(&pbSuccess);
			if(!pbSuccess)
			{
				no_data_value = 0.0;
			}
		}
		catch(out_of_range &out_of_range1)
		{
			no_data_value = 0.0;
		}
		stringstream ss;
		ss << "No data value is: " << no_data_value << endl;
		writeInfo(ss.str());
		// Check sizes match
		gdal_data_type = po_band->GetRasterDataType();
		const static double default_values[6] = {0.0, 1.0, 0.0, 0.0, 0.0, 1.0};
		double geoTransform[6] = {0.0, 1.0, 0.0, 0.0, 0.0, 1.0};
		writeInfo("Getting geo transform...");
		cpl_error = po_dataset->GetGeoTransform(geoTransform);
		if(cpl_error >= CE_Warning)
		{
			ss.str("");
			ss << "cpl error detected greater than or equal to warning level." << endl;
			writeInfo(ss.str());
			string message = "No transform present in dataset for " + file_name;
			cplNecsimCustomErrorHandler(cpl_error, 6, message.c_str());
			CPLErrorReset();
			cpl_error = CE_None;
			std::copy(std::begin(default_values), std::end(default_values), std::begin(geoTransform));
		}
		writeInfo("done\n");
		ss.str("");
		ss << "Affine transform is " << geoTransform[0] << ", " << geoTransform[1] << ", " << geoTransform[2] << ", ";
		ss << geoTransform[3] << ", " << geoTransform[4] << ", " << geoTransform[5] << endl;
		writeInfo(ss.str());
		upper_left_x = geoTransform[0];
		upper_left_y = geoTransform[3];
		x_res = geoTransform[1];
		y_res = -geoTransform[5];
//		checkTifImportFailure();
#ifdef DEBUG
		printMetaData();
#endif // DEBUG
	}

#ifdef DEBUG
	void printMetaData()
	{
		stringstream ss;
		const char *dt_name = GDALGetDataTypeName(gdal_data_type);
		ss << "Filename: " << file_name << endl;
		writeLog(10, ss.str());
		ss.str("");
		ss << "data type: " << gdal_data_type << "(" << dt_name << ")" << endl;
		writeLog(10, ss.str());
		ss.str("");
		ss << "Geo-transform (ulx, uly, x res, y res): " << upper_left_x << ", " << upper_left_y << ", ";
		ss << x_res << ", " << y_res << ", " <<endl;
		writeLog(10, ss.str());
		ss.str("");
		ss << "No data value: " << no_data_value << endl;
		writeLog(10, ss.str());

	}
#endif //DEBUG

	/**
	 * @brief Gets the upper left x (longitude) coordinate
	 * @return upper left x of the map
	 */
	double getUpperLeftX()
	{
		return upper_left_x;
	}

	/**
	 * @brief Gets the upper left y (latitude) coordinate
	 * @return upper left y of the map
	 */
	double getUpperLeftY()
	{
		return upper_left_y;
	}

	/**
	 * @brief Imports the matrix from a csv file.
	 *
	 * @throws runtime_error: if type detection for the filename fails.
	 * @param filename the file to import.
	 */
	void import(const string &filename) override
	{
		if(!importTif(filename))
		{
			Matrix<T>::import(filename);
		}
	}

	/**
	 * @brief Imports the matrix from a tif file using the gdal library functions.
	 * @note Opens a connection to the file object, which should be closed.
	 * @param filename the path to the file to import.
	 */
	bool importTif(const string &filename)
	{

		if(filename.find(".tif") != string::npos)
		{
			stringstream ss;
			ss << "Importing " << filename << " " << endl;
			writeInfo(ss.str());
			open(filename);
			getRasterBand();
			getBlockSizes();
			getMetaData();
			// If the sizes are 0 then use the raster sizes
			if(num_cols == 0 || num_rows == 0)
			{
				setSize(block_y_size, block_x_size);
			}
			// Check sizes
			if((num_cols != block_x_size || num_rows != block_y_size) || num_cols == 0 ||
			   num_rows == 0)
			{
				stringstream stringstream1;
				stringstream1 << "Raster data size does not match inputted dimensions for " << filename
							  << ". Using raster sizes."
							  << endl;
				stringstream1 << "Old dimensions: " << num_cols << ", " << num_rows << endl;
				stringstream1 << "New dimensions: " << block_x_size << ", " << block_y_size << endl;
				writeWarning(stringstream1.str());
				setSize(block_y_size, block_x_size);
			}
			// Check the data types are support
			const char *dt_name = GDALGetDataTypeName(gdal_data_type);
			if(gdal_data_type == 0 || gdal_data_type > 7)
			{
				throw FatalException("Data type of " + string(dt_name) + " is not supported.");
			}
#ifdef DEBUG
			if(sizeof(T) * 8 != gdal_data_sizes[gdal_data_type])
			{
				stringstream ss2;
				ss2 << "Object data size: " << sizeof(T) * 8 << endl;
				ss2 << "Tif data type: " << dt_name << ": " << gdal_data_sizes[gdal_data_type] << " bytes" << endl;
				ss2 << "Tif data type does not match object data size in " << file_name << endl;
				writeWarning(ss2.str());
			}
#endif
			// Just use the overloaded method for importing between types

			internalImport();
			return true;
		}
		return false;
	}

	/**
	 * @brief Opens the offset map and fetches the metadata.
	 * @param offset_map the offset map to open (should be the larger map).
	 * @return true if the offset map is opened within this function
	 */
	bool openOffsetMap(Map &offset_map)
	{
		bool opened_here = false;
		if(!offset_map.isOpen())
		{
			opened_here = true;
			offset_map.open();
		}
		offset_map.getRasterBand();
		offset_map.getMetaData();
		return opened_here;
	}

	void closeOffsetMap(Map &offset_map, const bool &opened_here)
	{
		if(opened_here)
		{
			offset_map.close();
		}
	}

	/**
	 * @brief Calculates the offset between the two maps.
	 *
	 * The offset_map should be larger and contain this map, otherwise returned values will be negative
	 *
	 * @note Opens a connection to the tif file (if it has not already been opened), which is then closed. If the
	 * 		 connection is already open, then it will not be closed and it is assumed logic elsewhere achieves this.
	 *
	 * @note Offsets are returned as rounded integers at the resolution of the smaller map.
	 *
	 * @param offset_map the offset map to read from
	 * @param offset_x the x offset variable to fill
	 * @param offset_y the y offset variable to fill
	 */
	void calculateOffset(Map &offset_map, long &offset_x, long &offset_y)
	{
		auto opened_here = openOffsetMap(offset_map);
		offset_x = static_cast<long>(round((upper_left_x - offset_map.upper_left_x) / x_res));
		offset_y = static_cast<long>(round((offset_map.upper_left_y - upper_left_y) / y_res));
		closeOffsetMap(offset_map, opened_here);
	}

	/**
	 * @brief Calculates the relative scale of this map compared to the offset map.
	 *
	 * The offset map should be larger and contain this map.
	 *
	 * @note Only the x resolution is checked, it is assumed the x and y resolutions of both maps is the same (i.e. each
	 * 		 cell on the map is a square.
	 *
	 * @param offset_map the offset map object to read from
	 * @return the relative scale of the offset map
	 */
	unsigned long roundedScale(Map &offset_map)
	{
		auto opened_here = openOffsetMap(offset_map);
		closeOffsetMap(offset_map, opened_here);
		return static_cast<unsigned long>(floor(offset_map.x_res / x_res));
	}

	/**
	 * @brief Default importer when we rely on the default gdal method of converting between values.
	 * Note that importing doubles to ints results in the values being rounded down.
	 * @return true if a tif file exists and can be imported, false otherwise.
	 */
	void internalImport()
	{
		writeWarning("No type detected for Map type. Attempting default importing (potentially undefined behaviour).");
		defaultImport();
		writeInfo("done\n");
	}

	/**
	 * @brief Default import routine for any type. Provided as a separate function so implementation can be called from
	 * any template class type.
	 */
	void defaultImport()
	{
		unsigned int number_printed = 0;
		for(uint32_t j = 0; j < num_rows; j++)
		{
			printNumberComplete(j, number_printed);
			cpl_error = po_band->RasterIO(GF_Read, 0, j, static_cast<int>(block_x_size), 1, &matrix[j][0],
										  static_cast<int>(block_x_size), 1, gdal_data_type, 0, 0);
			checkTifImportFailure();
			// Now convert the no data values to 0
			for(uint32_t i = 0; i < num_cols; i++)
			{
				if(matrix[j][i] == no_data_value)
				{
					matrix[j][i] = 0;
				}
			}
		}
	}

	/**
	 * @brief Imports from the supplied filename into the GeoTiff object, converting doubles to booleans.
	 * The threshold for conversion is x>0.5 -> true, false otherwise.
	 */
	void importFromDoubleAndMakeBool()
	{
		writeInfo("Importing and converting from double to boolean.\n");
		unsigned int number_printed = 0;
		// create an empty row of type float
		double *t1;
		t1 = (double *) CPLMalloc(sizeof(double) * num_cols);
		// import the data a row at a time, using our template row.
		for(uint32_t j = 0; j < num_rows; j++)
		{
			printNumberComplete(j, number_printed);
			cpl_error = po_band->RasterIO(GF_Read, 0, j, static_cast<int>(block_x_size), 1, &t1[0],
										  static_cast<int>(block_x_size), 1, GDT_Float64, 0, 0);
			checkTifImportFailure();
			// now copy the data to our Map, converting float to int. Round or floor...? hmm, floor?
			for(unsigned long i = 0; i < num_cols; i++)
			{
				if(t1[i] == no_data_value)
				{
					matrix[j][i] = false;
				}
				else
				{
					matrix[j][i] = t1[i] >= 0.5;
				}
			}
		}
		CPLFree(t1);
	}

	/**
	 * @brief Imports from the supplied filename into the GeoTiff object, converting doubles to booleans.
	 * The threshold for conversion is x>0.5 -> true, false otherwise.
	 *
	 * @param dt_buff: the buffer type for the data
	 * @tparam T2 the template type for data reading.
	 */
	template<typename T2>
	void importUsingBuffer(GDALDataType dt_buff)
	{
		stringstream ss;
		ss << "Importing using buffer of type " << GDALGetDataTypeName(dt_buff) << " into array of dimensions ";
		ss << num_cols << " by " << num_rows << " with block size " << block_x_size << ", " << block_y_size << endl;
		writeInfo(ss.str());
		unsigned int number_printed = 0;
		// create an empty row of type float
		T2 *t1;
		t1 = (T2 *) CPLMalloc(sizeof(T2) * num_cols);
		if(CPLGetLastErrorNo() != CE_None)
		{
			stringstream os;
			os << "Error thrown by CPLMalloc: " <<  CPLGetLastErrorType() << ": " << CPLGetLastErrorMsg() << endl;
			throw FatalException(os.str());
		}
		// import the data a row at a time, using our template row.
		auto int_block_x_size = static_cast<int>(block_x_size);
#ifdef DEBUG
		if(sizeof(T2)*8 != GDALGetDataTypeSize(dt_buff))
		{
			stringstream ss0;
			ss0 << "Size of template (" << sizeof(T2) << ") does not equal size of gdal buffer (";
			ss0 << GDALGetDataTypeSize(dt_buff) << "). Please report this bug." << endl;
			throw FatalException(ss0.str());
		}
		if(t1 == nullptr)
		{
			stringstream ss1;
			ss1 << "CPL malloc could not acquire " << sizeof(T2) * num_cols << " of space and returned a nullptr.";
			ss1 << " Please check that your install of gdal is fully functional, and otherwise report this bug."
				<< endl;
			throw FatalException(ss1.str());
		}
		if(static_cast<unsigned long>(int_block_x_size) != block_x_size)
		{
			stringstream ss2;
			ss2 << "Error in integer conversion - please report this bug: " << int_block_x_size << " != ";
			ss2 << block_x_size << endl;
			throw FatalException(ss2.str());
		}
		if(po_band == nullptr)
		{
			throw FatalException("po_band is nullptr during import using buffer - please report this bug.");
		}
#endif // DEBUG
		for(uint32_t j = 0; j < num_rows; j++)
		{
			printNumberComplete(j, number_printed);
			cpl_error = po_band->RasterIO(GF_Read, 0, j, int_block_x_size, 1, t1,
										  int_block_x_size, 1, dt_buff, 0, 0);
			checkTifImportFailure();
			for(unsigned long i = 0; i < num_cols; i++)
			{
				if(t1[i] == no_data_value)
				{
					matrix[j][i] = static_cast<T>(0);
				}
				else
				{
					matrix[j][i] = static_cast<T>(t1[i]);
				}
			}
		}
		CPLFree(t1);
	}

	/**
	 * @brief Print the percentage complete during import
	 * @param j the reference for the counter
	 * @param number_printed the number of previously printed lines
	 */
	void printNumberComplete(const uint32_t &j, unsigned int &number_printed)
	{
		double dComplete = ((double) j / (double) num_rows) * 20;
		if(number_printed < dComplete)
		{
			stringstream os;
			os << "\rImporting " << file_name << " ";
			number_printed = 0;
			while(number_printed < dComplete)
			{
				os << ".";
				number_printed++;
			}
			os << flush;
			writeInfo(os.str());
		}
	}

	/**
	 * @brief Checks the error code of the CPLErr object and formats the error
	 */
	void checkTifImportFailure()
	{
		if(cpl_error >= CE_Warning)
		{
			stringstream ss;
			ss << "\nCPL error thrown during import of " << file_name << endl;
			cplNecsimCustomErrorHandler(cpl_error, 3, ss.str().c_str());
			CPLErrorReset();
			cpl_error = CE_None;
		}
	}

	/**
	 * @brief Output operator
	 * @param os the output stream
	 * @param m the object to write out
	 * @return the modified output stream
	 */
	friend ostream &operator>>(ostream &os, const Map &m)
	{
		return Matrix<T>::writeOut(os, m);
	}

	/**
	 * @brief Input operator
	 * @param is the input stream
	 * @param m the object to write in
	 * @return the modified input stream
	 */
	friend istream &operator<<(istream &is, Map &m)
	{
		return Matrix<T>::readIn(is, m);
	}

	/**
	 * @brief Equality operator
	 * @param m the Map object to copy from
	 * @return the self Map object
	 */
	Map &operator=(const Map &m)
	{
		Matrix<T>::operator=(m);
		this->po_dataset = m.po_dataset;
		this->po_band = m.po_band;
		this->block_x_size = m.block_x_size;
		this->block_y_size = m.block_y_size;
		this->no_data_value = m.no_data_value;
		this->file_name = m.file_name;
		this->gdal_data_type = m.gdal_data_type;
		this->cpl_error = m.cpl_error;
		this->upper_left_x = m.upper_left_x;
		this->upper_left_y = m.upper_left_y;
		this->x_res = m.x_res;
		this->y_res = m.y_res;
		return *this;
	}

};

/**
 * @brief Overloaded imported for handling conversion of types to boolean. This function should only be once
 * elsewhere, so inlining is fine, allowing this file to remain header only.
 */
template<>
inline void Map<bool>::internalImport()
{
	if(gdal_data_type <= 5)
	{
		// Then the tif file type is an int/byte
		// we can just import as it is
		importUsingBuffer<uint8_t>(GDT_Byte);
	}
	else
	{
		// Conversion from double to boolean
		importFromDoubleAndMakeBool();
	}
}

/**
 * @brief Overloaded functions for importing from tifs and matching between gdal and C types.
 */
template<>
inline void Map<int8_t>::internalImport()
{
	importUsingBuffer<int16_t>(GDT_Int16);
}

template<>
inline void Map<uint8_t>::internalImport()
{
	gdal_data_type = GDT_Byte;
	defaultImport();
}

template<>
inline void Map<int16_t>::internalImport()
{
	gdal_data_type = GDT_Int16;
	defaultImport();
}

template<>
inline void Map<uint16_t>::internalImport()
{
	gdal_data_type = GDT_UInt16;
	defaultImport();
}

template<>
inline void Map<int32_t>::internalImport()
{
	gdal_data_type = GDT_Int32;
	defaultImport();
}

template<>
inline void Map<uint32_t>::internalImport()
{
	gdal_data_type = GDT_UInt32;
	defaultImport();
}

template<>
inline void Map<float>::internalImport()
{
	gdal_data_type = GDT_Float32;
	defaultImport();
}

template<>
inline void Map<double>::internalImport()
{
	gdal_data_type = GDT_Float64;
	defaultImport();
}

#endif // with_gdal
#endif //MAP_H
