/* -*- mia-c++  -*-
 *
 * This file is part of MIA - a toolbox for medical image analysis 
 * Copyright (c) Leipzig, Madrid 1999-2017 Gert Wollny
 *
 * MIA is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * 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 MIA; if not, see <http://www.gnu.org/licenses/>.
 *
 */

#ifndef mia_core_boundary_conditions_hh
#define mia_core_boundary_conditions_hh

#include <mia/core/msgstream.hh>
#include <mia/core/type_traits.hh>
#include <mia/core/factory.hh>
#include <mia/core/product_base.hh>
#include <mia/core/splinekernel.hh>

#include <vector>
#include <memory>

NS_MIA_BEGIN

enum EBoundaryConditions  {
	bc_mirror_on_bounds, 
	bc_repeat, 
	bc_zero, 
	bc_unknown
}; 

/**
   \ingroup interpol 

   \brief Abstract base class for B-spline interpolation boundary conditions 
   
   This class is the abstract base class for B-spline interpolation boundary conditions. 

   The actual boundary conditions are implemented as plug-ins and instances are 
   created by calling produce_spline_boundary_condition. 
*/
class EXPORT_CORE CSplineBoundaryCondition : public CProductBase{
public: 

	/// helper typedef for plug-in handling 
	typedef CSplineBoundaryCondition plugin_data; 
	
	/// helper typedef for plug-in handling 
	typedef CSplineBoundaryCondition plugin_type; 

	/// type portion of the plugin search path 
	static const char * const type_descr; 
	
	/// data portion of the plugin search path 
	static const char * const data_descr; 


	/// pointer type to this boundary condition
	typedef std::unique_ptr<CSplineBoundaryCondition> Pointer; 

	CSplineBoundaryCondition(); 


	/**
	   Delete copy operations 
	 */
	CSplineBoundaryCondition(const CSplineBoundaryCondition& /*other*/) = delete;

	CSplineBoundaryCondition& operator = (const CSplineBoundaryCondition& /*other*/) = delete; 

	/**
	   Constructor for the boundary conditions. 
	   \param width size of the coefficent domain 
	 */
	
	CSplineBoundaryCondition(int width); 

	/**
	   Apply the boundary conditions 
	   \param index - indices into the coeffisicnt domain will be changed to fit domain 
	   \param weights - according weights
	   \returns true if the index set was in the coefficient domain 
	 */
	bool apply(CSplineKernel::VIndex& index, CSplineKernel::VWeight& weights) const;
	
	/**
	   (re-)set the width of the supported index range 
	   \param width new width 
	 */

	void set_width(int width); 

	/// \returns the width of the coefficient domain 
	int get_width() const {
		return m_width; 
	}
	
	/**
	   Prefiltering function to convert a vector of input data to spline coefficients 
	   \tparam T must either be a scalar type or an array of scalar data types whose elements 
	   can be accessed by using the operator[]. This restriction is currently necessary to 
	   allow a dynamic polymorphic implementation  of the pre-filtering step needed for 
           different boundary condition models. 
	   \param[in,out] coeff vector of function values that will be converted to spline coefficients 
	   \param poles the poles of the B-spline the coefficients are created for 
	   
	 */
	template <typename T> 
	void filter_line(std::vector<T>& coeff, const std::vector<double>& poles) const;

        /**
	   Prefiltering function to convert a vector of double valued input data to spline coefficients 
	   This is the actual work routine that will be called by the other filter_line functions 
	   after type conversion and decomposition has been executed 
	   \param[in,out] coeff vector of function values that will be converted to spline coefficients 
	   \param poles the poles of the B-spline the coefficients are created for 
	*/
	void filter_line(std::vector<double>& coeff, const std::vector<double>& poles) const;

	/**
	   Prefiltering function to convert a vector of scalar valued input data to spline coefficients. 
	   \tparam T a scalar type. 
	   \param[in,out] coeff vector of function values that will be converted to spline coefficients 
	   \param poles the poles of the B-spline the coefficients are created for 
	*/
	
	template <typename T> 
	void template_filter_line(std::vector<T>& coeff, const std::vector<double>& poles) const;

	/**
	   \returns a copy of the (derived) instance of this boundary condition 
	 */
	virtual 
		CSplineBoundaryCondition *clone() const __attribute__((warn_unused_result)) = 0 ; 
private:

	virtual void do_apply(CSplineKernel::VIndex& index, CSplineKernel::VWeight& weights) const = 0;
	virtual void test_supported(int npoles) const = 0;
	
	virtual void do_set_width(int width); 

	
	virtual double initial_coeff(const std::vector<double>& coeff, double pole) const = 0;
	virtual double initial_anti_coeff(const std::vector<double>& coeff, double pole)const = 0;


