/*
 * (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 <cmath>
#include <fstream>
#include <string>

#include "eckit/types/FloatCompare.h"

#include "atlas/array/Array.h"
#include "atlas/array/MakeView.h"
#include "atlas/field/Field.h"
#include "atlas/field/FieldSet.h"
#include "atlas/functionspace/FunctionSpace.h"
#include "atlas/functionspace/NodeColumns.h"
#include "atlas/grid/Grid.h"
#include "atlas/grid/detail/grid/Unstructured.h"
#include "atlas/library/config.h"
#include "atlas/mesh/Mesh.h"
#include "atlas/mesh/Nodes.h"
#include "atlas/output/detail/PointCloudIO.h"
#include "atlas/parallel/mpi/mpi.h"

#include "tests/AtlasTestEnvironment.h"

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

namespace {

namespace test_arrays {

const size_t nb_pts     = 5;
const size_t nb_fld     = 2;
const size_t nb_columns = 2 + nb_fld;

const double lon[] = {-31.233, -28.717, -27.217, -25.750, -16.917};
const double lat[] = {39.467, 38.583, 38.483, 37.817, 32.650};

const double helper_f1[] = {1., 2., 3., 4., 5.};
const double helper_f2[] = {-0.1, -0.2, -0.3, -0.4, -0.5};

const double* fvalues[] = {helper_f1, helper_f2};
const char* fnames[]    = {" f_1  ", "f    2 "};

}  // namespace test_arrays

namespace test_vectors {

const size_t nb_pts     = test_arrays::nb_pts;
const size_t nb_fld     = test_arrays::nb_fld;
const size_t nb_columns = test_arrays::nb_columns;

const std::vector<double> lon( test_arrays::lon, test_arrays::lon + nb_pts );
const std::vector<double> lat( test_arrays::lat, test_arrays::lat + nb_pts );

std::vector<double> helper_f1( test_arrays::helper_f1, test_arrays::helper_f1 + nb_pts );
std::vector<double> helper_f2( test_arrays::helper_f2, test_arrays::helper_f2 + nb_pts );
std::vector<double>* helper_fvalues[] = {&helper_f1, &helper_f2};

const std::vector<std::vector<double>*> fvalues( helper_fvalues, helper_fvalues + nb_fld );
const std::vector<std::string> fnames( test_arrays::fnames, test_arrays::fnames + nb_fld );

}  // namespace test_vectors

bool test_write_file( const std::string& file_path, const size_t& nb_pts, const size_t& nb_columns ) {
    if ( !nb_pts ) {
        return false;
    }
    std::ofstream f( file_path.c_str() );
    return ( f && f << "PointCloudIO " << nb_pts << "	" << nb_columns
                    << "  lon	lat	f_1				"
                       "__f3			more resilience	\n"
                       "-31.233	39.467	1.	-0.1\n"
                       "-28.717	38.583	2.	-0.2 even	more "
                       "resilience\n"
                       "-27.217	38.483	3.	-0.3\n"
                       "-25.750	37.817	4.	-0.4\n"
                       "-16.917	32.650	5.	-0.5\n" );
}

bool test_write_file_bad( const std::string& file_path ) {
    std::ofstream f( file_path.c_str() );
    return ( f && f << '?' );
}

}  // end anonymous namespace

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

namespace atlas {
namespace test {

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

CASE( "read_inexistent_file" ) {
    EXPECT_THROWS_AS( output::detail::PointCloudIO::read( "pointcloud.txt_should_not_exist" ), eckit::CantOpenFile );
}

CASE( "read_badly_formatted_file" ) {
    EXPECT( test_write_file_bad( "pointcloud.txt" ) );
    EXPECT_THROWS_AS( output::detail::PointCloudIO::read( "pointcloud.txt" ), eckit::Exception );
}

CASE( "read_grid_sample_file" ) {
    // test sample file, header properly formatted (some fluff is present)
    EXPECT( test_write_file( "pointcloud.txt", test_arrays::nb_pts, test_arrays::nb_columns ) );

    Log::info() << "pointcloud.txt created" << std::endl;

    Mesh mesh = output::detail::PointCloudIO::read( "pointcloud.txt" );

    Log::info() << "Mesh created" << std::endl;
    Grid grid( new grid::detail::grid::Unstructured( mesh ) );
    EXPECT( grid );

    EXPECT( grid.size() == test_arrays::nb_pts );
    EXPECT( mesh.nodes().has_field( "f_1" ) == true );
    EXPECT( mesh.nodes().has_field( "f3" ) == true );
}

CASE( "read_grid_sample_file_header_less_rows" ) {
    Log::info() << "Creating Mesh..." << std::endl;
    // test sample file with (wrong) header with less rows
    EXPECT( test_write_file( "pointcloud.txt", test_arrays::nb_pts - 2, test_arrays::nb_columns ) );

    Log::info() << "Creating Mesh..." << std::endl;
    Mesh mesh = output::detail::PointCloudIO::read( "pointcloud.txt" );
    Log::info() << "Creating Mesh...done" << std::endl;
    Grid grid( new grid::detail::grid::Unstructured( mesh ) );
    EXPECT( grid );

    EXPECT( grid.size() == test_arrays::nb_pts - 2 );
    EXPECT( mesh.nodes().has_field( "f_1" ) == true );
    EXPECT( mesh.nodes().has_field( "f3" ) == true );
}

CASE( "read_grid_sample_file_header_less_columns_1" ) {
    // test sample file with (wrong) header with one field less
    EXPECT( test_write_file( "pointcloud.txt", test_arrays::nb_pts, test_arrays::nb_columns - 1 ) );

    Mesh mesh = output::detail::PointCloudIO::read( "pointcloud.txt" );
    Grid grid( new grid::detail::grid::Unstructured( mesh ) );
    EXPECT( grid );

    EXPECT( grid.size() == test_arrays::nb_pts );
    EXPECT( mesh.nodes().has_field( "f_1" ) == true );
    EXPECT( mesh.nodes().has_field( "f3" ) == false );
}

CASE( "read_grid_sample_file_header_less_columns_2" ) {
    // test sample file with (wrong) header with no fields
    EXPECT( test_write_file( "pointcloud.txt", test_arrays::nb_pts, test_arrays::nb_columns - test_arrays::nb_fld ) );

    Mesh mesh = output::detail::PointCloudIO::read( "pointcloud.txt" );
    Grid grid( new grid::detail::grid::Unstructured( mesh ) );
    EXPECT( grid );

    EXPECT( grid.size() == test_arrays::nb_pts );
    EXPECT( mesh.nodes().has_field( "f_1" ) == false );
    EXPECT( mesh.nodes().has_field( "f3" ) == false );
}

CASE( "write_array" ) {
    std::ifstream f;
    std::string signature, str_lon, str_lat, str_f1, str_f2;
    size_t nb_pts, nb_columns;

    output::detail::PointCloudIO::write( "pointcloud.txt", test_arrays::nb_pts, test_arrays::lon, test_arrays::lat );
    f.open( "pointcloud.txt" );
    EXPECT( f );
    f >> signature >> nb_pts >> nb_columns >> str_lon >> str_lat >> str_f1 >> str_f2;
    f.close();

    EXPECT( nb_pts == test_arrays::nb_pts );
    EXPECT( nb_columns == test_arrays::nb_columns - test_arrays::nb_fld );
    EXPECT( str_lon == "lon" );
    EXPECT( str_lat == "lat" );
    EXPECT( str_f1 != "f_1" );     // (this column is not written)
    EXPECT( str_f2 != "f____2" );  // (this column is not written)
}

CASE( "write_array_less_rows" ) {
    std::ifstream f;
    std::string signature, str_lon, str_lat, str_f1, str_f2;
    size_t nb_pts, nb_columns;

    output::detail::PointCloudIO::write( "pointcloud.txt", test_arrays::nb_pts - 1 /* deliberate */, test_arrays::lon,
                                         test_arrays::lat, test_arrays::nb_fld, test_arrays::fvalues,
                                         test_arrays::fnames );
    f.open( "pointcloud.txt" );
    EXPECT( f );
    f >> signature >> nb_pts >> nb_columns >> str_lon >> str_lat >> str_f1 >> str_f2;
    f.close();

    EXPECT( nb_pts == test_arrays::nb_pts - 1 );  // (one row is not written)
    EXPECT( nb_columns == test_arrays::nb_columns );
    EXPECT( str_lon == "lon" );
    EXPECT( str_lat == "lat" );
    EXPECT( str_f1 == "f_1" );
    EXPECT( str_f2 == "f____2" );
}

