/*
 *  This code 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 2 of the License, or
 *  (at your option) any later version.
 *
 *  This code 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 code; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

/*
 *  Copyright (C) 2020-2025 Max-Planck-Society
 *  Author: Martin Reinecke
 */

#include "ducc0/../../python/module_adders.h"
#include "ducc0/bindings/pybind_utils.h"
#include "ducc0/sht/totalconvolve.h"

namespace ducc0 {

namespace detail_pymodule_totalconvolve {

using namespace std;

template<typename T> class Py_ConvolverPlan: public ConvolverPlan<T>
  {
  private:
    using ConvolverPlan<T>::lmax;
    using ConvolverPlan<T>::ConvolverPlan;
    using ConvolverPlan<T>::getPlane;
    using ConvolverPlan<T>::interpol;
    using ConvolverPlan<T>::deinterpol;
    using ConvolverPlan<T>::updateSlm;
    using ConvolverPlan<T>::getPatchInfo;
    using ConvolverPlan<T>::prepPsi;
    using ConvolverPlan<T>::deprepPsi;

  public:
    using ConvolverPlan<T>::Ntheta;
    using ConvolverPlan<T>::Nphi;
    using ConvolverPlan<T>::Npsi;
    // for backwards compatibility
    Py_ConvolverPlan(size_t lmax_, size_t kmax_, double sigma,
      double epsilon, size_t nthreads_)
      : Py_ConvolverPlan(lmax_, kmax_, 1000000000,
                         sigma-0.05, sigma+0.05, epsilon, nthreads_) {}
    vector<size_t> Py_getPatchInfo(T theta_lo, T theta_hi, T phi_lo, T phi_hi)
      { return getPatchInfo(theta_lo, theta_hi, phi_lo, phi_hi); }
    void Py_getPlane(const CNpArr &slm_, const CNpArr &blm_,
      size_t mbeam, NpArr &planes_) const
      {
      auto slm = to_cmav<complex<T>,1>(slm_);
      auto blm = to_cmav<complex<T>,1>(blm_);
      auto planes = to_vmav<T,3>(planes_);
      {
      py::gil_scoped_release release;
      getPlane(slm, blm, mbeam, planes);
      }
      }
    void Py_prepPsi(const NpArr &subcube_) const
      {
      auto subcube = to_vmav<T,3>(subcube_);
      {
      py::gil_scoped_release release;
      prepPsi(subcube);
      }
      }
    void Py_deprepPsi(const NpArr &subcube_) const
      {
      auto subcube = to_vmav<T,3>(subcube_);
      {
      py::gil_scoped_release release;
      deprepPsi(subcube);
      }
      }
    void Py_interpol(const CNpArr &cube_, size_t itheta0, size_t iphi0,
      const CNpArr &theta_, const CNpArr &phi_, const CNpArr &psi_,
      NpArr &signal_)
      {
      auto cube = to_cmav<T,3>(cube_);
      auto theta = to_cmav<T,1>(theta_);
      auto phi = to_cmav<T,1>(phi_);
      auto psi = to_cmav<T,1>(psi_);
      auto signal = to_vmav<T,1>(signal_);
      {
      py::gil_scoped_release release;
      interpol(cube, itheta0, iphi0, theta, phi, psi, signal);
      }
      }
    void Py_deinterpol(NpArr &cube_, size_t itheta0, size_t iphi0,
      const CNpArr &theta_, const CNpArr &phi_, const CNpArr &psi_,
      const CNpArr &signal_)
      {
      auto cube = to_vmav<T,3>(cube_);
      auto theta = to_cmav<T,1>(theta_);
      auto phi = to_cmav<T,1>(phi_);
      auto psi = to_cmav<T,1>(psi_);
      auto signal = to_cmav<T,1>(signal_);
      {
      py::gil_scoped_release release;
      deinterpol(cube, itheta0, iphi0, theta, phi, psi, signal);
      }
      }
    void Py_updateSlm(NpArr &slm_, const CNpArr &blm_,
      size_t mbeam, NpArr &planes_) const
      {
      auto slm = to_vmav<complex<T>,1>(slm_);
      auto blm = to_cmav<complex<T>,1>(blm_);
      auto planes = to_vmav<T,3>(planes_);
      {
      py::gil_scoped_release release;
      updateSlm(slm, blm, mbeam, planes);
      }
      }
  };


template<typename T> class Py_Interpolator
  {
  private:
    ConvolverPlan<T> conv;
    vmav<T,4> cube;

  public:
    Py_Interpolator(const CNpArr &slm_, const CNpArr &blm_,
      bool separate, size_t lmax, size_t kmax, size_t npoints, double sigma_min, double sigma_max, double epsilon, int nthreads)
      : conv(lmax, kmax, npoints, sigma_min, sigma_max, epsilon, nthreads),
        cube(conv.buildCube(separate ? size_t(slm_.shape(0)) : 1u))
      {
      auto vslm = to_cmav<complex<T>,2>(slm_);
      auto vblm = to_cmav<complex<T>,2>(blm_);
      {
      py::gil_scoped_release release;
      if (separate)
        for (size_t i=0; i<vslm.shape(0); ++i)
          {
          auto vslmi = subarray<2>(vslm, {{i,i+1},{}});
          auto vblmi = subarray<2>(vblm, {{i,i+1},{}});
          conv.getPlane(vslmi, vblmi, 0, subarray<3>(cube, {{i},{0,1},{},{}}));
          for (size_t k=1; k<kmax+1; ++k)
            conv.getPlane(vslmi, vblmi, k, subarray<3>(cube, {{i},{2*k-1, 2*k+1},{},{}}));
          }
      else
        {
        conv.getPlane(vslm, vblm, 0, subarray<3>(cube, {{0},{0,1},{},{}}));
        for (size_t k=1; k<kmax+1; ++k)
          conv.getPlane(vslm, vblm, k, subarray<3>(cube, {{0},{2*k-1,2*k+1},{},{}}));
        }
      for (size_t i=0; i<cube.shape(0); ++i)
        conv.prepPsi(subarray<3>(cube, {{i},{},{},{}}));
      }
      }
    Py_Interpolator(size_t lmax, size_t kmax, size_t ncomp_, size_t npoints, double sigma_min, double sigma_max, double epsilon, int nthreads)
      : conv(lmax, kmax, npoints, sigma_min, sigma_max, epsilon, nthreads),
        cube({size_t(ncomp_), conv.Npsi(), conv.Ntheta(), conv.Nphi()})
      {}
    //for backwards compatibility
    Py_Interpolator(const CNpArr &slm_, const CNpArr &blm_,
      bool separate, size_t lmax, size_t kmax, T epsilon, T ofactor, int nthreads)
      : Py_Interpolator(slm_, blm_,separate, lmax, kmax, 1000000000,
                        ofactor-0.05, ofactor+0.05, epsilon, nthreads) {}
    Py_Interpolator(size_t lmax, size_t kmax, size_t ncomp_, T epsilon, T ofactor, int nthreads)
      : Py_Interpolator(lmax, kmax, ncomp_, 1000000000,
                        ofactor-0.05, ofactor+0.05, epsilon, nthreads) {}

    template<typename Tloc> NpArr Py_Interpol2(const CNpArr &ptg) const
      {
      auto ptg2 = to_cmav<Tloc,2>(ptg);
      auto ptheta = subarray<1>(ptg2, {{},{0}});
      auto pphi = subarray<1>(ptg2, {{},{1}});
      auto ppsi = subarray<1>(ptg2, {{},{2}});
      size_t ncomp = cube.shape(0);
      auto res = make_Pyarr<T>({ncomp,ptg2.shape(0)});
      auto res2 = to_vmav<T,2>(res);
      {
      py::gil_scoped_release release;
      for (size_t i=0; i<ncomp; ++i)
        conv.interpol(subarray<3>(cube, {{i},{},{},{}}), 0, 0,
          ptheta, pphi, ppsi, subarray<1>(res2, {{i},{}}));
      }
      return res;
      }
    NpArr Py_Interpol(const CNpArr &ptg) const
      {
      if (isPyarr<float>(ptg))
        return Py_Interpol2<float>(ptg);
      if (isPyarr<double>(ptg))
        return Py_Interpol2<double>(ptg);
      MR_fail("type matching failed: 'ptg' has neither type 'f4' nor 'f8'");
      }

    template<typename Tloc> void Py_deinterpol2(const CNpArr &ptg, const CNpArr &data)
      {
      auto ptg2 = to_cmav<Tloc,2>(ptg);
      auto ptheta = subarray<1>(ptg2, {{},{0}});
      auto pphi = subarray<1>(ptg2, {{},{1}});
      auto ppsi = subarray<1>(ptg2, {{},{2}});
      size_t ncomp = cube.shape(0);
      auto data2 = to_cmav<T,2>(data);
      {
      py::gil_scoped_release release;
      for (size_t i=0; i<ncomp; ++i)
        conv.deinterpol(subarray<3>(cube, {{i},{},{},{}}), 0, 0,
          ptheta, pphi, ppsi, subarray<1>(data2, {{i},{}}));
      }
      }
    void Py_deinterpol(const CNpArr &ptg, const CNpArr &data)
      {
      if (isPyarr<float>(ptg))
        return Py_deinterpol2<float>(ptg, data);
      else if (isPyarr<double>(ptg))
        return Py_deinterpol2<double>(ptg, data);
      MR_fail("type matching failed: 'ptg' has neither type 'f4' nor 'f8'");
      }
    NpArr Py_getSlm(const CNpArr &blm_)
      {
      size_t lmax=conv.Lmax(), kmax=conv.Kmax();
      auto vblm = to_cmav<complex<T>,2>(blm_);
      size_t ncomp = vblm.shape(0);
      bool separate = cube.shape(0)>1;
      if (separate) MR_assert(ncomp==cube.shape(0), "dimension mismatch");
      {
      py::gil_scoped_release release;
      for (size_t i=0; i<cube.shape(0); ++i)
        conv.deprepPsi(subarray<3>(cube, {{i},{},{},{}}));
      }
      auto res = make_Pyarr<complex<T>>({ncomp, Alm_Base::Num_Alms(lmax, lmax)});
      auto vslm = to_vmav<complex<T>,2>(res);
      {
      py::gil_scoped_release release;
      mav_apply([](complex<T> &v){v=T(0);}, 1, vslm);
      if (separate)
        for (size_t i=0; i<ncomp; ++i)
          {
          auto vslmi = subarray<2>(vslm, {{i,i+1},{}});
          auto vblmi = subarray<2>(vblm, {{i,i+1},{}});
          conv.updateSlm(vslmi, vblmi, 0, subarray<3>(cube, {{i},{0,1},{},{}}));
          for (size_t k=1; k<kmax+1; ++k)
            conv.updateSlm(vslmi, vblmi, k, subarray<3>(cube, {{i},{2*k-1,2*k+1},{},{}}));
          }
      else
        {
        conv.updateSlm(vslm, vblm, 0, subarray<3>(cube, {{0},{0,1},{},{}}));
        for (size_t k=1; k<kmax+1; ++k)
          conv.updateSlm(vslm, vblm, k, subarray<3>(cube, {{0},{2*k-1,2*k+1},{},{}}));
        }
      }
      return res;
      }
  };

constexpr const char *totalconvolve_DS = R"""(
Python interface for total convolution/interpolation library

All arrays containing spherical harmonic coefficients are assumed to have the
following format:

  - values for m=0, l going from 0 to lmax
    (these values must have an imaginary part of zero)
  - values for m=1, l going from 1 to lmax
    (these values can be fully complex)
  - values for m=2, l going from 2 to lmax
  - ...
  - values for m=mmax, l going from mmax to lmax

Error conditions are reported by raising exceptions.
)""";

constexpr const char *Py_ConvolverPlan_DS = R"""(
Class encapsulating the low-level interface for convolution/interpolation.
Computations are performed on double precision data.
)""";

constexpr const char *Py_ConvolverPlan_f_DS = R"""(
Class encapsulating the low-level interface for convolution/interpolation.
Computations are performed on single precision data.
)""";

constexpr const char *Py_ConvolverPlan_init_DS = R"""(
ConvolverPlan constructor

Parameters
----------
lmax : int, 0 <= lmax
    maximum l for the sky and beam coefficients; maximum m for sky coefficients
    In other words, the band limit of the involved functions
kmax : int, 0 <= kmax <= lmax
    maximum m (or azimuthal moment) for the beam coefficients
npoints : int
    total number of irregularly spaced points you want to use this object for
    (only used for performance fine-tuning)
sigma_min, sigma_max: float
    minimum and maximum allowed oversampling factors
    1.2 <= sigma_min < sigma_max <= 2.5
epsilon : float, 1e-12 <= epsilon <= 1e-1
    the desired relative accuracy of the interpolation
    NOTE: epsilons near the accuracy limit can only be reached by choosing
    a sufficiently high value for sigma!
nthreads : int 0 <= nthreads
    the number of threads to use for all computations
    A value of 0 implies that the full number of hardware threads on the system
    will be used.
)""";

constexpr const char *Py_ConvolverPlan_f_init_DS = R"""(
ConvolverPlan constructor

Parameters
----------
lmax : int, 0 <= lmax
    maximum l for the sky and beam coefficients; maximum m for sky coefficients
    In other words, the band limit of the involved functions
kmax : int, 0 <= kmax <= lmax
    maximum m (or azimuthal moment) for the beam coefficients
npoints : int
    total number of irregularly spaced points you want to use this object for
    (only used for performance fine-tuning)
sigma_min, sigma_max: float
    minimum and maximum allowed oversampling factors
    1.2 <= sigma_min < sigma_max <= 2.5
epsilon : float, 3e-5 <= epsilon <= 1e-1
    the desired relative accuracy of the interpolation
    NOTE: epsilons near the accuracy limit can only be reached by choosing
    a sufficiently high value for sigma!
nthreads : int 0 <= nthreads
    the number of threads to use for all computations
    A value of 0 implies that the full number of hardware threads on the system
    will be used.
)""";

constexpr const char *Py_ConvolverPlan_Ntheta_DS = R"""(
Returns
-------
The full data cube dimension in theta direction (second axis)
)""";

constexpr const char *Py_ConvolverPlan_Nphi_DS = R"""(
Returns
-------
The full data cube dimension in phi direction (third axis)
)""";

constexpr const char *Py_ConvolverPlan_Npsi_DS = R"""(
Returns
-------
The full data cube dimension in psi direction (first axis)
)""";

constexpr const char *Py_ConvolverPlan_getPatchInfo_DS = R"""(
Returns information necessary to extract a given sub-area from the data cube.

Parameters
----------
theta_lo, theta_hi : float, 0 <= theta_lo < theta_hi <= pi
    colatitude borders of the requested patch
phi_lo, phi_hi : float, 0 <= phi_lo < phi_hi <= 2*pi
    longitude borders of the requested patch

Returns
-------
tuple(int) with 4 elements itheta_lo, itheta_hi, iphi_lo, iphi_hi
    The sub-array [:, itheta_lo:itheta_hi, iphi_lo:iphi_hi] of a full data cube
    will contain all information necessary to interpolate pointings within the
    specified patch.
)""";

constexpr const char *Py_ConvolverPlan_getPlane_DS = R"""(
Computes a single (real or complex) sub-plane in (theta, phi) of the data cube

Parameters
----------
slm : numpy.ndarray((nalm_sky,), dtype=numpy.complex128), or
      numpy.ndarray((ncomp, nalm_sky), dtype=numpy.complex128)
    spherical harmonic coefficients of the sky.
blm : numpy.ndarray((nalm_beam,), dtype=numpy.complex128), or
      numpy.ndarray((ncomp, nalm_beam), dtype=numpy.complex128)
    spherical harmonic coefficients of the beam.
mbeam : int, 0 <= mbeam <= kmax
    requested m moment of the beam
planes : numpy.ndarray((nplanes, Ntheta(), Nphi()), dtype=numpy.float64)
    nplanes must be 1 for mbeam==0, else 2
    will be filled with the real part (and the imaginary part for mbeam>0)
    of the requested sub-planes on exit

Notes
-----
If the `slm` and `blm` arrays have a second dimension, the contributions of all
components will be added together in `re` and `im`.
)""";

constexpr const char *Py_ConvolverPlan_f_getPlane_DS = R"""(
Computes a single (real or complex) sub-plane in (theta, phi) of the data cube

Parameters
----------
slm : numpy.ndarray((nalm_sky,), dtype=numpy.complex64), or
      numpy.ndarray((ncomp, nalm_sky), dtype=numpy.complex)
    spherical harmonic coefficients of the sky.
blm : numpy.ndarray((nalm_beam,), dtype=numpy.complex64), or
      numpy.ndarray((ncomp, nalm_beam), dtype=numpy.complex)
    spherical harmonic coefficients of the beam.
mbeam : int, 0 <= mbeam <= kmax
    requested m moment of the beam
planes : numpy.ndarray((nplanes, Ntheta(), Nphi()), dtype=numpy.float32)
    nplanes must be 1 for mbeam==0, else 2
    will be filled with the real part (and the imaginary part for mbeam>0)
    of the requested sub-planes on exit

Notes
-----
If the `slm` and `blm` arrays have a second dimension, the contributions of all
components will be added together in `re` and `im`.
)""";

constexpr const char *Py_ConvolverPlan_prepPsi_DS = R"""(
Pepares a data cube for for actual interpolation.

Parameters
----------
subcube : numpy.ndarray((Npsi(), :, :), dtype=numpy.float64)
    On entry the part [0:2*kmax+1, :, :] must be filled with results from
    getPlane() calls.
    On exit, the entire array will be filled in a form that can be used for
    subsequent `interpol` calls.
)""";

constexpr const char *Py_ConvolverPlan_f_prepPsi_DS = R"""(
Pepares a data cube for for actual interpolation.

Parameters
----------
subcube : numpy.ndarray((Npsi(), :, :), dtype=numpy.float32)
    On entry the part [0:2*kmax+1, :, :] must be filled with results from
    getPlane() calls.
    On exit, the entire array will be filled in a form that can be used for
    subsequent `interpol` calls.
)""";

constexpr const char *Py_ConvolverPlan_deprepPsi_DS = R"""(
Adjoint of `prepPsi`.

Parameters
----------
subcube : numpy.ndarray((Npsi(), :, :), dtype=numpy.float64)
    On entry this must be an array filled by one or more `deinterpol` calls.
    On exit, only the part [0:2*kmax+1, :, :] is relevant and can be used for
    `updateSlm` calls.
)""";

constexpr const char *Py_ConvolverPlan_f_deprepPsi_DS = R"""(
Adjoint of `prepPsi`.

Parameters
----------
subcube : numpy.ndarray((Npsi(), :, :), dtype=numpy.float32)
    On entry this must be an array filled by one or more `deinterpol` calls.
    On exit, only the part [0:2*kmax+1, :, :] is relevant and can be used for
    `updateSlm` calls.
)""";

constexpr const char *Py_ConvolverPlan_interpol_DS = R"""(
Computes the interpolated values for a given set of angle triplets

Parameters
----------
cube : numpy.ndarray((Npsi(), :, :), dtype=numpy.float64)
    (Partial) data cube generated with `prepPsi`.
itheta0, iphi0 : int
    starting indices in theta and phi direction of the provided cube relative
    to the full cube.
theta, phi, psi : numpy.ndarray(nptg, dtype=numpy.float64 or numpy.float32)
    angle triplets at which the interpolated values will be computed
    Theta and phi must lie inside the ranges covered by the supplied cube.
    No constraints on psi.
signal : numpy.ndarray(nptg, dtype=numpy.float64)
    array into which the results will be written

Notes
-----
Repeated calls to this method are fine, but for good performance the
number of pointings passed per call should be as large as possible.
)""";

constexpr const char *Py_ConvolverPlan_f_interpol_DS = R"""(
Computes the interpolated values for a given set of angle triplets

Parameters
----------
cube : numpy.ndarray((Npsi(), :, :), dtype=numpy.float32)
    (Partial) data cube generated with `prepPsi`.
itheta0, iphi0 : int
    starting indices in theta and phi direction of the provided cube relative
    to the full cube.
theta, phi, psi : numpy.ndarray(nptg, dtype=numpy.float32 or numpy.float64)
    angle triplets at which the interpolated values will be computed
    Theta and phi must lie inside the ranges covered by the supplied cube.
    No constraints on psi.
signal : numpy.ndarray(nptg, dtype=numpy.float32)
    array into which the results will be written

Notes
-----
Repeated calls to this method are fine, but for good performance the
number of pointings passed per call should be as large as possible.
)""";

constexpr const char *Py_ConvolverPlan_deinterpol_DS = R"""(
Adjoint of `interpol`.
Spreads the values in `signal` over the appropriate regions of `cube`

Parameters
----------
cube : numpy.ndarray((Npsi(), :, :), dtype=numpy.float64)
    (Partial) data cube to which the deinterpolated values will be added.
    Must be zeroed before the first call to `deinterpol`!
itheta0, iphi0 : int
    starting indices in theta and phi direction of the provided cube relative
    to the full cube.
theta, phi, psi : numpy.ndarray(nptg, dtype=numpy.float64 or numpy.float32)
    angle triplets at which the interpolated values will be computed
    Theta and phi must lie inside the ranges covered by the supplied cube.
    No constraints on psi.
signal : numpy.ndarray(nptg, dtype=numpy.float64)
    signal values that will be deinterpolated into `cube`.

Notes
-----
Repeated calls to this method are fine, but for good performance the
number of pointings passed per call should be as large as possible.
)""";

constexpr const char *Py_ConvolverPlan_f_deinterpol_DS = R"""(
Adjoint of `interpol`.
Spreads the values in `signal` over the appropriate regions of `cube`

Parameters
----------
cube : numpy.ndarray((Npsi(), :, :), dtype=numpy.float32)
    (Partial) data cube to which the deinterpolated values will be added.
    Must be zeroed before the first call to `deinterpol`!
itheta0, iphi0 : int
    starting indices in theta and phi direction of the provided cube relative
    to the full cube.
theta, phi, psi : numpy.ndarray(nptg, dtype=numpy.float32 or numpy.float64)
    angle triplets at which the interpolated values will be computed
    Theta and phi must lie inside the ranges covered by the supplied cube.
    No constraints on psi.
signal : numpy.ndarray(nptg, dtype=numpy.float32)
    signal values that will be deinterpolated into `cube`.

Notes
-----
Repeated calls to this method are fine, but for good performance the
number of pointings passed per call should be as large as possible.
)""";

constexpr const char *Py_ConvolverPlan_updateSlm_DS = R"""(
Updates a set of sky spherical harmonic coefficients resulting from adjoint
interpolation.

Parameters
----------
slm : numpy.ndarray((nalm_sky,), dtype=numpy.complex128), or
      numpy.ndarray((ncomp, nalm_sky), dtype=numpy.complex128)
    The deinterpolated spherical harmonic coefficients will be added to this
    array.
    Must be zeroed before the first call to `updateSlm`!
blm : numpy.ndarray((nalm_beam,), dtype=numpy.complex128), or
      numpy.ndarray((ncomp, nalm_beam), dtype=numpy.complex128)
    spherical harmonic coefficients of the beam.
mbeam : int, 0 <= mbeam <= kmax
    requested m moment of the beam
planes : numpy.ndarray((nplanes, Ntheta(), Nphi()), dtype=numpy.float64)
    nplanes must be 1 for mbeam==0, else 2
    real part (and imaginary part for mbeam>0) of the requested plane

Notes
-----
If the `slm` and `blm` arrays have a second dimension, the `slm` will be
computed in a fashion that is adjoint to `getPlane`.
)""";

constexpr const char *Py_ConvolverPlan_f_updateSlm_DS = R"""(
Updates a set of sky spherical harmonic coefficients resulting from adjoint
interpolation.

Parameters
----------
slm : numpy.ndarray((nalm_sky,), dtype=numpy.complex64), or
      numpy.ndarray((ncomp, nalm_sky), dtype=numpy.complex64)
    The deinterpolated spherical harmonic coefficients will be added to this
    array.
    Must be zeroed before the first call to `updateSlm`!
blm : numpy.ndarray((nalm_beam,), dtype=numpy.complex64), or
      numpy.ndarray((ncomp, nalm_beam), dtype=numpy.complex64)
    spherical harmonic coefficients of the beam.
mbeam : int, 0 <= mbeam <= kmax
    requested m moment of the beam
planes : numpy.ndarray((nplanes, Ntheta(), Nphi()), dtype=numpy.float32)
    nplanes must be 1 for mbeam==0, else 2
    real part (and imaginary part for mbeam>0) of the requested plane

Notes
-----
If the `slm` and `blm` arrays have a second dimension, the `slm` will be
computed in a fashion that is adjoint to `getPlane`.
)""";

constexpr const char *Py_Interpolator_DS = R"""(
Class encapsulating the convolution/interpolation functionality

The class can be configured for interpolation or for adjoint interpolation, by
means of two different constructors.

Constructor for interpolation mode

Parameters
----------
sky : numpy.ndarray((ncomp, nalm_sky), dtype=numpy.complex)
    spherical harmonic coefficients of the sky. ncomp can be 1 or 3.
beam : numpy.ndarray((ncomp, nalm_beam), dtype=numpy.complex)
    spherical harmonic coefficients of the beam. ncomp can be 1 or 3
separate : bool
    whether contributions of individual components should be added together.
lmax : int
    maximum l in the coefficient arays
kmax : int
    maximum azimuthal moment in the beam coefficients
npoints : int
    total number of irregularly spaced points you want to use this object for
    (only used for performance fine-tuning)
sigma_min, sigma_max: float
    minimum and maximum allowed oversampling factors
    1.2 <= sigma_min < sigma_max <= 2.5
epsilon : float
    desired accuracy for the interpolation; a typical value is 1e-5
nthreads : the number of threads to use for computation

Constructor for adjoint interpolation mode

Parameters
----------
lmax : int
    maximum l in the coefficient arays
kmax : int
    maximum azimuthal moment in the beam coefficients
ncomp : int
    the number of components which are going to input to `deinterpol`.
    Can be 1 or 3.
npoints : int
    total number of irregularly spaced points you want to use this object for
    (only used for performance fine-tuning)
sigma_min, sigma_max: float
    minimum and maximum allowed oversampling factors
    1.2 <= sigma_min < sigma_max <= 2.5
epsilon : float
    desired accuracy for the interpolation; a typical value is 1e-5
nthreads : the number of threads to use for computation

)""";

constexpr const char *initnormal_DS = R"""(
Constructor for interpolation mode

Parameters
----------
sky : numpy.ndarray((ncomp, nalm_sky), dtype=numpy.complex)
    spherical harmonic coefficients of the sky. ncomp can be 1 or 3.
beam : numpy.ndarray((ncomp, nalm_beam), dtype=numpy.complex)
    spherical harmonic coefficients of the beam. ncomp can be 1 or 3
separate : bool
    whether contributions of individual components should be added together.
lmax : int
    maximum l in the coefficient arays
kmax : int
    maximum azimuthal moment in the beam coefficients
npoints : int
    total number of irregularly spaced points you want to use this object for
    (only used for performance fine-tuning)
sigma_min, sigma_max: float
    minimum and maximum allowed oversampling factors
    1.2 <= sigma_min < sigma_max <= 2.5
epsilon : float
    desired accuracy for the interpolation; a typical value is 1e-5
nthreads : the number of threads to use for computation
)""";

constexpr const char *initadjoint_DS = R"""(
Constructor for adjoint interpolation mode

Parameters
----------
lmax : int
    maximum l in the coefficient arays
kmax : int
    maximum azimuthal moment in the beam coefficients
ncomp : int
    the number of components which are going to input to `deinterpol`.
    Can be 1 or 3.
npoints : int
    total number of irregularly spaced points you want to use this object for
    (only used for performance fine-tuning)
sigma_min, sigma_max: float
    minimum and maximum allowed oversampling factors
    1.2 <= sigma_min < sigma_max <= 2.5
epsilon : float
    desired accuracy for the interpolation; a typical value is 1e-5
nthreads : the number of threads to use for computation
)""";

constexpr const char *interpol_DS = R"""(
Computes the interpolated values for a given set of angle triplets

Parameters
----------
ptg : numpy.ndarray((N, 3), dtype=numpy.float64)
    theta, phi and psi angles (in radian) for N pointings
    theta must be in the range [0; pi]
    phi must be in the range [0; 2pi]
    psi should be in the range [-2pi; 2pi]

Returns
-------
numpy.array((n2, N), dtype=numpy.float64)
    the interpolated values
    n2 is either 1 (if separate=True was used in the constructor) or the
    second dimension of the input slm and blm arrays (otherwise)

Notes
-----
    - Can only be called in "normal" (i.e. not adjoint) mode
    - repeated calls to this method are fine, but for good performance the
      number of pointings passed per call should be as large as possible.
)""";

constexpr const char *deinterpol_DS = R"""(
Takes a set of angle triplets and interpolated values and spreads them onto the
data cube.

Parameters
----------
ptg : numpy.ndarray((N,3), dtype=numpy.float64)
    theta, phi and psi angles (in radian) for N pointings
    theta must be in the range [0; pi]
    phi must be in the range [0; 2pi]
    psi should be in the range [-2pi; 2pi]
data : numpy.ndarray((n2, N), dtype=numpy.float64)
    the interpolated values
    n2 must match the `ncomp` value specified in the constructor.

Notes
-----
    - Can only be called in adjoint mode
    - repeated calls to this method are fine, but for good performance the
      number of pointings passed per call should be as large as possible.
)""";

constexpr const char *getSlm_DS = R"""(
Returns a set of sky spherical harmonic coefficients resulting from adjoint
interpolation

Parameters
----------
beam : numpy.array(nalm_beam, nbeam), dtype=numpy.complex)
    spherical harmonic coefficients of the beam with lmax and kmax defined
    in the constructor call
    nbeam must match the ncomp specified in the constructor, unless ncomp was 1.

Returns
-------
numpy.array(nalm_sky, nbeam), dtype=numpy.complex):
    spherical harmonic coefficients of the sky with lmax defined
    in the constructor call

Notes
-----
    - Can only be called in adjoint mode
    - must be the last call to the object
)""";

void add_totalconvolve(py::module_ &msup)
  {
  using namespace py::literals;
  auto m = msup.def_submodule("totalconvolve");

  m.doc() = totalconvolve_DS;

  using conv_d = Py_ConvolverPlan<double>;
  py::class_<conv_d> (m, "ConvolverPlan", /*py::module_local(), */Py_ConvolverPlan_DS)
    .def(py::init<size_t, size_t, size_t, double, double, double, size_t>(), Py_ConvolverPlan_init_DS,
      "lmax"_a, "kmax"_a, "npoints"_a=1000000000, "sigma_min"_a=1.1, "sigma_max"_a=2.6, "epsilon"_a, "nthreads"_a=0)
// for backwards compatibility
    .def(py::init<size_t, size_t, double, double, size_t>(),
      "lmax"_a, "kmax"_a, "sigma"_a, "epsilon"_a, "nthreads"_a=0)
    .def("Ntheta", &conv_d::Ntheta, Py_ConvolverPlan_Ntheta_DS)
    .def("Nphi", &conv_d::Nphi, Py_ConvolverPlan_Nphi_DS)
    .def("Npsi", &conv_d::Npsi, Py_ConvolverPlan_Npsi_DS)
    .def("getPatchInfo", &conv_d::Py_getPatchInfo, Py_ConvolverPlan_getPatchInfo_DS,
      "theta_lo"_a, "theta_hi"_a, "phi_lo"_a, "phi_hi"_a)
    .def("getPlane", &conv_d::Py_getPlane, Py_ConvolverPlan_getPlane_DS,
      "slm"_a, "blm"_a, "mbeam"_a, "planes"_a)
    .def("prepPsi", &conv_d::Py_prepPsi, Py_ConvolverPlan_prepPsi_DS, "subcube"_a)
    .def("deprepPsi", &conv_d::Py_deprepPsi, Py_ConvolverPlan_deprepPsi_DS, "subcube"_a)
    .def("interpol", &conv_d::Py_interpol, Py_ConvolverPlan_interpol_DS,
      "cube"_a, "itheta0"_a, "iphi0"_a, "theta"_a, "phi"_a, "psi"_a, "signal"_a)
    .def("deinterpol", &conv_d::Py_deinterpol, Py_ConvolverPlan_deinterpol_DS,
      "cube"_a, "itheta0"_a, "iphi0"_a, "theta"_a, "phi"_a, "psi"_a, "signal"_a)
    .def("updateSlm", &conv_d::Py_updateSlm, Py_ConvolverPlan_updateSlm_DS,
      "slm"_a, "blm"_a, "mbeam"_a, "planes"_a);
  using conv_f = Py_ConvolverPlan<float>;
  py::class_<conv_f> (m, "ConvolverPlan_f", /*py::module_local(), */Py_ConvolverPlan_f_DS)
    .def(py::init<size_t, size_t, size_t, double, double, double, size_t>(), Py_ConvolverPlan_f_init_DS,
      "lmax"_a, "kmax"_a, "npoints"_a=1000000000, "sigma_min"_a=1.1, "sigma_max"_a=2.6, "epsilon"_a, "nthreads"_a=0)
// for backwards compatibility
    .def(py::init<size_t, size_t, double, double, size_t>(),
      "lmax"_a, "kmax"_a, "sigma"_a, "epsilon"_a, "nthreads"_a=0)
    .def("Ntheta", &conv_f::Ntheta, Py_ConvolverPlan_Ntheta_DS)
    .def("Nphi", &conv_f::Nphi, Py_ConvolverPlan_Nphi_DS)
    .def("Npsi", &conv_f::Npsi, Py_ConvolverPlan_Npsi_DS)
    .def("getPatchInfo", &conv_f::Py_getPatchInfo, Py_ConvolverPlan_getPatchInfo_DS,
      "theta_lo"_a, "theta_hi"_a, "phi_lo"_a, "phi_hi"_a)
    .def("getPlane", &conv_f::Py_getPlane, Py_ConvolverPlan_f_getPlane_DS,
      "slm"_a, "blm"_a, "mbeam"_a, "planes"_a)
    .def("prepPsi", &conv_f::Py_prepPsi, Py_ConvolverPlan_f_prepPsi_DS, "subcube"_a)
    .def("deprepPsi", &conv_f::Py_deprepPsi, Py_ConvolverPlan_f_deprepPsi_DS, "subcube"_a)
    .def("interpol", &conv_f::Py_interpol, Py_ConvolverPlan_f_interpol_DS,
      "cube"_a, "itheta0"_a, "iphi0"_a, "theta"_a, "phi"_a, "psi"_a, "signal"_a)
    .def("deinterpol", &conv_f::Py_deinterpol, Py_ConvolverPlan_f_deinterpol_DS,
      "cube"_a, "itheta0"_a, "iphi0"_a, "theta"_a, "phi"_a, "psi"_a, "signal"_a)
    .def("updateSlm", &conv_f::Py_updateSlm, Py_ConvolverPlan_f_updateSlm_DS,
      "slm"_a, "blm"_a, "mbeam"_a, "planes"_a);

  using inter_d = Py_Interpolator<double>;
  py::class_<inter_d> (m, "Interpolator", /*py::module_local(), */Py_Interpolator_DS)
    .def(py::init<const CNpArr &, const CNpArr &, bool, size_t, size_t, size_t, double, double, double, int>(),
      initnormal_DS, "sky"_a, "beam"_a, "separate"_a, "lmax"_a, "kmax"_a, "npoints"_a=1000000000, "sigma_min"_a=1.1, "sigma_max"_a=2.6, "epsilon"_a, "nthreads"_a=0)
    .def(py::init<size_t, size_t, size_t, size_t, double, double, double, int>(), initadjoint_DS,
      "lmax"_a, "kmax"_a, "ncomp"_a, "npoints"_a=1000000000, "sigma_min"_a=1.1, "sigma_max"_a=2.6,"epsilon"_a, "nthreads"_a=0)
// for backwards compatibility
    .def(py::init<const CNpArr &, const CNpArr &, bool, size_t, size_t, double, double, int>(),
      "sky"_a, "beam"_a, "separate"_a, "lmax"_a, "kmax"_a, "epsilon"_a, "ofactor"_a=1.5,
      "nthreads"_a=0)
    .def(py::init<size_t, size_t, size_t, double, double, int>(),
      "lmax"_a, "kmax"_a, "ncomp"_a, "epsilon"_a, "ofactor"_a=1.5, "nthreads"_a=0)
    .def ("interpol", &inter_d::Py_Interpol, interpol_DS, "ptg"_a)
    .def ("deinterpol", &inter_d::Py_deinterpol, deinterpol_DS, "ptg"_a, "data"_a)
    .def ("getSlm", &inter_d::Py_getSlm, getSlm_DS, "beam"_a);
  using inter_f = Py_Interpolator<float>;
  py::class_<inter_f> (m, "Interpolator_f", /*py::module_local(), */Py_Interpolator_DS)
    .def(py::init<const CNpArr &, const CNpArr &, bool, size_t, size_t, size_t, double, double, double, int>(),
      initnormal_DS, "sky"_a, "beam"_a, "separate"_a, "lmax"_a, "kmax"_a, "npoints"_a=1000000000, "sigma_min"_a=1.1, "sigma_max"_a=2.6, "epsilon"_a, "nthreads"_a=0)
    .def(py::init<size_t, size_t, size_t, size_t, double, double, double, int>(), initadjoint_DS,
      "lmax"_a, "kmax"_a, "ncomp"_a, "npoints"_a=1000000000, "sigma_min"_a=1.1, "sigma_max"_a=2.6,"epsilon"_a, "nthreads"_a=0)
// for backwards compatibility
    .def(py::init<const CNpArr &, const CNpArr &, bool, size_t, size_t, float, float, int>(),
      "sky"_a, "beam"_a, "separate"_a, "lmax"_a, "kmax"_a, "epsilon"_a, "ofactor"_a=1.5,
      "nthreads"_a=0)
    .def(py::init<size_t, size_t, size_t, float, float, int>(),
      "lmax"_a, "kmax"_a, "ncomp"_a, "epsilon"_a, "ofactor"_a=1.5, "nthreads"_a=0)
    .def ("interpol", &inter_f::Py_Interpol, interpol_DS, "ptg"_a)
    .def ("deinterpol", &inter_f::Py_deinterpol, deinterpol_DS, "ptg"_a, "data"_a)
    .def ("getSlm", &inter_f::Py_getSlm, getSlm_DS, "beam"_a);
  }

}

using detail_pymodule_totalconvolve::add_totalconvolve;

}
