/*
 * (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/grid/detail/grid/Unstructured.h"

#include <initializer_list>
#include <iomanip>
#include <limits>
#include <memory>

#include "eckit/types/FloatCompare.h"
#include "eckit/utils/Hash.h"

#include "atlas/array/ArrayView.h"
#include "atlas/field/Field.h"
#include "atlas/grid/Iterator.h"
#include "atlas/grid/detail/grid/GridBuilder.h"
#include "atlas/grid/detail/grid/GridFactory.h"
#include "atlas/mesh/Mesh.h"
#include "atlas/mesh/Nodes.h"
#include "atlas/option.h"
#include "atlas/runtime/Exception.h"
#include "atlas/runtime/Log.h"
#include "atlas/util/CoordinateEnums.h"
#include "atlas/util/NormaliseLongitude.h"

namespace atlas {
namespace grid {
namespace detail {
namespace grid {

namespace {
static GridFactoryBuilder<Unstructured> __register_Unstructured( Unstructured::static_type() );
}


Unstructured::Unstructured( const Mesh& m ) : Grid(), points_( new std::vector<PointXY>( m.nodes().size() ) ) {
    util::Config config_domain;
    config_domain.set( "type", "global" );
    domain_ = Domain( config_domain );

    auto xy                 = array::make_view<double, 2>( m.nodes().xy() );
    std::vector<PointXY>& p = *points_;
    const idx_t npts        = static_cast<idx_t>( p.size() );

    for ( idx_t n = 0; n < npts; ++n ) {
        p[n].assign( xy( n, XX ), xy( n, YY ) );
    }
}


namespace {
class Normalise {
public:
    Normalise( const RectangularDomain& domain ) :
        degrees_( domain.units() == "degrees" ), normalise_( domain.xmin(), domain.xmax() ) {}

    double operator()( double x ) const {
        if ( degrees_ ) {
            x = normalise_( x );
        }
        return x;
    }

private:
    const bool degrees_;
    util::NormaliseLongitude normalise_;
};
}  // namespace


Unstructured::Unstructured( const Grid& grid, Domain domain ) : Grid() {
    domain_ = domain;
    points_.reset( new std::vector<PointXY> );
    points_->reserve( grid.size() );
    if ( not domain_ ) {
        domain_ = GlobalDomain();
    }
    atlas::grid::IteratorXY it( grid.xy_begin() );
    PointXY p;
    if ( RectangularDomain( domain_ ) ) {
        auto normalise = Normalise( RectangularDomain( domain_ ) );
        while ( it.next( p ) ) {
            p.x() = normalise( p.x() );
            if ( domain_.contains( p ) ) {
                points_->emplace_back( p );
            }
        }
    }
    else if ( ZonalBandDomain( domain_ ) ) {
        while ( it.next( p ) ) {
            if ( domain_.contains( p ) ) {
                points_->emplace_back( p );
            }
        }
    }
    else {
        while ( it.next( p ) ) {
            points_->emplace_back( p );
        }
    }
    points_->shrink_to_fit();
}

Unstructured::Unstructured( const util::Config& config ) : Grid() {
    util::Config config_domain;
    if ( not config.get( "domain", config_domain ) ) {
        config_domain.set( "type", "global" );
    }
    domain_ = Domain( config_domain );
    std::vector<double> xy;
    if ( config.get( "xy", xy ) ) {
        const size_t N = xy.size() / 2;
        points_.reset( new std::vector<PointXY> );
        points_->reserve( N );
        for ( size_t n = 0; n < N; ++n ) {
            points_->emplace_back( PointXY{xy[2 * n], xy[2 * n + 1]} );
        }
    }
    else {
        std::vector<double> x;
        std::vector<double> y;
        if ( not config.get( "x", x ) ) {
            throw_Exception( "x missing from configuration" );
        }
        if ( not config.get( "y", y ) ) {
            throw_Exception( "y missing from configuration" );
        }
        ATLAS_ASSERT( x.size() == y.size() );
        points_.reset( new std::vector<PointXY> );
        points_->reserve( x.size() );
        for ( size_t n = 0; n < x.size(); ++n ) {
            points_->emplace_back( PointXY{x[n], y[n]} );
        }
    }
}

Unstructured::Unstructured( std::vector<PointXY>* pts ) : Grid(), points_( pts ) {
    domain_ = GlobalDomain();
}

Unstructured::Unstructured( std::vector<PointXY>&& pts ) :
    Grid(), points_( new std::vector<PointXY>( std::move( pts ) ) ) {
    domain_ = GlobalDomain();
}

Unstructured::Unstructured( const std::vector<PointXY>& pts ) : Grid(), points_( new std::vector<PointXY>( pts ) ) {
    domain_ = GlobalDomain();
}

Unstructured::Unstructured( std::initializer_list<PointXY> initializer_list ) :
    Grid(), points_( new std::vector<PointXY>( initializer_list ) ) {
    domain_ = GlobalDomain();
}

Unstructured::~Unstructured() = default;

Grid::uid_t Unstructured::name() const {
    if ( shortName_.empty() ) {
        std::ostringstream s;
        s << "unstructured." << Grid::hash().substr( 0, 7 );
        shortName_ = s.str();
    }
    return shortName_;
}

void Unstructured::hash( eckit::Hash& h ) const {
    ATLAS_ASSERT( points_ != nullptr );

    const std::vector<PointXY>& pts = *points_;
    h.add( &pts[0], sizeof( PointXY ) * pts.size() );

    for ( idx_t i = 0, N = static_cast<idx_t>( pts.size() ); i < N; i++ ) {
        const PointXY& p = pts[i];
        h << p.x() << p.y();
    }

    projection().hash( h );
}

RectangularLonLatDomain Unstructured::lonlatBoundingBox() const {
    return projection_ ? projection_.lonlatBoundingBox( domain_ ) : domain_;
}

idx_t Unstructured::size() const {
    ATLAS_ASSERT( points_ != nullptr );
    return static_cast<idx_t>( points_->size() );
}

Grid::Spec Unstructured::spec() const {
    if ( cached_spec_ ) {
        return *cached_spec_;
    }

    cached_spec_.reset( new Grid::Spec );

    cached_spec_->set( "type", static_type() );

    cached_spec_->set( "domain", domain().spec() );
    cached_spec_->set( "projection", projection().spec() );

    auto it = xy_begin();
    std::vector<double> coords( 2 * size() );
    idx_t c( 0 );
    PointXY xy;
    while ( it->next( xy ) ) {
        coords[c++] = xy.x();
        coords[c++] = xy.y();
    }

    cached_spec_->set( "xy", coords );

    return *cached_spec_;
}

Grid::Config Unstructured::meshgenerator() const {
    return Config( "type", "delaunay" );
}
Grid::Config Unstructured::partitioner() const {
    return Config( "type", "serial" );
}

void Unstructured::print( std::ostream& os ) const {
    os << "Unstructured(Npts:" << size() << ")";
}

namespace {  // anonymous

static class unstructured : public GridBuilder {
    using Implementation = atlas::Grid::Implementation;
    using Config         = Grid::Config;

public:
    unstructured() : GridBuilder( "unstructured" ) {}

    void print( std::ostream& os ) const override {
        os << std::left << std::setw( 20 ) << " "
           << "Unstructured grid";
    }

    const Implementation* create( const std::string& /* name */, const Config& ) const override {
        throw_NotImplemented( "Cannot create unstructured grid from name", Here() );
    }

    const Implementation* create( const Config& config ) const override { return new Unstructured( config ); }

} unstructured_;

}  // anonymous namespace

extern "C" {
const Unstructured* atlas__grid__Unstructured__points( const double xy[], int shapef[], int stridesf[] ) {
    size_t nb_points = shapef[1];
    ATLAS_ASSERT( shapef[0] == 2 );
    size_t stride_n = stridesf[1];
    size_t stride_v = stridesf[0];
    std::vector<PointXY> points;
    points.reserve( nb_points );
    for ( size_t n = 0; n < nb_points; ++n ) {
        points.emplace_back( PointXY{xy[n * stride_n + 0], xy[n * stride_n + 1 * stride_v]} );
    }
    return new Unstructured( std::move( points ) );
}

const Unstructured* atlas__grid__Unstructured__config( util::Config* conf ) {
    ATLAS_ASSERT( conf != nullptr );
    const Unstructured* grid = dynamic_cast<const Unstructured*>( Grid::create( *conf ) );
    ATLAS_ASSERT( grid != nullptr );
    return grid;
}
}


}  // namespace grid
}  // namespace detail
}  // namespace grid
}  // namespace atlas
