/*****************************************************************************************
 *                                                                                       *
 * OpenSpace                                                                             *
 *                                                                                       *
 * Copyright (c) 2014-2022                                                               *
 *                                                                                       *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this  *
 * software and associated documentation files (the "Software"), to deal in the Software *
 * without restriction, including without limitation the rights to use, copy, modify,    *
 * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to    *
 * permit persons to whom the Software is furnished to do so, subject to the following   *
 * conditions:                                                                           *
 *                                                                                       *
 * The above copyright notice and this permission notice shall be included in all copies *
 * or substantial portions of the Software.                                              *
 *                                                                                       *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,   *
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A         *
 * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT    *
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF  *
 * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE  *
 * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                                         *
 ****************************************************************************************/

#include <modules/skybrowser/skybrowsermodule.h>

#include <modules/skybrowser/include/renderableskytarget.h>
#include <modules/skybrowser/include/screenspaceskybrowser.h>
#include <modules/skybrowser/include/targetbrowserpair.h>
#include <modules/skybrowser/include/wwtdatahandler.h>
#include <openspace/camera/camera.h>
#include <openspace/engine/globalscallbacks.h>
#include <openspace/engine/windowdelegate.h>
#include <openspace/navigation/navigationhandler.h>
#include <openspace/scene/scene.h>
#include <openspace/scene/scenegraphnode.h>
#include <openspace/util/factorymanager.h>

#include "skybrowsermodule_lua.inl"

namespace {
    constexpr openspace::properties::Property::PropertyInfo EnabledInfo = {
        "Enabled",
        "Enabled",
        "Decides if the GUI for this module should be enabled"
    };

    constexpr openspace::properties::Property::PropertyInfo ShowTitleInGuiBrowserInfo = {
        "ShowTitleInGuiBrowser",
        "Show Title in Gui Browser",
        "If true, the name of the currently selected sky browser is shown in the WebGUI "
        "browser"
    };

    constexpr openspace::properties::Property::PropertyInfo AllowRotationInfo = {
        "AllowCameraRotation",
        "Allow Camera Rotation",
        "Toggles if the camera should rotate to look at the sky target if it is going "
        "outside of the current field of view"
    };

    constexpr openspace::properties::Property::PropertyInfo CameraRotSpeedInfo = {
        "CameraRotationSpeed",
        "Camera Rotation Speed",
        "The speed of the rotation of the camera when the camera rotates to look at a "
        "coordinate which is outside of the field of view"
    };

    constexpr openspace::properties::Property::PropertyInfo TargetSpeedInfo = {
        "TargetAnimationSpeed",
        "Target Animation Speed",
        "This determines the speed of the animation of the sky target"
    };

    constexpr openspace::properties::Property::PropertyInfo BrowserSpeedInfo = {
        "BrowserAnimationSpeed",
        "Field of View Animation Speed",
        "This determines the speed of the animation of the field of view in the browser"
    };

    constexpr openspace::properties::Property::PropertyInfo HideWithGuiInfo = {
        "HideTargetsBrowsersWithGui",
        "Hide Targets and Browsers with GUI",
        "If checked, the targets and browsers will be disabled when the sky browser "
        "panel is minimized"
    };

    constexpr openspace::properties::Property::PropertyInfo InverseZoomInfo = {
        "InverseZoomDirection",
        "Inverse Zoom Direction",
        "If checked, the zoom direction of the scroll over the AAS WWT browser will be "
        "inversed"
    };

    constexpr openspace::properties::Property::PropertyInfo SpaceCraftTimeInfo = {
        "SpaceCraftAnimationTime",
        "Space Craft Animation Time",
        "Sets the duration (in seconds) of the animation of the space craft when it is "
        "pointed to where the target is aiming"
    };

    constexpr openspace::properties::Property::PropertyInfo ImageCollectionInfo = {
       "WwtImageCollectionUrl",
       "AAS WorldWide Telescope Image Collection Url",
       "The url of the image collection which is loaded into AAS WorldWide Telescope"
    };

    struct [[codegen::Dictionary(SkyBrowserModule)]] Parameters {
        // [[codegen::verbatim(EnabledInfo.description)]]
        std::optional<bool> enabled;

