/*****************************************************************************************
 *                                                                                       *
 * 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 <openspace/rendering/renderengine.h>

#include <openspace/openspace.h>
#include <openspace/engine/configuration.h>
#include <openspace/engine/globals.h>
#include <openspace/engine/globalscallbacks.h>
#include <openspace/engine/openspaceengine.h>
#include <openspace/engine/windowdelegate.h>
#include <openspace/events/event.h>
#include <openspace/events/eventengine.h>
#include <openspace/navigation/navigationhandler.h>
#include <openspace/navigation/orbitalnavigator.h>
#include <openspace/mission/missionmanager.h>
#include <openspace/rendering/dashboard.h>
#include <openspace/rendering/deferredcastermanager.h>
#include <openspace/rendering/helper.h>
#include <openspace/rendering/framebufferrenderer.h>
#include <openspace/rendering/luaconsole.h>
#include <openspace/rendering/raycastermanager.h>
#include <openspace/rendering/screenspacerenderable.h>
#include <openspace/scene/scene.h>
#include <openspace/scripting/scriptengine.h>
#include <openspace/util/memorymanager.h>
#include <openspace/util/timemanager.h>
#include <openspace/util/screenlog.h>
#include <openspace/util/updatestructures.h>
#include <openspace/util/versionchecker.h>
#include <ghoul/filesystem/filesystem.h>
#include <ghoul/font/font.h>
#include <ghoul/font/fontmanager.h>
#include <ghoul/font/fontrenderer.h>
#include <ghoul/io/texture/texturereader.h>
#include <ghoul/io/texture/texturereadercmap.h>
#include <ghoul/io/model/modelreader.h>
#include <ghoul/io/model/modelreaderassimp.h>
#include <ghoul/io/model/modelreaderbinary.h>
#include <ghoul/io/texture/texturereaderstb.h>
#include <ghoul/logging/logmanager.h>
#include <ghoul/misc/easing.h>
#include <ghoul/misc/profiling.h>
#include <ghoul/misc/stringconversion.h>
#include <ghoul/opengl/programobject.h>
#include <ghoul/opengl/openglstatecache.h>
#include <ghoul/systemcapabilities/openglcapabilitiescomponent.h>

#include "renderengine_lua.inl"

namespace {
    constexpr std::string_view _loggerCat = "RenderEngine";

    constexpr std::chrono::seconds ScreenLogTimeToLive(15);
    constexpr std::string_view RenderFsPath = "${SHADERS}/render.frag";

    constexpr std::string_view KeyFontMono = "Mono";
    constexpr std::string_view KeyFontLight = "Light";

    constexpr openspace::properties::Property::PropertyInfo ShowOverlayClientsInfo = {
        "ShowOverlayOnClients",
        "Show Overlay Information on Clients",
        "If this value is enabled, the overlay information text is also automatically "
        "rendered on client nodes. This values is disabled by default"
    };

    constexpr openspace::properties::Property::PropertyInfo ShowLogInfo = {
        "ShowLog",
        "Show the on-screen log",
        "This value determines whether the on-screen log will be shown or hidden. Even "
        "if it is shown, all 'Debug' and 'Trace' level messages are omitted from this "
        "log"
    };

    constexpr openspace::properties::Property::PropertyInfo VerticalLogOffsetInfo = {
        "VerticalLogOffset",
        "Vertical Log Offset",
        "The vertical offset for the on-screen log in [0,1] coordinates, a factor that "
        "is scaled with the vertical resolution"
    };

    constexpr openspace::properties::Property::PropertyInfo ShowVersionInfo = {
        "ShowVersion",
        "Shows the version on-screen information",
        "This value determines whether the Git version information (branch and commit) "
        "hash are shown on the screen"
    };

    constexpr openspace::properties::Property::PropertyInfo ShowCameraInfo = {
        "ShowCamera",
        "Shows information about the current camera state, such as friction",
        "This value determines whether the information about the current camrea state is "
        "shown on the screen"
    };

    constexpr openspace::properties::Property::PropertyInfo ScreenshotWindowIdsInfo = {
        "ScreenshotWindowId",
        "Screenshow Window Ids",
        "The list of window identifiers whose screenshot will be taken the next time "
        "anyone triggers a screenshot. If this list is empty (the default), all windows "
        "will have their screenshot taken. Id's that do not exist are silently ignored"
    };

    constexpr openspace::properties::Property::PropertyInfo ApplyWarpingInfo = {
        "ApplyWarpingScreenshot",
        "Apply Warping to Screenshots",
        "This value determines whether a warping should be applied before taking a "
        "screenshot. If it is enabled, all post processing is applied as well, which "
        "includes everything rendered on top of the rendering, such as the user "
        "interface"
    };

    constexpr openspace::properties::Property::PropertyInfo ShowStatisticsInfo = {
        "ShowStatistics",
        "Show Statistics",
        "Show updating, rendering, and network statistics on all rendering nodes"
    };

    constexpr openspace::properties::Property::PropertyInfo ScreenshotUseDateInfo = {
        "ScreenshotUseDate",
        "Screenshot Folder uses Date",
        "If this value is set to 'true', screenshots will be saved to a folder that "
        "contains the time at which this value was enabled"
    };

    constexpr openspace::properties::Property::PropertyInfo ShowFrameNumberInfo = {
        "ShowFrameInformation",
        "Show Frame Information",
        "If this value is enabled, the current frame number and frame times are rendered "
        "into the window"
    };

    constexpr openspace::properties::Property::PropertyInfo DisableMasterInfo = {
        "DisableMasterRendering",
        "Disable Master Rendering",
        "If this value is enabled, the rendering on the master node will be disabled. "
        "Every other aspect of the application will be unaffected by this and it will "
        "still respond to user input. This setting is reasonably only useful in the case "
        "of multi-pipeline environments, such as planetariums, where the output of the "
        "master node is not required and performance can be gained by disabling it"
    };

    constexpr openspace::properties::Property::PropertyInfo GlobalRotationInfo = {
        "GlobalRotation",
        "Global Rotation",
        "Applies a global view rotation. Use this to rotate the position of the "
        "focus node away from the default location on the screen. This setting "
        "persists even when a new focus node is selected. Defined using pitch, yaw, "
        "roll in radians"
    };

    constexpr openspace::properties::Property::PropertyInfo ScreenSpaceRotationInfo = {
        "ScreenSpaceRotation",
        "Screen Space Rotation",
        "Applies a rotation to all screen space renderables. "
        "Defined using pitch, yaw, roll in radians"
    };

    constexpr openspace::properties::Property::PropertyInfo MasterRotationInfo = {
        "MasterRotation",
        "Master Rotation",
        "Applies a view rotation for only the master node, defined using "
        "pitch, yaw, roll in radians. This can be used to compensate the master view "
        "direction for tilted display systems in clustered immersive environments"
    };

    constexpr openspace::properties::Property::PropertyInfo DisableHDRPipelineInfo = {
       "DisableHDRPipeline",
       "Disable HDR Rendering",
       "If this value is enabled, the rendering will disable the HDR color handling "
       "and the LDR color pipeline will be used. Be aware of possible over exposure "
       "in the final colors"
    };

    constexpr openspace::properties::Property::PropertyInfo HDRExposureInfo = {
        "HDRExposure",
        "HDR Exposure",
        "This value determines the amount of light per unit area reaching the "
        "equivalent of an electronic image sensor"
    };

    constexpr openspace::properties::Property::PropertyInfo GammaInfo = {
        "Gamma",
        "Gamma Correction",
        "Gamma, is the nonlinear operation used to encode and decode luminance or "
        "tristimulus values in the image"
    };

    constexpr openspace::properties::Property::PropertyInfo HueInfo = {
        "Hue",
        "Hue",
        "Hue"
    };

    constexpr openspace::properties::Property::PropertyInfo SaturationInfo = {
        "Saturation",
        "Saturation",
        "Saturation"
    };

    constexpr openspace::properties::Property::PropertyInfo ValueInfo = {
        "Value",
        "Value",
        "Value"
    };

    constexpr openspace::properties::Property::PropertyInfo FramerateLimitInfo = {
        "FramerateLimit",
        "Framerate Limit",
        "If set to a value bigger than 0, the framerate will be limited to that many "
        "frames per second without using V-Sync"
    };

    constexpr openspace::properties::Property::PropertyInfo HorizFieldOfViewInfo = {
        "HorizFieldOfView",
        "Horizontal Field of View",
        "Adjusts the degrees of the horizontal field of view. The vertical field of "
        "view will be automatically adjusted to match, according to the current "
        "aspect ratio"
    };

    constexpr openspace::properties::Property::PropertyInfo GlobalBlackoutFactorInfo = {
        "BlackoutFactor",
        "Blackout Factor",
        "The blackout factor of the rendering. This can be used for fading in or out the "
        "rendering window"
    };

    constexpr openspace::properties::Property::PropertyInfo FXAAInfo = {
        "FXAA",
        "Enable FXAA",
        "Enable FXAA"
    };

    constexpr openspace::properties::Property::PropertyInfo EnabledFontColorInfo = {
        "EnabledFontColor",
        "Enabled Font Color",
        "The font color used for enabled options"
    };

    constexpr openspace::properties::Property::PropertyInfo DisabledFontColorInfo = {
        "DisabledFontColor",
        "Disabled Font Color",
        "The font color used for disabled options"
    };
} // namespace

namespace openspace {

RenderEngine::RenderEngine()
    : properties::PropertyOwner({ "RenderEngine" })
    , _showOverlayOnClients(ShowOverlayClientsInfo, false)
    , _showLog(ShowLogInfo, true)
    , _verticalLogOffset(VerticalLogOffsetInfo, 0.f, 0.f, 1.f)
    , _showVersionInfo(ShowVersionInfo, true)
    , _showCameraInfo(ShowCameraInfo, true)
    , _screenshotWindowIds(ScreenshotWindowIdsInfo)
    , _applyWarping(ApplyWarpingInfo, false)
    , _showStatistics(ShowStatisticsInfo, false)
    , _screenshotUseDate(ScreenshotUseDateInfo, false)
    , _showFrameInformation(ShowFrameNumberInfo, false)
    , _disableMasterRendering(DisableMasterInfo, false)
    , _globalBlackOutFactor(GlobalBlackoutFactorInfo, 1.f, 0.f, 1.f)
    , _enableFXAA(FXAAInfo, true)
    , _disableHDRPipeline(DisableHDRPipelineInfo, false)
    , _hdrExposure(HDRExposureInfo, 3.7f, 0.01f, 10.f)
    , _gamma(GammaInfo, 0.95f, 0.01f, 5.f)
    , _hue(HueInfo, 0.f, 0.f, 360.f)
    , _saturation(SaturationInfo, 1.f, 0.0f, 2.f)
    , _value(ValueInfo, 1.f, 0.f, 2.f)
    , _framerateLimit(FramerateLimitInfo, 0, 0, 500)
    , _horizFieldOfView(HorizFieldOfViewInfo, 80.f, 1.f, 179.f)
    , _globalRotation(
        GlobalRotationInfo,
        glm::vec3(0.f),
        glm::vec3(-glm::pi<float>()),
        glm::vec3(glm::pi<float>())
    )
    , _screenSpaceRotation(
        ScreenSpaceRotationInfo,
        glm::vec3(0.f),
        glm::vec3(-glm::pi<float>()),
        glm::vec3(glm::pi<float>())
    )
    , _masterRotation(
        MasterRotationInfo,
        glm::vec3(0.f),
        glm::vec3(-glm::pi<float>()),
        glm::vec3(glm::pi<float>())
    )
    , _enabledFontColor(EnabledFontColorInfo, glm::vec4(0.2f, 0.75f, 0.2f, 1.f))
    , _disabledFontColor(DisabledFontColorInfo, glm::vec4(0.55f, 0.2f, 0.2f, 1.f))
{
    addProperty(_showOverlayOnClients);
    addProperty(_showLog);
    addProperty(_verticalLogOffset);
    addProperty(_showVersionInfo);
    addProperty(_showCameraInfo);

    _enableFXAA.onChange([this]() { _renderer.enableFXAA(_enableFXAA); });
    addProperty(_enableFXAA);

    _disableHDRPipeline.onChange([this]() {
        _renderer.setDisableHDR(_disableHDRPipeline);
    });
    addProperty(_disableHDRPipeline);

    _hdrExposure.onChange([this]() { _renderer.setHDRExposure(_hdrExposure); });
    addProperty(_hdrExposure);

    _gamma.onChange([this]() { _renderer.setGamma(_gamma); });
    addProperty(_gamma);

    _hue.onChange([this]() { _renderer.setHue(_hue / 360.f); });
    addProperty(_hue);

    _saturation.onChange([this]() { _renderer.setSaturation(_saturation); });
    addProperty(_saturation);

    _value.onChange([this]() { _renderer.setValue(_value); });
    addProperty(_value);

    addProperty(_globalBlackOutFactor);
    addProperty(_screenshotWindowIds);
    addProperty(_applyWarping);

    _showStatistics.onChange([this]() {
        global::windowDelegate->showStatistics(_showStatistics);
    });
    addProperty(_showStatistics);

    _screenshotUseDate.onChange([this]() {
        // If there is no screenshot folder, don't bother with handling the change
        if (!FileSys.hasRegisteredToken("${STARTUP_SCREENSHOT}")) {
            return;
        }

        if (_screenshotUseDate) {
            // Going from 'false' -> 'true'
            // We might need to create the folder first

            std::time_t now = std::time(nullptr);
            std::tm* nowTime = std::localtime(&now);
            char date[128];
            strftime(date, sizeof(date), "%Y-%m-%d-%H-%M", nowTime);

            std::filesystem::path newFolder = absPath(
                "${STARTUP_SCREENSHOT}/" + std::string(date)
            );

            FileSys.registerPathToken(
                "${SCREENSHOTS}",
                newFolder,
                ghoul::filesystem::FileSystem::Override::Yes
            );
        }
        else {
            // Going from 'true' -> 'false'
            // We reset the screenshot folder back to what it was in the beginning
            FileSys.registerPathToken(
                "${SCREENSHOTS}",
                absPath("${STARTUP_SCREENSHOT}"),
                ghoul::filesystem::FileSystem::Override::Yes
            );
        }
        global::windowDelegate->setScreenshotFolder(absPath("${SCREENSHOTS}").string());
    });
    addProperty(_screenshotUseDate);

    _horizFieldOfView.onChange([this]() {
        if (global::windowDelegate->isMaster()) {
            global::windowDelegate->setHorizFieldOfView(_horizFieldOfView);
        }
    });
    addProperty(_horizFieldOfView);

    addProperty(_showFrameInformation);

    addProperty(_framerateLimit);
    addProperty(_globalRotation);
    addProperty(_screenSpaceRotation);
    addProperty(_masterRotation);
    addProperty(_disableMasterRendering);

    _enabledFontColor.setViewOption(openspace::properties::Property::ViewOptions::Color);
    addProperty(_enabledFontColor);

    _disabledFontColor.setViewOption(openspace::properties::Property::ViewOptions::Color);
    addProperty(_disabledFontColor);
}

RenderEngine::~RenderEngine() {} // NOLINT

void RenderEngine::initialize() {
    ZoneScoped

    // We have to perform these initializations here as the OsEng has not been initialized
    // in our constructor
    _globalRotation = global::configuration->globalRotation;
    _screenSpaceRotation = global::configuration->screenSpaceRotation;
    _masterRotation = global::configuration->masterRotation;
    _disableMasterRendering = global::configuration->isRenderingOnMasterDisabled;
    _screenshotUseDate = global::configuration->shouldUseScreenshotDate;

    ghoul::io::TextureReader::ref().addReader(
        std::make_unique<ghoul::io::TextureReaderSTB>()
    );

    ghoul::io::TextureReader::ref().addReader(
        std::make_unique<ghoul::io::TextureReaderCMAP>()
    );

    ghoul::io::ModelReader::ref().addReader(
        std::make_unique<ghoul::io::ModelReaderAssimp>()
    );

    ghoul::io::ModelReader::ref().addReader(
        std::make_unique<ghoul::io::ModelReaderBinary>()
    );

    _versionString = OPENSPACE_VERSION_STRING_FULL;
    if (global::versionChecker->hasLatestVersionInfo()) {
        VersionChecker::SemanticVersion latest = global::versionChecker->latestVersion();

        VersionChecker::SemanticVersion current{
            OPENSPACE_VERSION_MAJOR,
            OPENSPACE_VERSION_MINOR,
            OPENSPACE_VERSION_PATCH
        };
        if (current < latest) {
            _versionString += fmt::format(
                " [Available: {}.{}.{}]", latest.major, latest.minor, latest.patch
            );
        }
    }
}

void RenderEngine::initializeGL() {
    ZoneScoped

    LTRACE("RenderEngine::initializeGL(begin)");

    _renderer.setResolution(renderingResolution());
    _renderer.enableFXAA(_enableFXAA);
    _renderer.setHDRExposure(_hdrExposure);
    _renderer.initialize();

    // set the close clip plane and the far clip plane to extreme values while in
    // development
    global::windowDelegate->setNearFarClippingPlane(0.001f, 1000.f);

    // Set horizontal FOV value with whatever the field of view (in degrees) is of the
    // initialized window
    _horizFieldOfView = static_cast<float>(global::windowDelegate->getHorizFieldOfView());

    configuration::Configuration::FontSizes fontSize = global::configuration->fontSize;
    {
        ZoneScopedN("Fonts")
        TracyGpuZone("Fonts")
        _fontFrameInfo = global::fontManager->font(KeyFontMono, fontSize.frameInfo);
        _fontShutdown = global::fontManager->font(KeyFontMono, fontSize.shutdown);
        _fontCameraInfo = global::fontManager->font(KeyFontMono, fontSize.cameraInfo);
        _fontVersionInfo = global::fontManager->font(KeyFontMono, fontSize.versionInfo);
        _fontLog = global::fontManager->font(KeyFontLight, fontSize.log);
    }

    {
        ZoneScopedN("Log")
        LINFO("Initializing Log");
        auto log = std::make_unique<ScreenLog>(ScreenLogTimeToLive);
        _log = log.get();
        ghoul::logging::LogManager::ref().addLog(std::move(log));
    }

    LINFO("Finished initializing GL");
    LTRACE("RenderEngine::initializeGL(end)");
}

void RenderEngine::deinitializeGL() {
    ZoneScoped

    _renderer.deinitialize();
}

void RenderEngine::updateScene() {
    ZoneScoped

    if (!_scene) {
        return;
    }

    _scene->updateInterpolations();

    const Time& currentTime = global::timeManager->time();
    const Time& integrateFromTime = global::timeManager->integrateFromTime();

    _scene->update({
        TransformData{ glm::dvec3(0.0), glm::dmat3(1.0), glm::dvec3(1.0) },
        currentTime,
        integrateFromTime
    });

    LTRACE("RenderEngine::updateSceneGraph(end)");
}

void RenderEngine::updateShaderPrograms() {
    ZoneScoped

    for (ghoul::opengl::ProgramObject* program : _programs) {
        try {
            if (program->isDirty()) {
                program->rebuildFromFile();
            }
        }
        catch (const ghoul::opengl::ShaderObject::ShaderCompileError& e) {
            LERRORC(e.component, e.what());
        }
    }
}

void RenderEngine::updateRenderer() {
    ZoneScoped

    const bool windowResized = global::windowDelegate->windowHasResized();

    if (windowResized) {
        _renderer.setResolution(renderingResolution());

        using FR = ghoul::fontrendering::FontRenderer;
        FR::defaultRenderer().setFramebufferSize(fontResolution());
        FR::defaultProjectionRenderer().setFramebufferSize(renderingResolution());
        //Override the aspect ratio property value to match that of resized window
        _horizFieldOfView =
            static_cast<float>(global::windowDelegate->getHorizFieldOfView());
    }

    _renderer.update();
}

void RenderEngine::updateScreenSpaceRenderables() {
    ZoneScoped

    for (std::unique_ptr<ScreenSpaceRenderable>& ssr : *global::screenSpaceRenderables) {
        ssr->update();
    }
}

glm::ivec2 RenderEngine::renderingResolution() const {
    return global::windowDelegate->currentDrawBufferResolution();
}

glm::ivec2 RenderEngine::fontResolution() const {
    const std::string& value = global::configuration->onScreenTextScaling;
    if (value == "framebuffer") {
        return global::windowDelegate->currentViewportSize();
    }
    else {
        return global::windowDelegate->currentSubwindowSize();
    }
}

glm::mat4 RenderEngine::globalRotation() const {
    glm::vec3 rot = _globalRotation;

    glm::quat pitch = glm::angleAxis(rot.x, glm::vec3(1.f, 0.f, 0.f));
    glm::quat yaw = glm::angleAxis(rot.y, glm::vec3(0.f, 1.f, 0.f));
    glm::quat roll = glm::angleAxis(rot.z, glm::vec3(0.f, 0.f, 1.f));

    return glm::mat4_cast(glm::normalize(pitch * yaw * roll));
}

glm::mat4 RenderEngine::screenSpaceRotation() const {
    glm::vec3 rot = _screenSpaceRotation;

    glm::quat pitch = glm::angleAxis(rot.x, glm::vec3(1.f, 0.f, 0.f));
    glm::quat yaw = glm::angleAxis(rot.y, glm::vec3(0.f, 1.f, 0.f));
    glm::quat roll = glm::angleAxis(rot.z, glm::vec3(0.f, 0.f, 1.f));

    return glm::mat4_cast(glm::normalize(pitch * yaw * roll));
}

glm::mat4 RenderEngine::nodeRotation() const {
    if (!global::windowDelegate->isMaster()) {
        return glm::mat4(1.f);
    }
    glm::vec3 rot = _masterRotation;

    glm::quat pitch = glm::angleAxis(rot.x, glm::vec3(1.f, 0.f, 0.f));
    glm::quat yaw = glm::angleAxis(rot.y, glm::vec3(0.f, 1.f, 0.f));
    glm::quat roll = glm::angleAxis(rot.z, glm::vec3(0.f, 0.f, 1.f));

    return glm::mat4_cast(glm::normalize(pitch * yaw * roll));
}

uint64_t RenderEngine::frameNumber() const {
    return _frameNumber;
}

void RenderEngine::render(const glm::mat4& sceneMatrix, const glm::mat4& viewMatrix,
                          const glm::mat4& projectionMatrix)
{
    ZoneScoped

    LTRACE("RenderEngine::render(begin)");

    const WindowDelegate& delegate = *global::windowDelegate;

    const glm::mat4 globalRot = globalRotation();
    const glm::mat4 nodeRot = nodeRotation();
    glm::mat4 combinedGlobalRot = nodeRot * globalRot;

    if (_camera) {
        _camera->sgctInternal.setViewMatrix(
            viewMatrix * combinedGlobalRot * sceneMatrix
        );
        _camera->sgctInternal.setSceneMatrix(combinedGlobalRot * sceneMatrix);
        _camera->sgctInternal.setProjectionMatrix(projectionMatrix);
        _camera->invalidateCache();
    }

    const int fpsLimit = _framerateLimit;
    if (fpsLimit > 0) {
        // Using a sleep here is not optimal, but we are not looking for FPS-perfect
        // limiting
        std::this_thread::sleep_until(_lastFrameTime);
        const double delta = (1.0 / fpsLimit) * 1000.0 * 1000.0;
        auto now = std::chrono::high_resolution_clock::now();
        _lastFrameTime = now + std::chrono::microseconds(static_cast<int>(delta));
    }

    const bool renderingEnabled = delegate.isMaster() ? !_disableMasterRendering : true;
    if (renderingEnabled && !delegate.isGuiWindow() && _globalBlackOutFactor > 0.f) {
        _renderer.render(
            _scene,
            _camera,
            _globalBlackOutFactor
        );
    }

    // The CEF webbrowser fix has to be called at least once per frame and we are doing
    // that in the renderer::render method.  So if we disable the rendering, that fix is
    // no longer called as we lose access to the Web UI.  Since we are calling the fix
    // many times anyway, we can just add one call to it here and not lose much
    if (global::callback::webBrowserPerformanceHotfix) {
        (*global::callback::webBrowserPerformanceHotfix)();
    }

    if (_showFrameInformation) {
        ZoneScopedN("Show Frame Information")

        glm::vec2 penPosition = glm::vec2(
            fontResolution().x / 2 - 50,
            fontResolution().y / 3
        );

        std::string fn = std::to_string(_frameNumber);
        WindowDelegate::Frustum frustum = global::windowDelegate->frustumMode();
        std::string fr = [](WindowDelegate::Frustum f) -> std::string {
            switch (f) {
                case WindowDelegate::Frustum::Mono:     return "";
                case WindowDelegate::Frustum::LeftEye:  return "(left)";
                case WindowDelegate::Frustum::RightEye: return "(right)";
                default:                              throw ghoul::MissingCaseException();
            }
        }(frustum);

        std::string sgFn = std::to_string(global::windowDelegate->swapGroupFrameNumber());
        std::string dt = std::to_string(global::windowDelegate->deltaTime());
        std::string avgDt = std::to_string(global::windowDelegate->averageDeltaTime());

        std::string res = fmt::format(
            "Frame: {} {}\nSwap group frame: {}\nDt: {}\nAvg Dt: {}",
            fn, fr, sgFn, dt, avgDt
        );
        RenderFont(*_fontFrameInfo, penPosition, res);
    }

    if (renderingEnabled && !delegate.isGuiWindow() && _globalBlackOutFactor > 0.f) {
        ZoneScopedN("Render Screenspace Renderable")

        std::vector<ScreenSpaceRenderable*> ssrs;
        ssrs.reserve(global::screenSpaceRenderables->size());
        for (const std::unique_ptr<ScreenSpaceRenderable>& ssr :
            *global::screenSpaceRenderables)
        {
            if (ssr->isEnabled() && ssr->isReady()) {
                ssrs.push_back(ssr.get());
            }
        }

        std::sort(
            ssrs.begin(),
            ssrs.end(),
            [](ScreenSpaceRenderable* lhs, ScreenSpaceRenderable* rhs) {
                // Render back to front.
                return lhs->depth() > rhs->depth();
            }
        );


        glDisable(GL_DEPTH_TEST);
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        for (ScreenSpaceRenderable* ssr : ssrs) {
            ssr->render();
        }
        glDisable(GL_BLEND);
    }
    LTRACE("RenderEngine::render(end)");
}

bool RenderEngine::mouseActivationCallback(const glm::dvec2& mousePosition) const {
    auto intersects = [](const glm::dvec2& mousePos, const glm::ivec4& bbox) {
        return mousePos.x >= bbox.x && mousePos.x <= bbox.x + bbox.z &&
               mousePos.y <= bbox.y && mousePos.y >= bbox.y - bbox.w;
    };


    if (intersects(mousePosition, _cameraButtonLocations.rotation)) {
        constexpr const char ToggleRotationFrictionScript[] = R"(
            local f = 'NavigationHandler.OrbitalNavigator.Friction.RotationalFriction';
            openspace.setPropertyValueSingle(f, not openspace.getPropertyValue(f));)";

        global::scriptEngine->queueScript(
            ToggleRotationFrictionScript,
            scripting::ScriptEngine::RemoteScripting::Yes
        );
        return true;
    }

    if (intersects(mousePosition, _cameraButtonLocations.zoom)) {
        constexpr const char ToggleZoomFrictionScript[] = R"(
            local f = 'NavigationHandler.OrbitalNavigator.Friction.ZoomFriction';
            openspace.setPropertyValueSingle(f, not openspace.getPropertyValue(f));)";

        global::scriptEngine->queueScript(
            ToggleZoomFrictionScript,
            scripting::ScriptEngine::RemoteScripting::Yes
        );
        return true;
    }

    if (intersects(mousePosition, _cameraButtonLocations.roll)) {
        constexpr const char ToggleRollFrictionScript[] = R"(
            local f = 'NavigationHandler.OrbitalNavigator.Friction.RollFriction';
            openspace.setPropertyValueSingle(f, not openspace.getPropertyValue(f));)";

        global::scriptEngine->queueScript(
            ToggleRollFrictionScript,
            scripting::ScriptEngine::RemoteScripting::Yes
        );
        return true;
    }

    return false;
}

void RenderEngine::renderOverlays(const ShutdownInformation& shutdownInfo) {
    ZoneScoped

    const bool isMaster = global::windowDelegate->isMaster();
    if (isMaster || _showOverlayOnClients) {
        renderScreenLog();
        renderVersionInformation();
        renderDashboard();
        renderCameraInformation();

        if (shutdownInfo.inShutdown) {
            renderShutdownInformation(shutdownInfo.timer, shutdownInfo.waitTime);
        }
    }
}

void RenderEngine::renderEndscreen() {
    glEnable(GL_BLEND);

    rendering::helper::renderBox(
        glm::vec2(0.f),
        glm::vec2(1.f),
        glm::vec4(0.f, 0.f, 0.f, 0.5f)
    );

    const glm::vec2 dpiScaling = global::windowDelegate->dpiScaling();
    const glm::ivec2 res =
        glm::vec2(global::windowDelegate->currentSubwindowSize()) / dpiScaling;
    glViewport(0, 0, res.x, res.y);

    constexpr std::string_view Text = "Shutting down";
    const glm::vec2 size = _fontShutdown->boundingBox(Text);
    glm::vec2 penPosition = glm::vec2(
        fontResolution().x / 2 - size.x / 2,
        fontResolution().y / 2 - size.y / 2
    );
    RenderFont(*_fontShutdown, penPosition, Text);
}

void RenderEngine::renderShutdownInformation(float timer, float fullTime) {
    ZoneScoped

    timer = std::max(timer, 0.f);

    // Render progressive overlay
    glEnable(GL_BLEND);

    // t = 1.f -> start of shutdown counter    t = 0.f -> timer has reached shutdown
    float t = 1.f - (timer / fullTime);

    rendering::helper::renderBox(
        glm::vec2(0.f),
        glm::vec2(1.f),
        glm::vec4(0.f, 0.f, 0.f, ghoul::circularEaseOut(t))
    );

    // No need to print the text if we are just about to finish since otherwise we'll be
    // overplotting the actual "shutdown in progress" text
    if (timer == 0.f) {
        return;
    }

    constexpr std::string_view FirstLine = "Shutdown in: {:.2f}s/{:.2f}s";
    const glm::vec2 size1 = _fontShutdown->boundingBox(
        fmt::format(FirstLine, timer, fullTime)
    );

    glm::vec2 penPosition = glm::vec2(
        fontResolution().x / 2 - size1.x / 2,
        fontResolution().y / 2 - size1.y / 2
    );

    RenderFont(
        *_fontShutdown,
        penPosition,
        fmt::format(FirstLine, timer, fullTime),
        ghoul::fontrendering::CrDirection::Down
    );
    // Important: Length of this string is the same as the first line to make them align
    RenderFont(*_fontShutdown, penPosition, "Press ESC again to abort");
}

void RenderEngine::renderDashboard() {
    ZoneScoped

    glm::vec2 dashboardStart = global::dashboard->getStartPositionOffset();
    glm::vec2 penPosition = glm::vec2(
        dashboardStart.x,
        dashboardStart.y + fontResolution().y - global::luaConsole->currentHeight()
    );

    global::dashboard->render(penPosition);
}

void RenderEngine::postDraw() {
    ZoneScoped

    ++_frameNumber;
}

Scene* RenderEngine::scene() {
    return _scene;
}

void RenderEngine::setScene(Scene* scene) {
    _scene = scene;
}

void RenderEngine::setCamera(Camera* camera) {
    _camera = camera;
}

ghoul::opengl::OpenGLStateCache& RenderEngine::openglStateCache() {
    if (_openglStateCache == nullptr) {
        _openglStateCache = ghoul::opengl::OpenGLStateCache::instance();
    }
    return *_openglStateCache;
}

float RenderEngine::globalBlackOutFactor() {
    return _globalBlackOutFactor;
}

void RenderEngine::setGlobalBlackOutFactor(float opacity) {
    _globalBlackOutFactor = opacity;
}

float RenderEngine::hdrExposure() const {
    return _hdrExposure;
}

bool RenderEngine::isHdrDisabled() const {
    return _disableHDRPipeline;
}

/**
 * Build a program object for rendering with the used renderer
 */