CASE( "write_array_less_columns" ) {
    std::ifstream f;
    std::string signature, str_lon, str_lat, str_f1, str_f2;
    size_t nb_pts, nb_columns;

    output::detail::PointCloudIO::write( "pointcloud.txt", test_arrays::nb_pts, test_arrays::lon, test_arrays::lat,
                                         test_arrays::nb_fld - 1 /* deliberate */, test_arrays::fvalues,
                                         test_arrays::fnames );
    f.open( "pointcloud.txt" );
    EXPECT( f );
    f >> signature >> nb_pts >> nb_columns >> str_lon >> str_lat >> str_f1 >> str_f2;
    f.close();

    EXPECT( nb_pts == test_arrays::nb_pts );
    EXPECT( nb_columns == test_arrays::nb_columns - 1 );  // (one column is not written)
    EXPECT( str_lon == "lon" );
    EXPECT( str_lat == "lat" );
    EXPECT( str_f1 == "f_1" );
    EXPECT( str_f2 != "f____2" );  // (this column is not written)
}

CASE( "write_vector_all_fields" ) {
    std::ifstream f;
    std::string signature, str_lon, str_lat, str_f1, str_f2;
    size_t nb_pts, nb_columns;

    output::detail::PointCloudIO::write( "pointcloud.txt", test_vectors::lon, test_vectors::lat, test_vectors::fvalues,
                                         test_vectors::fnames );
    f.open( "pointcloud.txt" );
    EXPECT( f );
    f >> signature >> nb_pts >> nb_columns >> str_lon >> str_lat >> str_f1 >> str_f2;
    f.close();

    EXPECT( nb_pts == test_vectors::nb_pts );
    EXPECT( nb_columns == test_vectors::nb_columns );
    EXPECT( str_lon == "lon" );
    EXPECT( str_lat == "lat" );
    EXPECT( str_f1 == "f_1" );
    EXPECT( str_f2 == "f____2" );
}

