/*
 *  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
 *
 */

/*
 * GEOGRAM example program:
 * simple mesh raytracing using AABB tree.
 * GUI version of geogram/simple_raytrace
 * disclaimer: not the fastest, lighting model is ridiculous, there is no
 *  antialiasing etc... (just a demo program for the mesh AABB class).
 * mouse interaction is nearly unusable...
 * usage: geogram_demo_Raytrace
 *        geogram_demo_Raytrace meshfile.(obj|ply|mesh|...)
 */

#include <geogram_gfx/gui/simple_application.h>
#define RAYTRACE_GUI
#include "../../geogram/simple_raytrace/raytracing.h"
#include <geogram/basic/stopwatch.h>

namespace {
    using namespace GEO;

// Some phones (OpenGL ES) only support GL_RGBA as
// internal format and also *image* format.
#if defined(GEO_OS_ANDROID) || defined(GEO_OS_EMSCRIPTEN)
    const GLenum image_format = GL_RGBA;
#else
    const GLenum image_format = GL_RGB;
#endif

#if defined(GEO_OS_EMSCRIPTEN) && !defined(GEO_WEBGL2)
    const GLenum internal_image_format = GL_RGBA;
#else
    const GLenum internal_image_format = GL_RGBA8;
#endif

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

        /**
         * \brief DemoRaytraceApplication constructor.
         */
        DemoRaytraceApplication() :
            SimpleApplication("RayTrace"),
            camera_(
                get_width(),
		get_height(),
                (image_format == GL_RGBA) ? 4 : 3
            )
            {
                texture_ = 0;
                total_time_ = 0.0;
                frames_ = 0;

                // The tradition !
                scene_.add_object(new HorizontalCheckerboardPlane(0.0))
                    ->rename("Checkerboard");

                scene_.add_object(                         // A sphere
                    new Sphere(vec3(-0.7, -0.7, 1.0),0.7)
                )->set_reflection_coefficient(vec3(0.8, 0.8, 0.8))
                    ->set_diffuse_coefficient(vec3(0.2, 0.2, 0.2))
                    ->rename("Sphere 1");


                scene_.add_object(                         // Another sphere
                    new Sphere(vec3( 0.0, 0.0, 0.0),0.25)
                )->set_diffuse_coefficient(vec3(0.0, 1.0, 0.5))
                    ->rename("Sphere 2");


                // Let there be (two) lights !
                scene_.add_object(new Light(
                                      vec3(0.5, 1.3, 0.5), // Position
                                      0.02,                // Radius
                                      vec3(0.0, 0.5, 1.0)  // Color
                                  )
                                 )->rename("Light 1");

                scene_.add_object(new Light(
                                      vec3(1.0, 0.2, 1.0), // Position
                                      0.02,                // Radius
                                      vec3(1.0, 1.0, 1.0)  // Color
                                  )
                                 )->rename("Light 2");

                scene_changed_ = true;
                set_region_of_interest(-1.0, -1.0, -1.0, 1.0, 1.0, 1.0);

                FOR(i,4) {
                    my_viewport_[i] = 0.0;
                }
            }

        /**
         * \brief DemoRaytraceApplication destructor.
         */
        ~DemoRaytraceApplication() override {
        }


        /**
         * \copydoc SimpleApplication::GL_terminate()
         */
        void GL_terminate() override {
            if(texture_ != 0) {
                glDeleteTextures(1,&texture_);
                texture_ = 0;
            }
            SimpleApplication::GL_terminate();
        }

