/*
 * (C) Copyright 2013 ECMWF.
 *
 * This software is licensed under the terms of the Apache Licence Version 2.0
 * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
 * In applying this licence, ECMWF does not waive the privileges and immunities
 * granted to it by virtue of its status as an intergovernmental organisation
 * nor does it submit to any jurisdiction.
 */

#include "atlas/trans/local/TransLocal.h"

#include <cmath>
#include <cstdlib>
#include <fstream>

#include "atlas/linalg/dense.h"
#include "eckit/config/YAMLConfiguration.h"
#include "eckit/eckit.h"
#include "eckit/io/DataHandle.h"
#include "eckit/linalg/LinearAlgebra.h"
#include "eckit/log/Bytes.h"
#include "eckit/log/JSON.h"
#include "eckit/types/FloatCompare.h"

#include "atlas/array.h"
#include "atlas/field.h"
#include "atlas/grid/Iterator.h"
#include "atlas/grid/StructuredGrid.h"
#include "atlas/option.h"
#include "atlas/parallel/mpi/mpi.h"
#include "atlas/runtime/Exception.h"
#include "atlas/runtime/Log.h"
#include "atlas/trans/Trans.h"
#include "atlas/trans/VorDivToUV.h"
#include "atlas/trans/detail/TransFactory.h"
#include "atlas/trans/local/LegendrePolynomials.h"
#include "atlas/util/Constants.h"

#include "atlas/library/defines.h"
#if ATLAS_HAVE_FFTW
#include "fftw3.h"
#endif

// move latitudes at the poles to the following latitude:
// (otherwise we would divide by zero when computing u,v from U,V)
static constexpr double latPole = 89.9999999;
// (latPole=89.9999999 seems to produce the best accuracy. Moving it further away
//  or closer to the pole both increase the errors!)

namespace atlas {
namespace trans {

namespace {
static TransBuilderGrid<TransLocal> builder("local", "local");
}  // namespace

namespace {
class TransParameters {
public:
    TransParameters(const eckit::Configuration& config): config_(config) {}
    ~TransParameters() = default;

    /*
     * For the future
     */
    //    bool scalar_derivatives() const { return config_.getBool( "scalar_derivatives", false ); }

    //    bool wind_EW_derivatives() const { return config_.getBool( "wind_EW_derivatives", false ); }

    //    bool vorticity_divergence_fields() const { return config_.getBool( "vorticity_divergence_fields", false ); }

    //    std::string read_legendre() const { return config_.getString( "read_legendre", "" ); }
    //    bool global() const { return config_.getBool( "global", false ); }

    //    std::string read_fft() const { return config_.getString( "read_fft", "" ); }


    std::string write_legendre() const { return config_.getString("write_legendre", ""); }

    std::string write_fft() const { return config_.getString("write_fft", ""); }


    bool export_legendre() const { return config_.getBool("export_legendre", false); }

    int warning() const { return config_.getInt("warning", 1); }

    int fft() const {
        static const std::map<std::string, int> string_to_FFT = {{"OFF", static_cast<int>(option::FFT::OFF)},
                                                                 {"FFTW", static_cast<int>(option::FFT::FFTW)}};
#ifdef ATLAS_HAVE_FFTW
        return string_to_FFT.at(config_.getString("fft", "FFTW"));
#else
        return string_to_FFT.at("OFF");
#endif
    }

    std::string matrix_multiply() const { return config_.getString("matrix_multiply", ""); }


private:
    const eckit::Configuration& config_;
};

struct ReadCache {
    ReadCache(const void* cache) {
        begin = reinterpret_cast<const char*>(cache);
        pos   = 0;
    }
    template <typename T>
    T* read(size_t size) {
        const T* v = reinterpret_cast<const T*>(begin + pos);
        pos += size * sizeof(T);
        return const_cast<T*>(v);
    }

    const char* begin;
    size_t pos;
};

struct WriteCache {
    WriteCache(const eckit::PathName& file_path): dh_(file_path.fileHandle(/*overwrite = */ true)) {
        if (file_path.exists()) {
            std::stringstream err;
            err << "Cannot open cache file " << file_path << " for writing as it already exists. Remove first.";
            throw_Exception(err.str(), Here());
        }
        dh_->openForWrite(0);
        pos = 0;
    }
    ~WriteCache() { dh_->close(); }
    template <typename T>
    void write(const T* v, long size) {
        dh_->write(v, size * sizeof(T));
        pos += size * sizeof(T);
    }

    //void write( long v ) {
    //    dh_->write( &v , sizeof(long) );
    //    pos += sizeof(long);
    //}

    //void write( const Grid& grid ) {
    //    std::stringstream s;
    //    eckit::JSON json(s);
    //    json << grid.spec();
    //    std::string grid_spec( s.str() );
    //    long size = grid_spec.size();
    //    write( size );
    //    dh_->write( grid_spec.c_str(), grid_spec.size() );
    //    pos += grid_spec.size();
    //}


