/************************************************************************/
/*                                                                      */
/*    vspline - a set of generic tools for creation and evaluation      */
/*              of uniform b-splines                                    */
/*                                                                      */
/*            Copyright 2015 - 2023 by Kay F. Jahnke                    */
/*                                                                      */
/*    The git repository for this software is at                        */
/*                                                                      */
/*    https://bitbucket.org/kfj/vspline                                 */
/*                                                                      */
/*    Please direct questions, bug reports, and contributions to        */
/*                                                                      */
/*    kfjahnke+vspline@gmail.com                                        */
/*                                                                      */
/*    Permission is hereby granted, free of charge, to any person       */
/*    obtaining a copy of this software and associated documentation    */
/*    files (the "Software"), to deal in the Software without           */
/*    restriction, including without limitation the rights to use,      */
/*    copy, modify, merge, publish, distribute, sublicense, and/or      */
/*    sell copies of the Software, and to permit persons to whom the    */
/*    Software is furnished to do so, subject to the following          */
/*    conditions:                                                       */
/*                                                                      */
/*    The above copyright notice and this permission notice shall be    */
/*    included in all copies or substantial portions of the             */
/*    Software.                                                         */
/*                                                                      */
/*    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND    */
/*    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES   */
/*    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND          */
/*    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT       */
/*    HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,      */
/*    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING      */
/*    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR     */
/*    OTHER DEALINGS IN THE SOFTWARE.                                   */
/*                                                                      */
/************************************************************************/

/*! \file alternative_basis.h

    \brief collection of alternative basis functions

    Having opened up the evaluation code to accept arbitrary basis
    functors to replace the b-spline basis functor, we can now
    investigate concrete examples and test their properties.

    This file provides a few examples which are taken from vspline's
    companion project lux (https://bitbucket.org/kfj/pv), where they
    are already in productive use. I have put this code in the opt/
    section, because it's not used within 'vspline proper'.
*/

namespace vspline
{

// evaluate b-splines with a convolution kernel 'slotted in'
// 
// This variant of class evaluator yields the same result as evaluating
// a spline of the given degree over knot point values which have been
// filtered with the given kernel. Instead of applying the kernel to the
// whole knot point array with an array-wide convolution, here we
// convolve locally in a subset of the coefficients just large enough
// to fit with the support needed for the convolved b-spline kernel:
// Instead of applying the kernel to the coefficients, we apply it to
// the weights of the b-spline basis function, which can be done more
// efficiently, since the basis function is separable and we can access
// it's per-axis weights easily - this is part of spline evaluation
// anyway. So all we need to do is to obtain the per-axis weights
// as usual and convolve them with the kernel, yielding a larger
// set of per-axis weights, which, when applied, will yield the
// desired result. We need to take care, though, to provide a spline
// with sufficient 'headroom' to accomodate the larger weight sets.
// Conceptually, the method has two parts: the 'initial' weights derived
// from the b-spline basis function provide 'localization', and the
// kernel provides an arbitrary FIR filter. To be applicable, we still
// need a prefiltered coefficient array (unless we decide to make the
// overall result a tad more low-pass by omitting prefiltering).
// How efficient the method is - compared to filtering the coefficients
// (or, before that, the know points) - will depend on the number of
// evaluations and the cost of accessing memory.
// When using a low-pass filter as kernel, this is an easy route to
// decimation. It's advantage over the usual methods is that, due to the
// accurate localization, values in the decimated result can correspond
// to off-grid positions in the source array, making it possible to
// produce decimated arrays with the same boundary conditions as the
// source array, even if this would not be possible due to numeric
// constraints. The slight deviations from the 'true' decimation will
// hardly matter.
// 'Slotting in' the FIR filter like this is simply a different route
// to the same end, compared to FIR-filtering the coefficients, and the
// differences in the result are minimal and stem from the differing
// routes - if the mathematics involved were 'perfect', we'd expect
// both routes to produce identical values.

/// convolving_basis_functor is the modified basis functor we'll use in
/// class convolving_evaluator. It takes the convolution kernel as it's
/// third c'tor argument.

template < typename math_type >
struct convolving_basis_functor
{
  const vspline::basis_functor < math_type > bf ;
  const int bf_degree ;
  const int degree ;
  vigra::MultiArray < 1 , math_type > kernel ;