std::unique_ptr<ghoul::opengl::ProgramObject> RenderEngine::buildRenderProgram(
                                                                  const std::string& name,
                                                      const std::filesystem::path& vsPath,
                                                             std::filesystem::path fsPath,
                                                                   ghoul::Dictionary data)
{
    ghoul::Dictionary dict = std::move(data);

    // set path to the current renderer's main fragment shader
    dict.setValue("rendererData", _rendererData);
    // parameterize the main fragment shader program with specific contents.
    // fsPath should point to a shader file defining a Fragment getFragment() function
    // instead of a void main() setting glFragColor, glFragDepth, etc.
    dict.setValue("fragmentPath", fsPath.string());

    using namespace ghoul::opengl;
    std::unique_ptr<ProgramObject> program = ProgramObject::Build(
        name,
        vsPath,
        absPath(RenderFsPath),
        std::move(dict)
    );

    if (program) {
        _programs.push_back(program.get());
    }
    return program;
}

/**
* Build a program object for rendering with the used renderer
*/
std::unique_ptr<ghoul::opengl::ProgramObject> RenderEngine::buildRenderProgram(
                                                                  const std::string& name,
                                                      const std::filesystem::path& vsPath,
                                                             std::filesystem::path fsPath,
                                                      const std::filesystem::path& csPath,
                                                                   ghoul::Dictionary data)
{
    ghoul::Dictionary dict = std::move(data);
    dict.setValue("rendererData", _rendererData);

    // parameterize the main fragment shader program with specific contents.
    // fsPath should point to a shader file defining a Fragment getFragment() function
    // instead of a void main() setting glFragColor, glFragDepth, etc.
    dict.setValue("fragmentPath", fsPath.string());

    using namespace ghoul::opengl;
    std::unique_ptr<ProgramObject> program = ProgramObject::Build(
        name,
        vsPath,
        absPath(RenderFsPath),
        csPath,
        std::move(dict)
    );

    if (program) {
        _programs.push_back(program.get());
    }
    return program;
}

