/*---------------------------------------------------------------------------*\

    geophase: A C++ template library for geometrical operations on non-convex
    polyhedra with non-planar faces
     
    Copyright (C) 2019 Tomislav Maric, TU Darmstadt 

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

Application
    geophaseUnitTests 

Description
    Unit tests. 

Authors
    Tomislav Maric maric@mma.tu-darmstadt.de

\*---------------------------------------------------------------------------*/

#include "Halfspace.hpp"
#include "Polyhedron.hpp"
#include "Distance.hpp"
#include "WriteVtkPolyData.hpp"
#include "ReadVtkPolyData.hpp"
#include "Make.hpp"

using namespace geophase;

// TESTING
#include "gtest/gtest.h" 
#include "testUtilities.hpp"
#include <random>

// Traits tests.
static_assert(std::is_same_v<
        std::enable_if_t<type_tagged<vector, vector_tag>(), bool>, 
        enable_if_type_tagged_t<vector, vector_tag, bool>
        >
    );

static_assert(std::is_same_v<
        std::enable_if_t<type_tagged<halfspace, halfspace_tag>(), bool>, 
        enable_if_type_tagged_t<halfspace, halfspace_tag, bool>
        >
    );

struct dummy_tag {};

static_assert(!types_tagged<vector1Dd, dummy_tag, vector2Dd, vector_tag, vector, vector_tag>());
static_assert(!types_tagged<vector1Dd, vector_tag, vector2Dd, dummy_tag, vector, vector_tag>());
static_assert(!types_tagged<vector1Dd, vector_tag, vector2Dd, vector_tag, vector, dummy_tag>());

static_assert(types_tagged_with_tag <vector_tag, vector1Dd, vector2Dd, vector>()); 
static_assert(!types_tagged_with_tag <dummy_tag, vector1Dd, vector2Dd, vector>()); 

// Helper functions
template<typename Container,  typename Ilist> 
bool container_equals_ilist(Container const& cont, Ilist const& ilist)
{
    if (cont.size() != ilist.size()) 
        return false; 

    auto cit = cont.cbegin(); 
    auto ilit = ilist.begin(); 
    for (decltype(cont.size()) i = 0; i < cont.size(); ++i)
        if (*std::next(cit,i) != *std::next(ilit,i))
            return false; 
    return true;
}

template<typename Type, template <typename> typename Allocator=std::allocator> 
void testSequenceTag()
{
    static_assert(std::is_same_v
        <
            tag_t<std::vector<Type, Allocator<Type>>>, 
            sequence_tag
        >
    );
    static_assert(std::is_same_v
        <
            tag_t<std::list<Type, Allocator<Type>>>, 
            sequence_tag
        >
    );
    static_assert(std::is_same_v
        <
            tag_t<std::deque<Type, Allocator<Type>>>, 
            sequence_tag
        >
    );
}

TEST(tag, sequence)
{
    testSequenceTag<point>(); 
    testSequenceTag<vectorPolygon>(); 
    testSequenceTag<triangle>(); 
    testSequenceTag<rectangle>(); 
    testSequenceTag<hexagon>(); 
    testSequenceTag<vectorPolyhedron>(); 
}

// Tests 
TEST(vector1D, arithmetic)
{
    vector1Dd p1 {1.};  
    vector1Dd q1 {1.};  

    std::cout << "WKT format stream p1= " 
        << p1 << std::endl;

    auto r1 = p1 + q1;
    ASSERT_EQ(r1[0], p1[0] + q1[0]); 

    r1 = p1 - q1;
    ASSERT_EQ(r1[0], p1[0] - q1[0]); 

    r1 += q1; 
    ASSERT_EQ(r1[0], q1[0]); 
    r1 -= q1; 
    ASSERT_EQ(r1[0], 0.); 

    r1 = p1 * 2.;  
    ASSERT_EQ(r1[0], 2*p1[0]); 
    r1 /= 2.; 
    ASSERT_EQ(r1[0], p1[0]); 

}

