/*
 * (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 "LambertConformalConicProjection.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 {

namespace {

inline double normalise(double a, double minimum, double globe) {
    while (a >= minimum + globe) {
        a -= globe;
    }
    while (a < minimum) {
        a += globe;
    }
    return a;
}

inline double tan_d(double lat_d) {
    return std::tan(util::Constants::degreesToRadians() * (45. + lat_d * 0.5));
}

inline double sin_d(double theta_d) {
    return std::sin(util::Constants::degreesToRadians() * theta_d);
}

inline double cos_d(double theta_d) {
    return std::cos(util::Constants::degreesToRadians() * theta_d);
}

static ProjectionBuilder<LambertConformalConicProjection> register_1(LambertConformalConicProjection::static_type());

}  // namespace

LambertConformalConicProjection::LambertConformalConicProjection(const eckit::Parametrisation& params) {
    ATLAS_ASSERT(params.get("longitude0", lon0_ = 0));
    ATLAS_ASSERT(params.get("latitude0", lat0_ = 0));
    if (not params.get("latitude1", lat1_)) {
        lat1_ = lat0_;
    }
    if (not params.get("latitude2", lat2_)) {
        lat2_ = lat0_;
    }
    params.get("radius", radius_ = util::Earth::radius());

    n_     = eckit::types::is_approximately_equal(lat1_, lat2_)
                 ? sin_d(lat2_)
                 : std::log(cos_d(lat1_) / cos_d(lat2_)) / std::log(tan_d(lat2_) / tan_d(lat1_));
    inv_n_ = 1. / n_;
    sign_  = n_ < 0. ? -1. : 1.;  // n < 0: adjustment for southern hemisphere

    F_    = (cos_d(lat1_) * std::pow(tan_d(lat1_), n_)) / n_;
    rho0_ = sign_ * radius_ * F_ * std::pow(tan_d(lat0_), -n_);
}

void LambertConformalConicProjection::lonlat2xy(double crd[]) const {
    double rho   = radius_ * F_ * std::pow(tan_d(crd[1]), -n_);
    double theta = n_ * normalise(crd[0] - lon0_, -180, 360);

    crd[XX] = rho * sin_d(theta);
    crd[YY] = rho0_ - rho * cos_d(theta);
}

void LambertConformalConicProjection::xy2lonlat(double crd[]) const {
    double x = sign_ * crd[XX];
    double y = rho0_ - sign_ * crd[YY];

    double rho   = sign_ * std::sqrt(x * x + y * y);
    double theta = std::atan2(x, y) * inv_n_;

    crd[LON] = util::Constants::radiansToDegrees() * theta + lon0_;
    crd[LAT] = eckit::types::is_approximately_equal(rho, 0.)
                   ? 90 * sign_
                   : util::Constants::radiansToDegrees() * 2. * std::atan(std::pow(radius_ * F_ / rho, inv_n_)) - 90.;
}

ProjectionImpl::Jacobian LambertConformalConicProjection::jacobian(const PointLonLat& lonlat) const {
    ProjectionImpl::Jacobian jac;

    const double deg2rad = util::Constants::degreesToRadians();

    double lat = lonlat.lat(), lon = lonlat.lon();
    double tanlat = tan_d(lat), coslat = cos_d(45. + lat * 0.5);

    double rho   = radius_ * F_ * std::pow(tanlat, -n_);
    double theta = n_ * normalise(lon - lon0_, -180, 360);

    double costheta = cos_d(theta), sintheta = sin_d(theta);

    double x = rho * sintheta;
    double y = rho0_ - rho * costheta;

    auto cpj = [&](const double dlon, const double dlat, double& dx, double& dy) {
        double drho   = deg2rad * radius_ * F_ * (-n_) * std::pow(tanlat, -n_ - 1) / (coslat * coslat) * dlat * 0.5;
        double dtheta = +n_ * dlon;
        dx            = +drho * sintheta + rho * costheta * deg2rad * dtheta;
        dy            = -drho * costheta + rho * sintheta * deg2rad * dtheta;
    };

    const double dlat = 1.0f;
    const double dlon = 1.0f;

    cpj(dlon, 0, jac[0][0], jac[1][0]);
    cpj(0, dlat, jac[0][1], jac[1][1]);

    return jac;
}

LambertConformalConicProjection::Spec LambertConformalConicProjection::spec() const {
    Spec spec;
    spec.set("type", static_type());
    spec.set("longitude0", lon0_);
    spec.set("latitude0", lat0_);
    spec.set("latitude1", lat1_);
    spec.set("latitude2", lat2_);
    if (!eckit::types::is_approximately_equal(radius_, util::Earth::radius())) {
        spec.set("radius", radius_);
    }
    return spec;
}

void LambertConformalConicProjection::hash(eckit::Hash& h) const {
    h.add(static_type());
    h.add(lat1_);
    h.add(lat2_);
    h.add(lat0_);
    h.add(lon0_);
    h.add(radius_);
}

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