void RenderEngine::removeRenderProgram(ghoul::opengl::ProgramObject* program) {
    if (!program) {
        return;
    }

    auto it = std::find(
        _programs.begin(),
        _programs.end(),
        program
    );

    if (it != _programs.end()) {
        _programs.erase(it);
    }
}

/**
* Set renderer data
* Called from the renderer, whenever it needs to update
* the dictionary of all rendering programs.
*/
void RenderEngine::setRendererData(ghoul::Dictionary rendererData) {
    _rendererData = std::move(rendererData);
    for (ghoul::opengl::ProgramObject* program : _programs) {
        ghoul::Dictionary dict = program->dictionary();
        dict.setValue("rendererData", _rendererData);
        program->setDictionary(dict);
    }
}

/**
* Set resolve data
* Called from the renderer, whenever it needs to update
* the dictionary of all post rendering programs.
*/
void RenderEngine::setResolveData(ghoul::Dictionary resolveData) {
    _resolveData = std::move(resolveData);
    for (ghoul::opengl::ProgramObject* program : _programs) {
        ghoul::Dictionary dict = program->dictionary();
        dict.setValue("resolveData", _resolveData);
        program->setDictionary(dict);
    }
}

/**
 * Take a screenshot and store it in the ${SCREENSHOTS} directory
 */