TEST(vector3D, arithmetic)
{
    vector p1 {1.,2.,3.};  
    vector q1 {1.,2.,3.};  

    std::cout << "WKT format stream = p1 = " 
        << p1 << std::endl;

    // Decay to std::array works.
    ASSERT_TRUE(p1 == q1); 

    // Test arithmetic
    auto r1 = p1 + q1;
    ASSERT_EQ(r1[0], p1[0] + q1[0]); 
    ASSERT_EQ(r1[1], p1[1] + q1[1]); 
    ASSERT_EQ(r1[2], p1[2] + q1[2]); 

    r1 = p1 - q1;
    ASSERT_EQ(r1[0], p1[0] - q1[0]); 
    ASSERT_EQ(r1[1], p1[1] - q1[1]); 
    ASSERT_EQ(r1[2], p1[2] - q1[2]); 

    r1 += q1; 
    ASSERT_EQ(r1[0], q1[0]); 
    ASSERT_EQ(r1[1], q1[1]); 
    ASSERT_EQ(r1[2], q1[2]); 

    r1 -= q1; 
    ASSERT_EQ(r1[0], 0.); 
    ASSERT_EQ(r1[1], 0.); 
    ASSERT_EQ(r1[2], 0.); 

    r1 = p1 * 2.;  
    ASSERT_EQ(r1[0], 2*p1[0]); 
    ASSERT_EQ(r1[1], 2*p1[1]); 
    ASSERT_EQ(r1[2], 2*p1[2]); 

    r1 /= 2.; 
    ASSERT_EQ(r1[0], p1[0]); 
    ASSERT_EQ(r1[1], p1[1]); 
    ASSERT_EQ(r1[2], p1[2]); 

    ASSERT_EQ(
        sum_sqr(p1), 
        p1[0]*p1[0] + 
        p1[1]*p1[1] + 
        p1[2]*p1[2] 
    );

    ASSERT_EQ(mag(p1), sqrt(sum_sqr(p1)));

    vector e1{1.,0.,0.}; 
    vector e2{0.,1.,0.}; 
    vector e3{0.,0.,1.}; 

    const auto EPS = 
        std::numeric_limits<vector::value_type>::epsilon();

    ASSERT_EQ(cross(e1,e2), e3); 
    ASSERT_EQ(cross(e2,e1), -e3); 
    ASSERT_LE(mag(cross(e1,e3)+e2), EPS); 
    ASSERT_EQ(cross(e3,e1), e2); 
    ASSERT_EQ(cross(e2,e3), e1);
    ASSERT_LE(mag(cross(e3,e2)+e1), EPS); 
}

TEST(Halfspace, unit)
{
    constexpr auto POSITION  = vector {1.,2.,3.};
    constexpr auto DIRECTION = vector {1.,1.,1.};

    halfspace hDef; 
    halfspace hCmpt (POSITION, DIRECTION);
    halfspace hCopy(hCmpt); 

    ASSERT_TRUE(hCopy == hCmpt); 

    auto hMove (halfspace(POSITION, DIRECTION)); 

    ASSERT_TRUE(hMove == hCmpt); 

    auto hFlip (hCopy); 
    hFlip.flip(); 
    ASSERT_TRUE(hFlip.direction() == hCopy.direction() * -1.); 
    ASSERT_TRUE(hFlip.direction() == -1.*hCopy.direction()); 

    hFlip = std::make_pair(DIRECTION, POSITION); 

    ASSERT_EQ(hFlip.direction(), POSITION);  
    ASSERT_EQ(hFlip.position(), DIRECTION); 

    hFlip.setDirection(DIRECTION); 
    hFlip.setPosition(POSITION); 

    ASSERT_EQ(hFlip, halfspace(POSITION, DIRECTION));  
}


TEST(Polygon, unit)
{
    // Empty construction available.
    vectorPolygon vp = {};  
    listPolygon   lp = {}; 
    dequePolygon  dp = {}; 

    std::cout << "WKT format stream ap = " 
        << make_unit<rectangle>() << std::endl; 

    // Initializer (bracketed) list initialization available works.
    std::initializer_list<vector> ILIST = {{0.,0.,0.}, {1.,0.,0.},{1.,1.,0.},{0.,1.,0.}};
    vectorPolygon vpIlist (ILIST);
    dequePolygon  dpIlist (ILIST);  
    listPolygon   lpIlist (ILIST);  
    rectangle     apIlist = {vector{0.,0.,0.}, vector{1.,0.,0.},
                             vector{1.,1.,0.}, vector{0.,1.,0.}};

    ASSERT_TRUE(container_equals_ilist(vpIlist, ILIST));
    ASSERT_TRUE(container_equals_ilist(dpIlist, ILIST));
    ASSERT_TRUE(container_equals_ilist(lpIlist, ILIST));
    ASSERT_TRUE(container_equals_ilist(apIlist, ILIST));
}

TEST(AxisAlignedBoundingBox, unit)
{
    vector min = point(0.,0.,0.);  
    vector max = point(1.,1.,1.);
    auto box = make<aabbox>(min,max);
    ASSERT_TRUE(box.minPoint() == min);
    ASSERT_TRUE(box.maxPoint() == max);
}

TEST(distance, HalfspaceToPoint)
{
    halfspace hspace(vector{0.,0.,0.}, vector{1.,1.,1.});
    vector testPoint {0.,0.,0.}; 

    ASSERT_EQ(signed_distance(testPoint, hspace), 0.); 
    ASSERT_EQ(signed_distance(hspace, testPoint), 0.); 
    ASSERT_EQ(signed_distance(hspace, testPoint), signed_distance(testPoint,hspace)); 
}

