/*
 *  Copyright (c) 2000-2022 Inria
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions are met:
 *
 *  * Redistributions of source code must retain the above copyright notice,
 *  this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright notice,
 *  this list of conditions and the following disclaimer in the documentation
 *  and/or other materials provided with the distribution.
 *  * Neither the name of the ALICE Project-Team nor the names of its
 *  contributors may be used to endorse or promote products derived from this
 *  software without specific prior written permission.
 * 
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 *
 *  Contact: Bruno Levy
 *
 *     https://www.inria.fr/fr/bruno-levy
 *
 *     Inria,
 *     Domaine de Voluceau,
 *     78150 Le Chesnay - Rocquencourt
 *     FRANCE
 *
 */

#include <geogram_gfx/gui/simple_application.h>
#include <geogram_gfx/GLUP/GLUP_private.h>
#include <geogram/delaunay/periodic_delaunay_3d.h>


namespace {

    using namespace GEO;

    /**
     * \brief An application that demonstrates both
     *  GLUP primitives and glup_viewer application
     *  framework.
     */
    class DemoDelaunay3dApplication : public SimpleApplication {
    public:

        /**
         * \brief DemoDelaunay3dApplication constructor.
         */
        DemoDelaunay3dApplication() : SimpleApplication("Delaunay3d") {

	    periodic_ = false;
	    
            // Define the 3d region that we want to display
            // (xmin, ymin, zmin, xmax, ymax, zmax)
            set_region_of_interest(0.0, 0.0, 0.0, 1.0, 1.0, 1.0);

	    draw_points_ = false;
	    draw_cells_ = true;
	    point_size_ = 10.0f;
	    cells_shrink_ = 0.1f;
	    nb_points_ = 100;
	    draw_box_ = false;
	    draw_period_ = false;
	    
	    start_animation();
        }

	void geogram_initialize(int argc, char** argv) override {
	    SimpleApplication::geogram_initialize(argc, argv);
	    delaunay_ = new PeriodicDelaunay3d(periodic_, 1.0);
	    // In non-periodic mode, we need to keep the infinite
	    // tetrahedra to be able to query the combinatorics of
	    // all cells (including the infinite ones).
	    if(!periodic_) {
		delaunay_->set_keeps_infinite(true);
	    }
	    init_random_points(nb_points_);
	}
	
        /**
         * \brief DemoDelaunay3dApplication destructor.
         */
	~DemoDelaunay3dApplication() override {
        }
        
        /**
         * \brief Displays and handles the GUI for object properties.
         * \details Overloads Application::draw_object_properties().
         */
	void draw_object_properties() override {
	    SimpleApplication::draw_object_properties();	    
	    ImGui::Checkbox("Animate", animate_ptr());
	    
	    if(ImGui::Button("One iteration", ImVec2(-1.0, 0.0))) {
		Lloyd_iteration();
	    }

	    if(
		ImGui::Button(
		    "+",
		    ImVec2(-ImGui::GetContentRegionAvail().x/2.0f,0.0f)
		) && nb_points_ < 10000
	    ) {
		++nb_points_;
		init_random_points(nb_points_);				
	    }
	    ImGui::SameLine();
	    if(
		ImGui::Button(
		    "-", ImVec2(-1.0f, 0.0f)
		) && nb_points_ > 4
	    ) {
		--nb_points_;
		init_random_points(nb_points_);				
	    }
	    
	    if(ImGui::SliderInt("N", &nb_points_, 4, 10000)) {
		init_random_points(nb_points_);		
	    }
	    
	    if(ImGui::Button("Reset", ImVec2(-1.0, 0.0))) {
		init_random_points(nb_points_);
	    }
	    
	    if(ImGui::Checkbox("Periodic", &periodic_)) {
		delaunay_ = new PeriodicDelaunay3d(periodic_, 1.0);
		// In non-periodic mode, we need to keep the infinite
		// tetrahedra to be able to query the combinatorics of
		// all cells (including the infinite ones).
		if(!periodic_) {
		    delaunay_->set_keeps_infinite(true);
		}
		delaunay_->set_vertices(points_.size()/3, points_.data());
		delaunay_->compute();
	    }

	    ImGui::Spacing();
	    ImGui::Separator();
	    ImGui::Spacing();
	    
	    ImGui::Checkbox("Box", &draw_box_);
	    if(ImGui::Checkbox("Period 3x3x3", &draw_period_)) {
		if(draw_period_) {
		    set_region_of_interest(
			-1.0, -1.0, -1.0, 2.0, 2.0, 2.0
		    );
		} else {
		    set_region_of_interest(
			0.0, 0.0, 0.0, 1.0, 1.0, 1.0
		    );
		}
	    }
	    ImGui::Checkbox("Points", &draw_points_);
	    ImGui::SameLine();
	    ImGui::SliderFloat("##PtSz.", &point_size_, 1.0f, 50.0f, "%.1f");
	    ImGui::Checkbox("Cells", &draw_cells_);
	    ImGui::SameLine();
	    ImGui::SliderFloat("##Shrk.", &cells_shrink_, 0.0f, 1.0f, "%.2f");
        }

