/*
** This file is part of the Interactive Medical Simulation Toolkit (iMSTK)
** iMSTK is distributed under the Apache License, Version 2.0.
** See accompanying NOTICE for details.
*/

#include "imstkCamera.h"
#include "imstkCameraController.h"
#include "imstkDeviceManager.h"
#include "imstkDeviceManagerFactory.h"
#include "imstkKeyboardDeviceClient.h"
#include "imstkKeyboardSceneControl.h"
#include "imstkMeshIO.h"
#include "imstkMouseDeviceClient.h"
#include "imstkMouseSceneControl.h"
#include "imstkPlane.h"
#include "imstkScene.h"
#include "imstkSceneManager.h"
#include "imstkSceneObject.h"
#include "imstkSimulationManager.h"
#include "imstkSimulationUtils.h"
#include "imstkSpotLight.h"
#include "imstkSurfaceMesh.h"
#include "imstkVTKViewer.h"

using namespace imstk;

///
/// \brief This example demonstrates controlling the camera using external
/// device. Attached is a spotlight.
/// Alternatively use a Pbd or RbdObjectController with virtual coupling
/// for smoothness
///
int
main()
{
    // Setup logger (write to file and stdout)
    Logger::startLogger();

    // Create Scene
    auto scene = std::make_shared<Scene>("CameraController");

    // Setup default haptics manager
    std::shared_ptr<DeviceManager> hapticManager = DeviceManagerFactory::makeDeviceManager();
    std::shared_ptr<DeviceClient>  deviceClient  = hapticManager->makeDeviceClient();

    // Load Mesh
    auto mesh = MeshIO::read<SurfaceMesh>(iMSTK_DATA_ROOT "/asianDragon/asianDragon.obj");
    mesh->scale(0.01, Geometry::TransformType::ApplyToData);
    auto meshObj = std::make_shared<SceneObject>("MeshObj");
    meshObj->setVisualGeometry(mesh);
    scene->addSceneObject(meshObj);

    auto planeObj = std::make_shared<SceneObject>("Plane");
    auto plane    = std::make_shared<Plane>(Vec3d(0.0, -0.05, 0.0));
    plane->setWidth(0.5);
    planeObj->setVisualGeometry(plane);
    scene->addSceneObject(planeObj);

    // Update Camera position
    scene->getActiveCamera()->setPosition(Vec3d(0.0, 0.0, 1.0));

    // Light
    auto light = std::make_shared<SpotLight>();
    light->setFocalPoint(Vec3d(0.0, 0.0, 0.0));
    light->setPosition(Vec3d(0.0, 10.0, 0.0));
    light->setIntensity(1.0);
    light->setSpotAngle(10.0);
    //light->setAttenuationValues(0.0, 0.0, 1.0); // Constant
    //light->setAttenuationValues(0.0, 0.5, 0.0); // Linear falloff
    light->setAttenuationValues(50.0, 0.0, 0.0); // Quadratic
    scene->addLight("light0", light);

    // Run the simulation
    {
        // Setup a viewer to render
        auto viewer = std::make_shared<VTKViewer>();
        viewer->setActiveScene(scene);

        // Setup a scene manager to advance the scene
        auto sceneManager = std::make_shared<SceneManager>();
        sceneManager->setActiveScene(scene);

        auto driver = std::make_shared<SimulationManager>();
        driver->addModule(hapticManager);
        driver->addModule(viewer);
        driver->addModule(sceneManager);
        driver->setDesiredDt(0.001);

        // Attach the camera controller to the viewer
        auto camController = std::make_shared<CameraController>();
        camController->setCamera(scene->getActiveCamera());
        camController->setDevice(deviceClient);
        scene->addControl(camController);

        // Add default mouse and keyboard controls to the viewer
        std::shared_ptr<Entity> mouseAndKeyControls =
            SimulationUtils::createDefaultSceneControl(driver);
        scene->addSceneObject(mouseAndKeyControls);

        // Change the spot angle when haptic button is pressed
        connect<ButtonEvent>(deviceClient, &DeviceClient::buttonStateChanged,
            [&](ButtonEvent* e)
            {
                if (e->m_buttonState == BUTTON_PRESSED)
                {
                    if (e->m_button == 0)
                    {
                        light->setSpotAngle(light->getSpotAngle() + 5.0);
                    }
                    else if (e->m_button == 1)
                    {
                        light->setSpotAngle(light->getSpotAngle() - 5.0);
                    }
                }
            });

        // Manually make the light follow the camera controller
        connect<Event>(sceneManager, &SceneManager::postUpdate,
            [&](Event*)
            {
                const Vec3d pos = camController->getPosition();
                const Quatd orientation = camController->getOrientation();

                light->setPosition(pos);
                light->setFocalPoint(pos - orientation.toRotationMatrix().col(2));
            });

        driver->start();
    }

    return 0;
}
