#include "Base/Const/Units.h"

#include "Base/Const/PhysicalConstants.h"
#include "Base/Vector/RotMatrix.h"
#include "Base/Vector/WavevectorInfo.h"
#include "Sample/Material/MaterialFactoryFuncs.h"
#include "Sample/Material/MaterialUtil.h"
#include "Sample/Scattering/Rotations.h"
#include "Tests/GTestWrapper/google_test.h"
#include <iostream>

using PhysConsts::pi;

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);

    Material material = RefractiveMaterial("MagMaterial", refIndex, magnetism);
    EXPECT_EQ(material_data, material.refractiveIndex_or_SLD());
    EXPECT_EQ(magnetism, material.magnetization());

    Material material2 =
        RefractiveMaterial("MagMaterial", material_data.real(), material_data.imag(), magnetism);
    EXPECT_EQ(material_data, material2.refractiveIndex_or_SLD());
    EXPECT_EQ(magnetism, material2.magnetization());

    Material material3 =
        MaterialBySLD("MagMaterial", material_data.real(), material_data.imag(), magnetism);
    EXPECT_EQ(material_data, material3.refractiveIndex_or_SLD());
    EXPECT_EQ(magnetism, material3.magnetization());

    R3 default_magnetism = R3{};

    Material material4 = RefractiveMaterial("Material", refIndex);
    EXPECT_EQ(material_data, material4.refractiveIndex_or_SLD());
    EXPECT_EQ(default_magnetism, material4.magnetization());

    Material material5 = RefractiveMaterial("Material", material_data.real(), material_data.imag());
    EXPECT_EQ(material_data, material5.refractiveIndex_or_SLD());
    EXPECT_EQ(default_magnetism, material5.magnetization());

    Material material6 = MaterialBySLD("Material", 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);

    Material material = RefractiveMaterial("Material", refIndex, magnetism);
    Material material2 = material.rotatedMaterial(transform.rotMatrix());
    EXPECT_EQ(material_data, material2.refractiveIndex_or_SLD());
    // 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);

    Material material3 =
        MaterialBySLD("Material", material_data.real(), material_data.imag(), magnetism);
    Material material4 = material.rotatedMaterial(transform.rotMatrix());
    EXPECT_EQ(material_data, material4.refractiveIndex_or_SLD());
    // 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, DefaultMaterials)
{
    Material material = Vacuum();
    const double dummy_wavelength = 1.0;

    EXPECT_EQ(material.refractiveIndex_or_SLD(), complex_t());
    EXPECT_EQ(material.refractiveIndex_or_SLD(), MaterialBySLD().refractiveIndex_or_SLD());

    EXPECT_EQ(material.magnetization(), R3{});
    EXPECT_EQ(material.magnetization(), MaterialBySLD().magnetization());

    EXPECT_EQ(material.refractiveIndex(dummy_wavelength), complex_t(1.0, 0.0));
    EXPECT_EQ(material.refractiveIndex(dummy_wavelength),
              MaterialBySLD().refractiveIndex(dummy_wavelength));

    EXPECT_TRUE(material.typeID() == Vacuum().typeID());
    EXPECT_FALSE(material.typeID() == MaterialBySLD().typeID());
}

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}
    Material material = MaterialBySLD("Fe", sld_real * sq_angstroms, sld_imag * sq_angstroms);

    complex_t sld_res_2200 = (1.0 - material.refractiveIndex2(basic_wavelength)) / wl_factor_2200;
    complex_t sld_res_1100 =
        (1.0 - material.refractiveIndex2(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);

    Material material2 =
        RefractiveMaterial("Fe", 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 Material material = RefractiveMaterial("Material", 0.5, 0.5, magnetization);
    const Admixtures regions = {{OneAdmixture{0.25, material}, OneAdmixture{0.25, material}}};

    const Material material_avr = MaterialUtil::averagedMaterial(material, regions);
    EXPECT_EQ(material_avr.magnetization(), magnetization);
    EXPECT_DOUBLE_EQ(material_avr.refractiveIndex_or_SLD().real(), 0.5);
    EXPECT_DOUBLE_EQ(material_avr.refractiveIndex_or_SLD().imag(), 0.5);
    EXPECT_TRUE(material_avr.typeID() == MATERIAL_TYPES::RefractiveMaterial);

    const Material material3 = MaterialBySLD("Material3", 0.5, 0.5, magnetization);
    EXPECT_THROW(MaterialUtil::averagedMaterial(material3, regions), std::runtime_error);
}

TEST(Material, AverageSLDMaterials)
{
    R3 magnetization = R3{1.0, 0.0, 0.0};
    const Material material = MaterialBySLD("Material", 0.5, 0.5, magnetization);
    const Admixtures regions = {{OneAdmixture{0.25, material}, OneAdmixture{0.25, material}}};

    const Material material_avr = MaterialUtil::averagedMaterial(material, regions);
    EXPECT_EQ(material_avr.magnetization(), magnetization);
    EXPECT_DOUBLE_EQ(material_avr.refractiveIndex_or_SLD().real(), 0.5);
    EXPECT_DOUBLE_EQ(material_avr.refractiveIndex_or_SLD().imag(), 0.5);
    EXPECT_TRUE(material_avr.typeID() == MATERIAL_TYPES::MaterialBySLD);
}

TEST(Material, AverageMixedMaterials)
{
    R3 magnetization = R3{1.0, 0.0, 0.0};
    const Material material = RefractiveMaterial("Material", 0.5, 0.5, magnetization);
    const Admixtures regions = {{OneAdmixture{0.25, material}, OneAdmixture{0.25, material}}};

    const Material material3 = MaterialBySLD("Material3", 0.5, 0.5, magnetization);
    EXPECT_THROW(MaterialUtil::averagedMaterial(material3, regions), std::runtime_error);
}

TEST(Material, TypeIds)
{
    Material material = MaterialBySLD("Material", 1.0, 1.0);
    Material material2 = RefractiveMaterial("Material", 1.0, 1.0);
    EXPECT_TRUE(material.typeID() == MATERIAL_TYPES::MaterialBySLD);
    EXPECT_TRUE(material2.typeID() == MATERIAL_TYPES::RefractiveMaterial);
    EXPECT_TRUE(material.typeID() != material2.typeID());
    Material material3 = MaterialBySLD("Material", 1.0, 1.0);
    EXPECT_TRUE(material.typeID() == material3.typeID());
}

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

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

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

    EXPECT_FALSE(Vacuum() == MaterialBySLD());
    EXPECT_TRUE(Vacuum() != MaterialBySLD());
}

TEST(Material, MaterialCopy)
{
    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);
    Material material = RefractiveMaterial("MagMaterial", refIndex, magnetism);

    const Material& copy = material;
    EXPECT_EQ(material_data, copy.refractiveIndex_or_SLD());
    EXPECT_EQ(magnetism, copy.magnetization());
    EXPECT_TRUE(material == copy);
}