        /**
         * \brief Ray-traces a new frame.
         * \details Only if scene or viewing parameters
         *  changed.
         */
        void raytrace() {
            if(!scene_changed_) {
                return;
            }
            {
                double t0 = Stopwatch::now();
                parallel_for(
                    0, camera_.image_height(),
                    [this](index_t Y) {
                        for(index_t X=0; X<camera_.image_width(); ++X) {
                            Ray R = primary_ray(X,Y);
                            vec3 K = scene_.raytrace(R);
                            camera_.set_pixel(X,Y,K);
                        }
                    }
                );
                total_time_ += (Stopwatch::now() - t0);
                ++frames_;
            }
            if(texture_ != 0) {
                GEO_CHECK_GL();
                glActiveTexture(GL_TEXTURE0);
                GEO_CHECK_GL();
                glBindTexture(GL_TEXTURE_2D, texture_);
                GEO_CHECK_GL();
		glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
                GEO_CHECK_GL();
                glTexImage2D(
                    GL_TEXTURE_2D,
                    0,
                    internal_image_format,
                    GLsizei(camera_.image_width()),
                    GLsizei(camera_.image_height()),
                    0,
                    image_format,
                    GL_UNSIGNED_BYTE,
                    camera_.image_data()
                );
                GEO_CHECK_GL();
                glBindTexture(GL_TEXTURE_2D, 0);
                GEO_CHECK_GL();
            }
            scene_changed_ = false;
        }


        /**
         * \brief Displays and handles the GUI for object properties.
         * \details Overloads Application::draw_object_properties().
         */
        void draw_object_properties() override {
            SimpleApplication::draw_object_properties();
            if(ImGui::Button(
                   (icon_UTF8("home") + " Home [H]").c_str(), ImVec2(-1.0, 0.0))
              ) {
                home();
            }
            double fps = double(frames_) / total_time_;
            int ifps = int(fps);
            int ffps = int((fps - double(ifps))*100.0);
            ImGui::Text(
                "%s", (
                    String::to_string(ifps) +"." +
                    String::to_string(ffps) + " FPS"
                ).c_str()
            );
            if(scene_.draw_gui()) {
                scene_changed_ = true;
            }
        }


        /**
         * \brief Launches a primary ray.
         * \param[in] X , Y the pixel coordinates,
         *   in [0..width-1] x [0..height-1]
         */
        Ray primary_ray(index_t X, index_t Y) {
            double x = double(X);
            double y = double(Y);
            vec4 nnear(
                2.0*((x - my_viewport_[0]) / my_viewport_[2]-0.5),
                2.0*((y - my_viewport_[1]) / my_viewport_[3]-0.5),
                -1.0,
                1.0
            );
            vec4 ffar = nnear; // 'far' is reserved under Win32 !
            ffar.z = 1.0;
            nnear = mult(inv_project_modelview_,nnear);
            ffar =  mult(inv_project_modelview_,ffar);
            vec3 nearp = (1.0/nnear.w)*vec3(nnear.x, nnear.y, nnear.z);
            vec3 farp  = (1.0/ffar.w)*vec3(ffar.x , ffar.y , ffar.z );
            return Ray(nearp, farp-nearp);
        }

        /**
         * \brief Gets the viewing parameters from OpenGL and GLUP
         *  in a form suitable for software raytracing.
         */
        void get_viewing_parameters() {
            GLint viewport[4];
            glGetIntegerv(GL_VIEWPORT, viewport);
            FOR(i,4) {
                if(my_viewport_[i] != double(viewport[i])) {
                    scene_changed_ = true;
                }
                my_viewport_[i] = double(viewport[i]);
            }

            mat4 modelview;
            glupGetMatrixdv(GLUP_MODELVIEW_MATRIX, modelview.data());
            mat3 normalmatrix;
            FOR(i,3) {
                FOR(j,3) {
                    normalmatrix(i,j) = modelview(i,j);
                }
            }
            modelview = modelview.transpose();
            mat4 project;
            glupGetMatrixdv(GLUP_PROJECTION_MATRIX, project.data());
            project = project.transpose();

            mat4 inv_project_modelview = (project*modelview).inverse();
            FOR(i,4) {
                FOR(j, 4) {
                    if(inv_project_modelview(i,j) !=
                       inv_project_modelview_(i,j))  {
                        scene_changed_ = true;
                    }
                }
            }

            inv_project_modelview_ = inv_project_modelview;

            float Lf[3];
            glupGetLightVector3fv(Lf);

            vec3 L((double)Lf[0], (double)Lf[1], (double)Lf[2]);
            L = normalize(mult(normalmatrix,L));

            if(L_.x != L.x || L_.y != L.y || L_.z != L.z) {
                scene_changed_ = true;
            }
            L_ = L;
        }

