/*
 * (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 <algorithm>
#include <iomanip>

#include "eckit/utils/MD5.h"

#include "atlas/grid.h"
#include "atlas/option.h"
#include "atlas/parallel/mpi/mpi.h"
#include "atlas/runtime/Trace.h"
#include "atlas/trans/LegendreCacheCreator.h"
#include "atlas/trans/Trans.h"
#include "atlas/util/Constants.h"

#include "tests/AtlasTestEnvironment.h"

namespace atlas {
namespace test {

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

struct AtlasTransEnvironment : public AtlasTestEnvironment {
    AtlasTransEnvironment( int argc, char* argv[] ) : AtlasTestEnvironment( argc, argv ) {
        trans::Trans::backend( "local" );
        trans::Trans::config( option::warning( 1 ) );
    }
};

using trans::Cache;
using trans::LegendreCache;
using trans::LegendreCacheCreator;
using trans::Trans;
using XSpace        = StructuredGrid::XSpace;
using YSpace        = StructuredGrid::YSpace;
using LinearSpacing = grid::LinearSpacing;

eckit::PathName CacheFile( const std::string& path ) {
    eckit::PathName cachefile( path );
    if ( cachefile.exists() ) {
        cachefile.unlink();
    }
    return cachefile;
}

std::string hash( const trans::Cache& c ) {
    return eckit::MD5( c.legendre().data(), c.legendre().size() ).digest();
}

std::string hash( const eckit::PathName& f ) {
    return hash( LegendreCache( f ) );
}

std::string F( int n ) {
    return "F" + std::to_string( n );
}
std::string O( int n ) {
    return "O" + std::to_string( n );
}
std::string N( int n ) {
    return "N" + std::to_string( n );
}
std::string L( int n ) {
    return "L" + std::to_string( n );
}
std::string S( int n ) {
    return "S" + std::to_string( n );
}
std::string Slon( int n ) {
    return "Slon" + std::to_string( n );
}
std::string Slat( int n ) {
    return "Slat" + std::to_string( n );
}

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

CASE( "test_global_grids" ) {
    // auto resolutions = { 32, 64, 160, 320, 640 };
    auto resolutions = {32, 64};
    for ( int n : resolutions ) {
        int t      = n - 1;
        auto cases = {
            std::make_pair( F( n ), t ),    std::make_pair( O( n ), t ), std::make_pair( N( n ), t ),
            std::make_pair( L( n ), t ),    std::make_pair( S( n ), t ), std::make_pair( Slon( n ), t ),
            std::make_pair( Slat( n ), t ),
        };

        LegendreCacheCreator F_cache_creator( Grid( F( n ) ), t );
        EXPECT( F_cache_creator.supported() );
        auto F_cachefile = CacheFile( "leg_" + F_cache_creator.uid() + ".bin" );
        F_cache_creator.create( F_cachefile );
        Cache F_cache     = LegendreCache( F_cachefile );
        auto F_cache_hash = hash( F_cache );

        for ( auto _case : cases ) {
            auto gridname   = _case.first;
            auto truncation = _case.second;
            Log::info() << "Case " + gridname + " T" + std::to_string( truncation ) << std::endl;
            ATLAS_TRACE( "Case " + gridname + " T" + std::to_string( truncation ) );
            Grid grid( gridname );

            LegendreCacheCreator cache_creator( grid, truncation );
            EXPECT( cache_creator.supported() );
            auto cachefile = CacheFile( "leg_" + cache_creator.uid() + ".bin" );
            cache_creator.create( cachefile );
            if ( GaussianGrid( grid ) ) {
                EXPECT( hash( cachefile ) == F_cache_hash );
            }

            ATLAS_TRACE_SCOPE( "create without cache" )
            Trans( grid, truncation );

            Cache cache;
            ATLAS_TRACE_SCOPE( "read cache" )
            cache = LegendreCache( cachefile );
            ATLAS_TRACE_SCOPE( "create with cache" )
            Trans( cache, grid, truncation );
        }
    }
}

CASE( "test_global_grids_with_subdomain" ) {
    int n        = 64;
    int t        = n - 1;
    auto cases   = {std::make_pair( F( n ), t ),   std::make_pair( O( n ), t ), std::make_pair( N( n ), t ),
                  std::make_pair( L( n ), t ),   std::make_pair( S( n ), t ), std::make_pair( Slon( n ), t ),
                  std::make_pair( Slat( n ), t )};
    auto domains = std::vector<Domain>{
        ZonalBandDomain( {-10., 5.} ),
        RectangularDomain( {-1., 1.}, {50., 55.} ),
        RectangularDomain( {-1., 1.}, {-5., 40.} ),
    };
    for ( auto _case : cases ) {
        auto gridname   = _case.first;
        auto truncation = _case.second;

        ATLAS_TRACE( "Case " + gridname + " T" + std::to_string( truncation ) );

        Grid global_grid( gridname );

        LegendreCacheCreator global_cache_creator( Grid( gridname ), truncation );
        EXPECT( global_cache_creator.supported() );
        auto global_cachefile = CacheFile( "leg_" + global_cache_creator.uid() + ".bin" );
        ATLAS_TRACE_SCOPE( "Creating cache " + std::string( global_cachefile ) )
        global_cache_creator.create( global_cachefile );

        Cache global_cache;
        ATLAS_TRACE_SCOPE( "read cache" )
        global_cache     = LegendreCache( global_cachefile );
        auto global_hash = hash( global_cache );

        for ( auto domain : domains ) {
            Grid grid( gridname, domain );
            ATLAS_TRACE_SCOPE( "create with cache" )
            Trans( global_cache, global_grid, domain, truncation );
        }
    }
}

CASE( "test_regional_grids nested_in_global" ) {
    auto cachefile  = CacheFile( "regional_lonlat.bin" );
    auto truncation = 89;
    Cache cache;
    StructuredGrid grid_global( LinearSpacing( {0., 360.}, 360, false ), LinearSpacing( {90., -90.}, 181, true ) );
    EXPECT( grid_global.domain().global() );

    LegendreCacheCreator global_cache_creator( grid_global, truncation );
    EXPECT( global_cache_creator.supported() );
    auto global_cachefile = CacheFile( "leg_" + global_cache_creator.uid() + ".bin" );
    ATLAS_TRACE_SCOPE( "Creating cache " + std::string( cachefile ) )
    global_cache_creator.create( global_cachefile );


    StructuredGrid regional( LinearSpacing( {0., 180.}, 181 ), LinearSpacing( {0., 45.}, 46 ) );


    ATLAS_TRACE_SCOPE( "create without cache" )
    Trans( grid_global, regional.domain(), truncation );
    ATLAS_TRACE_SCOPE( "read cache" )
    cache = LegendreCache( global_cachefile );
    ATLAS_TRACE_SCOPE( "create with cache" )
    Trans( cache, grid_global, regional.domain(), truncation );
}

CASE( "test_regional_grids not nested" ) {
    auto truncation = 89;
    Cache cache;

    StructuredGrid grid( LinearSpacing( {0., 180.}, 181 ), LinearSpacing( {0., 45.}, 46 ) );

    LegendreCacheCreator cache_creator( grid, truncation );
    EXPECT( cache_creator.supported() );
    auto cachefile = CacheFile( "leg_" + cache_creator.uid() + ".bin" );

    ATLAS_TRACE_SCOPE( "Creating cache " + std::string( cachefile ) )
    cache_creator.create( cachefile );

    ATLAS_TRACE_SCOPE( "create without cache" )
    Trans( grid, truncation );
    ATLAS_TRACE_SCOPE( "read cache" )
    cache = LegendreCache( cachefile );
    ATLAS_TRACE_SCOPE( "create with cache" )
    Trans( cache, grid, truncation );
}

CASE( "test_regional_grids with projection" ) {
    auto cachefile  = CacheFile( "cache-regional.bin" );
    auto truncation = 89;
    Cache cache;

    Projection projection( util::Config( "type", "rotated_lonlat" )( "north_pole", std::vector<double>{4., 54.} ) );

    StructuredGrid grid( LinearSpacing( {0., 180.}, 181 ), LinearSpacing( {0., 45.}, 46 ), projection );
    Trans trans;
    ATLAS_TRACE_SCOPE( "create without cache" )
    trans = Trans( grid, truncation );

    // Note: caching not yet implemented for unstructured and projected grids
    LegendreCacheCreator legendre_cache_creator( grid, truncation );
    ATLAS_DEBUG_VAR( legendre_cache_creator.uid() );
    EXPECT( not legendre_cache_creator.supported() );

    std::vector<double> rspecg( trans.spectralCoefficients(), 0. );
    std::vector<double> rgp( trans.grid().size() );
    trans.invtrans( 1, rspecg.data(), rgp.data() );
}

CASE( "test cache creator to file" ) {
    auto truncation = 89;
    StructuredGrid grid_global( LinearSpacing( {0., 360.}, 360, false ), LinearSpacing( {90., -90.}, 181, true ) );

    LegendreCacheCreator legendre_cache_creator( grid_global, truncation );
    auto cachefile = CacheFile( legendre_cache_creator.uid() );
    ATLAS_TRACE_SCOPE( "Creating cache " + std::string( cachefile ) )
    legendre_cache_creator.create( cachefile );

    Cache c     = legendre_cache_creator.create();
    auto trans1 = Trans( c, grid_global, truncation );
    auto trans2 = Trans( c, grid_global, truncation );
}

CASE( "test cache creator in memory" ) {
    auto truncation = 89;
    StructuredGrid grid_global( LinearSpacing( {0., 360.}, 360, false ), LinearSpacing( {90., -90.}, 181, true ) );

    LegendreCacheCreator legendre_cache_creator( grid_global, truncation );

    Cache cache;
    ATLAS_TRACE_SCOPE( "Creating cache in memory" )
    cache = legendre_cache_creator.create();

    auto trans1 = Trans( cache, grid_global, truncation );
    auto trans2 = Trans( cache, grid_global, truncation );
}

CASE( "ATLAS-256: Legendre coefficient expected unique identifiers" ) {
    util::Config options;
    options.set( option::type( "local" ) );
    options.set( "flt", false );

    auto uids = {
        // clang-format off
        "local-T20-GaussianN320-OPT4189816c2e",
        "local-T20-GaussianN640-OPT4189816c2e",
        "local-T20-GaussianN1280-OPT4189816c2e",
        "local-T20-GaussianN320-OPT4189816c2e",
        "local-T20-GaussianN640-OPT4189816c2e",
        "local-T20-GaussianN1280-OPT4189816c2e",
        "local-T20-GaussianN320-OPT4189816c2e",
        "local-T20-GaussianN640-OPT4189816c2e",
        "local-T20-GaussianN1280-OPT4189816c2e",
        "local-T20-L-ny181-OPT4189816c2e",
        "local-T20-L-ny1801-OPT4189816c2e",
        "local-T639-GaussianN320-OPT4189816c2e",
        "local-T639-GaussianN640-OPT4189816c2e",
        "local-T639-GaussianN1280-OPT4189816c2e",
        "local-T639-GaussianN320-OPT4189816c2e",
        "local-T639-GaussianN640-OPT4189816c2e",
        "local-T639-GaussianN1280-OPT4189816c2e",
        "local-T639-GaussianN320-OPT4189816c2e",
        "local-T639-GaussianN640-OPT4189816c2e",
        "local-T639-GaussianN1280-OPT4189816c2e",
        "local-T639-L-ny181-OPT4189816c2e",
        "local-T639-L-ny1801-OPT4189816c2e",
        "local-T1279-GaussianN320-OPT4189816c2e",
        "local-T1279-GaussianN640-OPT4189816c2e",
        "local-T1279-GaussianN1280-OPT4189816c2e",
        "local-T1279-GaussianN320-OPT4189816c2e",
        "local-T1279-GaussianN640-OPT4189816c2e",
        "local-T1279-GaussianN1280-OPT4189816c2e",
        "local-T1279-GaussianN320-OPT4189816c2e",
        "local-T1279-GaussianN640-OPT4189816c2e",
        "local-T1279-GaussianN1280-OPT4189816c2e",
        "local-T1279-L-ny181-OPT4189816c2e",
        "local-T1279-L-ny1801-OPT4189816c2e",
        "local-T20-grid-800ac12540-OPT4189816c2e",
        "local-T20-grid-0915e0f040-OPT4189816c2e",
        "local-T20-grid-7c400822f0-OPT4189816c2e",
        "local-T20-grid-800ac12540-OPT4189816c2e",
        "local-T20-grid-0915e0f040-OPT4189816c2e",
        "local-T20-grid-7c400822f0-OPT4189816c2e",
        "local-T20-grid-800ac12540-OPT4189816c2e",
        "local-T20-grid-0915e0f040-OPT4189816c2e",
        "local-T20-grid-7c400822f0-OPT4189816c2e",
        "local-T20-grid-7824deccdf-OPT4189816c2e",
        "local-T20-grid-7d1771559e-OPT4189816c2e",
        "local-T639-grid-800ac12540-OPT4189816c2e",
        "local-T639-grid-0915e0f040-OPT4189816c2e",
        "local-T639-grid-7c400822f0-OPT4189816c2e",
        "local-T639-grid-800ac12540-OPT4189816c2e",
        "local-T639-grid-0915e0f040-OPT4189816c2e",
        "local-T639-grid-7c400822f0-OPT4189816c2e",
        "local-T639-grid-800ac12540-OPT4189816c2e",
        "local-T639-grid-0915e0f040-OPT4189816c2e",
        "local-T639-grid-7c400822f0-OPT4189816c2e",
        "local-T639-grid-7824deccdf-OPT4189816c2e",
        "local-T639-grid-7d1771559e-OPT4189816c2e",
        "local-T1279-grid-800ac12540-OPT4189816c2e",
        "local-T1279-grid-0915e0f040-OPT4189816c2e",
        "local-T1279-grid-7c400822f0-OPT4189816c2e",
        "local-T1279-grid-800ac12540-OPT4189816c2e",
        "local-T1279-grid-0915e0f040-OPT4189816c2e",
        "local-T1279-grid-7c400822f0-OPT4189816c2e",
        "local-T1279-grid-800ac12540-OPT4189816c2e",
        "local-T1279-grid-0915e0f040-OPT4189816c2e",
        "local-T1279-grid-7c400822f0-OPT4189816c2e",
        "local-T1279-grid-7824deccdf-OPT4189816c2e",
        "local-T1279-grid-7d1771559e-OPT4189816c2e",
        // clang-format on
    };
    auto uid = uids.begin();

    auto domains    = std::vector<Domain>{GlobalDomain(), RectangularDomain( {-10, 10}, {-20, 20} )};
    auto spectral_T = std::vector<int>{20, 639, 1279};
    auto grids      = std::vector<std::string>{"F320", "F640", "F1280", "N320", "N640", "N1280",
                                          "O320", "O640", "O1280", "L90",  "L900"};
    for ( auto& domain : domains ) {
        for ( int T : spectral_T ) {
            for ( auto name : grids ) {
                Log::info() << "Case name:'" << name << "', T:" << T << ", domain:" << domain << ", UID:'" << *uid
                            << "'" << std::endl;

                Grid grid( name, domain );
                auto test = trans::LegendreCacheCreator( grid, T, options ).uid();
                ATLAS_DEBUG_VAR( test );
                EXPECT( test == *uid );

                uid++;
            }
        }
    }
}

}  // namespace test
}  // namespace atlas

int main( int argc, char** argv ) {
    return atlas::test::run<atlas::test::AtlasTransEnvironment>( argc, argv );
}