	/**
	 * \brief Gets a Voronoi cell.
	 * \details In non-periodic mode, the cell is clipped by the domain.
	 * \param[in] v the index of the vertex
	 * \param[out] C the cell
	 */
	void get_cell(index_t v, ConvexCell& C) {
	    delaunay_->copy_Laguerre_cell_from_Delaunay(v, C, W_);
	    if(!periodic_) {
		C.clip_by_plane(vec4( 1.0, 0.0, 0.0, 0.0));
		C.clip_by_plane(vec4(-1.0, 0.0, 0.0, 1.0));
		C.clip_by_plane(vec4( 0.0, 1.0, 0.0, 0.0));
		C.clip_by_plane(vec4( 0.0,-1.0, 0.0, 1.0));	
		C.clip_by_plane(vec4( 0.0, 0.0, 1.0, 0.0));
		C.clip_by_plane(vec4( 0.0, 0.0,-1.0, 1.0));
	    }
	    C.compute_geometry();
	}

	/**
	 * \brief Draws a cell.
	 * \details Needs to be called between glupBegin(GLUP_TRIANGLES) 
	 *  and glupEnd().
	 */
	void draw_cell(ConvexCell& C, index_t instance = 0) {
	    double s = double(cells_shrink_);
	    double Tx = double(Periodic::translation[instance][0]);
	    double Ty = double(Periodic::translation[instance][1]);
	    double Tz = double(Periodic::translation[instance][2]);
	    
	    vec3 g;
	    if(cells_shrink_ != 0.0f) {
		g = C.barycenter();
	    }

	    // For each vertex of the Voronoi cell
	    // Start at 1 (vertex 0 is point at infinity)
	    for(index_t v=1; v<C.nb_v(); ++v) {
		index_t t = C.vertex_triangle(v);

		//  Happens if a clipping plane did not
		// clip anything.
		if(t == VBW::END_OF_LIST) {
		    continue;
		}

		//   Now iterate on the Voronoi vertices of the
		// Voronoi facet. In dual form this means iterating
		// on the triangles incident to vertex v
		
		vec3 P[3];
		index_t n=0;
		do {

		    //   Triangulate the Voronoi facet and send the
		    // triangles to OpenGL/GLUP.
		    if(n == 0) {
			P[0] = C.triangle_point(VBW::ushort(t));
		    } else if(n == 1) {
			P[1] = C.triangle_point(VBW::ushort(t));
		    } else {
			P[2] = C.triangle_point(VBW::ushort(t));
			if(s == 0.0) {
			    for(index_t i=0; i<3; ++i) {
				glupPrivateVertex3d(
				    P[i].x + Tx, P[i].y + Ty, P[i].z + Tz
				);
			    }
			} else {
			    for(index_t i=0; i<3; ++i) {  
				glupPrivateVertex3d(
				    s*g.x + (1.0-s)*P[i].x + Tx,
				    s*g.y + (1.0-s)*P[i].y + Ty,
				    s*g.z + (1.0-s)*P[i].z + Tz
				);
			    }
			}
			P[1] = P[2];
		    }

		    //   Move to the next triangle incident to
		    // vertex v.
		    index_t lv = C.triangle_find_vertex(t,v);		   
		    t = C.triangle_adjacent(t, (lv + 1)%3);
		    
		    ++n;
		} while(t != C.vertex_triangle(v));
	    }
	}
	