void RenderEngine::takeScreenshot() {
    // We only create the directory here, as we don't want to spam the users
    // screenshot folder everytime we start OpenSpace even when we are not taking any
    // screenshots. So the first time we actually take one, we create the folder:

    if (!std::filesystem::is_directory(absPath("${SCREENSHOTS}"))) {
        std::filesystem::create_directories(absPath("${SCREENSHOTS}"));
    }

    _latestScreenshotNumber = global::windowDelegate->takeScreenshot(
        _applyWarping,
        _screenshotWindowIds
    );
}

unsigned int RenderEngine::latestScreenshotNumber() const {
    return _latestScreenshotNumber;
}

scripting::LuaLibrary RenderEngine::luaLibrary() {
    return {
        "",
        {
            codegen::lua::AddScreenSpaceRenderable,
            codegen::lua::RemoveScreenSpaceRenderable,
            codegen::lua::TakeScreenshot,
            codegen::lua::DpiScaling
        }
    };
}

void RenderEngine::addScreenSpaceRenderable(std::unique_ptr<ScreenSpaceRenderable> s) {

    const std::string identifier = s->identifier();

    if (std::find_if(
        global::screenSpaceRenderables->begin(),
        global::screenSpaceRenderables->end(),
        [&identifier](const std::unique_ptr<ScreenSpaceRenderable>& ssr) {
            return ssr->identifier() == identifier;
        }) != global::screenSpaceRenderables->end()
    ) {
        LERROR(fmt::format(
            "Cannot add scene space renderable. "
            "An element with identifier '{}' already exists",
            identifier
        ));
        return;
    }

    s->initialize();
    s->initializeGL();

    ScreenSpaceRenderable* ssr = s.get();
    global::screenSpaceRootPropertyOwner->addPropertySubOwner(ssr);
    global::screenSpaceRenderables->push_back(std::move(s));

    global::eventEngine->publishEvent<events::EventScreenSpaceRenderableAdded>(ssr);
}

