#ifndef CONVOLUTIONS_H
#define CONVOLUTIONS_H

#include "../structures/types.h"

#include "../util/rng.h"

#include <cmath>

namespace algorithms {

class Convolutions {
 public:
  /**
   * This function will convolve data with the specified kernel. It will
   * place the element at position (kernelSize/2) of the kernel in the
   * centre, i.e. data[0] := sum over i in kS : data[i - kS + _kS/2_] *
   * kernel[i]. Therefore, it makes most sense to specify an odd kernelSize if
   * the kernel consists of a peak / symmetric function.
   *
   * This function assumes the function represented by @c data is zero outside
   * its domain [0..dataSize}.
   *
   * This function does not reverse one of the input functions, therefore this
   * function is not adhering to the strict definition of a convolution.
   *
   * This function does not use an FFT to calculate the convolution, and is
   * therefore O(dataSize x kernelSize).
   *
   * @param data The data to be convolved (will also be the output)
   * @param dataSize Number of samples in data
   * @param kernel The kernel to be convolved, central sample = kernelSize/2
   * @param kernelSize Number of samples in the kernel, probably desired to be
   * odd if the kernel is symmetric.
   */
  static void OneDimensionalConvolutionBorderZero(num_t* data,
                                                  unsigned dataSize,
                                                  const num_t* kernel,
                                                  unsigned kernelSize) {
    num_t* tmp = new num_t[dataSize];
    for (unsigned i = 0; i < dataSize; ++i) {
      unsigned kStart, kEnd;
      const int offset = i - kernelSize / 2;
      // Make sure that kStart >= 0
      if (offset < 0)
        kStart = -offset;
      else
        kStart = 0;
      // Make sure that kEnd + offset <= dataSize
      if (offset + kernelSize > dataSize)
        kEnd = dataSize - offset;
      else
        kEnd = kernelSize;
      num_t sum = 0.0;
      for (unsigned k = kStart; k < kEnd; ++k)
        sum += data[k + offset] * kernel[k];
      tmp[i] = sum;
    }
    for (unsigned i = 0; i < dataSize; ++i) data[i] = tmp[i];
    delete[] tmp;
  }

  /**
   * This function will convolve data with the specified kernel. It will
   * place the element at position (kernelSize/2) of the kernel in the
   * centre, i.e. data[0] := sum over i in kS : data[i - kS + _kS/2_] *
   * kernel[i]. Therefore, it makes most sense to specify an odd kernelSize if
   * the kernel consists of a peak / symmetric function.
   *
   * This function assumes the function represented by @c data is not zero
   * outside its domain [0..dataSize}, but is "unknown". It assumes that the
   * integral over @c kernel is one. If this is not the case, you have to
   * multiply all output values with the kernels integral after convolution.
   *
   * This function does not reverse one of the input functions, therefore this
   * function is not adhering to the strict definition of a convolution.
   *
   * This function does not use an FFT to calculate the convolution, and is
   * therefore O(dataSize x kernelSize).
   *
   * @param data The data to be convolved (will also be the output)
   * @param dataSize Number of samples in data
   * @param kernel The kernel to be convolved, central sample = kernelSize/2
   * @param kernelSize Number of samples in the kernel, probably desired to be
   * odd if the kernel is symmetric.
   */
  static void OneDimensionalConvolutionBorderInterp(num_t* data,
                                                    unsigned dataSize,
                                                    const num_t* kernel,
                                                    unsigned kernelSize) {
    num_t* tmp = new num_t[dataSize];
    for (unsigned i = 0; i < dataSize; ++i) {
      unsigned kStart, kEnd;
      const int offset = i - kernelSize / 2;
      // Make sure that kStart >= 0
      if (offset < 0)
        kStart = -offset;
      else
        kStart = 0;
      // Make sure that kEnd + offset <= dataSize
      if (offset + kernelSize > dataSize)
        kEnd = dataSize - offset;
      else
        kEnd = kernelSize;
      num_t sum = 0.0;
      num_t weight = 0.0;
      for (unsigned k = kStart; k < kEnd; ++k) {
        sum += data[k + offset] * kernel[k];
        weight += kernel[k];
      }
      // Weighting is performed to correct for the "missing data" outside the
      // domain of data. The actual correct value should be sum * (sumover
      // kernel / sumover kernel-used ). We however assume "sumover kernel" to
      // be 1. sumover kernel-used is the weight.
      tmp[i] = sum / weight;
    }
    for (unsigned i = 0; i < dataSize; ++i) data[i] = tmp[i];
    delete[] tmp;
  }

