/*
 * (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 "LambertAzimuthalEqualAreaProjection.h"

#include <cmath>

#include "eckit/config/Parametrisation.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/Config.h"
#include "atlas/util/Constants.h"
#include "atlas/util/CoordinateEnums.h"
#include "atlas/util/Earth.h"

namespace atlas {
namespace projection {
namespace detail {


LambertAzimuthalEqualAreaProjection::LambertAzimuthalEqualAreaProjection(const eckit::Parametrisation& params):
    radius_(util::Earth::radius()) {
    ATLAS_ASSERT(params.get("central_longitude", reference_[LON]));
    ATLAS_ASSERT(params.get("standard_parallel", reference_[LAT]));
    params.get("radius", radius_ = util::Earth::radius());
    params.get("false_northing", false_northing_);
    params.get("false_easting", false_easting_);


    lambda0_  = util::Constants::degreesToRadians() * reference_[LON];
    phi1_     = util::Constants::degreesToRadians() * reference_[LAT];
    sin_phi1_ = std::sin(phi1_);
    cos_phi1_ = std::cos(phi1_);
}


void LambertAzimuthalEqualAreaProjection::lonlat2xy(double crd[]) const {
    double dlambda     = util::Constants::degreesToRadians() * (crd[LON] - reference_.lon());
    double cos_dlambda = std::cos(dlambda);
    double sin_dlambda = std::sin(dlambda);

    double phi     = util::Constants::degreesToRadians() * crd[LAT];
    double sin_phi = std::sin(phi);
    double cos_phi = std::cos(phi);

    double kp = radius_ * std::sqrt(2. / (1. + sin_phi1_ * sin_phi + cos_phi1_ * cos_phi * cos_dlambda));

    crd[XX] = kp * cos_phi * sin_dlambda;
    crd[YY] = kp * (cos_phi1_ * sin_phi - sin_phi1_ * cos_phi * cos_dlambda);
    crd[XX] += false_easting_;
    crd[YY] += false_northing_;
}


void LambertAzimuthalEqualAreaProjection::xy2lonlat(double crd[]) const {
    const double x = crd[XX] - false_easting_;
    const double y = crd[YY] - false_northing_;

    const double rho = std::sqrt(x * x + y * y);
    if (std::abs(rho) < 1.e-12) {
        crd[LON] = reference_[LON];
        crd[LAT] = reference_[LAT];
        return;
    }

    double c     = 2. * std::asin(rho / (2. * radius_));
    double cos_c = std::cos(c);
    double sin_c = std::sin(c);

    double lon_r     = lambda0_ + std::atan2(x * sin_c, rho * cos_phi1_ * cos_c - y * sin_phi1_ * sin_c);
    double sin_lat_r = cos_c * sin_phi1_ + y * sin_c * cos_phi1_ / rho;

    crd[LON] = lon_r * util::Constants::radiansToDegrees();
    crd[LAT] = sin_lat_r > 1 ? 90 : sin_lat_r < -1 ? -90 : util::Constants::radiansToDegrees() * std::asin(sin_lat_r);
}


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

LambertAzimuthalEqualAreaProjection::Spec LambertAzimuthalEqualAreaProjection::spec() const {
    Spec proj;
    proj.set("type", static_type());
    proj.set("central_longitude", reference_[LON]);
    proj.set("standard_parallel", reference_[LAT]);
    proj.set("radius", radius_);
    proj.set("false_easting", false_easting_);
    proj.set("false_northing", false_northing_);

    return proj;
}


void LambertAzimuthalEqualAreaProjection::hash(eckit::Hash& h) const {
    h.add(static_type());
    h.add(radius_);
    h.add(reference_[LON]);
    h.add(reference_[LAT]);
}


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


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