#include "Base/Const/Units.h"
#include "Base/Vector/RotMatrix.h"
#include "Base/Vector/WavevectorInfo.h"
#include "Sample/Material/Admixtures.h"
#include "Sample/Material/RefractiveMaterial.h"
#include "Sample/Material/SLDMaterial.h"
#include "Sample/Particle/Rotations.h"
#include "Tests/GTestWrapper/google_test.h"
#include <iostream>
#include <numbers>

using std::numbers::pi;

namespace {

const MaterialColor TEST_COLOR(0.5, 0.5, 0.5);

} // namespace

TEST(Material, MaterialConstruction)
{
    complex_t material_data = complex_t(0.0, 2.0);
    complex_t refIndex = complex_t(1.0 - material_data.real(), material_data.imag());
    R3 magnetism = R3(3.0, 4.0, 5.0);

    const RefractiveMaterial material("MagMaterial", TEST_COLOR, refIndex, magnetism);
    EXPECT_EQ(material_data, material.rawData());
    EXPECT_EQ(magnetism, material.magnetization());

    const RefractiveMaterial material2("MagMaterial", TEST_COLOR, material_data.real(),
                                       material_data.imag(), magnetism);
    EXPECT_EQ(material_data, material2.rawData());
    EXPECT_EQ(magnetism, material2.magnetization());

    SLDMaterial material3("MagMaterial", TEST_COLOR, material_data.real(), material_data.imag(),
                          magnetism);
    EXPECT_EQ(material_data, material3.rawData());
    EXPECT_EQ(magnetism, material3.magnetization());

    R3 default_magnetism = R3{};

    const RefractiveMaterial material4("Material", TEST_COLOR, refIndex);
    EXPECT_EQ(material_data, material4.rawData());
    EXPECT_EQ(default_magnetism, material4.magnetization());

    RefractiveMaterial material5("Material", TEST_COLOR, material_data.real(),
                                 material_data.imag());
    EXPECT_EQ(material_data, material5.rawData());
    EXPECT_EQ(default_magnetism, material5.magnetization());

    const SLDMaterial material6("Material", TEST_COLOR, material_data.real(), material_data.imag());
    EXPECT_EQ(default_magnetism, material6.magnetization());
}

TEST(Material, MaterialTransform)
{
    complex_t material_data = complex_t(1.0, 0.0);
    complex_t refIndex = complex_t(1.0 - material_data.real(), material_data.imag());
    R3 magnetism = R3(1.0, 0.0, 0.0);
    RotationZ transform(90. * Units::deg);
    R3 transformed_mag = transform.transformed(magnetism);

    const RefractiveMaterial material("Material", TEST_COLOR, refIndex, magnetism);
    const auto material2 = material.rotatedMaterial(transform.rotMatrix());
    EXPECT_EQ(material_data, material2->rawData());
    // transformed_mag equals material2.magnetization except on i368:
    EXPECT_TRUE(fabs((transformed_mag - material2->magnetization()).x()) < 3e-16);
    EXPECT_TRUE(fabs((transformed_mag - material2->magnetization()).y()) < 3e-16);
    EXPECT_TRUE(fabs((transformed_mag - material2->magnetization()).z()) < 3e-16);

    SLDMaterial material3("Material", TEST_COLOR, material_data.real(), material_data.imag(),
                          magnetism);
    const auto material4 = material.rotatedMaterial(transform.rotMatrix());
    EXPECT_EQ(material_data, material4->rawData());
    // transformed_mag equals material4.magnetization except on i368:
    EXPECT_TRUE(fabs((transformed_mag - material4->magnetization()).x()) < 3e-16);
    EXPECT_TRUE(fabs((transformed_mag - material4->magnetization()).y()) < 3e-16);
    EXPECT_TRUE(fabs((transformed_mag - material4->magnetization()).z()) < 3e-16);
}

TEST(Material, Computation)
{
    // Reference data for Fe taken from
    // https://sld-calculator.appspot.com
    // http://www.ati.ac.at/~neutropt/scattering/table.html
    const double bc = 9.45e-6;      // nm, bound coherent scattering length
    const double abs_cs = 2.56e-10; // nm^2, absorption cross-section for 2200 m/s neutrons
    const double basic_wavelength = 0.1798197; // nm, wavelength of 2200 m/s neutrons
    const double mol_mass = 55.845;            // g/mol, Fe molar mass
    const double avog_number = 6.022e23;       // mol^{-1}, Avogadro number
    const double density = 7.874e-21;          // g/nm^3, Fe material density
    const double number_density = avog_number * density / mol_mass; // 1/nm^3, Fe number density
    const double sld_real = number_density * bc;
    const double sld_imag = number_density * abs_cs / (2.0 * basic_wavelength);
    const double sq_angstroms = Units::angstrom * Units::angstrom;

    const complex_t sld_ref(8.0241e-04, // nm^{-2}, reference data
                            6.0448e-8); // taken from https://sld-calculator.appspot.com/

    // checking input data and reference values are the same
    EXPECT_NEAR(2.0 * (sld_ref.real() - sld_real) / (sld_ref.real() + sld_real), 0.0, 1e-3);
    EXPECT_NEAR(2.0 * (sld_ref.imag() - sld_imag) / (sld_ref.imag() + sld_imag), 0.0, 1e-3);

    const double wl_factor_2200 = basic_wavelength * basic_wavelength / pi;
    const double wl_factor_1100 = 4.0 * basic_wavelength * basic_wavelength / pi;

    // MaterialBySLD accepts sld in AA^{-2}
    const SLDMaterial material("Fe", TEST_COLOR, sld_real * sq_angstroms, sld_imag * sq_angstroms);

    complex_t sld_res_2200 =
        (1.0 - material.squaredRefractiveIndex(basic_wavelength)) / wl_factor_2200;
    complex_t sld_res_1100 =
        (1.0 - material.squaredRefractiveIndex(2.0 * basic_wavelength)) / wl_factor_1100;

    // change the sign of imaginary part according to internal convention
    sld_res_2200 = std::conj(sld_res_2200);
    sld_res_1100 = std::conj(sld_res_1100);

    EXPECT_NEAR(0.0, (sld_ref.real() - sld_res_2200.real()) / sld_ref.real(), 1e-3);
    EXPECT_NEAR(0.0, (sld_ref.imag() - sld_res_2200.imag()) / sld_ref.imag(), 1e-3);
    EXPECT_NEAR(0.0, (sld_ref.real() - sld_res_1100.real()) / sld_ref.real(), 1e-3);
    EXPECT_NEAR(0.0, (sld_ref.imag() - sld_res_1100.imag()) / sld_ref.imag(), 1e-3);

    const complex_t refr_index = material.refractiveIndex(2.0 * basic_wavelength);
    WavevectorInfo wv_info(C3{}, C3{}, 2.0 * basic_wavelength);

    RefractiveMaterial material2("Fe", TEST_COLOR, 1.0 - refr_index.real(),
                                 std::abs(refr_index.imag()));
    const complex_t subtrSLD = material2.scalarSubtrSLD(wv_info);
    const complex_t subtrSLDWlIndep = material.scalarSubtrSLD(wv_info);
    EXPECT_NEAR(subtrSLD.real(), subtrSLDWlIndep.real(), 1e-10);
    EXPECT_NEAR(subtrSLD.imag(), subtrSLDWlIndep.imag(), 1e-10);
}

