/*****************************************************************************************
 *                                                                                       *
 * 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 <modules/iswa/rendering/kameleonplane.h>

#include <modules/iswa/rendering/iswabasegroup.h>
#include <modules/iswa/rendering/iswakameleongroup.h>
#include <modules/iswa/util/dataprocessorkameleon.h>
#include <modules/iswa/util/iswamanager.h>
#include <openspace/json.h>
#include <openspace/engine/globals.h>
#include <openspace/rendering/renderengine.h>
#include <openspace/scene/scene.h>
#include <openspace/scripting/scriptengine.h>
#include <ghoul/filesystem/filesystem.h>
#include <ghoul/logging/logmanager.h>
#include <ghoul/opengl/programobject.h>
#include <ghoul/opengl/texture.h>
#include <fstream>

namespace {
    using json = nlohmann::json;
    constexpr const char* _loggerCat = "KameleonPlane";

    constexpr openspace::properties::Property::PropertyInfo FieldLineSeedsInfo = {
        "FieldlineSeedsIndexFile",
        "Fieldline Seedpoints",
        "" // @TODO Missing documentation
    };

    constexpr openspace::properties::Property::PropertyInfo ResolutionInfo = {
        "Resolution",
        "Resolution%",
        "" // @TODO Missing documentation
    };

    constexpr openspace::properties::Property::PropertyInfo SliceInfo = {
        "Slice",
        "Slice",
        "" // @TODO Missing documentation
    };
} // namespace

namespace openspace {

KameleonPlane::KameleonPlane(const ghoul::Dictionary& dictionary)
    : DataCygnet(dictionary)
    , _fieldlines(FieldLineSeedsInfo)
    , _resolution(ResolutionInfo, 100.f, 10.f, 200.f)
    , _slice(SliceInfo, 0.f, 0.f, 1.f)
{
    addProperty(_resolution);
    addProperty(_slice);
    addProperty(_fieldlines);

    dictionary.getValue("kwPath", _kwPath);

    std::string fieldlineIndexFile;
    dictionary.getValue("fieldlineSeedsIndexFile", _fieldlineIndexFile);

    std::string axis;
    dictionary.getValue("axisCut", axis);

    if (axis == "x") {
        _cut = Cut::X;
    }
    else if (axis == "y") {
        _cut = Cut::Y;
    }
    else {
        _cut = Cut::Z;
    }

    _origOffset = _data.offset;

    _scale = _data.scale[_cut];
    _data.scale[_cut] = 0;
    _data.offset[_cut] = 0;

    _slice = (_data.offset[_cut] -_data.gridMin[_cut]) / _scale;

    setDimensions();
}

KameleonPlane::~KameleonPlane() {}

void KameleonPlane::deinitializeGL() {
    IswaCygnet::deinitialize();
    _fieldlines = std::vector<int>();
}

void KameleonPlane::initializeGL() {
    if (!_shader) {
        _shader = global::renderEngine.buildRenderProgram(
            "DataPlaneProgram",
            absPath("${MODULE_ISWA}/shaders/dataplane_vs.glsl"),
            absPath("${MODULE_ISWA}/shaders/dataplane_fs.glsl")
        );
    }

    if (!_data.groupName.empty()) {
        initializeGroup();
    }

    initializeTime();
    createGeometry();

    readFieldlinePaths(absPath(_fieldlineIndexFile));

    if (_group) {
        _dataProcessor = _group->dataProcessor();
        subscribeToGroup();
    }
    else {
        _dataProcessor = std::make_shared<DataProcessorKameleon>();

        //If autofiler is on, background values property should be hidden
        _autoFilter.onChange([this]() {
            // If autofiler is selected, use _dataProcessor to set backgroundValues
            // and unregister backgroundvalues property.
            if (_autoFilter) {
                _backgroundValues = _dataProcessor->filterValues();
                _backgroundValues.setVisibility(properties::Property::Visibility::Hidden);
                //_backgroundValues.setVisible(false);
            // else if autofilter is turned off, register backgroundValues
            }
            else {
                _backgroundValues.setVisibility(properties::Property::Visibility::All);
                //_backgroundValues.setVisible(true);
            }
        });
    }

    fillOptions(_kwPath);

    readTransferFunctions(_transferFunctionsFile);

    // Set Property Callbacks of DataCygnet (must be called after fillOptions)
    setPropertyCallbacks();

    // Set Property callback specific to KameleonPlane
    _resolution.onChange([this]() {
        for (size_t i = 0; i < _textures.size(); i++) {
            _textures[i] = nullptr;
        }

        updateTextureResource();
        setDimensions();

    });

    _slice.onChange([this]() { updateTextureResource(); });

    _fieldlines.onChange([this]() { updateFieldlineSeeds(); });

    std::dynamic_pointer_cast<DataProcessorKameleon>(_dataProcessor)->setDimensions(
        _dimensions
    );
    _dataProcessor->addDataValues(_kwPath, _dataOptions);
    // if this datacygnet has added new values then reload texture
    // for the whole group, including this datacygnet, and return after.
    if (_group) {
        _group->updateGroup();
    }
    updateTextureResource();
}

bool KameleonPlane::createGeometry() {
    glGenVertexArrays(1, &_quad); // generate array
    glGenBuffers(1, &_vertexPositionBuffer); // generate buffer

    // ============================
    //         GEOMETRY (quad)
    // ============================
    // GLfloat x,y, z;
    float s = _data.spatialScale.x;
    const GLfloat x = s * _data.scale.x / 2.f;
    const GLfloat y = s * _data.scale.y / 2.f;
    const GLfloat z = s * _data.scale.z / 2.f;
    const GLfloat w = _data.spatialScale.w;

    const GLfloat vertex_data[] = { // square of two triangles (sigh)
        //      x      y     z     w     s     t
        -x, -y,             -z,  w, 0, 1,
         x,  y,              z,  w, 1, 0,
        -x,  ((x>0)?y:-y),   z,  w, 0, 0,
        -x, -y,             -z,  w, 0, 1,
         x,  ((x>0)?-y:y),  -z,  w, 1, 1,
         x,  y,              z,  w, 1, 0,
    };

    glBindVertexArray(_quad); // bind array
    glBindBuffer(GL_ARRAY_BUFFER, _vertexPositionBuffer); // bind buffer
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertex_data), vertex_data, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 6, nullptr);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(
        1,
        2,
        GL_FLOAT,
        GL_FALSE,
        sizeof(GLfloat) * 6,
        reinterpret_cast<void*>(sizeof(GLfloat) * 4)
    );

    return true;
}

bool KameleonPlane::destroyGeometry() {
    glDeleteVertexArrays(1, &_quad);
    _quad = 0;

    glDeleteBuffers(1, &_vertexPositionBuffer);
    _vertexPositionBuffer = 0;

    return true;
}

void KameleonPlane::renderGeometry() const {
    glBindVertexArray(_quad);
    glDrawArrays(GL_TRIANGLES, 0, 6);
}

std::vector<float*> KameleonPlane::textureData() {
    DataProcessorKameleon* p = dynamic_cast<DataProcessorKameleon*>(_dataProcessor.get());
    p->setSlice(_slice);
    return p->processData(_kwPath, _dataOptions, _dimensions);
}

bool KameleonPlane::updateTextureResource() {
    _data.offset[_cut] = _slice * _scale + _data.gridMin[_cut];
    // _textureDirty = true;
    updateTexture();
    return true;
}

void KameleonPlane::setUniforms() {
    setTextureUniforms();
    _shader->setUniform("backgroundValues", _backgroundValues.value());
    _shader->setUniform("transparency", _alpha.value());
}

void KameleonPlane::updateFieldlineSeeds() {
    std::vector<int> selectedOptions = _fieldlines.value();

    // seedPath == map<int selectionValue, tuple<string name, string path, bool active>>
    for (auto& seedPath : _fieldlineState) {
        // if this option was turned off
        const auto it = std::find(
            selectedOptions.begin(),
            selectedOptions.end(),
            seedPath.first
        );
        if (it == selectedOptions.end() && std::get<2>(seedPath.second)) {
            SceneGraphNode* n = global::renderEngine.scene()->sceneGraphNode(
                std::get<0>(seedPath.second)
            );
            if (!n) {
                return;
            }

            LDEBUG("Removed fieldlines: " + std::get<0>(seedPath.second));
            global::scriptEngine.queueScript(
                "openspace.removeSceneGraphNode('" + std::get<0>(seedPath.second) + "')",
                scripting::ScriptEngine::RemoteScripting::Yes
            );
            std::get<2>(seedPath.second) = false;
        // if this option was turned on
        }
        else if (it != selectedOptions.end() && !std::get<2>(seedPath.second)) {
            SceneGraphNode* n = global::renderEngine.scene()->sceneGraphNode(
                std::get<0>(seedPath.second)
            );
            if (n) {
                return;
            }

            LDEBUG("Created fieldlines: " + std::get<0>(seedPath.second));
            IswaManager::ref().createFieldline(
                std::get<0>(seedPath.second),
                _kwPath,
                std::get<1>(seedPath.second)
            );
            std::get<2>(seedPath.second) = true;
        }
    }
}

void KameleonPlane::readFieldlinePaths(const std::string& indexFile) {
    LINFO(fmt::format("Reading seed points paths from file '{}'", indexFile));
    if (_group) {
        dynamic_cast<IswaKameleonGroup*>(_group)->setFieldlineInfo(
            indexFile,
            _kwPath
        );
        return;
    }

    // Read the index file from disk
    std::ifstream seedFile(indexFile);
    if (!seedFile.good()) {
        LERROR(fmt::format("Could not open seed points file '{}'", indexFile));
    }
    else {
        try {
            //Parse and add each fieldline as an selection
            json fieldlines = json::parse(seedFile);
            int i = 0;
            const std::string& fullName = identifier();
            std::string partName = fullName.substr(0,fullName.find_last_of("-"));
            for (json::iterator it = fieldlines.begin(); it != fieldlines.end(); ++it) {
                _fieldlines.addOption({i, it.key()});
                _fieldlineState[i] = std::make_tuple<std::string, std::string, bool>(
                    partName + "/" + it.key(),
                    it.value(),
                    false
                );
                i++;
            }
        } catch (const std::exception& e) {
            LERROR(
                "Error when reading json file with paths to seedpoints: " +
                std::string(e.what())
            );
        }
   }
}

void KameleonPlane::subscribeToGroup() {
    // Subscribe to DataCygnet events
    DataCygnet::subscribeToGroup();

    //Add additional Events specific to KameleonPlane
    ghoul::Event<ghoul::Dictionary>& groupEvent = _group->groupEvent();
    groupEvent.subscribe(identifier(), "resolutionChanged", [&](ghoul::Dictionary dict) {
        LDEBUG(identifier() + " Event resolutionChanged");
        if (dict.hasKeyAndValue<float>("resolution")) {
            _resolution = dict.value<float>("resolution");
        }
    });

    groupEvent.subscribe(identifier(), "cdfChanged", [&](ghoul::Dictionary dict) {
        LDEBUG(identifier() + " Event cdfChanged");
        if (dict.hasKeyAndValue<std::string>("path")) {
            const std::string& path = dict.value<std::string>("path");
            changeKwPath(path);
        }
        updateTexture();
    });
}

void KameleonPlane::setDimensions() {
    // the cdf files has an offset of 0.5 in normali resolution.
    // with lower resolution the offset increases.
    _data.offset = _origOffset - 0.5f * (100.f / _resolution);
    _dimensions = glm::size3_t(_data.scale * (_resolution / 100.f));
    _dimensions[_cut] = 1;

    if (_cut == Cut::X) {
        _textureDimensions = glm::size3_t(_dimensions.y, _dimensions.z, 1);
    }
    else if (_cut == Cut::Y) {
        _textureDimensions = glm::size3_t(_dimensions.x, _dimensions.z, 1);
    }
    else {
        _textureDimensions = glm::size3_t(_dimensions.x, _dimensions.y, 1);
    }
}

void KameleonPlane::changeKwPath(std::string kwPath) {
    _kwPath = std::move(kwPath);
}

}// namespace openspace
