/*
 * (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 <cmath>
#include <vector>

#include "atlas/runtime/Log.h"
#include "atlas/util/Config.h"
#include "atlas/util/Constants.h"
#include "atlas/util/Rotation.h"

#include "tests/AtlasTestEnvironment.h"

using atlas::util::Config;
using atlas::util::Rotation;

namespace atlas {
namespace test {

//-----------------------------------------------------------------------------

constexpr double eps = 1.e-5;
constexpr double d2r = atlas::util::Constants::degreesToRadians();
constexpr double r2d = atlas::util::Constants::radiansToDegrees();

bool equivalent(const PointLonLat& p1, const PointLonLat& p2) {
    using eckit::types::is_approximately_equal;
    auto f = [=](double lon) { return 10. + std::cos(lon * d2r); };

    return is_approximately_equal(p1.lat(), p2.lat(), eps) &&
           (std::abs(p2.lat()) == 90. || is_approximately_equal(f(p1.lon()), f(p2.lon()), eps));
}

#define EXPECT_EQUIVALENT(p1, p2) EXPECT(equivalent(p1, p2))

//-----------------------------------------------------------------------------

class MagicsRotation {
    // For reference, this what magics uses, it appears as if it originated from
    // fortran code
    // Strangely the definition of rotate and unrotate are switched.

public:
    MagicsRotation(const PointLonLat& south_pole): south_pole_(south_pole) {}

    PointLonLat rotate(const PointLonLat& point) const {
        return magics_unrotate(point);  /// Switch meaning !!!
    }

    PointLonLat unrotate(const PointLonLat& point) const {
        return magics_rotate(point);  /// Swich meaning !!!
    }

private:
    PointLonLat south_pole_;

    PointLonLat magics_rotate(const PointLonLat& point) const {
        double lat_y = point.lat();
        double lon_x = point.lon();

        double sin_south_pole_lat = std::sin(d2r * (south_pole_.lat() + 90.));
        double cos_south_pole_lat = std::cos(d2r * (south_pole_.lat() + 90.));

        double ZXMXC           = d2r * (lon_x - south_pole_.lon());
        double sin_lon_decr_sp = std::sin(ZXMXC);
        double cos_lon_decr_sp = std::cos(ZXMXC);
        double sin_lat         = std::sin(d2r * lat_y);
        double cos_lat         = std::cos(d2r * lat_y);
        double ZSYROT          = cos_south_pole_lat * sin_lat - sin_south_pole_lat * cos_lat * cos_lon_decr_sp;
        ZSYROT                 = std::max(std::min(ZSYROT, +1.0), -1.0);

        double PYROT = std::asin(ZSYROT) * r2d;

        double ZCYROT = std::cos(PYROT * d2r);
        double ZCXROT = (cos_south_pole_lat * cos_lat * cos_lon_decr_sp + sin_south_pole_lat * sin_lat) / ZCYROT;
        ZCXROT        = std::max(std::min(ZCXROT, +1.0), -1.0);
        double ZSXROT = cos_lat * sin_lon_decr_sp / ZCYROT;

        double PXROT = std::acos(ZCXROT) * r2d;

        if (ZSXROT < 0.0) {
            PXROT = -PXROT;
        }

        return PointLonLat(PXROT, PYROT);
    }

    PointLonLat magics_unrotate(const PointLonLat& point) const {
        double lat_y = point.lat();
        double lon_x = point.lon();

        double sin_south_pole_lat = std::sin(d2r * (south_pole_.lat() + 90.));
        double cos_south_pole_lat = std::cos(d2r * (south_pole_.lat() + 90.));
        double cos_lon            = std::cos(d2r * lon_x);
        double sin_lat            = std::sin(d2r * lat_y);
        double cos_lat            = std::cos(d2r * lat_y);
        double ZSYREG             = cos_south_pole_lat * sin_lat + sin_south_pole_lat * cos_lat * cos_lon;
        ZSYREG                    = std::max(std::min(ZSYREG, +1.0), -1.0);
        double PYREG              = std::asin(ZSYREG) * r2d;
        double ZCYREG             = std::cos(PYREG * d2r);
        double ZCXMXC             = (cos_south_pole_lat * cos_lat * cos_lon - sin_south_pole_lat * sin_lat) / ZCYREG;
        ZCXMXC                    = std::max(std::min(ZCXMXC, +1.0), -1.0);
        double ZSXMXC             = cos_lat * sin_lat / ZCYREG;
        double ZXMXC              = std::acos(ZCXMXC) * r2d;
        if (ZSXMXC < 0.0) {
            ZXMXC = -ZXMXC;
        }
        double PXREG = ZXMXC + south_pole_.lon();

        return PointLonLat(PXREG, PYREG);
    }
};

//-----------------------------------------------------------------------------


CASE("test_rotation_angle") {
    const int nx = 12, ny = 6;

    Rotation rot(Config("rotation_angle", 180.) | Config("north_pole", std::vector<double>{2.0, 46.7}));

    const PointLonLat ref[] = {
        {-178.00000, -46.70000}, {-178.00000, -16.70000}, {-178.00000, 13.30000},  {-178.00000, 43.30000},
        {-178.00000, 73.30000},  {2.00000, 76.70000},     {2.00000, 46.70000},     {-178.00000, -46.70000},
        {-162.62343, -19.46929}, {-152.02366, 8.65459},   {-139.57464, 36.43683},  {-113.10894, 61.43199},
        {-39.88245, 68.00825},   {2.00000, 46.70000},     {-178.00000, -46.70000}, {-148.83443, -27.31067},
        {-129.26346, -3.83700},  {-110.79116, 20.05422},  {-85.87917, 41.36507},   {-44.42496, 53.29508},
        {2.00000, 46.70000},     {-178.00000, -46.70000}, {-137.90794, -39.07002}, {-109.60146, -21.33906},
        {-88.00000, 0.00000},    {-66.39854, 21.33906},   {-38.09206, 39.07002},   {2.00000, 46.70000},
        {-178.00000, -46.70000}, {-131.57504, -53.29508}, {-90.12083, -41.36507},  {-65.20884, -20.05422},
        {-46.73654, 3.83700},    {-27.16557, 27.31067},   {2.00000, 46.70000},     {-178.00000, -46.70000},
        {-136.11755, -68.00825}, {-62.89106, -61.43199},  {-36.42536, -36.43683},  {-23.97634, -8.65459},
        {-13.37657, 19.46929},   {2.00000, 46.70000},     {-178.00000, -46.70000}, {-178.00000, -76.70000},
        {2.00000, -73.30000},    {2.00000, -43.30000},    {2.00000, -13.30000},    {2.00000, 16.70000},
        {2.00000, 46.70000},     {-178.00000, -46.70000}, {140.11755, -68.00825},  {66.89106, -61.43199},
        {40.42536, -36.43683},   {27.97634, -8.65459},    {17.37657, 19.46929},    {2.00000, 46.70000},
        {-178.00000, -46.70000}, {135.57504, -53.29508},  {94.12083, -41.36507},   {69.20884, -20.05422},
        {50.73654, 3.83700},     {31.16557, 27.31067},    {2.00000, 46.70000},     {-178.00000, -46.70000},
        {141.90794, -39.07002},  {113.60146, -21.33906},  {92.00000, 0.00000},     {70.39854, 21.33906},
        {42.09206, 39.07002},    {2.00000, 46.70000},     {-178.00000, -46.70000}, {152.83443, -27.31067},
        {133.26346, -3.83700},   {114.79116, 20.05422},   {89.87917, 41.36507},    {48.42496, 53.29508},
        {2.00000, 46.70000},     {-178.00000, -46.70000}, {166.62343, -19.46929},  {156.02366, 8.65459},
        {143.57464, 36.43683},   {117.10894, 61.43199},   {43.88245, 68.00825},    {2.00000, 46.70000},
    };


    for (int i = 0, jglo = 0; i < nx; i++) {
        for (int j = 0; j < ny + 1; j++, jglo++) {
            double lon = static_cast<double>(i) * 360. / static_cast<double>(nx);
            double lat = static_cast<double>(j - ny / 2) * 90. / static_cast<double>(ny / 2);
            PointLonLat p0(lon, lat);
            PointLonLat p1 = rot.rotate(p0);
            PointLonLat p2 = rot.unrotate(p1);
            EXPECT(equivalent(p0, p2));
            EXPECT(equivalent(p1, ref[jglo]));
        }
    }
}

CASE("test_rotation_construction") {
    static const PointLonLat SP{0., -90.};
    static const PointLonLat NP{180., 90.};

    std::vector<PointLonLat> rotation_poles = {SP, NP, {0., -90.1}, {0., 90.1}};

    for (auto& p : rotation_poles) {
        Rotation s(Config("south_pole", std::vector<double>{p.lon(), p.lat()}));
        Log::info() << "rotate_south_pole=" << s << std::endl;
        EXPECT(s.rotated() == (p != SP));

        Rotation n(Config("north_pole", std::vector<double>{p.lon(), p.lat()}));
        Log::info() << "rotate_north_pole=" << n << std::endl;
        EXPECT(n.rotated() == (p != NP));
    }
}

CASE("test_rotation") {
    Config config;
    config.set("north_pole", std::vector<double>{-176, 40});
    Rotation rotation(config);
    MagicsRotation magics(rotation.southPole());
    Log::info() << rotation << std::endl;

    EXPECT(rotation.rotated());

    PointLonLat p, r;

    p = {0., 90.};
    r = {-176., 40.};
    EXPECT_EQUIVALENT(rotation.rotate(p), r);
    EXPECT_EQUIVALENT(magics.rotate(p), r);
    EXPECT_EQUIVALENT(rotation.unrotate(r), p);
    EXPECT_EQUIVALENT(magics.unrotate(r), p);

    p = {0., 0.};
    r = {-176., -50.};
    EXPECT_EQUIVALENT(rotation.rotate(p), r);
    EXPECT_EQUIVALENT(magics.rotate(p), r);
    EXPECT_EQUIVALENT(rotation.unrotate(r), p);
    EXPECT_EQUIVALENT(magics.unrotate(r), p);

    p = {-180., 45.};
    r = {-176., 85.};
    EXPECT_EQUIVALENT(rotation.rotate(p), r);
    EXPECT_EQUIVALENT(magics.rotate(p), r);
    EXPECT_EQUIVALENT(rotation.unrotate(r), p);
    EXPECT_EQUIVALENT(magics.unrotate(r), p);
}

CASE("test_no_rotation") {
    Config config;
    Rotation rotation(config);
    MagicsRotation magics(rotation.southPole());

    Log::info() << rotation << std::endl;

    EXPECT(not rotation.rotated());

    PointLonLat p, r;

    p = {0., 90.};
    r = p;
    EXPECT_EQUIVALENT(rotation.rotate(p), r);
    EXPECT_EQUIVALENT(magics.rotate(p), r);
    EXPECT_EQUIVALENT(rotation.unrotate(r), p);
    EXPECT_EQUIVALENT(magics.unrotate(r), p);

    p = {0., 0.};
    r = p;
    EXPECT_EQUIVALENT(rotation.rotate(p), r);
    EXPECT_EQUIVALENT(magics.rotate(p), r);
    EXPECT_EQUIVALENT(rotation.unrotate(r), p);
    EXPECT_EQUIVALENT(magics.unrotate(r), p);

    p = {-180., 45.};
    r = p;
    EXPECT_EQUIVALENT(rotation.rotate(p), r);
    EXPECT_EQUIVALENT(magics.rotate(p), r);
    EXPECT_EQUIVALENT(rotation.unrotate(r), p);
    EXPECT_EQUIVALENT(magics.unrotate(r), p);
}

CASE("test_rotation_angle_only") {
    Config config;
    config.set("rotation_angle", -180.);
    Rotation rotation(config);
    MagicsRotation magics(rotation.southPole());

    Log::info() << rotation << std::endl;

    EXPECT(rotation.rotated());

    PointLonLat p, r;

    p = {0., 90.};
    r = {-180., 90};
    EXPECT_EQUIVALENT(rotation.rotate(p), r);
    EXPECT_EQUIVALENT(rotation.unrotate(r), p);

    p = {0., 0.};
    r = {-180., 0.};
    EXPECT_EQUIVALENT(rotation.rotate(p), r);
    EXPECT_EQUIVALENT(rotation.unrotate(r), p);

    p = {270., 25.};
    r = {90., 25.};
    EXPECT_EQUIVALENT(rotation.rotate(p), r);
    EXPECT_EQUIVALENT(rotation.unrotate(r), p);

    p = {-180., 45.};
    r = {-360., 45.};
    EXPECT_EQUIVALENT(rotation.rotate(p), r);
    EXPECT_EQUIVALENT(rotation.unrotate(r), p);
}

//-----------------------------------------------------------------------------

}  // namespace test
}  // namespace atlas

int main(int argc, char** argv) {
    return atlas::test::run(argc, argv);
}