  static void OneDimensionalGausConvolution(num_t* data, unsigned dataSize,
                                            num_t sigma) {
    unsigned kernelSize = (unsigned)std::round(sigma * 3.0L);
    if (kernelSize % 2 == 0) ++kernelSize;
    if (kernelSize > dataSize * 2) {
      if (dataSize == 0) return;
      kernelSize = dataSize * 2 - 1;
    }
    unsigned centreElement = kernelSize / 2;
    num_t* kernel = new num_t[kernelSize];
    for (unsigned i = 0; i < kernelSize; ++i) {
      num_t x = ((num_t)i - (num_t)centreElement);
      kernel[i] = RNG::EvaluateGaussian(x, sigma);
    }
    OneDimensionalConvolutionBorderInterp(data, dataSize, kernel, kernelSize);
    delete[] kernel;
  }

  /**
   * Perform a sinc convolution. This is equivalent with a low-pass filter,
   * where @c frequency specifies the frequency in index units.
   *
   * With frequency=1, the data will be convolved with the
   * delta function and have no effect. With frequency = 0.25,
   * all fringes faster than 0.25 fringes/index units will be
   * filtered.
   *
   * The function is O(dataSize^2).
   */
  static void OneDimensionalSincConvolution(num_t* data, unsigned dataSize,
                                            num_t frequency) {
    if (dataSize == 0) return;
    const unsigned kernelSize = dataSize * 2 - 1;
    const unsigned centreElement = kernelSize / 2;
    num_t* kernel = new num_t[kernelSize];
    const numl_t factor = 2.0 * frequency * M_PInl;
    numl_t sum = 0.0;
    for (unsigned i = 0; i < kernelSize; ++i) {
      const numl_t x = (((numl_t)i - (numl_t)centreElement) * factor);
      if (x != 0.0)
        kernel[i] = (num_t)(std::sin(x) / x);
      else
        kernel[i] = 1.0;
      sum += kernel[i];
    }
    for (unsigned i = 0; i < kernelSize; ++i) {
      kernel[i] /= sum;
    }
    OneDimensionalConvolutionBorderZero(data, dataSize, kernel, kernelSize);
    delete[] kernel;
  }

  /**
   * Perform a sinc convolution, with a convolution kernel that has been Hamming
   * windowed. Because of the hamming window, the filter has less of a steep
   * cutting edge, but less ripples.
   *
   * See OneDimensionalSincConvolution() for more info.
   *
   * The function is O(dataSize^2).
   */
  static void OneDimensionalSincConvolutionHammingWindow(num_t* data,
                                                         unsigned dataSize,
                                                         num_t frequency) {
    if (dataSize == 0) return;
    const unsigned kernelSize = dataSize * 2 - 1;
    const unsigned centreElement = kernelSize / 2;
    num_t* kernel = new num_t[kernelSize];
    const numl_t sincFactor = 2.0 * frequency * M_PInl;
    const numl_t hammingFactor = 2.0 * M_PInl * (numl_t)(kernelSize - 1);
    numl_t sum = 0.0;
    for (unsigned i = 0; i < kernelSize; ++i) {
      const numl_t hamming = 0.54 - 0.46 * std::cos(hammingFactor * (numl_t)i);
      const numl_t x = (((numl_t)i - (numl_t)centreElement) * sincFactor);
      if (x != 0.0)
        kernel[i] = (num_t)(std::sin(x) / x) * hamming;
      else
        kernel[i] = 1.0;
      sum += kernel[i];
    }
    for (unsigned i = 0; i < kernelSize; ++i) {
      kernel[i] /= sum;
    }
    OneDimensionalConvolutionBorderZero(data, dataSize, kernel, kernelSize);
    delete[] kernel;
  }
};

}  // namespace algorithms

#endif