	int m_width; 
}; 



/**  \ingroup interpol 
     Pointer type of the boundary conditions. 
*/
typedef CSplineBoundaryCondition::Pointer PSplineBoundaryCondition; 

extern template class EXPORT_CORE TFactory<CSplineBoundaryCondition>; 

/**  \ingroup interpol 
     \brief Base plugin for spline boundary conditions
*/
class EXPORT_CORE CSplineBoundaryConditionPlugin: public TFactory<CSplineBoundaryCondition> {
public: 
	/**
	   Constructor for the spline boundary conditions plug-ins. 
	 */

	CSplineBoundaryConditionPlugin(const char * name); 
private: 
	virtual CSplineBoundaryCondition *do_create() const;
	
	virtual CSplineBoundaryCondition *do_create(int width) const = 0; 

	int m_width; 
}; 



/**
   \ingroup interpol 
   Plugin handler for the creation of spline boundary conditions
*/
typedef THandlerSingleton<TFactoryPluginHandler<CSplineBoundaryConditionPlugin> > CSplineBoundaryConditionPluginHandler;

template<> 
const char * const TPluginHandler<CSplineBoundaryConditionPlugin>::m_help;

extern template class EXPORT_CORE THandlerSingleton<TFactoryPluginHandler<CSplineBoundaryConditionPlugin> >; 

/// make spline boundary conditions parsable by the command line 
FACTORY_TRAIT(CSplineBoundaryConditionPluginHandler); 


/**
   Create a specific instance of a spline interpolation boundary condition. 
   \param descr Description of the requested boundary conditions
   \returns the actual boundary condition 
*/
inline 
PSplineBoundaryCondition produce_spline_boundary_condition(const std::string& descr)
{
	return CSplineBoundaryConditionPluginHandler::instance().produce_unique(descr); 
}


/**
   Create a specific instance of a spline interpolation boundary condition. 
   \param descr Description of the requested boundary conditions
   \param width width of the input domain 
   \returns the actual boundary condition 
*/
EXPORT_CORE PSplineBoundaryCondition produce_spline_boundary_condition(const std::string& descr, int width)
 __attribute__((deprecated)); 


/// @cond INTERNAL 

/**
   \ingroup traits 
   \brief This trait handles dispatching the pre-filtering of coefficients
   
   This trait handles dispatching the pre-filtering of coefficients based on the number of 
   elements a coefficient holds. If the number of components is one, then the filtering can be called directly, 
   otherwise the data needs to be copied element-wiese  to and from a temporary array. 
   This is required to make the actual filtering template-free and, hence make it possible to call it as a virtual function. 

   \tparam T the data type of the coefficients, if the type holds more than one element, it must support the operator []
   for element indexing. 
   \tparam size the number of scalar elements the type holds 
   \remark performance wise it could be better to copy the data to a flat array and add a stride parametsr to the 
   filter algorithm
*/
template <typename T, int size>
struct __dispatch_filter_line {
	static void apply(const CSplineBoundaryCondition& bc, std::vector<T>& coeff, const std::vector<double>& poles); 
}; 

template <typename T, int size>
void __dispatch_filter_line<T, size>::apply(const CSplineBoundaryCondition& bc, std::vector<T>& coeff, 
					 const std::vector<double>& poles) 
{
	std::vector<double> temp(coeff.size());
	for (int i = 0; i < size; ++i) {
		std::transform(coeff.begin(), coeff.end(), temp.begin(), 
			       [i](const T& x) { return x[i]; }); 
		bc.filter_line(temp, poles); 
		for (size_t j = 0; j < coeff.size(); ++j)
			coeff[j][i] = temp[j]; 
	}
}

/**
   \ingroup traits 
   \brief This trait handles dispatching the pre-filtering of coefficients in the one-element case 

*/ 
template <typename T>
struct __dispatch_filter_line<T,1> {
	static void apply(const CSplineBoundaryCondition& bc, std::vector<T>& coeff, const std::vector<double>& poles) {
		bc.template_filter_line(coeff, poles); 
	}
}; 

/// @endcond 

template <typename T> 
void CSplineBoundaryCondition::filter_line(std::vector<T>& coeff, const std::vector<double>& poles) const
{
	typedef atomic_data<T> atom; 
	__dispatch_filter_line<T, atom::size>::apply(*this, coeff, poles); 
}


template <typename T> 
void CSplineBoundaryCondition::template_filter_line(std::vector<T>& coeff, const std::vector<double>& poles) const
{
	std::vector<double> temp(coeff.begin(), coeff.end()); 
	filter_line(temp, poles); 
	std::transform(temp.begin(), temp.end(), coeff.begin(), [](double x) {return static_cast<T>(x);});
}

NS_MIA_END
#endif