TEST(Material, AverageRefractiveMaterials)
{
    R3 magnetization = R3{1.0, 0.0, 0.0};
    const RefractiveMaterial material("Material", TEST_COLOR, 0.5, 0.5, magnetization);
    std::vector<OneAdmixture> regions;
    regions.emplace_back(0.25, material);
    regions.emplace_back(0.25, material);

    const auto material_avr = MixUtil::basePlusAdmixtures(material, 1.0, regions);
    EXPECT_EQ(material_avr->magnetization(), magnetization);
    EXPECT_DOUBLE_EQ(material_avr->rawData().real(), 0.5);
    EXPECT_DOUBLE_EQ(material_avr->rawData().imag(), 0.5);
    EXPECT_TRUE(material_avr->hasNativeRefractiveIndex());

    // Mixing SLD base with refractive admixtures now works - returns SLDMaterial
    const SLDMaterial material3("Material3", TEST_COLOR, 0.5, 0.5, magnetization);
    const auto mixed_avr = MixUtil::basePlusAdmixtures(material3, 1.0, regions);
    EXPECT_TRUE(mixed_avr->hasNativeSLD());
    EXPECT_EQ(mixed_avr->magnetization(), magnetization);
}

TEST(Material, AverageSLDMaterials)
{
    R3 magnetization = R3{1.0, 0.0, 0.0};
    const SLDMaterial material("Material", TEST_COLOR, 0.5, 0.5, magnetization);
    std::vector<OneAdmixture> regions;
    regions.emplace_back(0.25, material);
    regions.emplace_back(0.25, material);

    const auto material_avr = MixUtil::basePlusAdmixtures(material, 1.0, regions);
    EXPECT_EQ(material_avr->magnetization(), magnetization);
    EXPECT_DOUBLE_EQ(material_avr->rawData().real(), 0.5);
    EXPECT_DOUBLE_EQ(material_avr->rawData().imag(), 0.5);
    EXPECT_TRUE(material_avr->hasNativeSLD());
}

TEST(Material, AverageMixedMaterials)
{
    R3 magnetization = R3{1.0, 0.0, 0.0};
    const RefractiveMaterial refrMat("RefrMat", TEST_COLOR, 0.5, 0.5, magnetization);
    const SLDMaterial sldMat("SLDMat", TEST_COLOR, 0.5, 0.5, magnetization);

    // Mix refractive with SLD - should work and return SLDMaterial
    std::vector<OneAdmixture> regions;
    regions.emplace_back(0.25, refrMat);
    regions.emplace_back(0.25, sldMat);

    const auto mixed_avr = MixUtil::basePlusAdmixtures(sldMat, 1.0, regions);
    EXPECT_TRUE(mixed_avr->hasNativeSLD());
    EXPECT_EQ(mixed_avr->magnetization(), magnetization);

    // Also test with refractive base
    const auto mixed_avr2 = MixUtil::basePlusAdmixtures(refrMat, 1.0, regions);
    EXPECT_TRUE(mixed_avr2->hasNativeSLD());
    EXPECT_EQ(mixed_avr2->magnetization(), magnetization);
}

TEST(Material, MaterialComparison)
{
    const SLDMaterial material("Material", TEST_COLOR, 1.0, 1.0);
    const RefractiveMaterial material2("Material", TEST_COLOR, 1.0, 1.0);
    EXPECT_TRUE(material == material);
    EXPECT_FALSE(material != material);
    EXPECT_FALSE(material == material2);

    const RefractiveMaterial material3("Material3", TEST_COLOR, 2.0, 2.0);
    EXPECT_FALSE(material3 == material2);
    EXPECT_TRUE(material3 != material2);

    const RefractiveMaterial material4("Material", TEST_COLOR, 1.0, 1.0, R3{1.0, 2.0, 3.0});
    EXPECT_FALSE(material4 == material2);
    EXPECT_TRUE(material4 != material2);
}
