//# Image2DConvolver.cc:  convolution of an image by given Array
//# Copyright (C) 1995,1996,1997,1998,1999,2000,2001,2002
//# Associated Universities, Inc. Washington DC, USA.
//#
//# This library is free software; you can redistribute it and/or modify it
//# under the terms of the GNU Library General Public License as published by
//# the Free Software Foundation; either version 2 of the License, or (at your
//# option) any later version.
//#
//# This library 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 Library General Public
//# License for more details.
//#
//# You should have received a copy of the GNU Library General Public License
//# along with this library; if not, write to the Free Software Foundation,
//# Inc., 675 Massachusetts Ave, Cambridge, MA 02139, USA.
//#
//# Correspondence concerning AIPS++ should be addressed as follows:
//#        Internet email: aips2-request@nrao.edu.
//#        Postal address: AIPS++ Project Office
//#                        National Radio Astronomy Observatory
//#                        520 Edgemont Road
//#                        Charlottesville, VA 22903-2475 USA
//#
//   
#include <imageanalysis/ImageAnalysis/Image2DConvolver.h>

#include <casacore/casa/aips.h>
#include <casacore/casa/Arrays/IPosition.h>
#include <casacore/casa/Arrays/Array.h>
#include <casacore/casa/Arrays/ArrayMath.h>
#include <casacore/casa/Arrays/Vector.h>
#include <casacore/casa/Arrays/Matrix.h>
#include <casacore/casa/Exceptions/Error.h>
#include <components/ComponentModels/GaussianDeconvolver.h>
#include <components/ComponentModels/GaussianShape.h>
#include <components/ComponentModels/SkyComponentFactory.h>
#include <casacore/coordinates/Coordinates/CoordinateUtil.h>
#include <casacore/coordinates/Coordinates/CoordinateSystem.h>
#include <casacore/coordinates/Coordinates/DirectionCoordinate.h>
#include <casacore/lattices/LatticeMath/Fit2D.h>
#include <casacore/scimath/Functionals/Gaussian2D.h>
#include <imageanalysis/ImageAnalysis/ImageConvolver.h>
#include <imageanalysis/ImageAnalysis/ImageMetaData.h>
#include <casacore/images/Images/PagedImage.h>
#include <casacore/images/Images/TempImage.h>
#include <casacore/images/Images/ImageInterface.h>
#include <casacore/images/Images/ImageInfo.h>
#include <casacore/images/Images/ImageUtilities.h>
#include <casacore/images/Images/SubImage.h>
#include <casacore/casa/Logging/LogIO.h>
#include <casacore/scimath/Mathematics/Convolver.h>
#include <casacore/casa/Quanta/Quantum.h>
#include <casacore/casa/Quanta/MVAngle.h>
#include <casacore/casa/Quanta/Unit.h>
#include <casacore/casa/Quanta/QLogical.h>
#include <iostream>

#include <memory>

