/*****************************************************************************************
 *                                                                                       *
 * OpenSpace                                                                             *
 *                                                                                       *
 * Copyright (c) 2014-2020                                                               *
 *                                                                                       *
 * 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/screenspacerenderable.h>

#include <openspace/documentation/documentation.h>
#include <openspace/documentation/verifier.h>
#include <openspace/engine/globals.h>
#include <openspace/engine/windowdelegate.h>
#include <openspace/rendering/helper.h>
#include <openspace/rendering/renderengine.h>
#include <openspace/scripting/scriptengine.h>
#include <openspace/scene/scene.h>
#include <openspace/util/camera.h>
#include <openspace/util/factorymanager.h>
#include <ghoul/filesystem/filesystem.h>
#include <ghoul/misc/profiling.h>
#include <ghoul/opengl/programobject.h>
#include <ghoul/opengl/textureunit.h>

namespace {
    constexpr const char* KeyType = "Type";
    constexpr const char* KeyTag = "Tag";

    constexpr const std::array<const char*, 4> UniformNames = {
        "Alpha", "ModelTransform", "ViewProjectionMatrix", "texture1"
    };

    constexpr openspace::properties::Property::PropertyInfo EnabledInfo = {
        "Enabled",
        "Is Enabled",
        "This setting determines whether this sceen space plane will be visible or not."
    };

    constexpr openspace::properties::Property::PropertyInfo
    UseRadiusAzimuthElevationInfo = {
        "UseRadiusAzimuthElevation",
        "Use Radius Azimuth and Elevation",
        "This value determines whether the location of this screen space plane will be "
        "specified using radius, azimuth and elevation (if this is set to 'true') or "
        "using cartesian coordinates. By switching this value, the correct property will "
        "be shown or hidden. The Cartesian coordinate system is useful if a regular "
        "rendering is applied, whereas the radius azimuth elevation are most useful in a "
        "planetarium environment."
    };

    constexpr openspace::properties::Property::PropertyInfo
    UsePerspectiveProjectionInfo = {
        "UsePerspectiveProjection",
        "Use Perspective Projection",
        "Determines whetether the z/radius values affects the size of the plane or not."
    };

    constexpr openspace::properties::Property::PropertyInfo CartesianPositionInfo = {
        "CartesianPosition",
        "Cartesian coordinates",
        "This value determines the position of this screen space plane in Cartesian "
        "three-dimensional coordinates (meters)."
    };

    constexpr openspace::properties::Property::PropertyInfo RadiusAzimuthElevationInfo = {
        "RadiusAzimuthElevation",
        "Radius Azimuth Elevation",
        "This value determines the position of this screen space plane in a "
        "coordinate system based on radius (meters), azimuth (radians) and elevation "
        "(radians)."
    };

    constexpr openspace::properties::Property::PropertyInfo ScaleInfo = {
        "Scale",
        "Scale value",
        "This value determines a scale factor for the plane. The default size of a plane "
        "is determined by the concrete instance and reflects, for example, the size of "
        "the image being displayed."
    };

    constexpr openspace::properties::Property::PropertyInfo LocalRotationInfo = {
        "Rotation",
        "Local rotation",
        "An euler rotation (x, y, z) to apply to the plane."
    };


    constexpr openspace::properties::Property::PropertyInfo AlphaInfo = {
        "Alpha",
        "Transparency",
        "This value determines the transparency of the screen space plane. If this value "
        "is 1, the plane is completely opaque, if this value is 0, the plane is "
        "completely transparent."
    };

    constexpr openspace::properties::Property::PropertyInfo DeleteInfo = {
        "Delete",
        "Delete",
        "If this property is triggered, this screen space plane is removed from the "
        "scene."
    };

    constexpr openspace::properties::Property::PropertyInfo FaceCameraInfo = {
        "FaceCamera",
        "Face Camera",
        "If enabled, the local rotation is applied after the plane is rotated to face "
        "the camera."
    };

    float wrap(float value, float min, float max) {
        return glm::mod(value - min, max - min) + min;
    }

    glm::vec3 sanitizeSphericalCoordinates(glm::vec3 spherical) {
        const float r = spherical.x;
        float phi = spherical.z;

        // Sanitize coordinates.
        float theta = wrap(spherical.y, 0.f, glm::two_pi<float>());
        if (theta > glm::pi<float>()) {
            theta = glm::two_pi<float>() - theta;
            phi += glm::pi<float>();
        }

        return glm::vec3(r, theta, phi);
    }


    glm::vec3 sphericalToCartesian(glm::vec3 spherical) {
        // First convert to ISO convention spherical coordinates according to
        // https://en.wikipedia.org/wiki/Spherical_coordinate_system
        // (radius, theta, phi), where theta is the polar angle from the z axis,
        // and phi is the azimuth.

        const glm::vec3 sanitized = sanitizeSphericalCoordinates(std::move(spherical));
        const float x = sanitized[0] * sin(sanitized[1]) * cos(sanitized[2]);
        const float y = sanitized[0] * sin(sanitized[1]) * sin(sanitized[2]);
        const float z = sanitized[0] * cos(sanitized[1]);

        // Now, convert rotate the coordinate system, so that z maps to y,
        // and y maps to -z. We want the pole to be in y instead of z.
        return glm::vec3(x, -z, y);
    }

    glm::vec3 cartesianToSpherical(const glm::vec3& cartesian) {
        // Rotate cartesian coordinates.
        glm::vec3 rotated = glm::vec3(cartesian.x, cartesian.z, -cartesian.y);

        const float r = sqrt(
            pow(rotated.x, 2.f) + pow(rotated.y, 2.f) + pow(rotated.z, 2.f)
        );
        const float theta = acos(rotated.z/r);
        const float phi = atan2(rotated.y, rotated.x);
        return sanitizeSphericalCoordinates(glm::vec3(r, theta, phi));
    }

    // Radius, azimiuth, elevation to spherical coordinates.
    glm::vec3 raeToSpherical(glm::vec3 rae) {
        //return rae;
         const float r = rae.x;

         // Polar angle, theta, is elevation + pi/2.
         const float theta = rae.z + glm::half_pi<float>();

         // Azimuth in ISO spherical coordiantes (phi) is angle from x,
         // as opposed to from negative y on screen.
         const float phi = rae.y - glm::half_pi<float>();

         return glm::vec3(r, theta, phi);
    }

    // Spherical coordinates to radius, azimuth and elevation.
    glm::vec3 sphericalToRae(glm::vec3 spherical) {
        //return spherical;
        const float r = spherical.x;

        // Azimuth on screen is angle from negative y, as opposed to from x.
        float azimuth = spherical.z + glm::half_pi<float>();

        // Elevation is polar angle - pi/2
        float elevation = wrap(
            spherical.y - glm::half_pi<float>(),
            -glm::pi<float>(),
            glm::pi<float>()
        );

        return glm::vec3(
            r,
            wrap(azimuth, -glm::pi<float>(), glm::pi<float>()),
            wrap(elevation, -glm::pi<float>(), glm::pi<float>())
        );
    }
} // namespace

namespace openspace {

documentation::Documentation ScreenSpaceRenderable::Documentation() {
    using namespace openspace::documentation;

    return {
        "Screenspace Renderable",
        "core_screenspacerenderable",
        {
            {
                KeyType,
                new StringAnnotationVerifier("Must name a valid Screenspace renderable"),
                Optional::No,
                "The type of the Screenspace renderable that is to be created. The "
                "available types of Screenspace renderable depend on the configuration of"
                "the application and can be written to disk on application startup into "
                "the FactoryDocumentation."
            },
            {
                KeyName,
                new StringVerifier,
                Optional::Yes,
                "Specifies the name of this screenspace renderable. This does not have "
                "to be unique to the scene, but it is recommended to be."
            },
            {
                KeyIdentifier,
                new StringVerifier,
                Optional::Yes,
                "This is the unique identifier for this screenspace renderable. It has "
                "to be unique amongst all existing screenspace nodes that already have "
                "been added to the scene. The identifier is not allowed to have any "
                "whitespace or '.' and must not be empty."
            },
            {
                EnabledInfo.identifier,
                new BoolVerifier,
                Optional::Yes,
                EnabledInfo.description
            },
            {
                UseRadiusAzimuthElevationInfo.identifier,
                new BoolVerifier,
                Optional::Yes,
                UseRadiusAzimuthElevationInfo.description
            },
            {
                FaceCameraInfo.identifier,
                new BoolVerifier,
                Optional::Yes,
                FaceCameraInfo.description
            },
            {
                CartesianPositionInfo.identifier,
                new DoubleVector3Verifier,
                Optional::Yes,
                CartesianPositionInfo.description
            },
            {
                RadiusAzimuthElevationInfo.identifier,
                new DoubleVector3Verifier,
                Optional::Yes,
                RadiusAzimuthElevationInfo.description
            },
            {
                ScaleInfo.identifier,
                new DoubleVerifier,
                Optional::Yes,
                ScaleInfo.description
            },
            {
                AlphaInfo.identifier,
                new DoubleVerifier,
                Optional::Yes,
                AlphaInfo.description
            },
            {
                KeyTag,
                new OrVerifier({ new StringVerifier, new StringListVerifier }),
                Optional::Yes,
                "Defines either a single or multiple tags that apply to this "
                "ScreenSpaceRenderable, thus making it possible to address multiple, "
                "seprate Renderables with a single property change."
            }
        }
    };
}

std::unique_ptr<ScreenSpaceRenderable> ScreenSpaceRenderable::createFromDictionary(
                                                      const ghoul::Dictionary& dictionary)
{
    documentation::testSpecificationAndThrow(
        Documentation(),
        dictionary,
        "ScreenSpaceRenderable"
    );

    const std::string& renderableType = dictionary.value<std::string>(KeyType);
    ScreenSpaceRenderable* ssr =
        FactoryManager::ref().factory<ScreenSpaceRenderable>()->create(
            renderableType,
            dictionary
        );
    return std::unique_ptr<ScreenSpaceRenderable>(ssr);
}

std::string ScreenSpaceRenderable::makeUniqueIdentifier(std::string name) {
    std::vector<ScreenSpaceRenderable*> r = global::renderEngine.screenSpaceRenderables();

    auto nameTaken = [&r](const std::string& name) {
        bool nameTaken = std::any_of(
            r.begin(),
            r.end(),
            [&name](ScreenSpaceRenderable* r) { return r->identifier() == name; }
        );
        return nameTaken;
    };

    std::string baseName = name;
    int i = 1;
    while (nameTaken(name)) {
        name = baseName + std::to_string(i);
        i++;
    }
    return name;
}

ScreenSpaceRenderable::ScreenSpaceRenderable(const ghoul::Dictionary& dictionary)
    : properties::PropertyOwner({ "" })
    , _enabled(EnabledInfo, true)
    , _usePerspectiveProjection(UsePerspectiveProjectionInfo, false)
    , _useRadiusAzimuthElevation(UseRadiusAzimuthElevationInfo, false)
    , _faceCamera(FaceCameraInfo, true)
    , _cartesianPosition(
        CartesianPositionInfo,
        glm::vec3(0.f, 0.f, -2.f),
        glm::vec3(-4.f, -4.f, -10.f),
        glm::vec3(4.f, 4.f, 0.f)
    )
    , _raePosition(
        RadiusAzimuthElevationInfo,
        glm::vec3(2.f, 0.f, 0.f),
        glm::vec3(0.f, -glm::pi<float>(), -glm::half_pi<float>()),
        glm::vec3(10.f, glm::pi<float>(), glm::half_pi<float>())
    )
    , _localRotation(
        LocalRotationInfo,
        glm::vec3(0.f),
        glm::vec3(-glm::pi<float>()),
        glm::vec3(glm::pi<float>())
    )
    , _scale(ScaleInfo, 0.25f, 0.f, 2.f)
    , _alpha(AlphaInfo, 1.f, 0.f, 1.f)
    , _delete(DeleteInfo)
{
    if (dictionary.hasKey(KeyIdentifier)) {
        setIdentifier(dictionary.value<std::string>(KeyIdentifier));
    }

    if (dictionary.hasKey(KeyName)) {
        setGuiName(dictionary.value<std::string>(KeyName));
    }


    addProperty(_enabled);
    addProperty(_useRadiusAzimuthElevation);
    addProperty(_usePerspectiveProjection);
    addProperty(_faceCamera);
    addProperty(_cartesianPosition);
    addProperty(_raePosition);

    // Setting spherical/euclidean onchange handler
    _useRadiusAzimuthElevation.onChange([this]() {
        if (_useRadiusAzimuthElevation) {
            _raePosition = sphericalToRae(cartesianToSpherical(_cartesianPosition));
        }
        else {
            _cartesianPosition = sphericalToCartesian(raeToSpherical(_raePosition));
        }
    });

    addProperty(_scale);
    addProperty(_alpha);
    addProperty(_localRotation);

    if (dictionary.hasKey(EnabledInfo.identifier)) {
        _enabled = dictionary.value<bool>(EnabledInfo.identifier);
    }

    if (dictionary.hasKey(UseRadiusAzimuthElevationInfo.identifier)) {
        _useRadiusAzimuthElevation = dictionary.value<bool>(
            UseRadiusAzimuthElevationInfo.identifier
        );
    }

    if (_useRadiusAzimuthElevation) {
        if (dictionary.hasKey(RadiusAzimuthElevationInfo.identifier)) {
            _raePosition = dictionary.value<glm::vec3>(
                RadiusAzimuthElevationInfo.identifier
            );
        }
    }
    else {
        if (dictionary.hasKey(CartesianPositionInfo.identifier)) {
            _cartesianPosition = dictionary.value<glm::vec3>(
                CartesianPositionInfo.identifier
            );
        }
    }

    if (dictionary.hasKey(ScaleInfo.identifier)) {
        _scale = static_cast<float>(dictionary.value<double>(ScaleInfo.identifier));
    }

    if (dictionary.hasKey(AlphaInfo.identifier)) {
        _alpha = static_cast<float>(dictionary.value<double>(AlphaInfo.identifier));
    }

    if (dictionary.hasKey(UsePerspectiveProjectionInfo.identifier)) {
        _usePerspectiveProjection = static_cast<bool>(
            dictionary.value<bool>(UsePerspectiveProjectionInfo.identifier)
        );
    }

    if (dictionary.hasKey(FaceCameraInfo.identifier)) {
        _faceCamera = static_cast<bool>(
            dictionary.value<bool>(FaceCameraInfo.identifier)
        );
    }

    if (dictionary.hasKeyAndValue<std::string>(KeyTag)) {
        std::string tagName = dictionary.value<std::string>(KeyTag);
        if (!tagName.empty()) {
            addTag(std::move(tagName));
        }
    }
    else if (dictionary.hasKeyAndValue<ghoul::Dictionary>(KeyTag)) {
        const ghoul::Dictionary& tagNames = dictionary.value<ghoul::Dictionary>(KeyTag);
        const std::vector<std::string>& keys = tagNames.keys();
        for (const std::string& key : keys) {
            std::string tagName = tagNames.value<std::string>(key);
            if (!tagName.empty()) {
                addTag(std::move(tagName));
            }
        }
    }

    _delete.onChange([this](){
        std::string script =
            "openspace.removeScreenSpaceRenderable('" + identifier() + "');";
        global::scriptEngine.queueScript(
            script,
            scripting::ScriptEngine::RemoteScripting::No
        );
    });
    addProperty(_delete);
}

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

bool ScreenSpaceRenderable::initialize() {
    return true;
}

bool ScreenSpaceRenderable::initializeGL() {
    createShaders();
    return isReady();
}

bool ScreenSpaceRenderable::deinitialize() {
    return true;
}

bool ScreenSpaceRenderable::deinitializeGL() {
    if (_shader) {
        global::renderEngine.removeRenderProgram(_shader.get());
        _shader = nullptr;
    }

    return true;
}

void ScreenSpaceRenderable::render() {
    ZoneScoped

    draw(
        globalRotationMatrix() *
        translationMatrix() *
        localRotationMatrix() *
        scaleMatrix()
    );
}

bool ScreenSpaceRenderable::isReady() const {
    return _shader != nullptr;
}

void ScreenSpaceRenderable::update() {}

bool ScreenSpaceRenderable::isEnabled() const {
    return _enabled;
}

float ScreenSpaceRenderable::depth() {
    return _useRadiusAzimuthElevation ?
        _raePosition.value().x :
        cartesianToSpherical(_cartesianPosition).x;
}

void ScreenSpaceRenderable::createShaders() {
    ghoul::Dictionary dict = ghoul::Dictionary();

    auto res = global::windowDelegate.currentDrawBufferResolution();
    ghoul::Dictionary rendererData = {
        { "fragmentRendererPath", "${SHADERS}/framebuffer/renderframebuffer.frag" },
        { "windowWidth" , res.x },
        { "windowHeight" , res.y },
        { "hdrExposure", global::renderEngine.hdrExposure() },
        { "disableHDR", global::renderEngine.isHdrDisabled() }
    };

    dict.setValue("rendererData", rendererData);
    dict.setValue("fragmentPath", "${MODULE_BASE}/shaders/screenspace_fs.glsl");
    _shader = ghoul::opengl::ProgramObject::Build(
        "ScreenSpaceProgram",
        absPath("${MODULE_BASE}/shaders/screenspace_vs.glsl"),
        absPath("${SHADERS}/render.frag"),
        dict
    );

    ghoul::opengl::updateUniformLocations(*_shader, _uniformCache, UniformNames);
}

glm::mat4 ScreenSpaceRenderable::scaleMatrix() {
    glm::vec2 resolution = global::windowDelegate.currentDrawBufferResolution();

    //to scale the plane
    float textureRatio =
        static_cast<float>(_objectSize.y) / static_cast<float>(_objectSize.x);

    glm::mat4 scale = glm::scale(
        glm::mat4(1.f),
        glm::vec3(_scale, _scale * textureRatio, 1.f)
    );

    // Simulate orthographic projection by distance to plane.
    if (!_usePerspectiveProjection) {
        float distance = _useRadiusAzimuthElevation ?
            _raePosition.value().x :
            -_cartesianPosition.value().z;
        scale = glm::scale(scale, glm::vec3(distance));
    }

    return scale;
}

glm::mat4 ScreenSpaceRenderable::globalRotationMatrix() {
    // We do not want the screen space planes to be affected by
    // 1) The global rotation of the view applied in the render engine
    // 2) sgct's scene matrix (also called model matrix by sgct)

    glm::mat4 inverseRotation = glm::inverse(
        global::renderEngine.globalRotation() *
        global::windowDelegate.modelMatrix()
    );

    // The rotation of all screen space renderables is adjustable in the render engine:
    return global::renderEngine.screenSpaceRotation() * inverseRotation;
}

glm::mat4 ScreenSpaceRenderable::localRotationMatrix() {
    glm::mat4 rotation = glm::mat4(1.f);
    if (_faceCamera) {
        glm::vec3 translation = _useRadiusAzimuthElevation ?
            sphericalToCartesian(raeToSpherical(_raePosition)) :
            _cartesianPosition;

        rotation = glm::inverse(glm::lookAt(
            glm::vec3(0.f),
            glm::normalize(translation),
            glm::vec3(0.f, 1.f, 0.f)
        ));
    }

    float roll = _localRotation.value().x;
    float pitch = _localRotation.value().y;
    float yaw = _localRotation.value().z;
    return rotation * glm::mat4(glm::quat(glm::vec3(pitch, yaw, roll)));
}

glm::mat4 ScreenSpaceRenderable::translationMatrix() {
    glm::vec3 translation = _useRadiusAzimuthElevation ?
        sphericalToCartesian(raeToSpherical(_raePosition)) :
        _cartesianPosition;

    return glm::translate(glm::mat4(1.f), translation);
}

void ScreenSpaceRenderable::draw(glm::mat4 modelTransform) {
    glDisable(GL_CULL_FACE);

    _shader->activate();

    _shader->setUniform(_uniformCache.alpha, _alpha);
    _shader->setUniform(_uniformCache.modelTransform, modelTransform);

    _shader->setUniform(
        _uniformCache.viewProj,
        global::renderEngine.scene()->camera()->viewProjectionMatrix()
    );

    ghoul::opengl::TextureUnit unit;
    unit.activate();
    bindTexture();
    _shader->setUniform(_uniformCache.texture, unit);

    glBindVertexArray(rendering::helper::vertexObjects.square.vao);
    glDrawArrays(GL_TRIANGLES, 0, 6);

    glEnable(GL_CULL_FACE);

    _shader->deactivate();
    unbindTexture();
}

void ScreenSpaceRenderable::unbindTexture() {
}


} // namespace openspace