        /**
         * \brief Draws the scene according to currently set primitive and
         *  drawing modes.
         */
        void draw_scene() override {
            get_viewing_parameters();

            index_t w = index_t(double(get_width()) * pixel_ratio());
            index_t h = index_t(double(get_height()) * pixel_ratio());

            if(camera_.image_width() != w ||
               camera_.image_height() != h) {
                camera_.resize(w,h);
                scene_changed_ = true;
            }
            raytrace();
            glViewport(
                0, 0,
                GLsizei(camera_.image_width()),
                GLsizei(camera_.image_height())
            );
            glDisable(GL_DEPTH_TEST);
            GEO_CHECK_GL();
            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D, texture_);
            GEO_CHECK_GL();
            draw_unit_textured_quad();
            glBindTexture(GL_TEXTURE_2D, 0);
            GEO_CHECK_GL();
        }

        /**
         * \brief Creates the texture.
         * \details This function overloads Application::init_graphics(). It
         *  is called as soon as the OpenGL context is ready for rendering. It
         *  is meant to initialize the graphic objects used by the application.
         */
        void GL_initialize() override {
            SimpleApplication::GL_initialize();
            glGenTextures(1, &texture_);
            GEO_CHECK_GL();
            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D, texture_);
            GEO_CHECK_GL();
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            GEO_CHECK_GL();
        }

        std::string supported_read_file_extensions() override {
            std::vector<std::string> extensions;
            GEO::MeshIOHandlerFactory::list_creators(extensions);
            std::string result = String::join_strings(extensions, ';');
            return result;
        }

        /**
         * \brief Loads a mesh.
         * \param[in] filename the name of the file. Can be any mesh format
         *   supported by geogram.
         * \details If there is already a mesh in the scene, replace
         *  it.
         */
        bool load(const std::string& filename) override {
            // If there is already a mesh object in the scene,
            // delete it.
            for(index_t i=0; i<scene_.nb_objects(); ++i) {
                Object* o = scene_.ith_object(i);
                if(dynamic_cast<MeshObject*>(o) != nullptr) {
                    scene_.remove_object(o);
                    delete o;
                    break;
                }
            }
            scene_.add_object(new MeshObject(filename));
            scene_changed_ = true;
            return true;
        }

        void draw_application_menus() override {
            if(ImGui::BeginMenu("New...")) {
                if(ImGui::MenuItem("Sphere")) {
                    scene_.add_object(
                        new Sphere(vec3(0.5, 0.5, 0.5),0.25)
                    )->set_diffuse_coefficient(random_color());
                    scene_changed_ = true;
                }
                if(ImGui::MenuItem("Light")) {
                    scene_.add_object(
                        new Light(vec3(0.5, 0.5, 1.5),0.15,random_color())
                    );
                    scene_changed_ = true;
                }
                if(ImGui::MenuItem("Checkerboard")) {
                    scene_.add_object(new HorizontalCheckerboardPlane(0.0));
                    scene_changed_ = true;
                }
                ImGui::EndMenu();
            }
        }

        void geogram_initialize(int argc, char** argv) override {
            SimpleApplication::geogram_initialize(argc, argv);
            viewer_properties_visible_ = false;
            object_properties_visible_ = true;
        }

    private:
        Camera camera_;
        Scene scene_;
        GLuint texture_;
        bool scene_changed_;
        double total_time_;
        index_t frames_;


        double my_viewport_[4];
        mat4 inv_project_modelview_;
        vec3 L_; /**< light vector in object space. */
    };

}

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