/*
 * (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 <memory>

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

#include "atlas/domain.h"
#include "atlas/projection/detail/LonLatProjection.h"
#include "atlas/projection/detail/ProjectionFactory.h"
#include "atlas/runtime/Exception.h"
#include "atlas/runtime/Log.h"
#include "atlas/util/Config.h"

namespace atlas {
namespace projection {
namespace detail {

template <typename Rotation>
LonLatProjectionT<Rotation>::LonLatProjectionT( const eckit::Parametrisation& config ) :
    ProjectionImpl(), rotation_( config ) {}

template <>
void LonLatProjectionT<NotRotated>::xy2lonlat( double[] ) const {}

template <>
void LonLatProjectionT<NotRotated>::lonlat2xy( double[] ) const {}

template <>
ProjectionImpl::Jacobian LonLatProjectionT<NotRotated>::jacobian( const PointLonLat& ) const {
    Jacobian jac;
    jac[0] = {1.0, 0.0};
    jac[1] = {0.0, 1.0};
    return jac;
}

template <typename Rotation>
ProjectionImpl::Jacobian LonLatProjectionT<Rotation>::jacobian( const PointLonLat& ) const {
    throw_NotImplemented( "LonLatProjectionT::jacobian", Here() );
}

template <>
RectangularLonLatDomain LonLatProjectionT<NotRotated>::lonlatBoundingBox( const Domain& domain ) const {
    return domain;
}

template <typename Rotation>
RectangularLonLatDomain LonLatProjectionT<Rotation>::lonlatBoundingBox( const Domain& domain ) const {
    using eckit::types::is_strictly_greater;


    // 0. setup

    if ( domain.global() ) {
        return domain;
    }
    RectangularDomain rect( domain );
    ATLAS_ASSERT( rect );


    const std::string derivative = "central";
    constexpr double h           = 0.001;  // precision to millidegrees
    constexpr size_t Niter       = 100;


    // 1. determine box from projected corners

    const std::vector<PointXY> corners{
        {rect.xmin(), rect.ymax()}, {rect.xmax(), rect.ymax()}, {rect.xmax(), rect.ymin()}, {rect.xmin(), rect.ymin()}};

    BoundLonLat bounds;
    for ( auto& p : corners ) {
        bounds.extend( lonlat( p ), PointLonLat{h, h} );
    }


    // 2. locate latitude extrema by checking if poles are included (in the un-projected frame) and if not, find extrema
    // not at the corners by refining iteratively

    PointXY NP{xy( {0., 90.} )};
    PointXY SP{xy( {0., -90.} )};

    bounds.includesNorthPole( rect.contains( NP ) );
    bounds.includesSouthPole( rect.contains( SP ) );

    for ( size_t i = 0; i < corners.size(); ++i ) {
        if ( !bounds.includesNorthPole() || !bounds.includesSouthPole() ) {
            PointXY A = corners[i];
            PointXY B = corners[( i + 1 ) % corners.size()];

            std::unique_ptr<Derivate> derivate( DerivateFactory::build( derivative, *this, A, B, h ) );
            double dAdy = derivate->d( A ).lat();
            double dBdy = derivate->d( B ).lat();

            if ( !is_strictly_greater( dAdy * dBdy, 0. ) ) {
                for ( size_t cnt = 0; cnt < Niter; ++cnt ) {
                    PointXY M   = PointXY::middle( A, B );
                    double dMdy = derivate->d( M ).lat();
                    if ( is_strictly_greater( dAdy * dMdy, 0. ) ) {
                        A    = M;
                        dAdy = dMdy;
                    }
                    else if ( is_strictly_greater( dBdy * dMdy, 0. ) ) {
                        B    = M;
                        dBdy = dMdy;
                    }
                    else {
                        break;
                    }
                }

                // update extrema, extended by 'a small amount' (arbitrary)
                bounds.extend( lonlat( PointXY::middle( A, B ) ), PointLonLat{0, h} );
            }
        }
    }


    // 3. locate longitude extrema by checking if date line is crossed (in the un-projected frame), in which case we
    // assume periodicity and if not, find extrema not at the corners by refining iteratively

    if ( !bounds.crossesDateLine() ) {
        PointLonLat A{xy( {180., -10.} )};
        PointLonLat B{xy( {180., 10.} )};
        eckit::geometry::GreatCircle DL( A, B );

        for ( auto lon : {rect.xmin(), rect.xmax()} ) {
            if ( !bounds.crossesDateLine() ) {
                for ( auto lat : DL.latitude( lon ) ) {
                    if ( ( bounds.crossesDateLine( domain.contains( lon, lat ) ) ) ) {
                        break;
                    }
                }
            }
        }

        for ( auto lat : {rect.ymin(), rect.ymax()} ) {
            if ( !bounds.crossesDateLine() ) {
                for ( auto lon : DL.longitude( lat ) ) {
                    if ( ( bounds.crossesDateLine( domain.contains( lon, lat ) ) ) ) {
                        break;
                    }
                }
            }
        }
    }

    for ( size_t i = 0; i < corners.size(); ++i ) {
        if ( !bounds.crossesDateLine() ) {
            PointXY A = corners[i];
            PointXY B = corners[( i + 1 ) % corners.size()];

            std::unique_ptr<Derivate> derivate( DerivateFactory::build( derivative, *this, A, B, h ) );
            double dAdx = derivate->d( A ).lon();
            double dBdx = derivate->d( B ).lon();

            if ( !is_strictly_greater( dAdx * dBdx, 0. ) ) {
                for ( size_t cnt = 0; cnt < Niter; ++cnt ) {
                    PointXY M   = PointXY::middle( A, B );
                    double dMdx = derivate->d( M ).lon();
                    if ( is_strictly_greater( dAdx * dMdx, 0. ) ) {
                        A    = M;
                        dAdx = dMdx;
                    }
                    else if ( is_strictly_greater( dBdx * dMdx, 0. ) ) {
                        B    = M;
                        dBdx = dMdx;
                    }
                    else {
                        break;
                    }
                }

                // update extrema, extended by 'a small amount' (arbitrary)
                bounds.extend( lonlat( PointXY::middle( A, B ) ), PointLonLat{h, 0} );
            }
        }
    }


    // 4. return bounding box
    return bounds;
}

template <typename Rotation>
typename LonLatProjectionT<Rotation>::Spec LonLatProjectionT<Rotation>::spec() const {
    Spec proj_spec;
    proj_spec.set( "type", static_type() );
    rotation_.spec( proj_spec );
    return proj_spec;
}

template <typename Rotation>
void LonLatProjectionT<Rotation>::hash( eckit::Hash& hsh ) const {
    hsh.add( static_type() );
    rotation_.hash( hsh );
}

template class LonLatProjectionT<NotRotated>;
template class LonLatProjectionT<Rotated>;

namespace {
static ProjectionBuilder<LonLatProjection> register_1( LonLatProjection::static_type() );
static ProjectionBuilder<RotatedLonLatProjection> register_2( RotatedLonLatProjection::static_type() );
}  // namespace

// namespace

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