void RenderEngine::removeScreenSpaceRenderable(ScreenSpaceRenderable* s) {
    const auto it = std::find_if(
        global::screenSpaceRenderables->begin(),
        global::screenSpaceRenderables->end(),
        [s](const std::unique_ptr<ScreenSpaceRenderable>& r) { return r.get() == s; }
    );

    if (it != global::screenSpaceRenderables->end()) {
        global::eventEngine->publishEvent<events::EventScreenSpaceRenderableRemoved>(s);
        s->deinitializeGL();
        s->deinitialize();
        global::screenSpaceRootPropertyOwner->removePropertySubOwner(s);
        global::screenSpaceRenderables->erase(it);
    }
}

void RenderEngine::removeScreenSpaceRenderable(const std::string& identifier) {
    ScreenSpaceRenderable* s = screenSpaceRenderable(identifier);
    if (s) {
        removeScreenSpaceRenderable(s);
    }
}

ScreenSpaceRenderable* RenderEngine::screenSpaceRenderable(const std::string& identifier)
{
    const auto it = std::find_if(
        global::screenSpaceRenderables->begin(),
        global::screenSpaceRenderables->end(),
        [&identifier](const std::unique_ptr<ScreenSpaceRenderable>& s) {
            return s->identifier() == identifier;
        }
    );

    if (it != global::screenSpaceRenderables->end()) {
        return it->get();
    }
    else {
        return nullptr;
    }
}