namespace casa {

template <class T> const casacore::String Image2DConvolver<T>::CLASS_NAME
    = "Image2DConvolver";

template <class T> Image2DConvolver<T>::Image2DConvolver(
    const SPCIIT image, const casacore::Record *const &region,
    const casacore::String& mask, const casacore::String& outname,
    const bool overwrite
) : ImageTask<T>(image, "", region, "", "", "", mask, outname, overwrite),
    _type(casacore::VectorKernel::GAUSSIAN),  _scale(0), _major(), _minor(),
    _pa(), _axes(image->coordinates().directionAxesNumbers()) {
    this->_construct(true);
}

// TODO use GaussianBeams rather than casacore::Vector<casacore::Quantity>s, this method
// can probably be eliminated.
template <class T>
std::vector<casacore::Quantity> Image2DConvolver<T>::_getConvolvingBeamForTargetResolution(
    const std::vector<casacore::Quantity>& targetBeamParms,
    const casacore::GaussianBeam& inputBeam
) const {
    casacore::GaussianBeam convolvingBeam;
    casacore::GaussianBeam targetBeam(
        targetBeamParms[0], targetBeamParms[1],
        targetBeamParms[2]
    );
    try {
        if(
            GaussianDeconvolver::deconvolve(
                convolvingBeam, targetBeam, inputBeam
            )
        ) {
            // point source, or convolvingBeam nonsensical
            throw casacore::AipsError();
        }
    }
    catch (const casacore::AipsError& x) {
        ostringstream os;
        os << "Unable to reach target resolution of " << targetBeam << " Input "
            << "image beam " << inputBeam << " is (nearly) identical to or "
            << "larger than the output beam size";
        ThrowCc(os.str());
    }
    std::vector<casacore::Quantity> kernelParms {
        convolvingBeam.getMajor(),
        convolvingBeam.getMinor(),
        convolvingBeam.getPA(true)
    };
    return kernelParms;
}

template <class T> void Image2DConvolver<T>::setAxes(
    const std::pair<uint, uint>& axes
) {
    auto ndim = this->_getImage()->ndim();
    ThrowIf(axes.first == axes.second, "Axes must be different");
    ThrowIf(
        axes.first >= ndim || axes.second >= ndim,
        "Axis value must be less than number of axes in image"
    );
    if (_axes.size() != 2) {
        _axes.resize(2, false);
    }
    _axes[0] = axes.first;
    _axes[1] = axes.second;
}

template <class T> void Image2DConvolver<T>::setKernel(
    const casacore::String& type, const casacore::Quantity& major,
    const casacore::Quantity& minor, const casacore::Quantity& pa
) {
    ThrowIf (major < minor, "Major axis is less than minor axis");
    _type = casacore::VectorKernel::toKernelType(type);
    _major = major;
    _minor = minor;
    _pa = pa;
}

template <class T> SPIIT Image2DConvolver<T>::convolve() {
    ThrowIf(
        _axes.nelements() != 2, "You must give two pixel axes to convolve"
    );
    auto inc = this->_getImage()->coordinates().increment();
    auto units = this->_getImage()->coordinates().worldAxisUnits();
    ThrowIf(
        ! near (
            casacore::Quantity(fabs(inc[_axes[0]]), units[_axes[0]]),
            casacore::Quantity(fabs(inc[_axes[1]]), units[_axes[1]])
        ),
        "Pixels must be square, please regrid your image so that they are"
    );
    auto subImage = SubImageFactory<T>::createImage(
        *this->_getImage(), "", *this->_getRegion(), this->_getMask(),
        this->_getDropDegen(), false, false, this->_getStretch()
    );
    const auto nDim = subImage->ndim();
    ThrowIf(
        _axes(0) < 0 || _axes(0) >= nDim
        || _axes(1) < 0 || _axes(1) >= nDim,
        "The pixel axes " + casacore::String::toString(_axes) + " are illegal"
    );
    ThrowIf(
        nDim < 2, "The image axes must have at least 2 pixel axes"
    );
    shared_ptr<TempImage<T>> outImage(
        new casacore::TempImage<T>(
            subImage->shape(), subImage->coordinates()
        )
     );
    _convolve(
        outImage, *subImage, _type
    );
    if (subImage->isMasked()) {
        TempLattice<bool> mask(outImage->shape());
        ImageTask<T>::_copyMask(mask, *subImage);
        outImage->attachMask(mask);
    }
    return this->_prepareOutputImage(*outImage);
}

template <class T> void Image2DConvolver<T>::_convolve(
    SPIIT imageOut, const ImageInterface<T>& imageIn,
    VectorKernel::KernelTypes kernelType
) const {
    const auto& inShape = imageIn.shape();
    const auto& outShape = imageOut->shape();
    ThrowIf(
        ! inShape.isEqual(outShape),
        "Input and output images must have the same shape"
    );
    // Generate Kernel Array (height unity)
    ThrowIf(
        _targetres && kernelType != casacore::VectorKernel::GAUSSIAN,
        "targetres can only be true for a Gaussian convolving kernel"
    );
    // maybe can remove this comment if I'm smart enough
    // kernel needs to be type T because ultimately we use ImageConvolver which
    // requires the kernel and input image to be of the same type. This is
    // kind of stupid because our kernels are always real-valued, and we use
    // Fit2D which requires a real-valued kernel, so it seems we could support
    // complex valued images and real valued kernels if ImageConvolver was
    // smarter
    Array<double> kernel;
    // initialize to avoid compiler warning, kernelVolume will always be set to
    // something reasonable below before it is used.
    double kernelVolume = -1;
    std::vector<casacore::Quantity> originalParms{_major, _minor, _pa};
    if (! _targetres) {
        kernelVolume = _makeKernel(
            kernel, kernelType, originalParms, imageIn
        );
    }
    const auto& cSys = imageIn.coordinates();
    if (_major.getUnit().startsWith("pix")) {
        auto inc = cSys.increment()[_axes[0]];
        auto unit = cSys.worldAxisUnits()[_axes[0]];
        originalParms[0] = _major.getValue()*casacore::Quantity(abs(inc), unit);
    }
    if (_minor.getUnit().startsWith("pix")) {
        auto inc = cSys.increment()[_axes[1]];
        auto unit = cSys.worldAxisUnits()[_axes[1]];
        originalParms[1] = _minor.getValue()*casacore::Quantity(abs(inc), unit);
    }
    auto kernelParms = originalParms;
    // Figure out output image restoring beam (if any), output units and scale
    // factor for convolution kernel array
    GaussianBeam beamOut;
    const auto& imageInfo = imageIn.imageInfo();
    const auto& brightnessUnit = imageIn.units();
    String brightnessUnitOut;
    auto iiOut = imageOut->imageInfo();
    auto logFactors = false;
    double factor1 = -1;
    double pixelArea = 0;
    auto autoScale = _scale <= 0;
    if (autoScale) {
        auto bunitUp = brightnessUnit.getName();
        bunitUp.upcase();
        logFactors = bunitUp.contains("/BEAM");
        if (logFactors) {
            pixelArea = cSys.directionCoordinate().getPixelArea().getValue(
                "arcsec*arcsec"
            );
            if (! _targetres) {
                Vector<Quantity> const kernelParmsV(kernelParms);
                GaussianBeam kernelBeam(kernelParmsV);
                factor1 = pixelArea/kernelBeam.getArea("arcsec*arcsec");
            }
        }
    }
    if (imageInfo.hasMultipleBeams()) {
        _doMultipleBeams(
            iiOut, kernelVolume, imageOut, brightnessUnitOut,
            beamOut, factor1, imageIn, originalParms, kernelParms,
            kernel, kernelType, logFactors, pixelArea
        );
    }
    else {
        _doSingleBeam(
            iiOut, kernelVolume, kernelParms, kernel,
            brightnessUnitOut, beamOut, imageOut, imageIn,
            originalParms, kernelType, logFactors, factor1, 
            pixelArea
        );
    }
    imageOut->setUnits(brightnessUnitOut);
    imageOut->setImageInfo(iiOut);
    _logBeamInfo(imageInfo, "Original " + this->_getImage()->name());
    _logBeamInfo(iiOut, "Output " + this->_getOutname());
}

template <class T> void Image2DConvolver<T>::_logBeamInfo(
    const ImageInfo& imageInfo, const String& desc
) const {
    ostringstream oss;
    const auto& beamSet = imageInfo.getBeamSet();
    if (! imageInfo.hasBeam()) {
        oss << desc << " has no beam";
    }
    else if (imageInfo.hasSingleBeam()) {
        oss << desc << " resolution " << beamSet.getBeam();
    }
    else {
        oss << desc << " has multiple beams. Min area beam: "
            << beamSet.getMinAreaBeam() << ". Max area beam: "
            << beamSet.getMaxAreaBeam() << ". Median area beam "
            << beamSet.getMedianAreaBeam();
    }
    auto msg = oss.str();
    LogOrigin lor(getClass(), __func__);
    this->addHistory(lor, msg);
    _log(msg, LogIO::NORMAL);
}

template <class T> void Image2DConvolver<T>::_log(
    const String& msg, LogIO::Command priority
) const {
    if (! _suppressWarnings) {
        *this->_getLog() << priority << msg << LogIO::POST;
    }
}

template <class T> void Image2DConvolver<T>::_doSingleBeam(
    ImageInfo& iiOut, double& kernelVolume, vector<Quantity>& kernelParms,
    Array<double>& kernel, String& brightnessUnitOut, GaussianBeam& beamOut,
    SPIIT imageOut, const ImageInterface<T>& imageIn,
    const vector<Quantity>& originalParms, VectorKernel::KernelTypes kernelType,
    bool logFactors, double factor1, double pixelArea
) const {
    GaussianBeam inputBeam = imageIn.imageInfo().restoringBeam();
    Vector<Quantity> const kernelParmsV(kernelParms);
    Vector<Quantity> const originalParmsV(originalParms);
    if (_targetres) {
        kernelParms = _getConvolvingBeamForTargetResolution(
            originalParms, inputBeam
        );
        if (! _suppressWarnings) {
            *this->_getLog() << LogOrigin("Image2DConvolver<T>", __func__);
            ostringstream oss;
            oss << "Convolving image that has a beam of "
                << inputBeam << " with a Gaussian of "
                << GaussianBeam(kernelParmsV) << " to reach a target resolution of "
                << GaussianBeam(originalParmsV);
            _log(oss.str(), LogIO::NORMAL);
        }
        kernelVolume = _makeKernel(
            kernel, kernelType, kernelParms, imageIn
        );
    }
    const CoordinateSystem& cSys = imageIn.coordinates();
    auto scaleFactor = _dealWithRestoringBeam(
        brightnessUnitOut, beamOut, kernel, kernelVolume,
        kernelType, kernelParmsV, cSys, inputBeam,
        imageIn.units(), true
    );
    ostringstream oss;
    if (! _suppressWarnings) {
        oss << "Scaling pixel values by ";
    }
    if (logFactors) {
        if (_targetres) {
            GaussianBeam kernelBeam(kernelParmsV);
            factor1 = pixelArea/kernelBeam.getArea("arcsec*arcsec");
        }
        double factor2 = beamOut.getArea("arcsec*arcsec")/inputBeam.getArea("arcsec*arcsec");
        if (! _suppressWarnings) {
            oss << "inverse of area of convolution kernel in pixels (" << factor1
                << ") times the ratio of the beam areas (" << factor2 << ") = ";
        }
    }
    if (! _suppressWarnings) {
        oss << scaleFactor;
        _log(oss.str(), LogIO::NORMAL);
    }
    if (_targetres && near(beamOut.getMajor(), beamOut.getMinor(), 1e-7)) {
        // circular beam should have same PA as given by user if
        // targetres
        beamOut.setPA(originalParms[2]);
    }
    // Convolve.  We have already scaled the convolution kernel (with some
    // trickery cleverer than what ImageConvolver can do) so no more scaling
    ImageConvolver<T> aic;
    Array<T> modKernel(kernel.shape());
    casacore::convertArray(modKernel, scaleFactor*kernel);
    aic.convolve(
        *this->_getLog(), *imageOut, imageIn, modKernel,
        ImageConvolver<T>::NONE, 1.0, true
    );
    // Overwrite some bits and pieces in the output image to do with the
    // restoring beam  and image units
    bool holdsOneSkyAxis;
    const auto hasSky = casacore::CoordinateUtil::holdsSky(
        holdsOneSkyAxis, cSys, _axes.asVector()
    );
    if (hasSky && ! beamOut.isNull()) {
        if (_targetres) {
            Vector<Quantity> const originalParmsV(originalParms);
            casacore::GaussianBeam target(originalParmsV);
            iiOut.setRestoringBeam(target);
            if (
                ! _suppressWarnings && ! near(
                    beamOut, target, 1e-3, casacore::Quantity(0.01, "deg")
                ) 
            ) {
                *this->_getLog() << LogIO::WARN << "Fitted restoring beam is "
                    << beamOut << ", but putting requested target "
                    << "resolution beam " << target << " in the image "
                    << "metadata. Both beams may be considered consistent with "
                    << "the convolution result." << LogIO::POST;
            }
        }
        else {
            iiOut.setRestoringBeam(beamOut);
        }
    }
    else if (holdsOneSkyAxis) {
        // If one of the axes is in the sky plane, we must
        // delete the restoring beam as it is no longer meaningful
        iiOut.removeRestoringBeam();
        if (! _suppressWarnings) {
            oss.str("");
            oss << "Because you convolved just one of the sky axes" << endl;
            oss << "The output image does not have a valid spatial restoring beam";
            _log(oss.str(), LogIO::WARN);
        }
    }
}

template <class T> void Image2DConvolver<T>::_doMultipleBeams(
    ImageInfo& iiOut, double& kernelVolume, SPIIT imageOut,
    String& brightnessUnitOut, GaussianBeam& beamOut, Double factor1,
    const ImageInterface<T>& imageIn, const vector<Quantity>& originalParms,
    vector<Quantity>& kernelParms, Array<double>& kernel,
    VectorKernel::KernelTypes kernelType, bool logFactors, double pixelArea
) const {
    ImageMetaData<T> md(imageOut);
    auto nChan = md.nChannels();
    auto nPol = md.nStokes();
    // initialize all beams to be null
    iiOut.setAllBeams(nChan, nPol, casacore::GaussianBeam());
    const auto& cSys = imageIn.coordinates();
    auto specAxis = cSys.spectralAxisNumber();
    auto polAxis = cSys.polarizationAxisNumber();
    casacore::IPosition start(imageIn.ndim(), 0);
    auto end = imageIn.shape();
    if (nChan > 0) {
        end[specAxis] = 1;
    }
    if (nPol > 0) {
        end[polAxis] = 1;
    }
    int channel = -1;
    int polarization = -1;
    if (_targetres) {
        iiOut.removeRestoringBeam();
        Vector<Quantity> const kernelParmsV(kernelParms);
        iiOut.setRestoringBeam(casacore::GaussianBeam(kernelParmsV));
    }
    uint count = (nChan > 0 && nPol > 0)
        ? nChan * nPol
        : nChan > 0
          ? nChan
          : nPol;
    for (uint i=0; i<count; ++i) {
        if (nChan > 0) {
            channel = i % nChan;
            start[specAxis] = channel;
        }
        if (nPol > 0) {
            polarization = nChan > 1
                ? (i - channel) % nChan
                : i;
            start[polAxis] = polarization;
        }
        casacore::Slicer slice(start, end);
        casacore::SubImage<T> subImage(imageIn, slice);
        casacore::CoordinateSystem subCsys = subImage.coordinates();
        if (subCsys.hasSpectralAxis()) {
            auto subRefPix = subCsys.referencePixel();
            subRefPix[specAxis] = 0;
            subCsys.setReferencePixel(subRefPix);
        }
        auto inputBeam
            = imageIn.imageInfo().restoringBeam(channel, polarization);
        auto doConvolve = true;
        if (_targetres) {
            *this->_getLog() << LogIO::NORMAL;
            if (channel >= 0) {
                *this->_getLog() << "Channel " << channel << " of " << nChan;
                if (polarization >= 0) {
                    *this->_getLog() << ", ";
                }
            }
            if (polarization >= 0) {
                *this->_getLog() << "Polarization " << polarization
                    << " of " << nPol;
            }
            *this->_getLog() << " ";
            Vector<Quantity> const originalParmsV(originalParms);
            if (
                near(
                    inputBeam, GaussianBeam(originalParmsV), 1e-5,
                    casacore::Quantity(1e-2, "arcsec")
                )
            ) {
                doConvolve = false;
                *this->_getLog() << LogIO::NORMAL
                    << " Input beam is already near target resolution so this "
                    << "plane will not be convolved" << LogIO::POST;
            }
            else {
                kernelParms = _getConvolvingBeamForTargetResolution(
                    originalParms, inputBeam
                );
                kernelVolume = _makeKernel(
                    kernel, kernelType, kernelParms, imageIn
                );
                Vector<Quantity> const kernelParmsV(kernelParms);
                *this->_getLog() << ": Convolving image which has a beam of "
                    << inputBeam << " with a Gaussian of "
                    << GaussianBeam(kernelParmsV) << " to reach a target "
                    << "resolution of " << GaussianBeam(originalParmsV)
                    << LogIO::POST;
            }
        }
        casacore::TempImage<T> subImageOut(
            subImage.shape(), subImage.coordinates()
        );
        if (doConvolve) {
            Vector<Quantity> const kernelParmsV(kernelParms);
            auto scaleFactor = _dealWithRestoringBeam(
                brightnessUnitOut, beamOut, kernel, kernelVolume, kernelType,
                kernelParmsV, subCsys, inputBeam, imageIn.units(), i == 0
            );
            {
                *this->_getLog() << LogIO::NORMAL << "Scaling pixel values by ";
                if (logFactors) {
                    if (_targetres) {
                        casacore::GaussianBeam kernelBeam(kernelParmsV);
                        factor1 = pixelArea/kernelBeam.getArea("arcsec*arcsec");
                    }
                    auto factor2 = beamOut.getArea("arcsec*arcsec")
                        /inputBeam.getArea("arcsec*arcsec");
                    *this->_getLog() << "inverse of area of convolution kernel "
                        << "in pixels (" << factor1 << ") times the ratio of "
                        << "the beam areas (" << factor2 << ") = ";
                }
                *this->_getLog() << scaleFactor << " for ";
                if (channel >= 0) {
                    *this->_getLog() << "channel number " << channel;
                    if (polarization >= 0) {
                        *this->_getLog() << " and ";
                    }
                }
                if (polarization >= 0) {
                    *this->_getLog() << "polarization number " << polarization;
                }
                *this->_getLog() << casacore::LogIO::POST;
            }
            if (
                _targetres && near(beamOut.getMajor(), beamOut.getMinor(), 1e-7)
            ) {
                // circular beam should have same PA as given by user if
                // targetres
                beamOut.setPA(originalParms[2]);
            }
            Array<T> modKernel(kernel.shape());
            casacore::convertArray(modKernel, scaleFactor*kernel);
            ImageConvolver<T> aic;
            aic.convolve(
                *this->_getLog(), subImageOut, subImage, modKernel,
                ImageConvolver<T>::NONE, 1.0, true
            );

            // _doImageConvolver(subImageOut, subImage, scaleFactor*kernel);
        }
        else {
            brightnessUnitOut = imageIn.units().getName();
            beamOut = inputBeam;
            subImageOut.put(subImage.get());
        }
        {
            auto doMask = imageOut->isMasked() && imageOut->hasPixelMask();
            Lattice<bool>* pMaskOut = 0;
            if (doMask) {
                pMaskOut = &imageOut->pixelMask();
                if (! pMaskOut->isWritable()) {
                    doMask = false;
                }
            }
            auto cursorShape = subImageOut.niceCursorShape();
            auto outPos = start;
            LatticeStepper stepper(
                subImageOut.shape(), cursorShape,
                casacore::LatticeStepper::RESIZE
            );
            RO_MaskedLatticeIterator<T> iter(subImageOut, stepper);
            for (iter.reset(); !iter.atEnd(); iter++) {
                const auto cursorShape = iter.cursorShape();
                imageOut->putSlice(iter.cursor(), outPos);
                if (doMask) {
                    pMaskOut->putSlice(iter.getMask(), outPos);
                }
                outPos = outPos + cursorShape;
            }
        }
        if (_targetres) {
            Vector<Quantity> const originalParmsV(originalParms);
            GaussianBeam target(originalParmsV);
            if (
                ! _suppressWarnings && ! casacore::near(
                    beamOut, target, 1e-3, casacore::Quantity(0.01, "deg")
                )
            ) {
                *this->_getLog() << LogIO::WARN << "Fitted restoring beam for "
                    << " channel " << channel << " and polarization plane "
                    << polarization << " is " << beamOut << " but putting "
                    << "requested target resolution beam " << target << " in "
                    << "the image metadata. Both beams can be considered "
                    << "consistent with the convolution result." << LogIO::POST;
            }
        }
        else {
            iiOut.setBeam(channel, polarization, beamOut);
        }
    }
}

template <class T> double Image2DConvolver<T>::_makeKernel(
    casacore::Array<double>& kernelArray,
    casacore::VectorKernel::KernelTypes kernelType,
    const std::vector<casacore::Quantity>& parameters,
    const casacore::ImageInterface<T>& imageIn
) const {

// Check number of parameters

   Vector<Quantity> const parametersV(parameters);
   _checkKernelParameters(kernelType, parametersV);

// Convert kernel widths to pixels from world.  Demands major and minor
// both in pixels or both in world, else exception

   casacore::Vector<double> dParameters;
   const casacore::CoordinateSystem cSys = imageIn.coordinates();

// Use the reference value for the shape conversion direction

   casacore::Vector<casacore::Quantity> wParameters(5);
   for (uint i=0; i<3; i++) {
      wParameters(i+2) = parameters[i];
   }
//
   const casacore::Vector<double> refVal = cSys.referenceValue();
   const casacore::Vector<casacore::String> units = cSys.worldAxisUnits();
   uint wAxis = cSys.pixelAxisToWorldAxis(_axes(0));
   wParameters(0) = casacore::Quantity(refVal(wAxis), units(wAxis));
   wAxis = cSys.pixelAxisToWorldAxis(_axes(1));
   wParameters(1) = casacore::Quantity(refVal(wAxis), units(wAxis));
   SkyComponentFactory::worldWidthsToPixel(
       dParameters, wParameters, cSys, _axes, false
   );

// Create n-Dim kernel array shape

   auto kernelShape = _shapeOfKernel(kernelType, dParameters, imageIn.ndim());

// Create kernel array. We will fill the n-Dim array (shape non-unity
// only for pixelAxes) through its 2D casacore::Matrix incarnation. Aren't we clever.
   kernelArray = 0;
   kernelArray.resize(kernelShape);
   auto kernelArray2 = kernelArray.nonDegenerate(_axes);
   auto kernelMatrix = static_cast<casacore::Matrix<double>>(kernelArray2);

// Fill kernel casacore::Matrix with functional (height unity)

   return _fillKernel (kernelMatrix, kernelType, kernelShape, dParameters);
}

template <class T> double Image2DConvolver<T>::_dealWithRestoringBeam(
    String& brightnessUnitOut,
    GaussianBeam& beamOut, const casacore::Array<double>& kernelArray,
    double kernelVolume, const casacore::VectorKernel::KernelTypes,
    const casacore::Vector<casacore::Quantity>& parameters,
    const casacore::CoordinateSystem& cSys,
    const casacore::GaussianBeam& beamIn,
    const casacore::Unit& brightnessUnitIn, bool emitMessage
) const {
    *this->_getLog() << LogOrigin(CLASS_NAME, __func__);
    // Find out if convolution axes hold the sky.  Scaling from
    // Jy/beam and Jy/pixel only really makes sense if this is true
    bool holdsOneSkyAxis;
    auto hasSky = casacore::CoordinateUtil::holdsSky(
        holdsOneSkyAxis, cSys, _axes.asVector()
    );
    if (hasSky) {
        const casacore::DirectionCoordinate dc = cSys.directionCoordinate();
        auto inc = dc.increment();
        auto unit = dc.worldAxisUnits();
        casacore::Quantity x(inc[0], unit[0]);
        casacore::Quantity y(inc[1], unit[1]);
        auto diag = sqrt(x*x + y*y);
        auto minAx = parameters[1];
        if (minAx.getUnit().startsWith("pix")) {
            minAx.setValue(minAx.getValue()*x.getValue());
            minAx.setUnit(x.getUnit());
        }
        if (minAx < diag) {
            diag.convert(minAx.getFullUnit());
            if (! _suppressWarnings) {
                ostringstream oss;
                oss << "Convolving kernel has minor axis " << minAx << " which "
                    << "is less than the pixel diagonal length of " << diag
                    << ". Thus, the kernel is poorly sampled, and so the "
                    << "output of this application may not be what you expect. "
                    << "You should consider increasing the kernel size or "
                    << "regridding the image to a smaller pixel size";
                _log(oss.str(), LogIO::WARN);
            }
        }
        else if (
            beamIn.getMinor() < diag
            && beamIn != casacore::GaussianBeam::NULL_BEAM
        ) {
            diag.convert(beamIn.getMinor().getFullUnit());
            if (! _suppressWarnings) {
                ostringstream oss;
                oss << "Input beam has minor axis "
                    << beamIn.getMinor() << " which is less than the pixel "
                    << "diagonal length of " << diag << ". Thus, the beam is "
                    << "poorly sampled, and so the output of this application "
                    << "may not be what you expect. You should consider "
                    << "regridding the image to a smaller pixel size.";
                _log(oss.str(), LogIO::WARN);
            }
        }
    }
    if (emitMessage && ! _suppressWarnings) {
        ostringstream oss;
        oss << "You are " << (hasSky ? "" : " not ") << "convolving the sky";
        _log(oss.str(), LogIO::NORMAL);
    }
    beamOut = casacore::GaussianBeam();
    auto bUnitIn = upcase(brightnessUnitIn.getName());
    const auto& refPix = cSys.referencePixel();
    double scaleFactor = 1;
    brightnessUnitOut = brightnessUnitIn.getName();
    auto autoScale = _scale <= 0;
    if (hasSky && bUnitIn.contains("/PIXEL")) {
        // Easy case.  Peak of convolution kernel must be unity
        // and output units are Jy/beam.  All other cases require
        // numerical convolution of beams
        brightnessUnitOut = "Jy/beam";
        // Exception already generated if only
        // one of major and minor in pixel units
        auto majAx = parameters(0);
        auto minAx = parameters(1);
        if (majAx.getFullUnit().getName() == "pix") {
            casacore::Vector<double> pixelParameters(5);
            pixelParameters(0) = refPix(_axes(0));
            pixelParameters(1) = refPix(_axes(1));
            pixelParameters(2) = parameters(0).getValue();
            pixelParameters(3) = parameters(1).getValue();
            pixelParameters(4) = parameters(2).getValue(casacore::Unit("rad"));
            casacore::GaussianBeam worldParameters;
            SkyComponentFactory::pixelWidthsToWorld(
                worldParameters, pixelParameters,
                cSys, _axes, false
            );
            majAx = worldParameters.getMajor();
            minAx = worldParameters.getMinor();
        }
        beamOut = casacore::GaussianBeam(majAx, minAx, parameters(2));
        // casacore::Input p.a. is positive N->E
        if (! autoScale) {
            scaleFactor = _scale;
            _log(
                "Autoscaling is recommended for Jy/pixel convolution",
                LogIO::WARN
            );
        }
    }
    else {
        // Is there an input restoring beam and are we convolving the sky to
        // which it pertains?  If not, all we can do is use user scaling or
        // normalize the convolution kernel to unit volume.  There is no point
        // to convolving the input beam either as it pertains only to the sky
        if (hasSky && ! beamIn.isNull()) {
            // Convert restoring beam parameters to pixels.
            // Output pa is pos +x -> +y in pixel frame.
            casacore::Vector<casacore::Quantity> wParameters(5);
            const auto refVal = cSys.referenceValue();
            const auto units = cSys.worldAxisUnits();
            auto wAxis = cSys.pixelAxisToWorldAxis(_axes(0));
            wParameters(0) = casacore::Quantity(refVal(wAxis), units(wAxis));
            wAxis = cSys.pixelAxisToWorldAxis(_axes(1));
            wParameters(1) = casacore::Quantity(refVal(wAxis), units(wAxis));
            wParameters(2) = beamIn.getMajor();
            wParameters(3) = beamIn.getMinor();
            wParameters(4) = beamIn.getPA(true);
            casacore::Vector<double> dParameters;
            SkyComponentFactory::worldWidthsToPixel(
                dParameters, wParameters, cSys, _axes, false
            );
            // Create 2-D beam array shape
            // casacore::IPosition dummyAxes(2, 0, 1);
            auto beamShape = _shapeOfKernel(
                casacore::VectorKernel::GAUSSIAN, dParameters, 2
            );

            // Create beam casacore::Matrix and fill with height unity
   
            casacore::Matrix<double> beamMatrixIn(beamShape(0), beamShape(1));
            _fillKernel(
                beamMatrixIn, casacore::VectorKernel::GAUSSIAN, beamShape,
                dParameters
            );
            auto shape = beamMatrixIn.shape();
            // Get 2-D version of convolution kenrel
            auto kernelArray2 = kernelArray.nonDegenerate(_axes);
            auto kernelMatrix
                = static_cast<casacore::Matrix<double>>(kernelArray2);
            // Convolve input restoring beam array by convolution kernel array
            casacore::Matrix<double> beamMatrixOut;
            casacore::Convolver<double> conv(beamMatrixIn, kernelMatrix.shape());
            conv.linearConv(beamMatrixOut, kernelMatrix);
            // Scale kernel
            auto maxValOut = max(beamMatrixOut);
            scaleFactor = autoScale ? 1/maxValOut : _scale;
            Fit2D fitter(*this->_getLog());
            const uint n = beamMatrixOut.shape()(0);
            auto bParameters
                = fitter.estimate(casacore::Fit2D::GAUSSIAN, beamMatrixOut);
            casacore::Vector<bool> bParameterMask(
                bParameters.nelements(), true
            );
            bParameters(1) = (n-1)/2;          // x centre
            bParameters(2) = bParameters(1);    // y centre
            // Set range so we don't include too many pixels
            // in fit which will make it very slow
            fitter.addModel(
                casacore::Fit2D::GAUSSIAN, bParameters, bParameterMask
            );
            casacore::Array<double> sigma;
            fitter.setIncludeRange(maxValOut/10.0, maxValOut+0.1);
            auto error = fitter.fit(beamMatrixOut, sigma);
            ThrowIf(
                error == casacore::Fit2D::NOCONVERGE
                || error == casacore::Fit2D::FAILED
                || error == casacore::Fit2D::NOGOOD,
                "Failed to fit the output beam"
            );
            auto bSolution = fitter.availableSolution();
            // Convert to world units.
            casacore::Vector<double> pixelParameters(5);
            pixelParameters(0) = refPix(_axes(0));
            pixelParameters(1) = refPix(_axes(1));
            pixelParameters(2) = bSolution(3);
            pixelParameters(3) = bSolution(4);
            pixelParameters(4) = bSolution(5);
            SkyComponentFactory::pixelWidthsToWorld(
                beamOut, pixelParameters, cSys, _axes, false
            );
            if (
                ! brightnessUnitIn.getName().contains(
                    casacore::Regex(Regex::makeCaseInsensitive("beam"))
                )
            ) {
                scaleFactor *= beamIn.getArea("arcsec2")
                    /beamOut.getArea("arcsec2");
            }
        }
        else {
            scaleFactor = autoScale ? 1/kernelVolume : _scale;
        }
    }
    // Put beam position angle into range
    // +/- 180 in case it has eluded us so far
    if (! beamOut.isNull()) {
        casacore::MVAngle pa(
            beamOut.getPA(true).getValue(casacore::Unit("rad"))
        );
        pa();
        beamOut = casacore::GaussianBeam(
            beamOut.getMajor(), beamOut.getMinor(),
            casacore::Quantity(pa.degree(), casacore::Unit("deg"))
        );
    }
    return scaleFactor;
}

template <class T> void Image2DConvolver<T>::_checkKernelParameters(
    casacore::VectorKernel::KernelTypes kernelType,
    const casacore::Vector<casacore::Quantity >& parameters
) const {
    if (kernelType==casacore::VectorKernel::BOXCAR) {
        ThrowCc("Boxcar kernel not yet implemented");
        ThrowIf(
            parameters.nelements() != 3,
            "Boxcar kernels require 3 parameters"
        );
    }
    else if (kernelType==casacore::VectorKernel::GAUSSIAN) {
        ThrowIf(
            parameters.nelements() != 3,
            "Gaussian kernels require exactly 3 parameters"
        );
    }
    else {
        ThrowCc(
            "The kernel type " + casacore::VectorKernel::fromKernelType(kernelType) + " is not supported"
        );
    }
}

template <class T> casacore::IPosition Image2DConvolver<T>::_shapeOfKernel(
    const casacore::VectorKernel::KernelTypes kernelType,
    const casacore::Vector<double>& parameters,
    const uint ndim
) const {
//
// Work out how big the array holding the kernel should be.
// Simplest algorithm possible. Shape is presently square.
//

// Find 2D shape

   uint n;
   if (kernelType==casacore::VectorKernel::GAUSSIAN) {
      uint n1 = _sizeOfGaussian (parameters(0), 5.0);
      uint n2 = _sizeOfGaussian (parameters(1), 5.0);
      n = max(n1,n2);
      if (n%2==0) n++;                                     // Make shape odd so centres well
   } else if (kernelType==casacore::VectorKernel::BOXCAR) {
      n = 2 * int(max(parameters(0), parameters(1))+0.5);
      if (n%2==0) n++;                                     // Make shape odd so centres well
   } else {
     throw(casacore::AipsError("Unrecognized kernel type"));        // Earlier checking should prevent this
   }

// Now find the shape for the image and slot the 2D shape in
// in the correct axis locations

   casacore::IPosition shape(ndim,1);
   shape(_axes(0)) = n;
   shape(_axes(1)) = n;
   return shape;
}
   
template <class T>
uint Image2DConvolver<T>::_sizeOfGaussian(
    const double width, const double nSigma
) const {
// +/- 5sigma is a volume error of less than 6e-5%

   double sigma = width / sqrt(double(8.0) * C::ln2);
   return  (int(nSigma*sigma + 0.5) + 1) * 2;
}

template <class T> double Image2DConvolver<T>::_fillKernel(
    casacore::Matrix<double>& kernelMatrix,
    casacore::VectorKernel::KernelTypes kernelType,
    const casacore::IPosition& kernelShape,
    const casacore::Vector<double>& parameters
) const {

// Centre functional in array (shape is odd)

   auto xCentre = double((kernelShape[_axes[0]] - 1)/2.0);
   auto yCentre = double((kernelShape[_axes[1]] - 1)/2.0);
   double height = 1;

// Create functional.  We only have gaussian2d functionals
// at this point.  Later the filling code can be moved out
// of the if statement

   double maxValKernel;
   double volumeKernel = 0;
   auto pa = parameters[2];
   auto ratio = parameters[1]/parameters[0];
   auto major = parameters[0];
   if (kernelType==casacore::VectorKernel::GAUSSIAN) {
       _fillGaussian(
           maxValKernel, volumeKernel, kernelMatrix, height,
           xCentre, yCentre, major, ratio, pa
       );
   }
   else if (kernelType==casacore::VectorKernel::BOXCAR) {
       ThrowCc("Boxcar convolution not supported");
   }
   else {
       // Earlier checking should prevent this
       ThrowCc("Unrecognized kernel type");
   }
   return volumeKernel;
}         

template <class T> void Image2DConvolver<T>::_fillGaussian(
    double& maxVal, double& volume, casacore::Matrix<double>& pixels,
    double height, double xCentre, double yCentre, double majorAxis,
    double ratio, double positionAngle
) const {
// 
// pa positive in +x ->+y pixel coordinate frame
//
   uint n1 = pixels.shape()(0);
   uint n2 = pixels.shape()(1);
   AlwaysAssert(n1==n2,casacore::AipsError);
   positionAngle += C::pi_2;        // +y -> -x
   casacore::Gaussian2D<double> g2d(height, xCentre, yCentre, majorAxis,
               ratio, positionAngle);
   maxVal = -1.0e30;
   volume = 0.0;
   casacore::Vector<double> pos(2);
   for (uint j=0; j<n1; ++j) {
      pos[1] = j;
      for (uint i=0; i<n1; ++i) {
         pos[0] = i;
         double val = g2d(pos);
         pixels(i,j) = val;
         maxVal = max(val, maxVal);
         volume += val;
      }
   } 
}

}