  convolving_basis_functor ( int _degree ,
              int _derivative ,
              const vigra::MultiArrayView < 1 , math_type > & _kernel )
  : bf ( _degree , _derivative ) ,
    bf_degree ( _degree ) ,
    degree ( _degree + _kernel.size() - 1 ) ,
    kernel ( _kernel )
  {
//     std::cout << "convolving_basis_functor: bf_degree = " << bf_degree
//               << " degree = " << degree << std::endl ;
  } ;

  convolving_basis_functor ( int _degree ,
              int _derivative ,
              const std::vector < math_type > & _kernel )
  : bf ( _degree , _derivative ) ,
    bf_degree ( _degree ) ,
    degree ( _degree + _kernel.size() - 1 ) ,
    kernel ( _kernel.size() )
  {
    for ( int i = 0 ; i < _kernel.size() ; i++ )
      kernel[i] = _kernel[i] ;
  } ;

  // we only provide one operator() template, which deposits a set of
  // weights in a target memory location. This is all we need for the
  // implementation of convolving_evaluator: there are no conditionals,
  // hence the code is good for scalar and vectorized operation.

  template < class target_type ,
             class delta_type >
  void operator() ( target_type* result ,
                    const delta_type & delta ) const
  {
    // clear result so we can use += to update it's content.
    // TODO may be unnecessary

    for ( int l = 0 ; l <= degree ; l++ )
      result [ l ] = 0 ;

    // get the b-spline basis weights for the localizing part

    target_type localize [ bf_degree + 1 ] ;
    bf ( localize , delta ) ;

    // convolve the localization with the kernel
    // note how we follow vspline's standard of using the kernel 'forwards'
    // which is more straigtforward and probably better for memory access.

    for ( int k = 0 ; k < kernel.size() ; k++ )
    {
      if ( kernel [ k ] )
      {
        for ( int l = 0 ; l <= bf_degree ; l++ )
        {
          result [ k + l ] += ( kernel [ k ] * localize [ l ] ) ;
        }
      }
    }
  }
} ;

/// Like for 'unconvolved', normal b-splines, we have the per-axis
/// derivative specification, so again we need to specialize
/// multi_bf_type to 'pull in' the per-axis derivative specification.

template < int ndim , typename math_type >
struct multi_bf_type < convolving_basis_functor < math_type > , ndim >
: public std::array < convolving_basis_functor < math_type > , ndim >
{
  private:
  
  template < typename kernel_type , std::size_t ... indices >
  multi_bf_type ( int _degree ,
                  const vigra::TinyVector < int , ndim > & _dspec ,
                  const kernel_type & _kernel ,
                  std14::index_sequence < indices ... > )
  : std::array < convolving_basis_functor < math_type > , ndim >
      { convolving_basis_functor < math_type >
          ( _degree , _dspec [ indices ] , _kernel ) ... }
  { }

  public:

  template < typename kernel_type >
  multi_bf_type ( int _degree ,
                  const vigra::TinyVector < int , ndim > & _dspec ,
                  const kernel_type & _kernel )
  : multi_bf_type ( _degree , _dspec , _kernel ,
                    std14::make_index_sequence<ndim>() )
  { }

} ;

/// ev_basis_functor uses another evaluator to provide basis function
/// values. Here we don't have per-axis arguments, so the multi_bf_type
/// for this basis functor can be formed with the unspecialized template.
/// What types of functors make sense in this context? What immediately
/// springs to mind is to use 1D splines over a small set of values, which
/// we might refer to as a 'kernel'. If this kernel is symmetric, the
/// leftmost and rightmost value is zero, we gain access to a whole
/// range of basis functors. An interesting special case is using
/// a spline over [0,1/4,1/2,1/4,0], derived from the first binomial
/// kernel, which will low-pass-filter the signal.
/// There is a set of interesting variants of this basis functor, where
/// the 'inner' evaluator is not evaluated at unit steps, but at steps
/// which are a precise fraction of the inner evaluator's period. If the
/// steps are less than one and ev is a b-spline over a symmetric kernel,
/// the resulting set of weights is roughly constant for delta in [0,1].
/// At times even step widths like 4/3 produce the desired partition
/// of a constant. The precision of the partition depends on the spline's
/// degree - the precise partition of a constant would require a spline
/// of infinite degree, but to reach a 'high' degree of precision even
/// 'moderate' spline degrees suffice.
/// The class is a bit 'rough and ready' and makes a few assumptions
/// about the evaluator it takes: it's silently assumed that this functor
/// will yield 0.0 for argument 0.0, then rises to it's maximum and falls
/// back to 0.0, with a symmetric shape - like [0,1/4,1/2,1/4,0] given
/// above. It's also assumed that the kernel has odd size.
/// Note that the b-spline over the kernel values is only one of many
/// curves which we might use as 'ev' in this functor, and that we can't
/// expect to obtain results which match approaches where the kernel is
/// used in a convolution - be it of the coefficients or the weights. So
/// this method will produce results 'similar' to the convolution-based
/// ones, but there will be a noticeable difference, especially if there
/// is high-frequency content in the signal.

template < typename ev_type >
struct ev_basis_functor
{
  ev_type ev ;
  int degree ;