std::vector<ScreenSpaceRenderable*> RenderEngine::screenSpaceRenderables() const {
    std::vector<ScreenSpaceRenderable*> res(global::screenSpaceRenderables->size());
    std::transform(
        global::screenSpaceRenderables->begin(),
        global::screenSpaceRenderables->end(),
        res.begin(),
        [](const std::unique_ptr<ScreenSpaceRenderable>& p) { return p.get(); }
    );
    return res;
}

void RenderEngine::renderCameraInformation() {
    ZoneScoped

    if (!_showCameraInfo) {
        return;
    }

    const glm::vec4 EnabledColor  = _enabledFontColor.value();
    const glm::vec4 DisabledColor = _disabledFontColor.value();

    const glm::vec2 rotationBox = _fontCameraInfo->boundingBox("Rotation");

    float penPosY = fontResolution().y - rotationBox.y;

    constexpr float YSeparation = 5.f;
    constexpr float XSeparation = 5.f;

    const interaction::OrbitalNavigator& nav =
        global::navigationHandler->orbitalNavigator();

    using FR = ghoul::fontrendering::FontRenderer;

    _cameraButtonLocations.rotation = {
        fontResolution().x - rotationBox.x - XSeparation,
        fontResolution().y - penPosY,
        rotationBox.x,
        rotationBox.y
    };
    FR::defaultRenderer().render(
        *_fontCameraInfo,
        glm::vec2(fontResolution().x - rotationBox.x - XSeparation, penPosY),
        "Rotation",
        nav.hasRotationalFriction() ? EnabledColor : DisabledColor
    );
    penPosY -= rotationBox.y + YSeparation;

    const glm::vec2 zoomBox = _fontCameraInfo->boundingBox("Zoom");

    _cameraButtonLocations.zoom = {
        fontResolution().x - zoomBox.x - XSeparation,
        fontResolution().y - penPosY,
        zoomBox.x,
        zoomBox.y
    };
    FR::defaultRenderer().render(
        *_fontCameraInfo,
        glm::vec2(fontResolution().x - zoomBox.x - XSeparation, penPosY),
        "Zoom",
        nav.hasZoomFriction() ? EnabledColor : DisabledColor
    );
    penPosY -= zoomBox.y + YSeparation;

    const glm::vec2 rollBox = _fontCameraInfo->boundingBox("Roll");

    _cameraButtonLocations.roll = {
        fontResolution().x - rollBox.x - XSeparation,
        fontResolution().y - penPosY,
        rollBox.x,
        rollBox.y
    };
    FR::defaultRenderer().render(
        *_fontCameraInfo,
        glm::vec2(fontResolution().x - rollBox.x - XSeparation, penPosY),
        "Roll",
        nav.hasRollFriction() ? EnabledColor : DisabledColor
    );
}