    std::unique_ptr<eckit::DataHandle> dh_;
    size_t pos;
};

}  // namespace

// --------------------------------------------------------------------------------------------------------------------
// Helper functions
// --------------------------------------------------------------------------------------------------------------------
namespace {  // anonymous

size_t legendre_size(const size_t truncation) {
    return (truncation + 2) * (truncation + 1) / 2;
}

//int nlats_northernHemisphere( const int nlats ) {
//    return ceil( nlats / 2. );
//    // using ceil here should make it possible to have odd number of latitudes (with the centre latitude being the equator)
//}

size_t num_n(const int truncation, const int m, const bool symmetric) {
    int len = (truncation - m + (symmetric ? 2 : 1)) / 2;
    ATLAS_ASSERT(len >= 0);
    return size_t(len);
}


[[noreturn]] void throw_AllocationFailed(size_t bytes, const eckit::CodeLocation& loc) {
    std::stringstream ss;
    ss << "AllocationFailed: Could not allocate " << eckit::Bytes(bytes);
    throw_Exception(ss.str(), loc);
}


void alloc_aligned(double*& ptr, size_t n) {
    const size_t alignment = 64 * sizeof(double);
    size_t bytes           = sizeof(double) * n;
    int err                = posix_memalign((void**)&ptr, alignment, bytes);
    if (err) {
        throw_AllocationFailed(bytes, Here());
    }
}

void free_aligned(double*& ptr) {
    free(ptr);
    ptr = nullptr;
}

void alloc_aligned(double*& ptr, size_t n, const char* msg) {
    ATLAS_ASSERT(msg);
    Log::debug() << "TransLocal: allocating '" << msg << "': " << eckit::Bytes(sizeof(double) * n) << std::endl;
    alloc_aligned(ptr, n);
}

void free_aligned(double*& ptr, const char* msg) {
    ATLAS_ASSERT(msg);
    Log::debug() << "TransLocal: deallocating '" << msg << "'" << std::endl;
    free_aligned(ptr);
}

size_t add_padding(size_t n) {
    return size_t(std::ceil(n / 8.)) * 8;
}

std::string detect_linalg_backend(const std::string& linalg_backend_) {
    linalg::dense::Backend linalg_backend = linalg::dense::Backend{linalg_backend_};
    if (linalg_backend.type() == linalg::dense::backend::eckit_linalg::type()) {
        std::string backend;
        linalg_backend.get("backend", backend);
        if (backend.empty() || backend == "default") {
#if ATLAS_ECKIT_HAVE_ECKIT_585
            return eckit::linalg::LinearAlgebraDense::backend().name();
#else
            return eckit::linalg::LinearAlgebra::backend().name();
#endif
        }
        return backend;
    }
    return linalg_backend.type();
};

bool using_eckit_default_backend(const std::string& linalg_backend_) {
    linalg::dense::Backend linalg_backend = linalg::dense::Backend{linalg_backend_};
    if (linalg_backend.type() == linalg::dense::backend::eckit_linalg::type()) {
        std::string backend;
        linalg_backend.get("backend", backend);
        if (backend.empty() || backend == "default") {
            return true;
        }
    }
    return false;
};


}  // namespace

int fourier_truncation(const int truncation,   // truncation
                       const int nx,           // number of longitudes
                       const int /*nxmax*/,    // maximum nx
                       const int ndgl,         // number of latitudes
                       const double lat,       // latitude in radian
                       const bool fullgrid) {  // regular grid
    int trc     = truncation;
    int trclin  = ndgl - 1;
    int trcquad = ndgl * 2 / 3 - 1;
    if (truncation >= trclin || fullgrid) {
        // linear
        trc = (nx - 1) / 2;
    }
    else if (truncation >= trcquad) {
        // quadratic
        double weight = 3 * (trclin - truncation) / ndgl;
        double sqcos  = std::pow(std::cos(lat), 2);

        trc = static_cast<int>((nx - 1) / (2 + weight * sqcos));
    }
    else {
        // cubic
        double sqcos = std::pow(std::cos(lat), 2);

        trc = static_cast<int>((nx - 1) / (2 + sqcos) - 1);
    }
    trc = std::min(truncation, trc);
    return trc;
}

namespace detail {
struct FFTW_Data {
#if ATLAS_HAVE_FFTW
    fftw_complex* in;
    double* out;
    std::vector<fftw_plan> plans;
#endif
};
}  // namespace detail


// --------------------------------------------------------------------------------------------------------------------
// Class TransLocal
// --------------------------------------------------------------------------------------------------------------------

bool TransLocal::warning(const eckit::Configuration& config) const {
    int warning = warning_;
    config.get("warning", warning);
    return (warning > 0 && grid_.size() >= warning);
}

TransLocal::TransLocal(const Cache& cache, const Grid& grid, const Domain& domain, const long truncation,
                       const eckit::Configuration& config):
    grid_(grid, domain),
    truncation_(static_cast<int>(truncation)),
    precompute_(config.getBool("precompute", true)),
    cache_(cache),
    legendre_cache_(cache.legendre().data()),
    legendre_cachesize_(cache.legendre().size()),
    fft_cache_(cache.fft().data()),
    fft_cachesize_(cache.fft().size()),
    fftw_(new detail::FFTW_Data),
    linalg_backend_(TransParameters{config}.matrix_multiply()),
    warning_(TransParameters{config}.warning()) {
    ATLAS_TRACE("TransLocal constructor");

    if (mpi::size() > 1) {
        ATLAS_THROW_EXCEPTION("TransLocal is not implemented for more than 1 MPI task.");
    }

    double fft_threshold = 0.0;  // fraction of latitudes of the full grid down to which FFT is used.
    // This threshold needs to be adjusted depending on the dgemm and FFT performance of the machine
    // on which this code is running!
    idx_t nlats       = 0;
    idx_t nlonsMax    = 0;
    idx_t neqtr       = 0;
    useFFT_           = TransParameters(config).fft();
    unstruct_precomp_ = (config.has("precompute") ? precompute_ : false);
    no_symmetry_      = false;
    nlatsNH_          = 0;
    nlatsSH_          = 0;
    nlatsLeg_         = 0;
    nlatsLegDomain_   = 0;
    nlatsLegReduced_  = 0;
    bool useGlobalLeg = true;
    bool no_nest      = false;

    if (StructuredGrid(grid_) && not grid_.projection()) {
        StructuredGrid g(grid_);
        nlats    = g.ny();
        nlonsMax = g.nxmax();

        // check location of domain relative to the equator:
        for (idx_t j = 0; j < nlats; ++j) {
            // assumptions: latitudes in g.y(j) are monotone and decreasing
            // no assumption on whether we have 0, 1 or 2 latitudes at the equator
            double lat = g.y(j);
            (eckit::types::is_approximately_equal(lat, 0.) ? neqtr : (lat < 0 ? nlatsSH_ : nlatsNH_))++;
        }
        if (neqtr > 0) {
            nlatsNH_++;
            nlatsSH_++;
        }
        if (nlatsNH_ >= nlatsSH_) {
            nlatsLegDomain_ = nlatsNH_;
        }
        else {
            nlatsLegDomain_ = nlatsSH_;
        }

        gridGlobal_ = grid;
        if (not gridGlobal_.domain().global()) {
            // The grid is not a nest of a global grid
            if (RegularGrid(grid_)) {
                no_nest         = true;
                no_symmetry_    = true;
                useFFT_         = false;
                nlatsNH_        = nlats;
                nlatsSH_        = 0;
                nlatsLegDomain_ = nlatsNH_;
                gridGlobal_     = grid_;
                useGlobalLeg    = false;
            }
            else {  // non-nested reduced grids are not supported
                std::ostringstream log;
                log << "Transforms to non-regular regional grids is not supported, unless it defined as a cropping of "
                       "a global grid."
                    << std::endl;
                log << "    Input grid: " << grid.spec() << std::endl;
                log << "    Applied domain: " << domain << std::endl;
                log << "    Regional grid after Domain applied (the output grid): " << grid_.spec() << std::endl;
                throw_NotImplemented(log.str(), Here());
            }
        }

        StructuredGrid gs_global(gridGlobal_);
        ATLAS_ASSERT(gs_global);  // assert structured grid
        StructuredGrid gsLeg = (useGlobalLeg ? gs_global : g);
        nlonsMaxGlobal_      = gs_global.nxmax();
        jlonMin_.resize(1);
        jlonMin_[0]  = 0;
        jlatMin_     = 0;
        nlatsGlobal_ = gs_global.ny();
        if (grid_.domain().global()) {
            Log::debug() << "Global grid with " << nlats << " latitudes." << std::endl;
        }
        else {
            Log::debug() << "Grid has " << nlats << " latitudes. Global grid has " << nlatsGlobal_ << std::endl;
        }
        if (useGlobalLeg) {
            nlatsLeg_ = (nlatsGlobal_ + 1) / 2;
        }
        else {
            nlatsLeg_        = nlatsLegDomain_;
            nlatsLegReduced_ = nlatsLeg_;
        }
        for (int jlat = 0; jlat < nlatsGlobal_; jlat++) {
            if (gs_global.y(jlat) > g.y(0)) {
                //Log::info() << gs_global.y( jlat ) << ">" << g.y( 0 ) << " ";
                jlatMin_++;
            };
        }
        //Log::info() << std::endl;
        int jlatMinLeg_ = jlatMin_;
        if (nlatsNH_ < nlatsSH_) {
            jlatMinLeg_ += nlatsNH_ - nlatsSH_;
        };
        if (jlatMin_ >= (nlatsGlobal_ + 1) / 2) {
            jlatMinLeg_ -= 2 * (jlatMin_ - (nlatsGlobal_ + 1) / 2);
            if (nlatsGlobal_ % 2 == 1) {
                jlatMinLeg_--;
            }
        };
        if (useGlobalLeg) {
            nlatsLegReduced_ = jlatMinLeg_ + nlatsLegDomain_;
        }

        // reduce truncation towards the pole for reduced meshes:
        nlat0_.resize(truncation_ + 1);
        if (no_nest) {
            for (int j = 0; j <= truncation_; j++) {
                nlat0_[j] = 0;
            }
        }
        else {
            int nmen0 = -1;
            for (int jlat = 0; jlat < nlatsGlobal_ / 2; jlat++) {
                double lat = gs_global.y(jlat) * util::Constants::degreesToRadians();
                int nmen   = fourier_truncation(truncation_, gs_global.nx(jlat), gs_global.nxmax(), nlatsGlobal_, lat,
                                                RegularGrid(gs_global));
                nmen       = std::max(nmen0, nmen);
                int ndgluj = nlatsLeg_ - std::min(nlatsLeg_, nlatsLeg_ + jlatMinLeg_ - jlat);
                if (useGlobalLeg) {
                    ndgluj = std::max(jlatMinLeg_, jlat);
                }
                for (int j = nmen0 + 1; j <= nmen; j++) {
                    nlat0_[j] = ndgluj;
                }
                nmen0 = nmen;
            }
            for (int j = nmen0 + 1; j <= truncation_; j++) {
                nlat0_[j] = nlatsLeg_;
            }
        }
        /*Log::info() << "nlats=" << g.ny() << " nlatsGlobal=" << gs_global.ny() << " jlatMin=" << jlatMin_
                    << " jlatMinLeg=" << jlatMinLeg_ << " nlatsGlobal/2-nlatsLeg=" << nlatsGlobal_ / 2 - nlatsLeg_
                    << " nlatsLeg_=" << nlatsLeg_ << " nlatsLegDomain_=" << nlatsLegDomain_ << std::endl;*/

        // compute longitudinal location of domain within global grid for using FFT:
        auto wrapAngle = [&](double angle) {
            double result = std::fmod(angle, 360.);
            if (result < 0.) {
                result += 360.;
            }
            return result;
        };
        if (useFFT_) {
            double lonmin = wrapAngle(g.x(0, 0));
            if (nlonsMax < fft_threshold * nlonsMaxGlobal_) {
                useFFT_ = false;
            }
            else {
                // need to use FFT with cropped grid
                if (RegularGrid(gridGlobal_)) {
                    for (idx_t jlon = 0; jlon < nlonsMaxGlobal_; ++jlon) {
                        if (gs_global.x(jlon, 0) < lonmin) {
                            jlonMin_[0]++;
                        }
                    }
                }
                else {
                    nlonsGlobal_.resize(nlats);
                    jlonMin_.resize(nlats);
                    for (idx_t jlat = 0; jlat < nlats; jlat++) {
                        double lonmin      = wrapAngle(g.x(0, jlat));
                        nlonsGlobal_[jlat] = gs_global.nx(jlat + jlatMin_);
                        jlonMin_[jlat]     = 0;
                        for (idx_t jlon = 0; jlon < nlonsGlobal_[jlat]; ++jlon) {
                            if (gs_global.x(jlon, jlat + jlatMin_) < lonmin) {
                                jlonMin_[jlat]++;
                            }
                        }
                    }
                }
            }
        }
        //Log::info() << "nlats=" << g.ny() << " nlatsGlobal=" << gs_global.ny() << std::endl;
        std::vector<double> lats(nlatsLeg_);
        std::vector<double> lons(nlonsMax);
        if (nlatsNH_ >= nlatsSH_ || useGlobalLeg) {
            for (idx_t j = 0; j < nlatsLeg_; ++j) {
                double lat = gsLeg.y(j);
                if (lat > latPole) {
                    lat = latPole;
                }
                if (lat < -latPole) {
                    lat = -latPole;
                }
                lats[j] = lat * util::Constants::degreesToRadians();
            }
        }
        else {
            for (idx_t j = nlats - 1, idx = 0; idx < nlatsLeg_; --j, ++idx) {
                double lat = gsLeg.y(j);
                if (lat > latPole) {
                    lat = latPole;
                }
                if (lat < -latPole) {
                    lat = -latPole;
                }
                lats[idx] = -lat * util::Constants::degreesToRadians();
            }
        }
        for (idx_t j = 0; j < nlonsMax; ++j) {
            lons[j] = g.x(j, 0) * util::Constants::degreesToRadians();
        }
        /*Log::info() << "lats: ";
        for ( int j = 0; j < nlatsLeg_; j++ ) {
            Log::info() << lats[j] << " ";
        }
        Log::info() << std::endl;*/


        Log::debug() << "TransLocal set up with:\n"
                     << " - grid: " << grid.name() << '\n'
                     << " - truncation: " << truncation << '\n';
        if (not domain.global()) {
            Log::debug() << " - domain: " << domain << '\n';
        }
        if (GlobalDomain(domain)) {
            if (GlobalDomain(domain).west() != 0.) {
                Log::debug() << " - global domain with modified west: " << GlobalDomain(domain).west() << '\n';
            }
        }
        Log::debug() << " - fft: " << std::boolalpha << useFFT_ << '\n';
        Log::debug() << " - linalg_backend: ";
        if (using_eckit_default_backend(linalg_backend_)) {
            Log::debug() << "eckit_linalg default (currently \"" << detect_linalg_backend(linalg_backend_)
                         << "\" but could be changed after setup, check invtrans debug output)" << '\n';
        }
        else {
            Log::debug() << detect_linalg_backend(linalg_backend_) << '\n';
        }
        Log::debug() << " - legendre_cache: " << std::boolalpha << bool(legendre_cache_) << std::endl;


        // precomputations for Legendre polynomials:
        {
            const auto nlatsLeg = size_t(nlatsLeg_);
            size_t size_sym     = 0;
            size_t size_asym    = 0;
            legendre_sym_begin_.resize(truncation_ + 3);
            legendre_asym_begin_.resize(truncation_ + 3);
            legendre_sym_begin_[0]  = 0;
            legendre_asym_begin_[0] = 0;
            for (idx_t jm = 0; jm <= truncation_ + 1; jm++) {
                size_sym += add_padding(num_n(truncation_ + 1, jm, /*symmetric*/ true) * nlatsLeg);
                size_asym += add_padding(num_n(truncation_ + 1, jm, /*symmetric*/ false) * nlatsLeg);
                legendre_sym_begin_[jm + 1]  = size_sym;
                legendre_asym_begin_[jm + 1] = size_asym;
            }

            if (legendre_cache_) {
                ReadCache legendre(legendre_cache_);
                legendre_sym_  = legendre.read<double>(size_sym);
                legendre_asym_ = legendre.read<double>(size_asym);
                ATLAS_ASSERT(legendre.pos == legendre_cachesize_);
                // TODO: check this is all aligned...
            }
            else {
                if (TransParameters(config).export_legendre()) {
                    ATLAS_ASSERT(not cache_.legendre());

                    size_t bytes = sizeof(double) * (size_sym + size_asym);
                    Log::debug() << "TransLocal: allocating LegendreCache: " << eckit::Bytes(bytes) << std::endl;
                    export_legendre_ = LegendreCache(bytes);

                    legendre_cachesize_ = export_legendre_.legendre().size();
                    legendre_cache_     = export_legendre_.legendre().data();
                    ReadCache legendre(legendre_cache_);
                    legendre_sym_  = legendre.read<double>(size_sym);
                    legendre_asym_ = legendre.read<double>(size_asym);
                }
                else {
                    alloc_aligned(legendre_sym_, size_sym, "Legendre coeffs symmetric");
                    alloc_aligned(legendre_asym_, size_asym, "Legendre coeffs asymmetric");
                }

                ATLAS_TRACE_SCOPE("Legendre precomputations (structured)") {
                    compute_legendre_polynomials(truncation_ + 1, nlatsLeg_, lats.data(), legendre_sym_, legendre_asym_,
                                                 legendre_sym_begin_.data(), legendre_asym_begin_.data());
                }
                std::string file_path = TransParameters(config).write_legendre();
                if (file_path.size()) {
                    ATLAS_TRACE("Write LegendreCache to file");
                    Log::debug() << "Writing Legendre cache file ..." << std::endl;
                    Log::debug() << "    path: " << file_path << std::endl;
                    WriteCache legendre(file_path);
                    legendre.write(legendre_sym_, size_sym);
                    legendre.write(legendre_asym_, size_asym);
                    Log::debug() << "    size: " << eckit::Bytes(legendre.pos) << std::endl;
                }
            }
        }

        // precomputations for Fourier transformations:
        if (useFFT_) {
#if ATLAS_HAVE_FFTW && !TRANSLOCAL_DGEMM2
            {
                ATLAS_TRACE("Fourier precomputations (FFTW)");
                int num_complex = (nlonsMaxGlobal_ / 2) + 1;
                fftw_->in       = fftw_alloc_complex(nlats * num_complex);
                fftw_->out      = fftw_alloc_real(nlats * nlonsMaxGlobal_);

                if (fft_cache_) {
                    Log::debug() << "Import FFTW wisdom from cache" << std::endl;
                    fftw_import_wisdom_from_string(static_cast<const char*>(fft_cache_));
                }
                //                std::string wisdomString( "" );
                //                std::ifstream read( "wisdom.bin" );
                //                if ( read.is_open() ) {
                //                    std::getline( read, wisdomString );
                //                    while ( read ) {
                //                        std::string line;
                //                        std::getline( read, line );
                //                        wisdomString += line;
                //                    }
                //                }
                //                read.close();
                //                if ( wisdomString.length() > 0 ) { fftw_import_wisdom_from_string( &wisdomString[0u] ); }
                if (RegularGrid(gridGlobal_)) {
                    fftw_->plans.resize(1);
                    fftw_->plans[0] =
                        fftw_plan_many_dft_c2r(1, &nlonsMaxGlobal_, nlats, fftw_->in, nullptr, 1, num_complex,
                                               fftw_->out, nullptr, 1, nlonsMaxGlobal_, FFTW_ESTIMATE);
                }
                else {
                    fftw_->plans.resize(nlatsLegDomain_);
                    for (int j = 0; j < nlatsLegDomain_; j++) {
                        int nlonsGlobalj = gs_global.nx(jlatMinLeg_ + j);
                        //ASSERT( nlonsGlobalj > 0 && nlonsGlobalj <= nlonsMaxGlobal_ );
                        fftw_->plans[j] = fftw_plan_dft_c2r_1d(nlonsGlobalj, fftw_->in, fftw_->out, FFTW_ESTIMATE);
                    }
                }
                std::string file_path = TransParameters(config).write_fft();
                if (file_path.size()) {
                    Log::debug() << "Write FFTW wisdom to file " << file_path << std::endl;
                    //bool success = fftw_export_wisdom_to_filename( "wisdom.bin" );
                    //ASSERT( success );
                    //std::ofstream write( file_path );
                    //write << FFTW_Wisdom();

                    FILE* file_fftw = fopen(file_path.c_str(), "wb");
                    fftw_export_wisdom_to_file(file_fftw);
                    fclose(file_fftw);
                }
                //                std::string newWisdom( fftw_export_wisdom_to_string() );
                //                if ( 1.1 * wisdomString.length() < newWisdom.length() ) {
                //                    std::ofstream write( "wisdom.bin" );
                //                    write << newWisdom;
                //                    write.close();
                //                }
            }
            // other FFT implementations should be added with #elif statements
#else
            useFFT_               = false;  // no FFT implemented => default to dgemm
            std::string file_path = TransParameters(config).write_fft();
            if (file_path.size()) {
                std::ofstream write(file_path);
                write << "No cache available, as FFTW is not enabled" << std::endl;
                write.close();
            }

#endif
        }
        if (!useFFT_) {
            Log::warning()
                << "WARNING: Spectral transform results may contain aliasing errors. This will be addressed soon."
                << std::endl;

            alloc_aligned(fourier_, 2 * (truncation_ + 1) * nlonsMax, "Fourier coeffs");
#if !TRANSLOCAL_DGEMM2
            {
                ATLAS_TRACE("Fourier precomputations (NoFFT)");
                int idx = 0;
                for (int jm = 0; jm < truncation_ + 1; jm++) {
                    double factor = 1.;
                    if (jm > 0) {
                        factor = 2.;
                    }
                    for (int jlon = 0; jlon < nlonsMax; jlon++) {
                        fourier_[idx++] = +std::cos(jm * lons[jlon]) * factor;  // real part
                    }
                    for (int jlon = 0; jlon < nlonsMax; jlon++) {
                        fourier_[idx++] = -std::sin(jm * lons[jlon]) * factor;  // imaginary part
                    }
                }
            }
#else
            {
                ATLAS_TRACE("precomp Fourier");
                int idx = 0;
                for (int jlon = 0; jlon < nlonsMax; jlon++) {
                    double factor = 1.;
                    for (int jm = 0; jm < truncation_ + 1; jm++) {
                        if (jm > 0) {
                            factor = 2.;
                        }
                        fourier_[idx++] = +std::cos(jm * lons[jlon]) * factor;  // real part
                        fourier_[idx++] = -std::sin(jm * lons[jlon]) * factor;  // imaginary part
                    }
                }
            }
#endif
        }
    }
    else {
        // unstructured grid
        if (unstruct_precomp_) {
            ATLAS_TRACE("Legendre precomputations (unstructured)");

            if (warning()) {
                Log::warning()
                    << "WARNING: Precomputations for spectral transforms could take a long time as there's no structure"
                       " to take advantage of!!!"
                    << std::endl
                    << "The precomputed values consume at least "
                    << eckit::Bytes(sizeof(double) * legendre_size(truncation_) * grid_.size()) << " ("
                    << eckit::Bytes(sizeof(double) * legendre_size(truncation_)) << " for each of " << grid_.size()
                    << " grid points )" << std::endl
                    << "Furthermore, results may contain aliasing errors." << std::endl;
            }

            std::vector<double> lats(grid_.size());
            alloc_aligned(legendre_, legendre_size(truncation_) * grid_.size(), "Legendre coeffs.");
            int j(0);
            for (PointLonLat p : grid_.lonlat()) {
                lats[j++] = p.lat() * util::Constants::degreesToRadians();
            }
            compute_legendre_polynomials_all(truncation_, grid_.size(), lats.data(), legendre_);
        }
        if (TransParameters(config).write_legendre().size()) {
            throw_NotImplemented(
                "Caching for unstructured grids or structured grids with projections not yet implemented", Here());
        }
    }
}

// --------------------------------------------------------------------------------------------------------------------

TransLocal::TransLocal(const Grid& grid, const long truncation, const eckit::Configuration& config):
    TransLocal(Cache(), grid, grid.domain(), truncation, config) {}

TransLocal::TransLocal(const Grid& grid, const Domain& domain, const long truncation,
                       const eckit::Configuration& config):
    TransLocal(Cache(), grid, domain, truncation, config) {}

TransLocal::TransLocal(const Cache& cache, const Grid& grid, const long truncation, const eckit::Configuration& config):
    TransLocal(cache, grid, grid.domain(), truncation, config) {}

// --------------------------------------------------------------------------------------------------------------------

TransLocal::~TransLocal() {
    if (StructuredGrid(grid_) && not grid_.projection()) {
        if (not legendre_cache_) {
            free_aligned(legendre_sym_, "symmetric");
            free_aligned(legendre_asym_, "asymmetric");
        }
        if (useFFT_) {
#if ATLAS_HAVE_FFTW && !TRANSLOCAL_DGEMM2
            for (idx_t j = 0, size = static_cast<idx_t>(fftw_->plans.size()); j < size; j++) {
                fftw_destroy_plan(fftw_->plans[j]);
            }
            fftw_free(fftw_->in);
            fftw_free(fftw_->out);
#endif
        }
        else {
            free_aligned(fourier_, "Fourier coeffs.");
        }
    }
    else {
        if (unstruct_precomp_) {
            free_aligned(legendre_, "Legendre coeffs.");
        }
    }
}

// --------------------------------------------------------------------------------------------------------------------

const functionspace::Spectral& TransLocal::spectral() const {
    if (not spectral_) {
        spectral_ = functionspace::Spectral(Trans(this));
    }
    return spectral_;
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::invtrans(const Field& spfield, Field& gpfield, const eckit::Configuration& config) const {
    // VERY PRELIMINARY IMPLEMENTATION WITHOUT ANY GUARANTEES
    int nb_scalar_fields = 1;
    ATLAS_ASSERT(spfield.rank() == 1, "Only rank-1 fields supported at the moment");
    ATLAS_ASSERT(gpfield.rank() == 1, "Only rank-1 fields supported at the moment");
    const auto scalar_spectra = array::make_view<double, 1>(spfield);
    auto gp_fields            = array::make_view<double, 1>(gpfield);

    if (gp_fields.shape(0) < grid().size()) {
        // Hopefully the halo (if present) is appended
        ATLAS_DEBUG_VAR(gp_fields.shape(0));
        ATLAS_DEBUG_VAR(grid().size());
        ATLAS_ASSERT(gp_fields.shape(0) < grid().size());
    }

    invtrans(nb_scalar_fields, scalar_spectra.data(), gp_fields.data(), config);
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::invtrans(const FieldSet& spfields, FieldSet& gpfields, const eckit::Configuration& config) const {
    // VERY PRELIMINARY IMPLEMENTATION WITHOUT ANY GUARANTEES
    ATLAS_ASSERT(spfields.size() == gpfields.size());
    for (idx_t f = 0; f < spfields.size(); ++f) {
        invtrans(spfields[f], gpfields[f], config);
    }
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::invtrans_grad(const Field& /*spfield*/, Field& /*gradfield*/, const eckit::Configuration&) const {
    ATLAS_NOTIMPLEMENTED;
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::invtrans_grad(const FieldSet& /*spfields*/, FieldSet& /*gradfields*/,
                               const eckit::Configuration&) const {
    ATLAS_NOTIMPLEMENTED;
}

// --------------------------------------------------------------------------------------------------------------------

void gp_transpose(const int nb_size, const int nb_fields, const double gp_tmp[], double gp_fields[]) {
    for (int jgp = 0; jgp < nb_size; jgp++) {
        for (int jfld = 0; jfld < nb_fields; jfld++) {
            gp_fields[jfld * nb_size + jgp] = gp_tmp[jgp * nb_fields + jfld];
        }
    }
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::invtrans_vordiv2wind(const Field& spvor, const Field& spdiv, Field& gpwind,
                                      const eckit::Configuration& config) const {
    // VERY PRELIMINARY IMPLEMENTATION WITHOUT ANY GUARANTEES
    ATLAS_ASSERT(spvor.rank() == 1, "Only rank-1 fields supported at the moment");
    ATLAS_ASSERT(spdiv.rank() == 1, "Only rank-1 fields supported at the moment");
    int nb_vordiv_fields          = 1;
    const auto vorticity_spectra  = array::make_view<double, 1>(spvor);
    const auto divergence_spectra = array::make_view<double, 1>(spdiv);
    auto gp_fields                = array::make_view<double, 2>(gpwind);

    if (gp_fields.shape(1) == grid().size() && gp_fields.shape(0) == 2) {
        invtrans(nb_vordiv_fields, vorticity_spectra.data(), divergence_spectra.data(), gp_fields.data(), config);
    }
    else if (gp_fields.shape(0) == grid().size() && gp_fields.shape(1) == 2) {
        array::ArrayT<double> gpwind_t(gp_fields.shape(1), gp_fields.shape(0));
        auto gp_fields_t = array::make_view<double, 2>(gpwind_t);
        invtrans(nb_vordiv_fields, vorticity_spectra.data(), divergence_spectra.data(), gp_fields_t.data(), config);
        gp_transpose(grid().size(), 2, gp_fields_t.data(), gp_fields.data());
    }
    else {
        ATLAS_NOTIMPLEMENTED;
    }
}

void TransLocal::invtrans_adj(const Field& gpfield, Field& spfield, const eckit::Configuration& config) const {
    ATLAS_NOTIMPLEMENTED;
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::invtrans_adj(const FieldSet& gpfields, FieldSet& spfields, const eckit::Configuration& config) const {
    ATLAS_NOTIMPLEMENTED;
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::invtrans_grad_adj(const Field& /*gradfield*/, Field& /*spfield*/, const eckit::Configuration&) const {
    ATLAS_NOTIMPLEMENTED;
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::invtrans_grad_adj(const FieldSet& /*gradfields*/, FieldSet& /*spfields*/,
                                   const eckit::Configuration&) const {
    ATLAS_NOTIMPLEMENTED;
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::invtrans_vordiv2wind_adj(const Field& gpwind, Field& spvor, Field& spdiv,
                                          const eckit::Configuration& config) const {
    ATLAS_NOTIMPLEMENTED;
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::invtrans(const int nb_scalar_fields, const double scalar_spectra[], double gp_fields[],
                          const eckit::Configuration& config) const {
    invtrans_uv(truncation_, nb_scalar_fields, 0, scalar_spectra, gp_fields, config);
}


// --------------------------------------------------------------------------------------------------------------------

void TransLocal::invtrans_legendre(const int truncation, const int nlats, const int nb_fields,
                                   const int /*nb_vordiv_fields*/, const double scalar_spectra[], double scl_fourier[],
                                   const eckit::Configuration&) const {
    // Legendre transform:
    {
        Log::debug() << "TransLocal::invtrans_legendre: Legendre GEMM with \"" << detect_linalg_backend(linalg_backend_)
                     << "\" using " << nlatsLegReduced_ - nlat0_[0] << " latitudes out of " << nlatsGlobal_ / 2
                     << std::endl;
        linalg::dense::Backend linalg_backend{linalg_backend_};
        ATLAS_TRACE("Inverse Legendre Transform (GEMM)");
        for (int jm = 0; jm <= truncation_; jm++) {
            size_t size_sym  = num_n(truncation_ + 1, jm, true);
            size_t size_asym = num_n(truncation_ + 1, jm, false);
            const int n_imag = (jm ? 2 : 1);
            int size_fourier = nb_fields * n_imag * (nlatsLegReduced_ - nlat0_[jm]);
            if (size_fourier > 0) {
                auto posFourier = [&](int jfld, int imag, int jlat, int jm, int nlatsH) {
                    return jfld + nb_fields * (imag + n_imag * (nlatsLegReduced_ - nlat0_[jm] - nlatsH + jlat));
                };
                // THESE ARE REALLOCATED FOR EACH jm ???
                // THEY COULD ALLOCATED ONCE BEFORE jm LOOP WITH A MAXIMUM SIZE?
                double* scalar_sym;
                double* scalar_asym;
                double* scl_fourier_sym;
                double* scl_fourier_asym;
                alloc_aligned(scalar_sym, n_imag * nb_fields * size_sym);
                alloc_aligned(scalar_asym, n_imag * nb_fields * size_asym);
                alloc_aligned(scl_fourier_sym, size_fourier);
                alloc_aligned(scl_fourier_asym, size_fourier);
                {
                    //ATLAS_TRACE( "Legendre split" );
                    idx_t idx = 0, is = 0, ia = 0, ioff = (2 * truncation + 3 - jm) * jm / 2 * nb_fields * 2;
                    // the choice between the following two code lines determines whether
                    // total wavenumbers are summed in an ascending or descending order.
                    // The trans library in IFS uses descending order because it should
                    // be more accurate (higher wavenumbers have smaller contributions).
                    // This also needs to be changed when splitting the spectral data in
                    // compute_legendre_polynomials!
                    //for ( int jn = jm; jn <= truncation_ + 1; jn++ ) {
                    for (int jn = truncation_ + 1; jn >= jm; jn--) {
                        for (int imag = 0; imag < n_imag; imag++) {
                            for (int jfld = 0; jfld < nb_fields; jfld++) {
                                idx = jfld + nb_fields * (imag + 2 * (jn - jm));
                                if (jn <= truncation && jm < truncation) {
                                    if ((jn - jm) % 2 == 0) {
                                        scalar_sym[is++] = scalar_spectra[idx + ioff];
                                    }
                                    else {
                                        scalar_asym[ia++] = scalar_spectra[idx + ioff];
                                    }
                                }
                                else {
                                    if ((jn - jm) % 2 == 0) {
                                        scalar_sym[is++] = 0.;
                                    }
                                    else {
                                        scalar_asym[ia++] = 0.;
                                    }
                                }
                            }
                        }
                    }
                    ATLAS_ASSERT(size_t(ia) == n_imag * nb_fields * size_asym &&
                                 size_t(is) == n_imag * nb_fields * size_sym);
                }
                if (nlatsLegReduced_ - nlat0_[jm] > 0) {
                    ATLAS_TRACE("matrix_multiply (" + std::string(linalg_backend) + ")");
                    {
                        linalg::Matrix A(scalar_sym, nb_fields * n_imag, size_sym);
                        linalg::Matrix B(legendre_sym_ + legendre_sym_begin_[jm] + nlat0_[jm] * size_sym, size_sym,
                                         nlatsLegReduced_ - nlat0_[jm]);
                        linalg::Matrix C(scl_fourier_sym, nb_fields * n_imag, nlatsLegReduced_ - nlat0_[jm]);
                        linalg::matrix_multiply(A, B, C, linalg_backend);
                        /*Log::info() << "sym: ";
                        for ( int j = 0; j < size_sym * ( nlatsLegReduced_ - nlat0_[jm] ); j++ ) {
                            Log::info() << legendre_sym_[j + legendre_sym_begin_[jm] + nlat0_[jm] * size_sym] << " ";
                        }
                        Log::info() << std::endl;*/
                    }
                    if (size_asym > 0) {
                        linalg::Matrix A(scalar_asym, nb_fields * n_imag, size_asym);
                        linalg::Matrix B(legendre_asym_ + legendre_asym_begin_[jm] + nlat0_[jm] * size_asym, size_asym,
                                         nlatsLegReduced_ - nlat0_[jm]);
                        linalg::Matrix C(scl_fourier_asym, nb_fields * n_imag, nlatsLegReduced_ - nlat0_[jm]);
                        linalg::matrix_multiply(A, B, C, linalg_backend);
                        /*Log::info() << "asym: ";
                        for ( int j = 0; j < size_asym * ( nlatsLegReduced_ - nlat0_[jm] ); j++ ) {
                            Log::info() << legendre_asym_[j + legendre_asym_begin_[jm] + nlat0_[jm] * size_asym] << " ";
                        }
                        Log::info() << std::endl;*/
                    }
                }
                {
                    //ATLAS_TRACE( "merge spheres" );
                    // northern hemisphere:
                    for (int jlat = 0; jlat < nlatsNH_; jlat++) {
                        if (nlatsLegReduced_ - nlat0_[jm] - nlatsNH_ + jlat >= 0) {
                            for (int imag = 0; imag < n_imag; imag++) {
                                for (int jfld = 0; jfld < nb_fields; jfld++) {
                                    int idx = posFourier(jfld, imag, jlat, jm, nlatsNH_);
                                    scl_fourier[posMethod(jfld, imag, jlat, jm, nb_fields, nlats)] =
                                        scl_fourier_sym[idx] + scl_fourier_asym[idx];
                                }
                            }
                        }
                        else {
                            for (int imag = 0; imag < n_imag; imag++) {
                                for (int jfld = 0; jfld < nb_fields; jfld++) {
                                    scl_fourier[posMethod(jfld, imag, jlat, jm, nb_fields, nlats)] = 0.;
                                }
                            }
                        }
                        /*for ( int imag = 0; imag < n_imag; imag++ ) {
                        for ( int jfld = 0; jfld < nb_fields; jfld++ ) {
                            if ( scl_fourier[posMethod( jfld, imag, jlat, jm, nb_fields, nlats )] > 0. ) {
                                Log::info() << "jm=" << jm << " jlat=" << jlat << " nlatsLeg_=" << nlatsLeg_
                                            << " nlat0=" << nlat0_[jm] << " nlatsNH=" << nlatsNH_ << std::endl;
                            }
                        }
                    }*/
                    }
                    // southern hemisphere:
                    for (int jlat = 0; jlat < nlatsSH_; jlat++) {
                        int jslat = nlats - jlat - 1;
                        if (nlatsLegReduced_ - nlat0_[jm] - nlatsSH_ + jlat >= 0) {
                            for (int imag = 0; imag < n_imag; imag++) {
                                for (int jfld = 0; jfld < nb_fields; jfld++) {
                                    int idx = posFourier(jfld, imag, jlat, jm, nlatsSH_);
                                    scl_fourier[posMethod(jfld, imag, jslat, jm, nb_fields, nlats)] =
                                        scl_fourier_sym[idx] - scl_fourier_asym[idx];
                                }
                            }
                        }
                        else {
                            for (int imag = 0; imag < n_imag; imag++) {
                                for (int jfld = 0; jfld < nb_fields; jfld++) {
                                    scl_fourier[posMethod(jfld, imag, jslat, jm, nb_fields, nlats)] = 0.;
                                }
                            }
                        }
                    }
                }
                free_aligned(scalar_sym);
                free_aligned(scalar_asym);
                free_aligned(scl_fourier_sym);
                free_aligned(scl_fourier_asym);
            }
            else {
                for (int jlat = 0; jlat < nlats; jlat++) {
                    for (int imag = 0; imag < n_imag; imag++) {
                        for (int jfld = 0; jfld < nb_fields; jfld++) {
                            scl_fourier[posMethod(jfld, imag, jlat, jm, nb_fields, nlats)] = 0.;
                        }
                    }
                }
            }
        }
    }
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::invtrans_fourier_regular(const int nlats, const int nlons, const int nb_fields, double scl_fourier[],
                                          double gp_fields[], const eckit::Configuration&) const {
    // Fourier transformation:
    if (useFFT_) {
#if ATLAS_HAVE_FFTW && !TRANSLOCAL_DGEMM2
        {
            int num_complex = (nlonsMaxGlobal_ / 2) + 1;
            {
                ATLAS_TRACE("Inverse Fourier Transform (FFTW, RegularGrid)");
                for (int jfld = 0; jfld < nb_fields; jfld++) {
                    int idx = 0;
                    for (int jlat = 0; jlat < nlats; jlat++) {
                        fftw_->in[idx++][0] = scl_fourier[posMethod(jfld, 0, jlat, 0, nb_fields, nlats)];
                        for (int jm = 1; jm < num_complex; jm++, idx++) {
                            for (int imag = 0; imag < 2; imag++) {
                                if (jm <= truncation_) {
                                    fftw_->in[idx][imag] =
                                        scl_fourier[posMethod(jfld, imag, jlat, jm, nb_fields, nlats)];
                                }
                                else {
                                    fftw_->in[idx][imag] = 0.;
                                }
                            }
                        }
                    }
                    fftw_execute_dft_c2r(fftw_->plans[0], fftw_->in, fftw_->out);
                    for (int jlat = 0; jlat < nlats; jlat++) {
                        for (int jlon = 0; jlon < nlons; jlon++) {
                            int j = jlon + jlonMin_[0];
                            if (j >= nlonsMaxGlobal_) {
                                j -= nlonsMaxGlobal_;
                            }
                            gp_fields[jlon + nlons * (jlat + nlats * jfld)] = fftw_->out[j + nlonsMaxGlobal_ * jlat];
                        }
                    }
                }
            }
        }
#endif
    }
    else {
        linalg::dense::Backend linalg_backend{linalg_backend_};
#if !TRANSLOCAL_DGEMM2
        // dgemm-method 1
        {
            ATLAS_TRACE("Inverse Fourier Transform (NoFFT,matrix_multiply=" + detect_linalg_backend(linalg_backend_) +
                        ")");
            linalg::Matrix A(fourier_, nlons, (truncation_ + 1) * 2);
            linalg::Matrix B(scl_fourier, (truncation_ + 1) * 2, nb_fields * nlats);
            linalg::Matrix C(gp_fields, nlons, nb_fields * nlats);

            linalg::matrix_multiply(A, B, C, linalg_backend);
        }
#else
        // dgemm-method 2
        // should be faster for small domains or large truncation
        // but have not found any significant speedup so far
        double* gp;
        alloc_aligned(gp, nb_fields * grid_.size());
        {
            ATLAS_TRACE("Fourier dgemm method 2");
            linalg::Matrix A(scl_fourier, nb_fields * nlats, (truncation_ + 1) * 2);
            linalg::Matrix B(fourier_, (truncation_ + 1) * 2, nlons);
            linalg::Matrix C(gp, nb_fields * nlats, nlons);
            linalg::matrix_multiply(A, B, C, linalg_backend);
        }

        // Transposition in grid point space:
        {
            ATLAS_TRACE("transposition in gp-space");
            int idx = 0;
            for (int jlon = 0; jlon < nlons; jlon++) {
                for (int jlat = 0; jlat < nlats; jlat++) {
                    for (int jfld = 0; jfld < nb_fields; jfld++) {
                        int pos_tp = jlon + nlons * (jlat + nlats * (jfld));
                        //int pos  = jfld + nb_fields * ( jlat + nlats * ( jlon ) );
                        gp_fields[pos_tp] = gp[idx++];  // = gp[pos]
                    }
                }
            }
        }
        free_aligned(gp);
#endif
    }
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::invtrans_fourier_reduced(const int nlats, const StructuredGrid& g, const int nb_fields,
                                          double scl_fourier[], double gp_fields[], const eckit::Configuration&) const {
    // Fourier transformation:
    if (useFFT_) {
#if ATLAS_HAVE_FFTW && !TRANSLOCAL_DGEMM2
        {
            {
                ATLAS_TRACE("Inverse Fourier Transform (FFTW, ReducedGrid)");
                int jgp = 0;
                for (int jfld = 0; jfld < nb_fields; jfld++) {
                    for (int jlat = 0; jlat < nlats; jlat++) {
                        int idx = 0;
                        //Log::info() << jlat << "in:" << std::endl;
                        int num_complex     = (nlonsGlobal_[jlat] / 2) + 1;
                        fftw_->in[idx++][0] = scl_fourier[posMethod(jfld, 0, jlat, 0, nb_fields, nlats)];
                        //Log::info() << fftw_->in[0][0] << " ";
                        for (int jm = 1; jm < num_complex; jm++, idx++) {
                            for (int imag = 0; imag < 2; imag++) {
                                if (jm <= truncation_) {
                                    fftw_->in[idx][imag] =
                                        scl_fourier[posMethod(jfld, imag, jlat, jm, nb_fields, nlats)];
                                }
                                else {
                                    fftw_->in[idx][imag] = 0.;
                                }
                                //Log::info() << fftw_->in[idx][imag] << " ";
                            }
                        }
                        //Log::info() << std::endl;
                        //Log::info() << jlat << "out:" << std::endl;
                        int jplan = nlatsLegDomain_ - nlatsNH_ + jlat;
                        if (jplan >= nlatsLegDomain_) {
                            jplan = nlats - 1 + nlatsLegDomain_ - nlatsSH_ - jlat;
                        };
                        //ASSERT( jplan < nlatsLeg_ && jplan >= 0 );
                        fftw_execute_dft_c2r(fftw_->plans[jplan], fftw_->in, fftw_->out);
                        for (int jlon = 0; jlon < g.nx(jlat); jlon++) {
                            int j = jlon + jlonMin_[jlat];
                            if (j >= nlonsGlobal_[jlat]) {
                                j -= nlonsGlobal_[jlat];
                            }
                            //Log::info() << fftw_->out[j] << " ";
                            ATLAS_ASSERT(j < nlonsMaxGlobal_);
                            gp_fields[jgp++] = fftw_->out[j];
                        }
                        //Log::info() << std::endl;
                    }
                }
            }
        }
#endif
    }
    else {
        throw_NotImplemented(
            "Using dgemm in Fourier transform for reduced grids is extremely slow. Please install and use FFTW!",
            Here());
    }
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::invtrans_unstructured_precomp(const int truncation, const int nb_fields, const int nb_vordiv_fields,
                                               const double scalar_spectra[], double gp_fields[],
                                               const eckit::Configuration&) const {
    ATLAS_TRACE("invtrans_uv unstructured");

    const int nlats        = grid_.size();
    const int size_fourier = nb_fields * 2;
    double* scl_fourier;
    double* scl_fourier_tp;
    double* fouriertp;
    double* gp_opt;
    alloc_aligned(scl_fourier, size_fourier * (truncation)*nlats);
    alloc_aligned(scl_fourier_tp, size_fourier * (truncation));
    alloc_aligned(fouriertp, 2 * (truncation));
    alloc_aligned(gp_opt, nb_fields);

    linalg::dense::Backend linalg_backend{linalg_backend_};

    {
        ATLAS_TRACE("Inverse Legendre Transform (GEMM," + std::string(linalg_backend) + ")");
        for (int jm = 0; jm < truncation; jm++) {
            const int noff = (2 * truncation + 3 - jm) * jm / 2, ns = truncation - jm + 1;
            linalg::Matrix A(
                eckit::linalg::Matrix(const_cast<double*>(scalar_spectra) + nb_fields * 2 * noff, nb_fields * 2, ns));
            linalg::Matrix B(legendre_ + noff * nlats, ns, nlats);
            linalg::Matrix C(scl_fourier + jm * size_fourier * nlats, nb_fields * 2, nlats);
            linalg::matrix_multiply(A, B, C, linalg_backend);
        }
    }

    // loop over all points:
    {
        ATLAS_TRACE("Inverse Fourier Transform (NoFFT," + std::string(linalg_backend) + ")");
        int ip = 0;
        for (const PointLonLat p : grid_.lonlat()) {
            const double lon = p.lon() * util::Constants::degreesToRadians();
            const double lat = p.lat() * util::Constants::degreesToRadians();
            {
                //ATLAS_TRACE( "opt transposition in Fourier" );
                for (int jm = 0; jm < truncation; jm++) {
                    int idx = nb_fields * 2 * (ip + nlats * jm);
                    for (int imag = 0; imag < 2; imag++) {
                        for (int jfld = 0; jfld < nb_fields; jfld++) {
                            const int pos_tp = imag + 2 * (jm + (truncation) * (jfld));
                            //int pos  = jfld + nb_fields * ( imag + 2 * ( jm ) );
                            scl_fourier_tp[pos_tp] = scl_fourier[idx++];  // = scl_fourier[pos]
                        }
                    }
                }
            }

            // Fourier transformation:
            {
                //ATLAS_TRACE( "opt compute fouriertp" );
                int idx          = 0;
                fouriertp[idx++] = 1.;  // real part
                fouriertp[idx++] = 0.;  // imaginary part
                for (int jm = 1; jm < truncation; jm++) {
                    fouriertp[idx++] = +2. * std::cos(jm * lon);  // real part
                    fouriertp[idx++] = -2. * std::sin(jm * lon);  // imaginary part
                }
            }
            {
                //ATLAS_TRACE( "opt Fourier dgemm" );
                linalg::Matrix A(fouriertp, 1, (truncation)*2);
                linalg::Matrix B(scl_fourier_tp, (truncation)*2, nb_fields);
                linalg::Matrix C(gp_opt, 1, nb_fields);
                linalg::matrix_multiply(A, B, C, linalg_backend);
                for (int j = 0; j < nb_fields; j++) {
                    gp_fields[ip + j * grid_.size()] = gp_opt[j];
                }
            }
            // Computing u,v from U,V:
            {
                if (nb_vordiv_fields > 0) {
                    //ATLAS_TRACE( " u,v from U,V" );
                    double coslat = std::cos(lat);
                    for (int j = 0; j < 2 * nb_vordiv_fields && j < nb_fields; j++) {
                        gp_fields[ip + j * grid_.size()] /= coslat;
                    }
                }
            }
            ++ip;
        }
    }
    free_aligned(scl_fourier);
    free_aligned(scl_fourier_tp);
    free_aligned(fouriertp);
    free_aligned(gp_opt);
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::invtrans_unstructured(const int truncation, const int nb_fields, const int nb_vordiv_fields,
                                       const double scalar_spectra[], double gp_fields[],
                                       const eckit::Configuration& config) const {
    ATLAS_TRACE("invtrans_unstructured");

    if (warning(config)) {
        Log::warning() << "WARNING: Spectral transforms could take a long time (unstructured grid approach). Results "
                          "may contain aliasing errors."
                       << std::endl;
    }

    linalg::dense::Backend linalg_backend{linalg_backend_};

    double* zfn;
    alloc_aligned(zfn, (truncation + 1) * (truncation + 1));
    compute_zfn(truncation, zfn);
    int size_fourier = nb_fields * 2;
    double* legendre;
    double* scl_fourier;
    double* scl_fourier_tp;
    double* fouriertp;
    double* gp_opt;
    alloc_aligned(legendre, legendre_size(truncation + 1));
    alloc_aligned(scl_fourier, size_fourier * (truncation + 1));
    alloc_aligned(scl_fourier_tp, size_fourier * (truncation + 1));
    alloc_aligned(fouriertp, 2 * (truncation + 1));
    alloc_aligned(gp_opt, nb_fields);


    // loop over all points:
    int ip = 0;
    for (const PointLonLat p : grid_.lonlat()) {
        const double lon = p.lon() * util::Constants::degreesToRadians();
        const double lat = p.lat() * util::Constants::degreesToRadians();
        compute_legendre_polynomials_lat(truncation, lat, legendre, zfn);
        // Legendre transform:
        {
            //ATLAS_TRACE( "opt Legendre dgemm" );
            ATLAS_TRACE("Legendre matrix_multiply (" + std::string(linalg_backend) + ")");
            for (int jm = 0; jm <= truncation; jm++) {
                const int noff = (2 * truncation + 3 - jm) * jm / 2, ns = truncation - jm + 1;
                linalg::Matrix A(eckit::linalg::Matrix(const_cast<double*>(scalar_spectra) + nb_fields * 2 * noff,
                                                       nb_fields * 2, ns));
                linalg::Matrix B(legendre + noff, ns, 1);
                linalg::Matrix C(scl_fourier + jm * size_fourier, nb_fields * 2, 1);
                linalg::matrix_multiply(A, B, C, linalg_backend);
            }
        }
        {
            //ATLAS_TRACE( "opt transposition in Fourier" );
            int idx = 0;
            for (int jm = 0; jm < truncation + 1; jm++) {
                for (int imag = 0; imag < 2; imag++) {
                    for (int jfld = 0; jfld < nb_fields; jfld++) {
                        const int pos_tp = imag + 2 * (jm + (truncation + 1) * (jfld));
                        //int pos  = jfld + nb_fields * ( imag + 2 * ( jm ) );
                        scl_fourier_tp[pos_tp] = scl_fourier[idx++];  // = scl_fourier[pos]
                    }
                }
            }
        }

        // Fourier transformation:
        int idx          = 0;
        fouriertp[idx++] = 1.;  // real part
        fouriertp[idx++] = 0.;  // imaginary part
        for (int jm = 1; jm < truncation + 1; jm++) {
            fouriertp[idx++] = +2. * std::cos(jm * lon);  // real part
            fouriertp[idx++] = -2. * std::sin(jm * lon);  // imaginary part
        }
        {
            ATLAS_TRACE("Fourier matrix_multiply (" + std::string(linalg_backend) + ")");
            linalg::Matrix A(fouriertp, 1, (truncation + 1) * 2);
            linalg::Matrix B(scl_fourier_tp, (truncation + 1) * 2, nb_fields);
            linalg::Matrix C(gp_opt, 1, nb_fields);
            linalg::matrix_multiply(A, B, C, linalg_backend);
            for (int j = 0; j < nb_fields; j++) {
                gp_fields[ip + j * grid_.size()] = gp_opt[j];
            }
        }
        // Computing u,v from U,V:
        {
            if (nb_vordiv_fields > 0) {
                //ATLAS_TRACE( "u,v from U,V" );
                const double coslat = std::cos(lat);
                for (int j = 0; j < 2 * nb_vordiv_fields && j < nb_fields; j++) {
                    gp_fields[ip + j * grid_.size()] /= coslat;
                }
            }
        }
        ++ip;
    }
    free_aligned(legendre);
    free_aligned(scl_fourier);
    free_aligned(scl_fourier_tp);
    free_aligned(fouriertp);
    free_aligned(gp_opt);
    free_aligned(zfn);
}

//-----------------------------------------------------------------------------
// Routine to compute the spectral transform by using a Local Fourier transformation
// for a grid (same latitude for all longitudes, allows to compute Legendre functions
// once for all longitudes). U and v components are divided by cos(latitude) for
// nb_vordiv_fields > 0.
//
// Legendre polynomials are computed up to truncation_+1 to be accurate for vorticity and
// divergence computation. The parameter truncation is the truncation used in storing the
// spectral data scalar_spectra and can be different from truncation_. If truncation is
// larger than truncation_+1 the transform will behave as if the spectral data was truncated
// to truncation_+1.
//
// Author:
// Andreas Mueller *ECMWF*
//
void TransLocal::invtrans_uv(const int truncation, const int nb_scalar_fields, const int nb_vordiv_fields,
                             const double scalar_spectra[], double gp_fields[],
                             const eckit::Configuration& config) const {
    if (nb_scalar_fields > 0) {
        int nb_fields = nb_scalar_fields;

        // Transform
        if (StructuredGrid(grid_) && not grid_.projection()) {
            auto g = StructuredGrid(grid_);
            ATLAS_TRACE("invtrans_uv structured");
            int nlats            = g.ny();
            int nlons            = g.nxmax();
            int size_fourier_max = nb_fields * 2 * nlats;
            double* scl_fourier;
            alloc_aligned(scl_fourier, size_fourier_max * (truncation_ + 1));

            // ATLAS-159 workaround begin
            for (int i = 0; i < size_fourier_max * (truncation_ + 1); ++i) {
                scl_fourier[i] = 0.;
            }
            // ATLAS-159 workaround end

            // Legendre transformation:
            invtrans_legendre(truncation, nlats, nb_scalar_fields, nb_vordiv_fields, scalar_spectra, scl_fourier,
                              config);

            // Fourier transformation:
            if (RegularGrid(gridGlobal_)) {
                invtrans_fourier_regular(nlats, nlons, nb_fields, scl_fourier, gp_fields, config);
            }
            else {
                invtrans_fourier_reduced(nlats, g, nb_fields, scl_fourier, gp_fields, config);
            }

            // Computing u,v from U,V:
            {
                if (nb_vordiv_fields > 0) {
                    ATLAS_TRACE("compute u,v from U,V");
                    std::vector<double> coslatinvs(nlats);
                    for (idx_t j = 0; j < nlats; ++j) {
                        double lat = g.y(j);
                        if (lat > latPole) {
                            lat = latPole;
                        }
                        if (lat < -latPole) {
                            lat = -latPole;
                        }
                        double coslat = std::cos(lat * util::Constants::degreesToRadians());
                        coslatinvs[j] = 1. / coslat;
                        //Log::info() << "lat=" << g.y( j ) << " coslat=" << coslat << std::endl;
                    }
                    int idx = 0;
                    for (idx_t jfld = 0; jfld < 2 * nb_vordiv_fields && jfld < nb_fields; jfld++) {
                        for (idx_t jlat = 0; jlat < g.ny(); jlat++) {
                            for (idx_t jlon = 0; jlon < g.nx(jlat); jlon++) {
                                gp_fields[idx] *= coslatinvs[jlat];
                                idx++;
                            }
                        }
                    }
                }
            }
            free_aligned(scl_fourier);
        }
        else {
            if (unstruct_precomp_) {
                invtrans_unstructured_precomp(truncation, nb_scalar_fields, nb_vordiv_fields, scalar_spectra, gp_fields,
                                              config);
            }
            else {
                invtrans_unstructured(truncation, nb_scalar_fields, nb_vordiv_fields, scalar_spectra, gp_fields,
                                      config);
            }
        }
    }
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::invtrans(const int nb_vordiv_fields, const double vorticity_spectra[],
                          const double divergence_spectra[], double gp_fields[],
                          const eckit::Configuration& config) const {
    invtrans(0, nullptr, nb_vordiv_fields, vorticity_spectra, divergence_spectra, gp_fields, config);
}

// --------------------------------------------------------------------------------------------------------------------

void extend_truncation(const int old_truncation, const int nb_fields, const double old_spectra[],
                       double new_spectra[]) {
    int k = 0, k_old = 0;
    for (int m = 0; m <= old_truncation + 1; m++) {             // zonal wavenumber
        for (int n = m; n <= old_truncation + 1; n++) {         // total wavenumber
            for (int imag = 0; imag < 2; imag++) {              // imaginary/real part
                for (int jfld = 0; jfld < nb_fields; jfld++) {  // field
                    if (m == old_truncation + 1 || n == old_truncation + 1) {
                        new_spectra[k++] = 0.;
                    }
                    else {
                        new_spectra[k++] = old_spectra[k_old++];
                    }
                }
            }
        }
    }
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::invtrans(const int nb_scalar_fields, const double scalar_spectra[], const int nb_vordiv_fields,
                          const double vorticity_spectra[], const double divergence_spectra[], double gp_fields[],
                          const eckit::Configuration& config) const {
    int nb_gp = grid_.size();
    if (nb_vordiv_fields > 0) {
        // collect all spectral data into one array "all_spectra":
        ATLAS_TRACE("TransLocal::invtrans");
        int nb_vordiv_spec_ext = 2 * legendre_size(truncation_ + 1) * nb_vordiv_fields;
        std::vector<double> U_ext;
        std::vector<double> V_ext;
        std::vector<double> scalar_ext;
        if (nb_vordiv_fields > 0) {
            std::vector<double> vorticity_spectra_extended(nb_vordiv_spec_ext);
            std::vector<double> divergence_spectra_extended(nb_vordiv_spec_ext);
            U_ext.resize(nb_vordiv_spec_ext);
            V_ext.resize(nb_vordiv_spec_ext);

            {
                ATLAS_TRACE("extend vordiv");
                // increase truncation in vorticity_spectra and divergence_spectra:
                extend_truncation(truncation_, nb_vordiv_fields, vorticity_spectra, vorticity_spectra_extended.data());
                extend_truncation(truncation_, nb_vordiv_fields, divergence_spectra,
                                  divergence_spectra_extended.data());
            }

            {
                ATLAS_TRACE("vordiv to UV");
                // call vd2uv to compute u and v in spectral space
                trans::VorDivToUV vordiv_to_UV_ext(truncation_ + 1, option::type("local"));
                vordiv_to_UV_ext.execute(nb_vordiv_spec_ext, nb_vordiv_fields, vorticity_spectra_extended.data(),
                                         divergence_spectra_extended.data(), U_ext.data(), V_ext.data());
            }
        }
        if (nb_scalar_fields > 0) {
            int nb_scalar_ext = 2 * legendre_size(truncation_ + 1) * nb_scalar_fields;
            scalar_ext.resize(nb_scalar_ext);
            extend_truncation(truncation_, nb_scalar_fields, scalar_spectra, scalar_ext.data());
        }
        int nb_all_fields = 2 * nb_vordiv_fields + nb_scalar_fields;
        int nb_all_size   = 2 * legendre_size(truncation_ + 1) * nb_all_fields;
        std::vector<double> all_spectra(nb_all_size);
        int k = 0, i = 0, j = 0, l = 0;
        {
            ATLAS_TRACE("merge all spectra");
            for (int m = 0; m <= truncation_ + 1; m++) {                       // zonal wavenumber
                for (int n = m; n <= truncation_ + 1; n++) {                   // total wavenumber
                    for (int imag = 0; imag < 2; imag++) {                     // imaginary/real part
                        for (int jfld = 0; jfld < nb_vordiv_fields; jfld++) {  // vorticity fields
                            all_spectra[k++] = U_ext[i++];
                        }
                        for (int jfld = 0; jfld < nb_vordiv_fields; jfld++) {  // divergence fields
                            all_spectra[k++] = V_ext[j++];
                        }
                        for (int jfld = 0; jfld < nb_scalar_fields; jfld++) {  // scalar fields
                            all_spectra[k++] = scalar_ext[l++];
                        }
                    }
                }
            }
        }
        int nb_vordiv_size = 2 * legendre_size(truncation_ + 1) * nb_vordiv_fields;
        int nb_scalar_size = 2 * legendre_size(truncation_ + 1) * nb_scalar_fields;
        ATLAS_ASSERT(k == nb_all_size);
        ATLAS_ASSERT(i == nb_vordiv_size);
        ATLAS_ASSERT(j == nb_vordiv_size);
        ATLAS_ASSERT(l == nb_scalar_size);
        invtrans_uv(truncation_ + 1, nb_all_fields, nb_vordiv_fields, all_spectra.data(), gp_fields, config);
    }
    else {
        if (nb_scalar_fields > 0) {
            invtrans_uv(truncation_, nb_scalar_fields, 0, scalar_spectra, gp_fields + 2 * nb_gp * nb_vordiv_fields,
                        config);
        }
    }
}

void TransLocal::invtrans_adj(const int nb_scalar_fields, const double gp_fields[], double scalar_spectra[],
                              const eckit::Configuration& config) const {
    ATLAS_NOTIMPLEMENTED;
    // Not implemented and not planned.
    // Use the TransIFS implementation instead.
}

void TransLocal::invtrans_adj(const int nb_vordiv_fields, const double gp_fields[], double vorticity_spectra[],
                              double divergence_spectra[], const eckit::Configuration& config) const {
    ATLAS_NOTIMPLEMENTED;
    // Not implemented and not planned.
    // Use the TransIFS implementation instead.
}

void TransLocal::invtrans_adj(const int nb_scalar_fields, const double gp_fields[], const int nb_vordiv_fields,
                              double vorticity_spectra[], double divergence_spectra[], double scalar_spectra[],
                              const eckit::Configuration& config) const {
    ATLAS_NOTIMPLEMENTED;
    // Not implemented and not planned.
    // Use the TransIFS implementation instead.
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::dirtrans(const Field& gpfield, Field& spfield, const eckit::Configuration& config) const {
    ATLAS_NOTIMPLEMENTED;
    // Not implemented and not planned.
    // Use the TransIFS implementation instead.
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::dirtrans(const FieldSet& gpfields, FieldSet& spfields, const eckit::Configuration& config) const {
    ATLAS_NOTIMPLEMENTED;
    // Not implemented and not planned.
    // Use the TransIFS implementation instead.
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::dirtrans_wind2vordiv(const Field& gpwind, Field& spvor, Field& spdiv,
                                      const eckit::Configuration& config) const {
    ATLAS_NOTIMPLEMENTED;
    // Not implemented and not planned.
    // Use the TransIFS implementation instead.
}


void TransLocal::dirtrans_adj(const Field& spfield, Field& gpfield,
                              const eckit::Configuration& config) const {
  ATLAS_NOTIMPLEMENTED;
  // Not implemented and not planned.
  // Use the TransIFS implementation instead.
}

void TransLocal::dirtrans_adj(const FieldSet& spfields, FieldSet& gpfields,
                              const eckit::Configuration& config) const {
  ATLAS_NOTIMPLEMENTED;
  // Not implemented and not planned.
  // Use the TransIFS implementation instead.
}

void TransLocal::dirtrans_wind2vordiv_adj(const Field& spvor, const Field& spdiv, Field& gpwind,
                                          const eckit::Configuration& config) const {
  ATLAS_NOTIMPLEMENTED;
  // Not implemented and not planned.
  // Use the TransIFS implementation instead.
}


// --------------------------------------------------------------------------------------------------------------------

void TransLocal::dirtrans(const int nb_fields, const double scalar_fields[], double scalar_spectra[],
                          const eckit::Configuration&) const {
    ATLAS_NOTIMPLEMENTED;
    // Not implemented and not planned.
    // Use the TransIFS implementation instead.
}

// --------------------------------------------------------------------------------------------------------------------

void TransLocal::dirtrans(const int nb_fields, const double wind_fields[], double vorticity_spectra[],
                          double divergence_spectra[], const eckit::Configuration&) const {
    ATLAS_NOTIMPLEMENTED;
    // Not implemented and not planned.
    // Use the TransIFS implementation instead.
}

// --------------------------------------------------------------------------------------------------------------------

}  // namespace trans
}  // namespace atlas