  ev_basis_functor ( int _degree ,
                     const ev_type & _ev )
  : degree ( _degree ) ,
    ev ( _ev )
  { } ;

  // we only provide one operator() function, which deposits a set of
  // weights in a target memory location.

  template < class target_type ,
             class delta_type >
  void operator() ( target_type* result ,
                    const delta_type & delta ) const
  {
    for ( int l = 0 ; l <= degree ; l++ )
    {
      ev.eval ( l + 1 - delta , result [ l ] ) ;
    }
  }
} ;

/// The next few basis functors implement 'area' interpolation. What
/// do I mean by this? It's a method I first found in opencv, see
/// https://docs.opencv.org/3.4/da/d54/group__imgproc__transform.html
/// and there, look for INTER_AREA. The decription says: "resampling
/// using pixel area relation. It may be a preferred method for image
/// decimation, as it gives moire'-free results. But when the image is
/// zoomed, it is similar to the INTER_NEAREST method." This already
/// points at the intended use of these basis functors: They are *not*
/// meant for interpolation, but for decimation - and if they are used
/// for interpolation, the step width mustn't get too large.
/// Let's look at the application of this basis functor in 2D. If we
/// think of the source image as a set of square pixels of constant
/// colour, the result is the total light emitted through a square
/// of a given size, divided by the window's area. So we get an average
/// over a window of the given area . This is a good basis function
/// for very fast decimation down to half size.
/// We start out with two special cases - the first one is an 'area'
/// interpolation using a fixed 2X2 window, which works well for a
/// decimation to half size. The second one uses the same structure,
/// but allows variation of the central 'peak' value, which has the
/// same effect as varying the window's size, but only works for
/// certain deltas, so the resampling has to follow a specific pattern.
/// The third one, finally, is the general case for window sizes in
/// the range of 1 to 2.

template < typename dtype >
struct less_basis_functor
{
  int degree ;

  less_basis_functor ( int _degree )
  : degree ( _degree )
  {
    assert ( degree == 2 ) ;
  } ;

  // we only provide one operator() function, which deposits a set of
  // weights in a target memory location.

  template < class target_type ,
             class delta_type >
  void operator() ( target_type* result ,
                    const delta_type & delta ) const
  {
    result[0] = ( 0.5f - delta ) * .5f ;
    result[1] = 0.5f ;
    result[2] = ( 0.5f + delta ) * .5f ;
  }
} ;

/// This variant of the functor above varies the weight given to the
/// central and the side factors.

template < typename ev_type >
struct less_basis_functor_2
{
  const int degree ;
  const float peak ;
  const float side ;

  less_basis_functor_2 ( const int & _degree ,
                         const float & _peak )
  : degree ( _degree ) ,
    peak ( _peak ) ,
    side ( 1.0f - _peak )
  {
    assert ( degree == 2 ) ;
    assert ( peak > 0.0f && peak < 1.0f ) ;
  } ;

  // we only provide one operator() function, which deposits a set of
  // weights in a target memory location.

