/*
 * (C) Copyright 1996- 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 "ProjProjection.h"

#include <proj.h>

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

#include "atlas/projection/detail/ProjectionFactory.h"
#include "atlas/runtime/Exception.h"
#include "atlas/util/CoordinateEnums.h"
#include "atlas/util/Point.h"

namespace atlas {
namespace projection {
namespace detail {

namespace {

bool proj_ellipsoid_params( PJ_CONTEXT* ctxt, const std::string& proj_str, double& a, double& b ) {
    using pj_t   = ProjProjection::pj_t;
    bool success = false;
    pj_t identity( proj_create_crs_to_crs( ctxt, proj_str.c_str(), proj_str.c_str(), nullptr ) );
    pj_t proj( proj_get_target_crs( ctxt, identity ) );
    pj_t proj_ellps( proj_get_ellipsoid( ctxt, proj ) );
    if ( proj_ellps ) {
        ATLAS_ASSERT( proj_ellipsoid_get_parameters( ctxt, proj_ellps, &a, &b, nullptr, nullptr ) );
        ATLAS_ASSERT( 0 < b && b <= a );
        success = true;
    }
    return success;
};

std::string source_str( PJ_CONTEXT* ctxt, const std::string& proj_str ) {
    double a, b;
    if ( proj_ellipsoid_params( ctxt, proj_str, a, b ) ) {
        auto axes = b < a ? " +a=" + std::to_string( a ) + " +b=" + std::to_string( b ) : " +R=" + std::to_string( a );
        return "+proj=lonlat" + axes;
    }
    else {
        return "EPSG:4326";  // WGS84 (lat,lon)
    }
}


std::string geocentric_str( PJ_CONTEXT* ctxt, const std::string& proj_str ) {
    double a, b;
    if ( proj_ellipsoid_params( ctxt, proj_str, a, b ) ) {
        return b < a ? "+proj=geocent +a=" + std::to_string( a ) + " +b=" + std::to_string( b )
                     : "+proj=geocent +R=" + std::to_string( a );
    }
    else {
        return "EPSG:4978";  // WGS84 (x,y,z)
    }
}

}  // namespace

ProjProjection::ProjProjection( const eckit::Parametrisation& param ) :
    normalise_( param ), sourceToTarget_( nullptr ), sourceToGeocentric_( nullptr ), context_( PJ_DEFAULT_CTX ) {
    ATLAS_ASSERT( param.get( "proj", proj_ ) && !proj_.empty() );
    source_encoded_     = param.get( "proj_source", source_ = source_str( context_, proj_ ) );
    geocentric_encoded_ = param.get( "proj_geocentric", geocentric_ = geocentric_str( context_, proj_ ) );


    // set x/y transformations to/from lon/lat and to/from geocentric coordinates
    {
        pj_t p1( proj_create_crs_to_crs( context_, source_.c_str(), proj_.c_str(), nullptr ) );
        ATLAS_ASSERT( p1 );
        sourceToTarget_.reset( proj_normalize_for_visualization( context_, p1 ) );
        ATLAS_ASSERT( sourceToTarget_ );
    }

    {
        pj_t p2( proj_create_crs_to_crs( context_, source_.c_str(), geocentric_.c_str(), nullptr ) );
        ATLAS_ASSERT( p2 );
        sourceToGeocentric_.reset( proj_normalize_for_visualization( context_, p2 ) );
        ATLAS_ASSERT( sourceToGeocentric_ );
    }

    // set semi-major/minor axis
    {
        double a, b;
        if ( proj_ellipsoid_params( context_, proj_, a, b ) ) {
            eckit::types::is_approximately_equal( a, b )
                ? extraSpec_.set( "radius", a )
                : extraSpec_.set( "semi_major_axis", a ).set( "semi_minor_axis", b );
        }
    }

    // set units
    {
        pj_t target( proj_get_target_crs( context_, sourceToTarget_ ) );
        ATLAS_ASSERT( target );

        pj_t coord( proj_crs_get_coordinate_system( context_, target ) );
        ATLAS_ASSERT( coord );
        ATLAS_ASSERT( proj_cs_get_axis_count( context_, coord ) > 0 );

        const char* units_c_str;
        if ( proj_cs_get_axis_info( nullptr, coord, 0, nullptr, nullptr, nullptr, nullptr, &units_c_str, nullptr,
                                    nullptr ) ) {
            std::string units( units_c_str );
            if ( !units.empty() ) {
                if ( units == "metre" ) {
                    units = "meters";
                }
                extraSpec_.set( "units", units );
            }
        }
    }
}  // namespace detail


void ProjProjection::xy2lonlat( double crd[] ) const {
    PJ_COORD P = proj_coord( crd[XX], crd[YY], 0, 0 );
    P          = proj_trans( sourceToTarget_, PJ_INV, P );

    //    std::memcpy(crd, &P, 2 * sizeof(double));
    crd[LON] = P.enu.e;
    crd[LAT] = P.enu.n;

    normalise_( crd );
}


void ProjProjection::lonlat2xy( double crd[] ) const {
    PJ_COORD P = proj_coord( crd[LON], crd[LAT], 0, 0 );
    P          = proj_trans( sourceToTarget_, PJ_FWD, P );

    //    std::memcpy(crd, &P, 2 * sizeof(double));
    crd[XX] = P.xy.x;
    crd[YY] = P.xy.y;
}


ProjectionImpl::Jacobian ProjProjection::jacobian( const PointLonLat& ) const {
    throw_NotImplemented( "ProjProjection::jacobian", Here() );
}

PointXYZ ProjProjection::xyz( const PointLonLat& lonlat ) const {
    PJ_COORD P = proj_coord( lonlat.lon(), lonlat.lat(), 0, 0 );
    P          = proj_trans( sourceToGeocentric_, PJ_FWD, P );
    return {P.xyz.x, P.xyz.y, P.xyz.z};
}


RectangularLonLatDomain ProjProjection::lonlatBoundingBox( const Domain& domain ) const {
    return ProjectionImpl::lonlatBoundingBox( domain );
}


ProjProjection::Spec ProjProjection::spec() const {
    Spec spec;
    spec.set( "type", type() );
    spec.set( "proj", proj_ );
    if ( source_encoded_ ) {
        spec.set( "proj_source", source_ );
    }
    if ( geocentric_encoded_ ) {
        spec.set( "proj_geocentric", geocentric_ );
    }
    normalise_.spec( spec );
    return spec | extraSpec_;
}


std::string ProjProjection::units() const {
    return extraSpec_.getString( "units", "" );
}


void ProjProjection::hash( eckit::Hash& h ) const {
    h.add( type() );
    h.add( proj_ );
    if ( source_encoded_ ) {
        h.add( source_ );
    }
    if ( geocentric_encoded_ ) {
        h.add( geocentric_ );
    }
    normalise_.hash( h );
}


namespace {
static ProjectionBuilder<ProjProjection> register_1( ProjProjection::static_type() );
}


}  // namespace detail
}  // namespace projection
}  // namespace atlas