        // [[codegen::verbatim(AllowRotationInfo.description)]]
        std::optional<bool> allowCameraRotation;

        // [[codegen::verbatim(CameraRotSpeedInfo.description)]]
        std::optional<double> cameraRotSpeed;

        // [[codegen::verbatim(TargetSpeedInfo.description)]]
        std::optional<double> targetSpeed;

        // [[codegen::verbatim(BrowserSpeedInfo.description)]]
        std::optional<double> browserSpeed;

        // [[codegen::verbatim(HideWithGuiInfo.description)]]
        std::optional<bool> hideTargetsBrowsersGui;

        // [[codegen::verbatim(InverseZoomInfo.description)]]
        std::optional<bool> inverseZoomDirection;

        // [[codegen::verbatim(SpaceCraftTimeInfo.description)]]
        std::optional<double> spaceCraftAnimationTime;

        // [[codegen::verbatim(SpaceCraftTimeInfo.description)]]
        std::optional<std::string> wwtImageCollectionUrl;
    };

#include "skybrowsermodule_codegen.cpp"
} // namespace

namespace openspace {

SkyBrowserModule::SkyBrowserModule()
    : OpenSpaceModule(SkyBrowserModule::Name)
    , _enabled(EnabledInfo)
    , _showTitleInGuiBrowser(ShowTitleInGuiBrowserInfo, true)
    , _allowCameraRotation(AllowRotationInfo, true)
    , _cameraRotationSpeed(CameraRotSpeedInfo, 0.5, 0.0, 1.0)
    , _targetAnimationSpeed(TargetSpeedInfo, 0.2, 0.0, 1.0)
    , _browserAnimationSpeed(BrowserSpeedInfo, 5.0, 0.0, 10.0)
    , _hideTargetsBrowsersWithGui(HideWithGuiInfo, false)
    , _inverseZoomDirection(InverseZoomInfo, false)
    , _spaceCraftAnimationTime(SpaceCraftTimeInfo, 2.0, 0.0, 10.0)
    , _wwtImageCollectionUrl(ImageCollectionInfo,
        "https://data.openspaceproject.com/wwt/1/imagecollection.wtml")
{
    addProperty(_enabled);
    addProperty(_showTitleInGuiBrowser);
    addProperty(_allowCameraRotation);
    addProperty(_cameraRotationSpeed);
    addProperty(_targetAnimationSpeed);
    addProperty(_browserAnimationSpeed);
    addProperty(_hideTargetsBrowsersWithGui);
    addProperty(_inverseZoomDirection);
    addProperty(_spaceCraftAnimationTime);
    addProperty(_wwtImageCollectionUrl);
    _wwtImageCollectionUrl.setReadOnly(true);

    // Set callback functions
    global::callback::mouseButton->emplace(
        global::callback::mouseButton->begin(),
        [&](MouseButton button, MouseAction action, KeyModifier, IsGuiWindow) -> bool {
            if (action == MouseAction::Press) {
                _cameraRotation.stop();
            }
            return false;
        }
    );

    global::callback::preSync->emplace_back([this]() {
        constexpr double SolarSystemRadius = 30.0 * distanceconstants::AstronomicalUnit;

        // Disable browser and targets when camera is outside of solar system
        bool camWasInSolarSystem = _isCameraInSolarSystem;
        glm::dvec3 cameraPos = global::navigationHandler->camera()->positionVec3();
        _isCameraInSolarSystem = glm::length(cameraPos) < SolarSystemRadius;
        bool vizModeChanged = _isCameraInSolarSystem != camWasInSolarSystem;

        // Visualization mode changed. Start fading in/out
        if (vizModeChanged) {
            constexpr float FadeDuration = 2.f;

            if (camWasInSolarSystem) { // Camera moved out of the solar system => fade out
                for (const std::unique_ptr<TargetBrowserPair>& pair : _targetsBrowsers) {
                    pair->startFading(0.f, FadeDuration);
                }

                // Also hide the hover circle
                disableHoverCircle();
            }
            else { // Camera moved into the solar system => fade in
                for (const std::unique_ptr<TargetBrowserPair>& pair : _targetsBrowsers) {
                    pair->startFading(1.f, FadeDuration);
                }
            }
        }

        if (_isCameraInSolarSystem) {
            for (const std::unique_ptr<TargetBrowserPair>& pair : _targetsBrowsers) {
                pair->synchronizeAim();
            }
            incrementallyAnimateTargets();
        }
        if (_cameraRotation.isAnimating() && _allowCameraRotation) {
            incrementallyRotateCamera();
        }
    });
}

void SkyBrowserModule::internalInitialize(const ghoul::Dictionary& dict) {
    const Parameters p = codegen::bake<Parameters>(dict);

    _enabled = p.enabled.value_or(true);
    _allowCameraRotation = p.allowCameraRotation.value_or(_allowCameraRotation);
    _cameraRotationSpeed = p.cameraRotSpeed.value_or(_cameraRotationSpeed);
    _targetAnimationSpeed = p.targetSpeed.value_or(_targetAnimationSpeed);
    _browserAnimationSpeed = p.browserSpeed.value_or(_browserAnimationSpeed);
    _inverseZoomDirection = p.inverseZoomDirection.value_or(_inverseZoomDirection);
    _wwtImageCollectionUrl = p.wwtImageCollectionUrl.value_or(_wwtImageCollectionUrl);
    _hideTargetsBrowsersWithGui = p.hideTargetsBrowsersGui.value_or(
        _hideTargetsBrowsersWithGui
    );
    _spaceCraftAnimationTime = p.spaceCraftAnimationTime.value_or(
        _spaceCraftAnimationTime
    );

    ghoul::TemplateFactory<ScreenSpaceRenderable>* fScreenSpaceRenderable =
        FactoryManager::ref().factory<ScreenSpaceRenderable>();
    ghoul_assert(fScreenSpaceRenderable, "ScreenSpaceRenderable factory was not created");

    // Register ScreenSpaceSkyBrowser
    fScreenSpaceRenderable->registerClass<ScreenSpaceSkyBrowser>("ScreenSpaceSkyBrowser");

    ghoul::TemplateFactory<Renderable>* fRenderable =
        FactoryManager::ref().factory<Renderable>();
    ghoul_assert(fRenderable, "Renderable factory was not created");

    // Register ScreenSpaceSkyTarget
    fRenderable->registerClass<RenderableSkyTarget>("RenderableSkyTarget");

    // Create data handler dynamically to avoid the linking error that
    // came up when including the include file in the module header file
    _dataHandler = std::make_unique<WwtDataHandler>();
}

void SkyBrowserModule::addTargetBrowserPair(const std::string& targetId,
                                            const std::string& browserId)
{
    if (!global::renderEngine->scene()) {
        return;
    }

    SceneGraphNode* target = global::renderEngine->scene()->sceneGraphNode(targetId);
    ScreenSpaceSkyBrowser* browser = dynamic_cast<ScreenSpaceSkyBrowser*>(
        global::renderEngine->screenSpaceRenderable(browserId)
    );

    // Ensure pair has both target and browser
    if (browser && target) {
        _targetsBrowsers.push_back(std::make_unique<TargetBrowserPair>(target, browser));
    }
    _uniqueIdentifierCounter++;
}

void SkyBrowserModule::removeTargetBrowserPair(const std::string& id) {
    TargetBrowserPair* found = pair(id);
    if (!found) {
        return;
    }

    auto it = std::remove_if(
        _targetsBrowsers.begin(),
        _targetsBrowsers.end(),
        [&](const std::unique_ptr<TargetBrowserPair>& pair) {
            // should this be?
            // found == pair.get()
            return found == pair.get();
        }
    );

    _targetsBrowsers.erase(it, _targetsBrowsers.end());
}

void SkyBrowserModule::lookAtTarget(const std::string& id) {
    TargetBrowserPair* found = pair(id);
    if (found) {
        startRotatingCamera(found->targetDirectionGalactic());
    }
}

void SkyBrowserModule::setHoverCircle(SceneGraphNode* circle) {
    _hoverCircle = circle;

    // Always disable it per default. It should only be visible on interaction
    disableHoverCircle();
}

void SkyBrowserModule::moveHoverCircle(int i, bool useScript) {
    const ImageData& image = _dataHandler->getImage(i);

    // Only move and show circle if the image has coordinates
    if (_hoverCircle && image.hasCelestialCoords && _isCameraInSolarSystem) {
        const std::string id = _hoverCircle->identifier();

        // Show the circle
        if (useScript) {
            const std::string script = fmt::format(
                "openspace.setPropertyValueSingle('Scene.{}.Renderable.Fade', 1.0);",
                id
            );
            global::scriptEngine->queueScript(
                script,
                scripting::ScriptEngine::RemoteScripting::Yes
            );
        }
        else {
            Renderable* renderable = _hoverCircle->renderable();
            if (renderable) {
                renderable->property("Fade")->set(1.f);
            }
        }

        // Set the exact target position
        // Move it slightly outside of the celestial sphere so it doesn't overlap with
        // the target
        glm::dvec3 pos = skybrowser::equatorialToGalactic(image.equatorialCartesian);
        pos *= skybrowser::CelestialSphereRadius * 1.1;

        // Note that the position can only be set through the script engine
        const std::string script = fmt::format(
            "openspace.setPropertyValueSingle('Scene.{}.Translation.Position', {});",
            id, ghoul::to_string(pos)
        );
        global::scriptEngine->queueScript(
            script,
            scripting::ScriptEngine::RemoteScripting::Yes
        );
    }
}

void SkyBrowserModule::disableHoverCircle(bool useScript) {
    if (_hoverCircle && _hoverCircle->renderable()) {
        if (useScript) {
            const std::string script = fmt::format(
                "openspace.setPropertyValueSingle('Scene.{}.Renderable.Fade', 0.0);",
                _hoverCircle->identifier()
            );
            global::scriptEngine->queueScript(
                script,
                scripting::ScriptEngine::RemoteScripting::Yes
            );
        }
        else {
            _hoverCircle->renderable()->property("Fade")->set(0.f);
        }
    }
}

void SkyBrowserModule::loadImages(const std::string& root,
                                  const std::filesystem::path& directory)
{
    _dataHandler->loadImages(root, directory);
}

int SkyBrowserModule::nLoadedImages() const {
    return _dataHandler->nLoadedImages();
}

const std::unique_ptr<WwtDataHandler>& SkyBrowserModule::getWwtDataHandler() const {
    return _dataHandler;
}

std::vector<std::unique_ptr<TargetBrowserPair>>& SkyBrowserModule::getPairs() {
    return _targetsBrowsers;
}

int SkyBrowserModule::nPairs() const {
    return static_cast<int>(_targetsBrowsers.size());
}

TargetBrowserPair* SkyBrowserModule::pair(const std::string& id) const {
    auto it = std::find_if(
        _targetsBrowsers.begin(),
        _targetsBrowsers.end(),
        [&](const std::unique_ptr<TargetBrowserPair>& pair) {
            const bool foundBrowser = pair->browserId() == id;
            const bool foundTarget = pair->targetRenderableId() == id;
            const bool foundTargetNode = pair->targetNodeId() == id;
            return foundBrowser || foundTarget || foundTargetNode;
        }
    );
    TargetBrowserPair* found = it != _targetsBrowsers.end() ? it->get() : nullptr;
    if (found == nullptr) {
        LINFO(fmt::format("Identifier '{}' not found", id));
    }
    return found;
}

void SkyBrowserModule::startRotatingCamera(glm::dvec3 endAnimation) {
    // Save coordinates to rotate to in galactic world coordinates
    glm::dvec3 start = skybrowser::cameraDirectionGalactic();
    double angle = skybrowser::angleBetweenVectors(start, endAnimation);
    double time = angle / _cameraRotationSpeed;
    _cameraRotation = skybrowser::Animation(start, endAnimation, time);
    _cameraRotation.start();
}

void SkyBrowserModule::incrementallyRotateCamera() {
    if (_cameraRotation.isAnimating()) {
        glm::dmat4 rotMat = _cameraRotation.getRotationMatrix();
        global::navigationHandler->camera()->rotate(glm::quat_cast(rotMat));
    }
}

void SkyBrowserModule::incrementallyAnimateTargets() {
    for (std::unique_ptr<TargetBrowserPair>& pair : _targetsBrowsers) {
        if (pair->isEnabled()) {
            pair->incrementallyAnimateToCoordinate();
        }
    }
}

double SkyBrowserModule::targetAnimationSpeed() const {
    return _targetAnimationSpeed;
}

double SkyBrowserModule::browserAnimationSpeed() const {
    return _browserAnimationSpeed;
}

double SkyBrowserModule::spaceCraftAnimationTime() const {
    return _spaceCraftAnimationTime;
}

std::string SkyBrowserModule::wwtImageCollectionUrl() const {
    return _wwtImageCollectionUrl;
}

void SkyBrowserModule::setSelectedBrowser(const std::string& id) {
    TargetBrowserPair* found = pair(id);
    if (found) {
        _selectedBrowser = id;
    }
}

std::string SkyBrowserModule::selectedBrowserId() const {
    return _selectedBrowser;
}

std::string SkyBrowserModule::selectedTargetId() const {
    TargetBrowserPair* found = pair(_selectedBrowser);
    return found ? found->targetRenderableId() : "";
}

int SkyBrowserModule::uniqueIdentifierCounter() const {
    return _uniqueIdentifierCounter;
}

bool SkyBrowserModule::isCameraInSolarSystem() const {
    return _isCameraInSolarSystem;
}

bool SkyBrowserModule::isSelectedPairUsingRae() const {
    TargetBrowserPair* found = pair(_selectedBrowser);
    return found ? found->isUsingRadiusAzimuthElevation() : false;
}

bool SkyBrowserModule::isSelectedPairFacingCamera() const {
    TargetBrowserPair* found = pair(_selectedBrowser);
    return found ? found->isFacingCamera() : false;
}

std::vector<documentation::Documentation> SkyBrowserModule::documentations() const {
    return {
        RenderableSkyTarget::Documentation(),
        ScreenSpaceSkyBrowser::Documentation()
    };
}

scripting::LuaLibrary SkyBrowserModule::luaLibrary() const {
    return {
        "skybrowser",
        {
            codegen::lua::StartSetup,
            codegen::lua::InitializeBrowser,
            codegen::lua::SendOutIdsToBrowsers,
            codegen::lua::GetListOfImages,
            codegen::lua::SetHoverCircle,
            codegen::lua::MoveCircleToHoverImage,
            codegen::lua::DisableHoverCircle,
            codegen::lua::LoadImagesToWWT,
            codegen::lua::SelectImage,
            codegen::lua::RemoveSelectedImageInBrowser,
            codegen::lua::AdjustCamera,
            codegen::lua::SetSelectedBrowser,
            codegen::lua::GetTargetData,
            codegen::lua::CreateTargetBrowserPair,
            codegen::lua::RemoveTargetBrowserPair,
            codegen::lua::SetOpacityOfImageLayer,
            codegen::lua::CenterTargetOnScreen,
            codegen::lua::SetImageLayerOrder,
            codegen::lua::AddPairToSkyBrowserModule,
            codegen::lua::SetEquatorialAim,
            codegen::lua::SetVerticalFov,
            codegen::lua::SetBorderColor,
            codegen::lua::TranslateScreenSpaceRenderable,
            codegen::lua::AddDisplayCopy,
            codegen::lua::SetBrowserRatio,
            codegen::lua::RemoveDisplayCopy,
            codegen::lua::StartFinetuningTarget,
            codegen::lua::FinetuneTargetPosition,
            codegen::lua::ScrollOverBrowser,
            codegen::lua::LoadingImageCollectionComplete,
            codegen::lua::ShowAllTargetsAndBrowsers,
            codegen::lua::PointSpaceCraft,
            codegen::lua::GetWwtImageCollectionUrl,
            codegen::lua::StopAnimations
        }
    };
}

} // namespace openspace