  template < class target_type ,
             class delta_type >
  void operator() ( target_type* result ,
                    const delta_type & delta ) const
  {
    result[0] = ( 0.5f - delta ) * side ;
    result[1] = peak ;
    result[2] = ( 0.5f + delta ) * side ;
  }
} ;

/// This basis functor implements an area filter's basis where the width
/// of the square 'pick up' window can be chosen in the range of 1 to 2.
/// The 'area' filter takes it's name from 2D processing: The source
/// image is interpreted as a set of square pixels with a certain area,
/// rather than a set of points representing the signal's value at a
/// given grid point. The area filter functions like a square window of
/// a given size (here limited to 1-2) which can be positioned freely
/// over the source image (so, not just at grid positions) whose content
/// is averaged. This basis function provides the necessary weights for
/// a single axis, and due to the dimension-agnostic evaluation code
/// used by class template vspline::evaluator, the method can be used
/// for arbitrary dimensions, not just for the 2D case. the area filter
/// in 2D processing goes beyond square pixels and windows - the concept
/// is much wider and encompasses source data consisting of arbitrarily
/// shaped areas and arbitrarily shaped pickup areas, so this 'square'
/// implementation is just a special case, but it's very useful for
/// decimation of raster data down to half size.
/// We might extend the method to larger windows, which would require
/// handling degrees greater than two (three taps) which is hard-coded
/// here. Metrics 'inside' the functor are scaled down by _width to
/// avoid the need for normalization of the result - we get away with
/// only one multiplication (to scale down delta) and the remainder of
/// the calculation only has a few additions and subtractions.

template < typename dtype >
struct area_basis_functor
{
  const int degree ;
  const dtype wr ;
  const dtype p ; // boundaries of the middle pixel: 1/2, scaled down

  const dtype one = 1.0 ;
  const dtype one_half = 0.5 ;

  area_basis_functor ( int _degree , dtype _w0 )
  : degree ( _degree ) ,
    wr ( 1.0 / _w0 ) ,
    p ( 1.0 / ( 2.0 * _w0 ) )
  {
    assert ( _degree == 2 ) ;
    assert ( _w0 <= 2.0 ) ;
  } ;

  // scalar form:

  void operator() ( dtype* result ,
                    const dtype & _delta ) const
  {
    // scale down delta

    auto delta = _delta * wr ;

    // left and right are the minimum and maximum values of the covered
    // area's extent along the axis this functor is applied to

    auto left = delta - one_half ;
    auto right = delta + one_half ;

    // we set result[1] to one initially, and subtract result[0] and
    // result[2] as we go along, giving un the desired partition of unity

    result[1] = one ;

    if ( left < - p )
    {
      // if left is below - p, we have a part of the area
      // 'sticking out' to the left, which goes into result[0]

      result[0] = - p - left ;

      // and is taken of the total weight we have to distribute

      result[1] -= result[0] ;
    }
    else
    {
      // nothing 'sticking out' to the left

      result[0] = 0.0f ;
    }

    // repeat process for the 'right side'

    if ( right > p )
    {
      result[2] = right - p ;
      result[1] -= result[2] ;
    }
    else
    {
      result[2] = 0.0f ;
    }
  }

  // vectorized form

  template < class target_type ,
             class delta_type >
  void operator() ( target_type* result ,
                    const delta_type & _delta ) const
  {
    // scale delta

    auto delta = _delta * wr ;

    // left and right are the minimum and maximum values of the covered
    // area's extent along the axis this functor is applied to
    // we set result[1] to one initially, and subtract result[0] and
    // result[2] as we go along, giving un the desired partition of unity

    result[1] = one ;

    // calculate 'sticking out' parts and process them

    auto left = delta - one_half ;

    result[0] = - p - left ;
    result[0] ( result[0] < 0 ) = dtype(0) ;
    result[1] -= result[0] ;

    auto right = delta + one_half ;

    result[2] = right - p ;
    result[2] ( result[2] < 0 ) = dtype(0) ;
    result[1] -= result[2] ;
  }

} ;

/// alias template to make the declaration of non-bspline evaluators
/// easier. Note that we fix the 'specialze' template parameter at -1
/// (no specialization) and pass the mbf type argument in third position.

template < typename _coordinate_type ,
           typename _trg_type ,
           typename _mbf_type ,
           size_t _vsize = vspline::vector_traits < _trg_type > :: size ,
           typename _math_ele_type
             = default_math_type < _coordinate_type , _trg_type > ,
           typename _cf_type = _trg_type
         >
using ev_evaluator = evaluator < _coordinate_type ,
                                  _trg_type ,
                                  _vsize ,
                                  -1 ,
                                  _math_ele_type ,
                                  _cf_type ,
                                  _mbf_type
                                > ;

} ; // end of namespace vspline