TEST(vtkPolyData, Point)
{
    vector v1write(1.00000000000007,1.,1.); 
    vector v2write(2.00000000000127,3.,0.1); 

    // Write two points 
    vtkPolyDataOStream os (fullTestFileName()); 
    os << v1write << v2write;  

    // Write a collection of random 3D points as vertices into ASCII VTK file. 
    std::random_device rd;  //Will be used to obtain a seed for the random number engine
    std::mt19937 gen(rd()); //Standard mersenne_twister_engine seeded with rd()
    std::uniform_real_distribution<> dis(1.0, 2.0);

    const std::size_t NPOINTS = 100;
    std::vector<vector> randomPoints; 
    randomPoints.reserve(NPOINTS); 

    for (std::size_t n = 0; n < NPOINTS; ++n) {
        randomPoints.emplace_back(vector(dis(gen), dis(gen), dis(gen)));
    }   
    os << randomPoints; 

    // Close the output stream (file). 
    os.close(); 
    
    // Read two points 
    vtkPolyDataIStream is (fullTestFileName()); 

    vector v1read;
    vector v2read; 

    is >> v1read >> v2read; 

    ASSERT_EQ(v1write, v1read); 
    ASSERT_EQ(v2write, v2read); 

    // Read randomly generated and written points.
    std::vector<vector> inRandomPoints; 
    is >> inRandomPoints;  

    for (std::size_t i = 0; i < inRandomPoints.size(); ++i)
    {
        ASSERT_EQ(randomPoints[i], inRandomPoints[i]); 
    }
}

TEST(vtkPolyData, Polygon)
{
    vectorPolygon poly {{0.,0.,0.}, {1.,0.,0.}, 
                        {1.,1.,0.},{0.,1.,0.}};

    rectangle sqr {vector{0.,0.,0.}, vector{1.,0.,0.}, 
                   vector{1.,1.,0.}, vector{0.,1.,0.}};

    triangle  tri {vector{0.,0.,0.}, vector{1.,0.,0.}, 
                   vector{1.,1.,0.}}; 

    vtkPolyDataOStream os(fullTestFileName()); 
    os << poly << sqr << tri; 
    os.close(); 

    vtkPolyDataIStream is(fullTestFileName()); 
    vectorPolygon ipoly; 
    rectangle isqr; 
    triangle itri;
    is >> ipoly >> isqr >> itri; 

    ASSERT_EQ(sqr, isqr);
    ASSERT_EQ(poly, ipoly);
    ASSERT_EQ(tri, itri);
}

TEST(vtkPolyData, Triangle)
{
    auto triO = make_unit<triangle>();

    write_vtk(triO, fullTestName() + "-triO.vtk");

    vtkPolyDataOStream os(fullTestFileName()); 
    os << triO << triO;  
    os.close(); 

    vtkPolyDataIStream is(fullTestFileName()); 
    triangle triI; 
    is >> triI;  

    ASSERT_EQ(triO, triI);
}

TEST(make, BoxFromPolyhedronAndBack)
{
    aabbox box(point(0.,0.,0.), point(1.,1.,1.));
    auto cube = make<vectorPolyhedron>(box); 
    auto rbox = make<aabbox>(cube); 
    ASSERT_EQ(box, rbox);
    auto cubeFromPoints = make<vectorPolyhedron>(point(0.,0.,0.), point(1.,1.,1.));
    ASSERT_EQ(cube, cubeFromPoints);
    std::cout << cube << std::endl;
}

TEST(vtkPolyData, Polyhedron)
{
    auto cube = make_unit_cube();
    auto tetrahedron = make_unit_tetrahedron();
    auto dodecahedron = make_unit_dodecahedron();
    auto endoDodecahedron = make_dodecahedron(-0.5);

    write_vtk(cube, fullTestName() + "-cube.vtk"); 
    write_vtk(tetrahedron, fullTestName() + "-tetrahedron.vtk"); 
    write_vtk(dodecahedron, fullTestName() + "-dodecahedron.vtk"); 
    write_vtk(endoDodecahedron, fullTestName() + "-endo_dodecahedron.vtk"); 

    vtkPolyDataOStream os(fullTestFileName()); 
    os << cube << tetrahedron 
        << dodecahedron << endoDodecahedron; 
    os.close(); 

    vtkPolyDataIStream is(fullTestFileName()); 

    vectorPolyhedron iCube(6), iTetrahedron(4), 
                     iDodecahedron(12), iEndoDodecahedron(12);
    is >> iCube;  
    ASSERT_EQ(cube, iCube);

    is >> iTetrahedron; 
    ASSERT_EQ(tetrahedron, iTetrahedron);

    is >> iDodecahedron; 
    ASSERT_EQ(dodecahedron, iDodecahedron)
        << "dodecahedron.size = " << dodecahedron.size() << std::endl
        << "iDodecahedron.size = " << iDodecahedron.size() << std::endl;

    is >> iEndoDodecahedron; 
    ASSERT_EQ(endoDodecahedron, iEndoDodecahedron);
}

// Main program:

int main(int argc, char *argv[])
{
    ::testing::InitGoogleTest(&argc, argv);

    return RUN_ALL_TESTS();
}


// ************************************************************************* //