void RenderEngine::renderVersionInformation() {
    ZoneScoped

    if (!_showVersionInfo) {
        return;
    }

    using FR = ghoul::fontrendering::FontRenderer;
    const glm::vec2 versionBox = _fontVersionInfo->boundingBox(_versionString);
    const glm::vec2 commitBox = _fontVersionInfo->boundingBox(OPENSPACE_GIT_FULL);

    FR::defaultRenderer().render(
        *_fontVersionInfo,
        glm::vec2(
            fontResolution().x - versionBox.x - 10.f,
            5.f
        ),
        _versionString,
        glm::vec4(0.5f, 0.5f, 0.5f, 1.f)
    );

    // If a developer hasn't placed the Git command in the path, this variable will be
    // empty
    if (!std::string_view(OPENSPACE_GIT_COMMIT).empty()) {
        // We check OPENSPACE_GIT_COMMIT but use OPENSPACE_GIT_FULL on purpose since
        // OPENSPACE_GIT_FULL will never be empty (always will contain at least @, but
        // checking for that is a bit brittle)
        FR::defaultRenderer().render(
            *_fontVersionInfo,
            glm::vec2(fontResolution().x - commitBox.x - 10.f, versionBox.y + 5.f),
            OPENSPACE_GIT_FULL,
            glm::vec4(0.5, 0.5, 0.5, 1.f)
        );
    }

#ifdef TRACY_ENABLE
    {
        // If we have Tracy enabled, we should inform the user about it that the
        // application will crash after a while if no profiler is attached

        ZoneScopedN("Tracy Information")

        const glm::vec2 tracyBox = _fontVersionInfo->boundingBox("TRACY PROFILING");
        const glm::vec2 penPosition = glm::vec2(
            fontResolution().x - tracyBox.x - 10.f,
            versionBox.y + commitBox.y + 5.f
        );
        FR::defaultRenderer().render(
            *_fontVersionInfo,
            penPosition,
            "TRACY PROFILING",
            glm::vec4(0.8f, 0.2f, 0.15f, 1.f)
        );
    }
#endif // TRACY_ENABLE

}