        /**
         * \brief Draws the scene according to currently set primitive and
         *  drawing modes.
         */
        void draw_scene() override {
	    // Avoid re-entry, for instance when a message is sent to
	    // the logger, this triggers a graphic redisplay.
	    static bool locked = false;
	    if(locked) {
		return;
	    }
	    locked = true;
	    if(animate()) {
		Lloyd_iteration();
	    }
            glupSetColor3f(GLUP_FRONT_AND_BACK_COLOR, 0.5f, 0.5f, 0.5f);
	    glupEnable(GLUP_LIGHTING);

	    index_t max_instance = (draw_period_ ? 27 : 1);


	    
	    if(draw_points_) {
		double R = double(point_size_) / 250.0;

		for(index_t i=0; i<max_instance; ++i) {
		    glupTranslated(
			double(Periodic::translation[i][0]),
			double(Periodic::translation[i][1]),
			double(Periodic::translation[i][2])
	    	    );
		
		    glupBegin(GLUP_SPHERES);
		    for(index_t v=0; v<points_.size()/3; ++v) {
			glupPrivateVertex4d(
			    points_[3*v],
			    points_[3*v+1],
			    points_[3*v+2],
			    R
			);
		    }
		    glupEnd();

		    glupTranslated(
			-double(Periodic::translation[i][0]),
			-double(Periodic::translation[i][1]),
			-double(Periodic::translation[i][2])
	    	    );
		}
		
	    }

	    if(draw_cells_) {
		ConvexCell C;
		glupBegin(GLUP_TRIANGLES);
		for(index_t v=0; v<points_.size()/3; ++v) {
		    get_cell(v, C);
		    if(draw_period_) {
			for(index_t i=0; i<27; ++i) {
			    draw_cell(C,i);
			}
		    } else {
			draw_cell(C);
		    }
		}
		glupEnd();
	    }

	    if(draw_box_) {
		static GLfloat cube[8][3] = {
		    {0.0f, 0.0f, 0.0f},
		    {0.0f, 0.0f, 1.0f},
		    {0.0f, 1.0f, 0.0f},
		    {0.0f, 1.0f, 1.0f},
		    {1.0f, 0.0f, 0.0f},
		    {1.0f, 0.0f, 1.0f},
		    {1.0f, 1.0f, 0.0f},
		    {1.0f, 1.0f, 1.0f}
		};
		
		
		glupEnable(GLUP_DRAW_MESH);
		glupEnable(GLUP_ALPHA_DISCARD);
		glupSetColor4f(
		    GLUP_FRONT_AND_BACK_COLOR, 1.0f, 1.0f, 1.0f, 0.0f
		);		
		glupSetMeshWidth(10);
		glupDisable(GLUP_LIGHTING);

		for(index_t i=0; i<max_instance; ++i) {
		    glupTranslated(
			double(Periodic::translation[i][0]),
			double(Periodic::translation[i][1]),
			double(Periodic::translation[i][2])
	    	    );
		    
		    glupBegin(GLUP_QUADS);
		    
		    glupVertex3fv(cube[0]);
		    glupVertex3fv(cube[1]);
		    glupVertex3fv(cube[3]);
		    glupVertex3fv(cube[2]);
		    
		    glupVertex3fv(cube[4]);
		    glupVertex3fv(cube[5]);
		    glupVertex3fv(cube[7]);
		    glupVertex3fv(cube[6]);
		    
		    glupVertex3fv(cube[0]);
		    glupVertex3fv(cube[1]);
		    glupVertex3fv(cube[5]);
		    glupVertex3fv(cube[4]);
		    
		    glupVertex3fv(cube[1]);
		    glupVertex3fv(cube[3]);
		    glupVertex3fv(cube[7]);
		    glupVertex3fv(cube[5]);
		    
		    glupVertex3fv(cube[3]);
		    glupVertex3fv(cube[2]);
		    glupVertex3fv(cube[6]);
		    glupVertex3fv(cube[7]);
		    
		    glupVertex3fv(cube[2]);
		    glupVertex3fv(cube[0]);
		    glupVertex3fv(cube[4]);
		    glupVertex3fv(cube[6]);
		
		    glupEnd();

		    glupTranslated(
			-double(Periodic::translation[i][0]),
			-double(Periodic::translation[i][1]),
			-double(Periodic::translation[i][2])
	    	    );
		}


		
		glupDisable(GLUP_DRAW_MESH);
		glupDisable(GLUP_ALPHA_DISCARD);
	    }
	    
	    locked = false;
        }

	/**
	 * \brief Initializes the Delaunay triangulation with 
	 *  random points.
	 * \param[in] nb_points_in number of points
	 */
	void init_random_points(int nb_points_in) {
	    index_t nb_points = index_t(nb_points_in);
	    bool bkp = draw_cells_;
	    draw_cells_ = false;
	    points_.resize(3*nb_points);
	    for(index_t i=0; i<3*nb_points; ++i) {
		points_[i] = Numeric::random_float64();
	    }
	    delaunay_->set_vertices(nb_points, points_.data());
	    delaunay_->compute();
	    draw_cells_ = bkp;
	}

	/**
	 * \brief Does one iteration of Lloyd relaxation.
	 * \details Moves each point to the centroid of its
	 *  Voronoi cell. In non-periodic mode, the Voronoi
	 *  cell is clipped by the domain.
	 */
	void Lloyd_iteration() {
	    vector<double> new_points(points_.size());
	    ConvexCell C;
	    for(index_t v=0; v<points_.size()/3; ++v) {
		get_cell(v, C);
		vec3 g = C.barycenter();
		new_points[3*v]   = g.x;
		new_points[3*v+1] = g.y;
		new_points[3*v+2] = g.z;		
	    }
	    // In periodic mode, points may escape out of
	    // the domain. Relocate them in [0,1]^3
	    for(index_t i=0; i<new_points.size(); ++i) {
		if(new_points[i] < 0.0) {
		    new_points[i] += 1.0;
		}
		if(new_points[i] > 1.0) {
		    new_points[i] -= 1.0;
		}
	    }
	    points_.swap(new_points);
	    delaunay_->set_vertices(points_.size()/3, points_.data());
	    delaunay_->compute();
	}
	
    private:
	SmartPointer<PeriodicDelaunay3d> delaunay_;
	bool periodic_;
	bool draw_points_;
        float point_size_;
	bool draw_cells_;
	bool draw_box_;
	bool draw_period_;
	float cells_shrink_;
	vector<double> points_;
	int nb_points_;
	PeriodicDelaunay3d::IncidentTetrahedra W_;
    };
      
}

int main(int argc, char** argv) {
    DemoDelaunay3dApplication app;
    app.start(argc, argv);
    return 0;
}