CASE( "write_vector_no_fields" ) {
    std::ifstream f;
    std::string signature, str_lon, str_lat, str_f1, str_f2;
    size_t nb_pts, nb_columns;

    output::detail::PointCloudIO::write( "pointcloud.txt", test_vectors::lon, test_vectors::lat );
    f.open( "pointcloud.txt" );
    EXPECT( f );
    f >> signature >> nb_pts >> nb_columns >> str_lon >> str_lat >> str_f1 >> str_f2;
    f.close();

    EXPECT( nb_pts == test_vectors::nb_pts );
    EXPECT( nb_columns == test_vectors::nb_columns - test_vectors::nb_fld );
    EXPECT( str_lon == "lon" );
    EXPECT( str_lat == "lat" );
    EXPECT( str_f1 != "f_1" );     // (this column is not written)
    EXPECT( str_f2 != "f____2" );  // (this column is not written)
}

static double funny_formula( int x ) {
    return ( (double)( x ) ) * std::pow( (double)-1., (int)( x ) );
}

CASE( "write_read_write_field" ) {
    // build suitable data structures do hold field name & values
    std::string field_name( "my_super_field" );
    std::vector<double> field_values( test_vectors::nb_pts, 0. );
    for ( size_t i = 0; i < test_vectors::nb_pts; ++i ) {
        field_values[i] = funny_formula( i );
    }

    // PART 1
    // write field vector values as column in file "pointcloud.txt"
    Log::info() << "Part 1" << std::endl;

    std::ifstream f;
    std::string signature, str_lon, str_lat, str_f;
    size_t nb_pts, nb_columns;

    output::detail::PointCloudIO::write( "pointcloud.txt", test_vectors::lon, test_vectors::lat,
                                         std::vector<std::vector<double>*>( 1, &field_values ),
                                         std::vector<std::string>( 1, field_name ) );
    f.open( "pointcloud.txt" );
    EXPECT( f );
    f >> signature >> nb_pts >> nb_columns >> str_lon >> str_lat >> str_f;
    f.close();

    EXPECT( nb_pts == test_vectors::nb_pts );
    EXPECT( nb_columns == 2 + 1 );  // (lon,lat,my_super_field)
    EXPECT( str_lon == "lon" );
    EXPECT( str_lat == "lat" );
    EXPECT( str_f == "my_super_field" );

    // PART 2
    // read field vector from just-created file
    Log::info() << "Part 2" << std::endl;

    Mesh mesh = output::detail::PointCloudIO::read( "pointcloud.txt" );
    Grid grid( new grid::detail::grid::Unstructured( mesh ) );
    EXPECT( grid );

    EXPECT( grid.size() == test_vectors::nb_pts );

    mesh::Nodes& nodes = mesh.nodes();
    EXPECT( nodes.has_field( "my_super_field" ) == true );
    EXPECT( nodes.has_field( "_StRaNgE_FiElD_NaMe_" ) == false );

    // PART 3
    // check field values to a very small tolerance (relative tol. 0.001%)
    Log::info() << "Part 3" << std::endl;

    Field& field( nodes.field( "my_super_field" ) );
    EXPECT(
        /* data used to write file*/ test_vectors::nb_pts ==
        /* data read from file*/ field.size() );

    array::ArrayView<double, 1> field_data = array::make_view<double, 1>( field );
    for ( idx_t i = 0; i < field_data.size(); ++i ) {
        EXPECT( eckit::types::is_approximately_equal( funny_formula( i ), field_data( i ),
                                                      0.001 ) );  // 0.001% relative error
        EXPECT( eckit::types::is_approximately_equal( funny_formula( i ), field_data( i ),
                                                      0.001 ) );  // 0.001% relative error
    }

    // PART 4
    // write to file a Field (the just-read one),
    // a FieldSet, and
    // a Grid (should be exactly the same)
    Log::info() << "Part 4" << std::endl;

    FieldSet fieldset;
    EXPECT_NO_THROW( fieldset.add( field ) );

    functionspace::NodeColumns functionspace( mesh );

    EXPECT_NO_THROW( output::detail::PointCloudIO::write( "pointcloud_FieldSet.txt", fieldset, functionspace ) );
    EXPECT_NO_THROW( output::detail::PointCloudIO::write( "pointcloud_Grid.txt", mesh ) );

    Mesh mesh_from_FieldSet = output::detail::PointCloudIO::read( "pointcloud_FieldSet.txt" );
    Grid grid_from_FieldSet( new grid::detail::grid::Unstructured( mesh_from_FieldSet ) );

    Mesh mesh_from_Grid( output::detail::PointCloudIO::read( "pointcloud_Grid.txt" ) );
    Grid grid_from_Grid( new grid::detail::grid::Unstructured( mesh_from_Grid ) );

    EXPECT( grid_from_FieldSet );
    EXPECT( grid_from_Grid );


    // PART 5
    // compare reading of reference data to:
    // - grid_from_FieldSet, and
    // - grid_from_Grid (all different but equivalent writing methods)
    Log::info() << "Part 5" << std::endl;

    // (header section)
    EXPECT( grid_from_FieldSet.size() == test_arrays::nb_pts );
    EXPECT( mesh_from_FieldSet.nodes().has_field( "my_super_field" ) == true );
    EXPECT( mesh_from_FieldSet.nodes().has_field( "_StRaNgE_FiElD_NaMe_" ) == false );

    EXPECT( grid_from_Grid.size() == test_arrays::nb_pts );
    EXPECT( mesh_from_FieldSet.nodes().has_field( "my_super_field" ) == true );
    EXPECT( mesh_from_FieldSet.nodes().has_field( "_StRaNgE_FiElD_NaMe_" ) == false );

    // (data section: guarantee data are from different places, to make checks
    // useful)
    const Field& field_from_FieldSet( mesh_from_FieldSet.nodes().field( "my_super_field" ) );
    const Field& field_from_Grid( mesh_from_FieldSet.nodes().field( "my_super_field" ) );
    EXPECT( field.array().data<double>() != field_from_FieldSet.array().data<double>() );
    EXPECT( field.array().data<double>() != field_from_Grid.array().data<double>() );

    auto field_from_FieldSet_data = array::make_view<double, 1>( field_from_FieldSet );
    auto field_from_Grid_data     = array::make_view<double, 1>( field_from_Grid );
    for ( size_t i = 0; i < test_arrays::nb_pts; ++i ) {
        EXPECT( eckit::types::is_approximately_equal( field_data( i ), field_from_FieldSet_data( i ),
                                                      0.001 ) );  // 0.001% relative error
        EXPECT( eckit::types::is_approximately_equal( field_data( i ), field_from_Grid_data( i ), 0.001 ) );  // ...
    }
}

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

}  // namespace test
}  // namespace atlas

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