#include "Base/Axis/Scale.h"
#include "Base/Const/Units.h"
#include "Base/Pixel/RectangularPixel.h"
#include "Device/Beam/Beam.h"
#include "Device/Coord/CoordSystem2D.h"
#include "Device/Detector/RectangularDetector.h"
#include "Tests/GTestWrapper/google_test.h"
#include <cmath>
#include <numbers>

using std::numbers::pi;

namespace {

const double det_width = 200.0;
const double det_height = 140.0;
const double det_distance = 1000.0;
const size_t det_nx = 100u;
const size_t det_ny = 70u;

} // namespace

class RectangularConverterTest : public ::testing::Test {
public:
    RectangularConverterTest();

protected:
    RectangularDetector m_detector;
    Beam m_beam;
    double m_phi, m_alpha;
    double m_kiz, m_kfy, m_kfz;
};

RectangularConverterTest::RectangularConverterTest()
    : m_detector(det_nx, det_width, det_ny, det_height)
    , m_beam(Beam(1, 1.0, 1 * Units::deg))
{
    m_detector.setPerpendicularToSampleX(det_distance, det_width / 2.0, 0.0);
    m_detector.setDetectorNormal(m_beam.ki());
    m_phi = std::atan2(det_width / 2.0, det_distance);
    m_alpha = std::atan2(det_height, det_distance / std::cos(m_phi));
    const auto k_i = m_beam.ki();
    m_kiz = k_i.z();
    double K = m_beam.wavenumber();
    m_kfy = K * std::sin(m_phi);
    m_kfz = K * std::sin(m_alpha);
}

TEST_F(RectangularConverterTest, ImageCoords)
{
    ImageCoords converter(m_detector.axesClippedToRegionOfInterest(), m_beam.ki(),
                          m_detector.regionOfInterestPixel());

    EXPECT_EQ(converter.rank(), 2u);

    EXPECT_NEAR(converter.calculateMin(0, Coords::UNDEFINED), 0.0, 1e-12);
    EXPECT_NEAR(converter.calculateMin(0, Coords::NBINS), 0.0, 1e-12);
    EXPECT_NEAR(converter.calculateMin(0, Coords::MM), 0.0, 1e-12);
    EXPECT_NEAR(converter.calculateMin(0, Coords::RADIANS), -m_phi, m_phi * 1e-10);
    EXPECT_NEAR(converter.calculateMin(0, Coords::DEGREES), Units::rad2deg(-m_phi),
                Units::rad2deg(m_phi) * 1e-10);
    EXPECT_NEAR(converter.calculateMin(0, Coords::QSPACE), -m_kfy, m_kfy * 1e-10);

    EXPECT_NEAR(converter.calculateMax(0, Coords::UNDEFINED), det_width, det_width * 1e-10);
    EXPECT_NEAR(converter.calculateMax(0, Coords::NBINS), static_cast<double>(det_nx),
                det_nx * 1e-10);
    EXPECT_NEAR(converter.calculateMax(0, Coords::MM), det_width, det_width * 1e-10);
    EXPECT_NEAR(converter.calculateMax(0, Coords::RADIANS), m_phi, m_phi * 1e-10);
    EXPECT_NEAR(converter.calculateMax(0, Coords::DEGREES), Units::rad2deg(m_phi),
                Units::rad2deg(m_phi) * 1e-10);
    EXPECT_NEAR(converter.calculateMax(0, Coords::QSPACE), m_kfy, m_kfy * 1e-10);

    EXPECT_NEAR(converter.calculateMin(1, Coords::UNDEFINED), 0.0, 1e-12);
    EXPECT_NEAR(converter.calculateMin(1, Coords::NBINS), 0.0, 1e-12);
    EXPECT_NEAR(converter.calculateMin(1, Coords::MM), 0.0, 1e-12);
    EXPECT_NEAR(converter.calculateMin(1, Coords::RADIANS), 0.0, 1e-12);
    EXPECT_NEAR(converter.calculateMin(1, Coords::DEGREES), 0.0, 1e-12);
    EXPECT_NEAR(converter.calculateMin(1, Coords::QSPACE), -m_kiz, std::abs(m_kiz) * 1e-1);

    EXPECT_NEAR(converter.calculateMax(1, Coords::UNDEFINED), det_height, det_height * 1e-10);
    EXPECT_NEAR(converter.calculateMax(1, Coords::NBINS), static_cast<double>(det_ny),
                det_ny * 1e-10);
    EXPECT_NEAR(converter.calculateMax(1, Coords::MM), det_height, det_height * 1e-10);
    EXPECT_NEAR(converter.calculateMax(1, Coords::RADIANS), m_alpha, m_alpha * 1e-10);
    EXPECT_NEAR(converter.calculateMax(1, Coords::DEGREES), Units::rad2deg(m_alpha),
                Units::rad2deg(m_alpha) * 1e-10);
    EXPECT_NEAR(converter.calculateMax(1, Coords::QSPACE), m_kfz - m_kiz,
                std::abs(m_kfz - m_kiz) * 1e-1);

    EXPECT_FAILED_ASSERT(converter.calculateMin(2, Coords::UNDEFINED));
    EXPECT_FAILED_ASSERT(converter.calculateMax(2, Coords::UNDEFINED));

    std::unique_ptr<Scale> axis(converter.convertedAxis(0, Coords::UNDEFINED));
    EXPECT_EQ(axis->min(), converter.calculateMin(0, Coords::UNDEFINED));
    EXPECT_EQ(axis->max(), converter.calculateMax(0, Coords::UNDEFINED));

    std::unique_ptr<Scale> axis2(converter.convertedAxis(1, Coords::QSPACE));
    EXPECT_EQ(axis2->min(), converter.calculateMin(1, Coords::QSPACE));
    EXPECT_EQ(axis2->max(), converter.calculateMax(1, Coords::QSPACE));

    EXPECT_FAILED_ASSERT(converter.convertedAxis(2, Coords::UNDEFINED));
}