void RenderEngine::renderScreenLog() {
    ZoneScoped

    if (!_showLog) {
        return;
    }

    _log->removeExpiredEntries();

    constexpr size_t MaxNumberMessages = 20;
    constexpr int CategoryLength = 30;
    constexpr int MessageLength = 280;
    constexpr std::chrono::seconds FadeTime(5);

    const std::vector<ScreenLog::LogEntry>& entries = _log->entries();
    const glm::ivec2 fontRes = fontResolution();

    size_t nRows = 1;
    const auto now = std::chrono::steady_clock::now();
    for (size_t i = 1; i <= std::min(MaxNumberMessages, entries.size()); i += 1) {
        ZoneScopedN("Entry")

        const ScreenLog::LogEntry& it = entries[entries.size() - i];

        const std::chrono::duration<double> diff = now - it.timeStamp;

        float alpha = 1.f;
        const std::chrono::duration<double> ttf = ScreenLogTimeToLive - FadeTime;
        if (diff > ttf) {
            const double d = (diff - ttf).count();
            const float t = static_cast<float>(d) / static_cast<float>(FadeTime.count());
            const float p = 0.8f - t;
            alpha = (p <= 0.f) ? 0.f : pow(p, 0.4f);
        }

        // Since all log entries are ordered, once one exceeds alpha, all have
        if (alpha <= 0.f) {
            break;
        }
        std::string_view message = std::string_view(it.message).substr(0, MessageLength);
        nRows += std::count(message.begin(), message.end(), '\n');

        const glm::vec4 white(0.9f, 0.9f, 0.9f, alpha);

        std::array<char, 15 + 1 + CategoryLength + 3> buf;
        {
            std::fill(buf.begin(), buf.end(), char(0));
            char* end = fmt::format_to(
                buf.data(),
                "{:<15} {}{}",
                it.timeString,
                std::string_view(it.category).substr(0, CategoryLength),
                it.category.length() > CategoryLength ? "..." : ""
            );
            std::string_view text = std::string_view(buf.data(), end - buf.data());

            RenderFont(
                *_fontLog,
                glm::vec2(
                    10.f,
                    _fontLog->pointSize() * nRows * 2 + fontRes.y * _verticalLogOffset
                ),
                text,
                white
            );
        }

        {
            const glm::vec4 color = [alpha, white](ScreenLog::LogLevel level) {
                switch (level) {
                    case ghoul::logging::LogLevel::Debug:
                        return glm::vec4(0.f, 1.f, 0.f, alpha);
                    case ghoul::logging::LogLevel::Warning:
                        return glm::vec4(1.f, 1.f, 0.f, alpha);
                    case ghoul::logging::LogLevel::Error:
                        return glm::vec4(1.f, 0.f, 0.f, alpha);
                    case ghoul::logging::LogLevel::Fatal:
                        return glm::vec4(0.3f, 0.3f, 0.85f, alpha);
                    default:
                        return white;
                }
            }(it.level);

            const std::string_view lvl = ghoul::to_string(it.level);
            std::fill(buf.begin(), buf.end(), char(0));
            char* end = fmt::format_to(buf.data(), "({})", lvl);
            std::string_view levelText = std::string_view(buf.data(), end - buf.data());
            RenderFont(
                *_fontLog,
                glm::vec2(
                    10 + (30 + 3) * _fontLog->pointSize(),
                    _fontLog->pointSize() * nRows * 2 + fontRes.y * _verticalLogOffset
                ),
                levelText,
                color
            );
        }

        RenderFont(
            *_fontLog,
            glm::vec2(
                10 + 44 * _fontLog->pointSize(),
                _fontLog->pointSize() * nRows * 2 + fontRes.y * _verticalLogOffset
            ),
            message,
            white
        );
        ++nRows;
    }
}

} // namespace openspace
