/*****************************************************************************************
 *                                                                                       *
 * OpenSpace                                                                             *
 *                                                                                       *
 * Copyright (c) 2014-2023                                                               *
 *                                                                                       *
 * 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/base/rendering/renderablelabel.h>

#include <modules/base/basemodule.h>
#include <openspace/documentation/documentation.h>
#include <openspace/documentation/verifier.h>
#include <openspace/engine/globals.h>
#include <openspace/rendering/renderengine.h>
#include <openspace/scene/scenegraphnode.h>
#include <openspace/util/updatestructures.h>
#include <ghoul/glm.h>
#include <ghoul/filesystem/filesystem.h>
#include <ghoul/font/fontmanager.h>
#include <ghoul/font/fontrenderer.h>
#include <ghoul/io/texture/texturereader.h>
#include <ghoul/logging/logmanager.h>
#include <ghoul/misc/crc32.h>
#include <ghoul/misc/defer.h>
#include <ghoul/misc/profiling.h>
#include <ghoul/misc/templatefactory.h>
#include <ghoul/opengl/openglstatecache.h>
#include <ghoul/opengl/programobject.h>
#include <ghoul/opengl/texture.h>
#include <ghoul/opengl/textureunit.h>
#include <glm/gtx/string_cast.hpp>
#include <optional>

namespace {
    constexpr std::string_view MeterUnit = "m";
    constexpr std::string_view KilometerUnit = "Km";
    constexpr std::string_view MegameterUnit = "Mm";
    constexpr std::string_view GigameterUnit = "Gm";
    constexpr std::string_view AstronomicalUnitUnit = "au";
    constexpr std::string_view TerameterUnit = "Tm";
    constexpr std::string_view PetameterUnit = "Pm";
    constexpr std::string_view ParsecUnit = "pc";
    constexpr std::string_view KiloparsecUnit = "Kpc";
    constexpr std::string_view MegaparsecUnit = "Mpc";
    constexpr std::string_view GigaparsecUnit = "Gpc";
    constexpr std::string_view GigalightyearUnit = "Gly";

    enum BlendMode {
        Normal = 0,
        Additive
    };

    enum Orientation {
        ViewDirection = 0,
        PositionNormal
    };

    enum Unit {
        Meter = 0,
        Kilometer,
        Megameter,
        Gigameter,
        AstronomicalUnit,
        Terameter,
        Petameter,
        Parsec,
        KiloParsec,
        MegaParsec,
        GigaParsec,
        GigaLightyear
    };

    constexpr double PARSEC = 0.308567756E17;

    constexpr openspace::properties::Property::PropertyInfo BlendModeInfo = {
        "BlendMode",
        "Blending Mode",
        "This determines the blending mode that is applied to the renderable",
        openspace::properties::Property::Visibility::AdvancedUser
    };

    constexpr openspace::properties::Property::PropertyInfo ColorInfo = {
        "Color",
        "Color",
        "The label text color",
        // @VISIBILITY(1.33)
        openspace::properties::Property::Visibility::NoviceUser
    };

    constexpr openspace::properties::Property::PropertyInfo FontSizeInfo = {
        "FontSize",
        "Font Size",
        "The font size (in points) for the label",
        // @VISIBILITY(2.5)
        openspace::properties::Property::Visibility::User
    };

    constexpr openspace::properties::Property::PropertyInfo SizeInfo = {
        "Size",
        "Size",
        "This value affects the size scale of the label",
        // @VISIBILITY(2.75)
        openspace::properties::Property::Visibility::User
    };

    constexpr openspace::properties::Property::PropertyInfo TextInfo = {
        "Text",
        "Text",
        "The text that will be displayed on screen",
        // @VISIBILITY(1.8)
        openspace::properties::Property::Visibility::NoviceUser
    };

    constexpr openspace::properties::Property::PropertyInfo MinMaxSizeInfo = {
        "MinMaxSize",
        "Min and Max Size",
        "The minimum and maximum size (in pixels) of the label",
        openspace::properties::Property::Visibility::AdvancedUser
    };

    constexpr openspace::properties::Property::PropertyInfo TransformationMatrixInfo = {
        "TransformationMatrix",
        "Transformation Matrix",
        "Transformation matrix to be applied to the label",
        // @VISIBILITY(3.6)
        openspace::properties::Property::Visibility::AdvancedUser
    };

    constexpr openspace::properties::Property::PropertyInfo OrientationOptionInfo = {
        "OrientationOption",
        "Orientation Option",
        "Label orientation rendering mode",
        // @VISIBILITY(2.8)
        openspace::properties::Property::Visibility::User
    };

    constexpr openspace::properties::Property::PropertyInfo EnableFadingEffectInfo = {
        "EnableFading",
        "Enable/Disable Fade-in Effect",
        "Enable/Disable the Fade-in effect",
        // @VISIBILITY(?)
        openspace::properties::Property::Visibility::User
    };

    constexpr openspace::properties::Property::PropertyInfo FadeWidthsInfo = {
        "FadeWidths",
        "Fade Widths",
        "The distances over which the fading takes place, given in the specified unit. "
        "The first value is the distance before the closest distance and the second "
        "the one after the furthest distance. For example, with the unit Parsec (pc), "
        "a value of {1, 2} will make the label being fully faded out 1 Parsec before "
        "the closest distance and 2 Parsec away from the furthest distance",
        openspace::properties::Property::Visibility::AdvancedUser
    };

    constexpr openspace::properties::Property::PropertyInfo FadeDistancesInfo = {
        "FadeDistances",
        "Fade Distances",
        "The distance range in which the labels should be fully opaque, specified in "
        "the chosen unit. The distance from the position of the label to the camera",
        openspace::properties::Property::Visibility::AdvancedUser
    };

    constexpr openspace::properties::Property::PropertyInfo FadeUnitOptionInfo = {
        "FadeUnit",
        "Fade Distance Unit",
        "Distance unit for fade-in/-out distance calculations. Defaults to \"au\"",
        openspace::properties::Property::Visibility::AdvancedUser
    };

    struct [[codegen::Dictionary(RenderableLabel)]] Parameters {
        enum class [[codegen::map(BlendMode)]] BlendMode {
            Normal,
            Additive
        };

        // [[codegen::verbatim(BlendModeInfo.description)]]
        std::optional<BlendMode> blendMode;

        enum class [[codegen::map(Orientation)]] Orientation {
            ViewDirection [[codegen::key("Camera View Direction")]],
            PositionNormal [[codegen::key("Camera Position Normal")]]
        };

        // [[codegen::verbatim(OrientationOptionInfo.description)]]
        std::optional<Orientation> orientationOption;

        // [[codegen::verbatim(ColorInfo.description)]]
        std::optional<glm::vec3> color [[codegen::color()]];

        // [[codegen::verbatim(TextInfo.description)]]
        std::optional<std::string> text;

        // [[codegen::verbatim(FontSizeInfo.description)]]
        std::optional<float> fontSize;

        // [[codegen::verbatim(SizeInfo.description)]]
        std::optional<float> size;

        // [[codegen::verbatim(MinMaxSizeInfo.description)]]
        std::optional<glm::ivec2> minMaxSize;

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

        // [[codegen::verbatim(TransformationMatrixInfo.description)]]
        std::optional<glm::dmat4x4> transformationMatrix;

        enum class [[codegen::map(Unit)]] Unit {
            Meter [[codegen::key("m")]],
            Kilometer [[codegen::key("Km")]],
            Megameter [[codegen::key("Mm")]],
            Gigameter [[codegen::key("Gm")]],
            Terameter [[codegen::key("Tm")]],
            Petameter [[codegen::key("Pm")]],
            AstronomicalUnit [[codegen::key("au")]],
            Parsec [[codegen::key("pc")]],
            KiloParsec [[codegen::key("Kpc")]],
            MegaParsec [[codegen::key("Mpc")]],
            GigaParsec [[codegen::key("Gpc")]],
            GigaLightyear [[codegen::key("Gly")]]
        };

        // [[codegen::verbatim(FadeUnitOptionInfo.description)]]
        std::optional<Unit> fadeUnit;

        // [[codegen::verbatim(FadeDistancesInfo.description)]]
        std::optional<glm::vec2> fadeDistances;

        // [[codegen::verbatim(FadeWidthsInfo.description)]]
        std::optional<glm::vec2> fadeWidths;
    };
#include "renderablelabel_codegen.cpp"
} // namespace

namespace openspace {

documentation::Documentation RenderableLabel::Documentation() {
    return codegen::doc<Parameters>("base_renderable_labels");
}

RenderableLabel::RenderableLabel(const ghoul::Dictionary& dictionary)
    : Renderable(dictionary, { .automaticallyUpdateRenderBin = false })
    , _blendMode(BlendModeInfo, properties::OptionProperty::DisplayType::Dropdown)
    , _color(ColorInfo, glm::vec3(1.f), glm::vec3(0.f), glm::vec3(1.f))
    , _fontSize(FontSizeInfo, 50.f, 1.f, 100.f)
    , _size(SizeInfo, 8.f, 0.5f, 30.f)
    , _minMaxSize(MinMaxSizeInfo, glm::ivec2(8, 20), glm::ivec2(0), glm::ivec2(100))
    , _text(TextInfo, "")
    , _enableFadingEffect(EnableFadingEffectInfo, false)
    , _fadeWidths(FadeWidthsInfo, glm::vec2(1.f), glm::vec2(0.f), glm::vec2(100.f))
    , _fadeDistances(FadeDistancesInfo, glm::vec2(1.f), glm::vec2(0.f), glm::vec2(100.f))
    , _fadeUnitOption(
        FadeUnitOptionInfo,
        properties::OptionProperty::DisplayType::Dropdown
    )
    , _orientationOption(
        OrientationOptionInfo,
        properties::OptionProperty::DisplayType::Dropdown
    )
{
    const Parameters p = codegen::bake<Parameters>(dictionary);

    addProperty(Fadeable::_opacity);

    _blendMode.addOptions({
        { BlendMode::Normal, "Normal" },
        { BlendMode::Additive, "Additive" }
    });
    _blendMode.onChange([this]() {
        switch (_blendMode) {
            case BlendMode::Normal:
                setRenderBinFromOpacity();
                break;
            case BlendMode::Additive:
                setRenderBin(Renderable::RenderBin::PreDeferredTransparent);
                break;
            default:
                throw ghoul::MissingCaseException();
        }
    });

    if (p.blendMode.has_value()) {
        _blendMode = codegen::map<BlendMode>(*p.blendMode);
    }

    addProperty(_blendMode);

    _orientationOption.addOption(ViewDirection, "Camera View Direction");
    _orientationOption.addOption(PositionNormal, "Camera Position Normal");

    _orientationOption = PositionNormal;
    if (p.orientationOption.has_value()) {
        _orientationOption = codegen::map<Orientation>(*p.orientationOption);
    }
    addProperty(_orientationOption);

    _text = p.text.value_or(_text);
    addProperty(_text);

    _color = p.color.value_or(_color);
    _color.setViewOption(properties::Property::ViewOptions::Color);
    addProperty(_color);

    _fontSize = p.fontSize.value_or(_fontSize);
    _fontSize.onChange([this]() {
        _font = global::fontManager->font(
            "Mono",
            _fontSize,
            ghoul::fontrendering::FontManager::Outline::Yes,
            ghoul::fontrendering::FontManager::LoadGlyphs::No
        );
    });
    addProperty(_fontSize);

    // @TODO (emmbr, 2021-05-31): Temporarily set as read only, to avoid errors from font
    // rendering/loading
    _fontSize.setReadOnly(true);

    _size = p.size.value_or(_size);
    addProperty(_size);

    _minMaxSize = p.minMaxSize.value_or(_minMaxSize);
    _minMaxSize.setViewOption(properties::Property::ViewOptions::MinMaxRange);
    addProperty(_minMaxSize);

    _transformationMatrix = p.transformationMatrix.value_or(_transformationMatrix);

    _enableFadingEffect = p.enableFading.value_or(_enableFadingEffect);
    addProperty(_enableFadingEffect);

    _fadeUnitOption.addOption(Meter, std::string(MeterUnit));
    _fadeUnitOption.addOption(Kilometer, std::string(KilometerUnit));
    _fadeUnitOption.addOption(Megameter, std::string(MegameterUnit));
    _fadeUnitOption.addOption(Gigameter, std::string(GigameterUnit));
    _fadeUnitOption.addOption(AstronomicalUnit, std::string(AstronomicalUnitUnit));
    _fadeUnitOption.addOption(Terameter, std::string(TerameterUnit));
    _fadeUnitOption.addOption(Petameter, std::string(PetameterUnit));
    _fadeUnitOption.addOption(Parsec, std::string(ParsecUnit));
    _fadeUnitOption.addOption(KiloParsec, std::string(KiloparsecUnit));
    _fadeUnitOption.addOption(MegaParsec, std::string(MegaparsecUnit));
    _fadeUnitOption.addOption(GigaParsec, std::string(GigaparsecUnit));
    _fadeUnitOption.addOption(GigaLightyear, std::string(GigalightyearUnit));

    if (p.fadeUnit.has_value()) {
        _fadeUnitOption = codegen::map<Unit>(*p.fadeUnit);
    }
    else {
        _fadeUnitOption = AstronomicalUnit;
    }
    addProperty(_fadeUnitOption);

    _fadeDistances = p.fadeDistances.value_or(_fadeDistances);
    _fadeDistances.setViewOption(properties::Property::ViewOptions::MinMaxRange);
    addProperty(_fadeDistances);

    _fadeWidths = p.fadeWidths.value_or(_fadeWidths);
    addProperty(_fadeWidths);
}

bool RenderableLabel::isReady() const {
    return true;
}

void RenderableLabel::initialize() {
    ZoneScoped;

    setRenderBin(Renderable::RenderBin::PreDeferredTransparent);
}

void RenderableLabel::initializeGL() {
    if (_font == nullptr) {
        _font = global::fontManager->font(
            "Mono",
            _fontSize,
            ghoul::fontrendering::FontManager::Outline::Yes,
            ghoul::fontrendering::FontManager::LoadGlyphs::No
        );
    }
}

void RenderableLabel::deinitializeGL() {}

void RenderableLabel::render(const RenderData& data, RendererTasks&) {
    glDepthMask(true);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);

    float fadeInVariable = 1.f;

    if (_enableFadingEffect) {
        float distanceNodeToCamera = static_cast<float>(
            glm::distance(data.camera.positionVec3(), data.modelTransform.translation)
        );
        fadeInVariable = computeFadeFactor(distanceNodeToCamera);
    }

    glm::dmat4 modelMatrix(1.0);
    glm::dmat4 modelViewMatrix = data.camera.combinedViewMatrix() * modelMatrix;
    glm::dmat4 projectionMatrix = glm::dmat4(data.camera.projectionMatrix());

    glm::dmat4 modelViewProjectionMatrix = projectionMatrix * modelViewMatrix;

    glm::dvec3 cameraViewDirectionWorld = -data.camera.viewDirectionWorldSpace();
    glm::dvec3 cameraUpDirectionWorld = data.camera.lookUpVectorWorldSpace();
    glm::dvec3 orthoRight = glm::normalize(
        glm::cross(cameraUpDirectionWorld, cameraViewDirectionWorld)
    );
    if (orthoRight == glm::dvec3(0.0)) {
        glm::dvec3 otherVector(
            cameraUpDirectionWorld.y,
            cameraUpDirectionWorld.x,
            cameraUpDirectionWorld.z
        );
        orthoRight = glm::normalize(glm::cross(otherVector, cameraViewDirectionWorld));
    }
    glm::dvec3 orthoUp = glm::normalize(glm::cross(cameraViewDirectionWorld, orthoRight));

    renderLabels(data, modelViewProjectionMatrix, orthoRight, orthoUp, fadeInVariable);

    global::renderEngine->openglStateCache().resetBlendState();
    global::renderEngine->openglStateCache().resetDepthState();
}


void RenderableLabel::setLabelText(const std::string & newText) {
    _text = newText;
}

void RenderableLabel::renderLabels(const RenderData& data,
                                    const glm::dmat4& modelViewProjectionMatrix,
                                    const glm::dvec3& orthoRight,
                                    const glm::dvec3& orthoUp, float fadeInVariable)
{
    glm::vec4 textColor = glm::vec4(glm::vec3(_color), 1.f);

    textColor.a *= fadeInVariable;
    textColor.a *= opacity();

    ghoul::fontrendering::FontRenderer::ProjectedLabelsInformation labelInfo;

    labelInfo.orthoRight = orthoRight;
    labelInfo.orthoUp = orthoUp;
    labelInfo.minSize = _minMaxSize.value().x;
    labelInfo.maxSize = _minMaxSize.value().y;
    labelInfo.cameraPos = data.camera.positionVec3();
    labelInfo.cameraLookUp = data.camera.lookUpVectorWorldSpace();
    labelInfo.renderType = _orientationOption;
    labelInfo.mvpMatrix = modelViewProjectionMatrix;
    labelInfo.scale = powf(10.f, _size);
    labelInfo.enableDepth = true;
    labelInfo.enableFalseDepth = false;

    // We don't use spice rotation and scale
    glm::vec3 transformedPos(
        _transformationMatrix * glm::dvec4(data.modelTransform.translation, 1.0)
    );

    ghoul::fontrendering::FontRenderer::defaultProjectionRenderer().render(
        *_font,
        transformedPos,
        _text.value(),
        textColor,
        labelInfo
    );
}

float RenderableLabel::computeFadeFactor(float distanceNodeToCamera) const {
    float distanceUnit = unit(_fadeUnitOption);

    float x = distanceNodeToCamera;
    float startX = _fadeDistances.value().x * distanceUnit;
    float endX = _fadeDistances.value().y * distanceUnit;

    // The distances over which the fading should happen
    float fadingStartDistance = _fadeWidths.value().x * distanceUnit;
    float fadingEndDistance = _fadeWidths.value().y * distanceUnit;

    if (x <= startX) {
        float f1 = 1.f - (startX - x) / fadingStartDistance;
        return std::clamp(f1, 0.f, 1.f);
    }
    else if (x > startX && x < endX) {
        return 1.f; // not faded
    }
    else { // x >= endX
        float f2 = 1.f - (x - endX) / fadingEndDistance;
        return std::clamp(f2, 0.f, 1.f);
    }
}

float RenderableLabel::unit(int unit) const {
    switch (static_cast<Unit>(unit)) {
        case Meter: return 1.f;
        case Kilometer: return 1e3f;
        case Megameter: return  1e6f;
        case Gigameter: return 1e9f;
        case AstronomicalUnit: return 149597870700.f;
        case Terameter: return 1e12f;
        case Petameter: return 1e15f;
        case Parsec: return static_cast<float>(PARSEC);
        case KiloParsec: return static_cast<float>(1e3 * PARSEC);
        case MegaParsec: return static_cast<float>(1e6 * PARSEC);
        case GigaParsec: return static_cast<float>(1e9 * PARSEC);
        case GigaLightyear: return static_cast<float>(306391534.73091 * PARSEC);
        default: throw std::logic_error("Missing case label");
    }
}

std::string_view RenderableLabel::toString(int unit) const {
    switch (static_cast<Unit>(unit)) {
        case Meter: return MeterUnit;
        case Kilometer: return KilometerUnit;
        case Megameter: return  MegameterUnit;
        case Gigameter: return GigameterUnit;
        case AstronomicalUnit: return AstronomicalUnitUnit;
        case Terameter: return TerameterUnit;
        case Petameter: return PetameterUnit;
        case Parsec: return ParsecUnit;
        case KiloParsec: return KiloparsecUnit;
        case MegaParsec: return MegaparsecUnit;
        case GigaParsec: return GigaparsecUnit;
        case GigaLightyear: return GigalightyearUnit;
        default: throw std::logic_error("Missing case label");
    }
}

} // namespace openspace