TEST_F(RectangularConverterTest, ImageCoordsClone)
{
    ImageCoords converter(m_detector.axesClippedToRegionOfInterest(), m_beam.ki(),
                          m_detector.regionOfInterestPixel());
    std::unique_ptr<ImageCoords> P_clone(converter.clone());

    EXPECT_EQ(P_clone->rank(), 2u);

    EXPECT_NEAR(P_clone->calculateMin(0, Coords::UNDEFINED), 0.0, 1e-12);
    EXPECT_NEAR(P_clone->calculateMin(0, Coords::NBINS), 0.0, 1e-12);
    EXPECT_NEAR(P_clone->calculateMin(0, Coords::MM), 0.0, 1e-12);
    EXPECT_NEAR(P_clone->calculateMin(0, Coords::RADIANS), -m_phi, m_phi * 1e-10);
    EXPECT_NEAR(P_clone->calculateMin(0, Coords::DEGREES), Units::rad2deg(-m_phi),
                Units::rad2deg(m_phi) * 1e-10);
    EXPECT_NEAR(P_clone->calculateMin(0, Coords::QSPACE), -m_kfy, m_kfy * 1e-10);

    EXPECT_NEAR(P_clone->calculateMax(0, Coords::UNDEFINED), det_width, det_width * 1e-10);
    EXPECT_NEAR(P_clone->calculateMax(0, Coords::NBINS), static_cast<double>(det_nx),
                det_nx * 1e-10);
    EXPECT_NEAR(P_clone->calculateMax(0, Coords::MM), det_width, det_width * 1e-10);
    EXPECT_NEAR(P_clone->calculateMax(0, Coords::RADIANS), m_phi, m_phi * 1e-10);
    EXPECT_NEAR(P_clone->calculateMax(0, Coords::DEGREES), Units::rad2deg(m_phi),
                Units::rad2deg(m_phi) * 1e-10);
    EXPECT_NEAR(P_clone->calculateMax(0, Coords::QSPACE), m_kfy, m_kfy * 1e-10);

    EXPECT_NEAR(P_clone->calculateMin(1, Coords::UNDEFINED), 0.0, 1e-12);
    EXPECT_NEAR(P_clone->calculateMin(1, Coords::NBINS), 0.0, 1e-12);
    EXPECT_NEAR(P_clone->calculateMin(1, Coords::MM), 0.0, 1e-12);
    EXPECT_NEAR(P_clone->calculateMin(1, Coords::RADIANS), 0.0, 1e-12);
    EXPECT_NEAR(P_clone->calculateMin(1, Coords::DEGREES), 0.0, 1e-12);
    EXPECT_NEAR(P_clone->calculateMin(1, Coords::QSPACE), -m_kiz, std::abs(m_kiz) * 1e-1);

    EXPECT_NEAR(P_clone->calculateMax(1, Coords::UNDEFINED), det_height, det_height * 1e-10);
    EXPECT_NEAR(P_clone->calculateMax(1, Coords::NBINS), static_cast<double>(det_ny),
                det_ny * 1e-10);
    EXPECT_NEAR(P_clone->calculateMax(1, Coords::MM), det_height, det_height * 1e-10);
    EXPECT_NEAR(P_clone->calculateMax(1, Coords::RADIANS), m_alpha, m_alpha * 1e-10);
    EXPECT_NEAR(P_clone->calculateMax(1, Coords::DEGREES), Units::rad2deg(m_alpha),
                Units::rad2deg(m_alpha) * 1e-10);
    EXPECT_NEAR(P_clone->calculateMax(1, Coords::QSPACE), m_kfz - m_kiz,
                std::abs(m_kfz - m_kiz) * 1e-1);

    EXPECT_FAILED_ASSERT(P_clone->calculateMin(2, Coords::UNDEFINED));
    EXPECT_FAILED_ASSERT(P_clone->calculateMax(2, Coords::UNDEFINED));
}

TEST_F(RectangularConverterTest, ImageCoordsWithROI)
{
    const double roi_xmin = 100;
    const double roi_xmax = 150; // xmax in roi will be 152 due to binning
    const double roi_ymin = 50;
    const double roi_ymax = 100; // ymax in roi will be 102 due to binning

    m_detector.setRegionOfInterest(roi_xmin, roi_ymin, roi_xmax, roi_ymax);
    ImageCoords converter(m_detector.axesClippedToRegionOfInterest(), m_beam.ki(),
                          m_detector.regionOfInterestPixel());

    EXPECT_EQ(converter.calculateMin(0, Coords::UNDEFINED), 100);
    EXPECT_EQ(converter.calculateMax(0, Coords::UNDEFINED), 150);
    EXPECT_EQ(converter.calculateMin(1, Coords::UNDEFINED), 50);
    EXPECT_EQ(converter.calculateMax(1, Coords::UNDEFINED), 100);
}
