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

#include <modules/space/rendering/renderablefluxnodes.h>

#include <openspace/engine/globals.h>
#include <openspace/engine/windowdelegate.h>
#include <openspace/navigation/navigationhandler.h>
#include <openspace/navigation/orbitalnavigator.h>
#include <openspace/rendering/renderengine.h>
#include <openspace/scene/scene.h>
#include <openspace/util/timemanager.h>
#include <openspace/util/updatestructures.h>
#include <openspace/query/query.h>
#include <ghoul/filesystem/filesystem.h>
#include <ghoul/logging/logmanager.h>
#include <ghoul/logging/consolelog.h>
#include <ghoul/logging/visualstudiooutputlog.h>
#include <ghoul/opengl/programobject.h>
#include <ghoul/opengl/textureunit.h>
#include <fstream>
#include <functional>
#include <optional>
#include <sys/stat.h>
#include <thread>

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

    constexpr std::array<const char*, 29> UniformNames = {
        "streamColor", "nodeSize", "proximityNodesSize",
        "thresholdFlux", "colorMode", "filterLower", "filterUpper", "scalingMode",
        "colorTableRange", "domainLimZ", "nodeSkip", "nodeSkipDefault", "nodeSkipEarth",
        "nodeSkipMethod", "nodeSkipFluxThreshold", "nodeSkipRadiusThreshold",
        "fluxColorAlpha", "earthPos", "distanceThreshold", "time", "maxNodeDistanceSize",
        "usingCameraPerspective", "drawCircles", "drawHollow", "useGaussian",
        "perspectiveDistanceFactor", "minMaxNodeSize", "usingPulse",
        "usingGaussianPulse"
    };

    constexpr openspace::properties::Property::PropertyInfo GoesEnergyBinsInfo = {
        "GoesEnergy",
        "GOES Energy",
        "Select which energy bin you want to show. GOES = Geostationary Operational "
        "Environmental Satellites. Emin01 is values > 10 MeV, "
        "Default is Emin03 where values > 100 MeV"
    };

    constexpr openspace::properties::Property::PropertyInfo ColorModeInfo = {
        "ColorMode",
        "Color Mode",
        "Color lines uniformly or using color tables based on specific values on nodes, "
        "for examples flux values"
    };

    constexpr openspace::properties::Property::PropertyInfo ColorTablePathInfo = {
        "ColorTablePath",
        "Path to Color Table",
        "Color Table/Transfer Function to use for 'By Flux Value' coloring"
    };

    constexpr openspace::properties::Property::PropertyInfo StreamColorInfo = {
        "Color",
        "Color",
        "Color of particles"
    };

    constexpr openspace::properties::Property::PropertyInfo NodeSizeInfo = {
       "NodeSize",
       "Size of nodes",
       "Change the size of the rendered points of the nodes"
    };

    constexpr openspace::properties::Property::PropertyInfo ThresholdFluxInfo = {
       "ThresholdFlux",
       "Threshold flux value",
       "This value specifies the threshold that will be changed with the flux value"
    };

    constexpr openspace::properties::Property::PropertyInfo FilteringInfo = {
        "FilterLower",
        "Filtering Lower Value in AU",
        "Use filtering to show nodes within a given range"
    };

    constexpr openspace::properties::Property::PropertyInfo FilteringUpperInfo = {
        "FilterUpper",
        "Filtering Upper Value in AU",
        "Use filtering to show nodes within a given range"
    };

    constexpr openspace::properties::Property::PropertyInfo AmountofNodesInfo = {
        "AmountOfNodes",
        "Every nth node to render in",
        "Show only every nth node"
    };

    constexpr openspace::properties::Property::PropertyInfo DefaultNodeSkipInfo = {
        "NodeSkip",
        "Every nth node to render default",
        "Show only every nth node outside of skippingmethod"
    };

    constexpr openspace::properties::Property::PropertyInfo EarthNodeSkipInfo = {
       "NodeSkipEarth",
       "Every nth node to render close to Earth",
       "Show only every nth node outside of skippingmethod"
    };

    constexpr openspace::properties::Property::PropertyInfo ScalingmethodInfo = {
        "ScalingFlux",
        "Scale the flux value with color table",
        "Use scaling to color nodes with a given method"
    };

    constexpr openspace::properties::Property::PropertyInfo NodeskipMethodInfo = {
        "SkippingNodes",
        "How to select nodes to skip",
        "Methods to select nodes to skip"
    };

    constexpr openspace::properties::Property::PropertyInfo colorTableRangeInfo = {
        "ColorTableRange",
        "Color Table Range",
        "Valid range for the color table as the exponent, with base 10, of flux values. "
        "[Min, Max]"
    };

    constexpr openspace::properties::Property::PropertyInfo DomainZInfo = {
        "ZLimit",
        "Z-limits",
        "Valid range along the Z-axis. [Min, Max]"
    };

    constexpr openspace::properties::Property::PropertyInfo FluxColorAlphaInfo = {
        "FluxColorAlpha",
        "Flux Color Alpha",
        "The value of alpha for the flux color mode"
    };

    constexpr openspace::properties::Property::PropertyInfo FluxNodeskipThresholdInfo = {
        "SkippingNodesByFlux",
        "Skipping Nodes By Flux",
        "Select nodes to skip depending on flux value"
    };

    constexpr openspace::properties::Property::PropertyInfo
                                                           RadiusNodeSkipThresholdInfo = {
        "SkippingNodesByRadius",
        "Skipping Nodes By Radius",
        "Select nodes to skip depending on Radius"
    };

    constexpr openspace::properties::Property::PropertyInfo DistanceThresholdInfo = {
        "DistancePlanetThreshold",
        "Threshold for distance between planet",
        "Changes threshold distance for highlighting nodes close to earth"
    };

    constexpr openspace::properties::Property::PropertyInfo ProximityNodesSizeInfo = {
        "ProximityNodesSize",
        "Earths Proximity Nodes Size",
        "Changes size of nodes only close to earth"
    };

    constexpr openspace::properties::Property::PropertyInfo MaxNodeDistanceSizeInfo = {
        "MaxNodeDistanceSize",
        "Max Node Distance Size",
        "The maximum size of the nodes at a certin distance"
    };
    
    constexpr openspace::properties::Property::PropertyInfo
                                                          CameraPerspectiveEnabledInfo = {
        "CameraPerspectiveEnabled",
        "Use Camera perspective",
        "Camera perspective changes the size of the nodes dependent on the "
        "distance from camera"
    };

    constexpr openspace::properties::Property::PropertyInfo DrawingCirclesInfo = {
        "RenderingCircles",
        "Render as circles",
        "Using fragment shader to draw nodes as circles instead of squares"
    };

    constexpr openspace::properties::Property::PropertyInfo DrawingHollowInfo = {
        "RenderingHollowCircles",
        "Render as hollow circles",
        "Using fragment shader to draw nodes as hollow circles"
    };

    constexpr openspace::properties::Property::PropertyInfo GaussiandAlphaFilterInfo = {
        "RenderingGaussianAlphaFilter",
        "Alpha by Gaussian",
        "Using fragment shader to draw nodes with Gaussian filter for alpha value"

    };

    constexpr openspace::properties::Property::PropertyInfo
                                                         PerspectiveDistanceFactorInfo = {
        "PerspectiveDistanceFactor",
        "Perspective Distance factor",
        "This value decides how far away the camera must be to start "
        "impacting the node size"
    };

    constexpr openspace::properties::Property::PropertyInfo MinMaxNodeSizeInfo = {
        "MinMaxNodeSize",
        "Min & Max node size",
        "The minimum and maximum node size"
    };

    constexpr openspace::properties::Property::PropertyInfo pulseEnabledInfo = {
        "PulseEnabled",
        "Nodes close to Earth pulsate",
        "Toggles the pulse for nodes close to Earth"
    };

    constexpr openspace::properties::Property::PropertyInfo gaussianPulseEnabledInfo = {
        "GaussianPulseEnabled",
        "Nodes close to Earth pulsate with alpha by gaussian",
        "Toggles the pulse with alpha by gaussian for nodes close to Earth"
    };

    struct [[codegen::Dictionary(RenderableFluxNodes)]] Parameters {
        // path to source folder with the 3 binary files in it
        std::filesystem::path sourceFolder [[codegen::directory()]];
        // [[codegen::verbatim(ColorTablePathInfo.description)]]
        std::string colorTablePath;
        // [[codegen::verbatim(GoesEnergyBinsInfo.description)]]
        std::optional<int> energyBin;
        // [[codegen::verbatim(colorTableRangeInfo.description)]]
        std::optional<glm::vec2> colorTableRange;
    };
#include "renderablefluxnodes_codegen.cpp"

} // namespace

namespace openspace {

documentation::Documentation RenderableFluxNodes::Documentation() {
    return codegen::doc<Parameters>("space_renderable_flux_nodes");
}

RenderableFluxNodes::RenderableFluxNodes(const ghoul::Dictionary& dictionary)
    : Renderable(dictionary)
    , _earthdistGroup({ "Earthfocus" })
    , _goesEnergyBins(GoesEnergyBinsInfo, properties::OptionProperty::DisplayType::Radio)
    , _styleGroup({ "Style" })
    , _colorMode(ColorModeInfo, properties::OptionProperty::DisplayType::Radio)
    , _streamColor(
        StreamColorInfo,
        glm::vec4(0.96f, 0.88f, 0.8f, 1.f),
        glm::vec4(0.f),
        glm::vec4(1.f)
    )
    , _colorTablePath(ColorTablePathInfo)
    , _colorTableRange(colorTableRangeInfo, { -2.f, 4.f }, { -8.f, -8.f }, { 8.f, 8.f })
    , _fluxColorAlpha(FluxColorAlphaInfo, 1.f, 0.f, 1.f)
    , _streamGroup({ "Streams" })
    , _scalingMethod(ScalingmethodInfo, properties::OptionProperty::DisplayType::Radio)
    , _nodesAmountGroup({ "NodeGroup" })
    , _nodeSize(NodeSizeInfo, 2.f, 1.f, 10.f)
    , _distanceThreshold(DistanceThresholdInfo, 0.f, 0.f, 1.f)
    , _proximityNodesSize(ProximityNodesSizeInfo, 1.f, 0.f, 100.f)
    , _maxNodeDistanceSize(MaxNodeDistanceSizeInfo, 1.f, 1.f, 10.f)
    , _minMaxNodeSize(MinMaxNodeSizeInfo, { 2.f, 30.f }, { 1.f, 1.f }, { 10.f, 200.f })
    , _domainZ(DomainZInfo, { -2.5f, 2.5f }, { -2.5f, -2.5f }, { 2.5f, 2.5f })
    , _thresholdFlux(ThresholdFluxInfo, -1.5f, -50.f, 10.f)
    , _filteringLower(FilteringInfo, 0.f, 0.f, 5.f)
    , _filteringUpper(FilteringUpperInfo, 5.f, 0.f, 5.f)
    , _amountofNodes(AmountofNodesInfo, 1, 1, 100)
    , _nodeskipMethod(NodeskipMethodInfo, properties::OptionProperty::DisplayType::Radio)
    , _defaultNodeSkip(DefaultNodeSkipInfo, 1, 1, 100)
    , _fluxNodeskipThreshold(FluxNodeskipThresholdInfo, 0, -20, 10)
    , _earthNodeSkip(EarthNodeSkipInfo, 1, 1, 100)
    , _radiusNodeSkipThreshold(RadiusNodeSkipThresholdInfo, 0.f, 0.f, 5.f)
    , _cameraPerspectiveGroup({ "CameraPerspective" })
    , _cameraPerspectiveEnabled(CameraPerspectiveEnabledInfo, false)
    , _drawingCircles(DrawingCirclesInfo, true)
    , _drawingHollow(DrawingHollowInfo, false)
    , _gaussianAlphaFilter(GaussiandAlphaFilterInfo, false)
    , _perspectiveDistanceFactor(PerspectiveDistanceFactorInfo, 2.67f, 1.f, 20.f)
    , _pulseEnabled(pulseEnabledInfo, false)
    , _gaussianPulseEnabled(gaussianPulseEnabledInfo, false)
{
    const Parameters p = codegen::bake<Parameters>(dictionary);

    _colorTablePath = p.colorTablePath;
    _transferFunction = std::make_unique<TransferFunction>(_colorTablePath);
    _colorTableRange = p.colorTableRange.value_or(_colorTableRange);

    _binarySourceFolderPath = p.sourceFolder;
    if (std::filesystem::is_directory(_binarySourceFolderPath)) {
        // Extract all file paths from the provided folder
        namespace fs = std::filesystem;
        for (const fs::directory_entry& e :
            fs::directory_iterator(_binarySourceFolderPath))
        {
            if (e.is_regular_file()) {
                _binarySourceFiles.push_back(e.path().string());
            }
        }
        std::sort(_binarySourceFiles.begin(), _binarySourceFiles.end());

        // Ensure that there are available and valid source files left
        if (_binarySourceFiles.empty()) {
            LERROR(fmt::format("{} contains no files", _binarySourceFolderPath));
        }
    }
    else {
        LERROR(fmt::format(
            "Source folder {} is not a valid directory", _binarySourceFolderPath
        ));
    }

    _goesEnergyBins.addOption(static_cast<int>(GoesEnergyBins::Emin01), "Emin01");
    _goesEnergyBins.addOption(static_cast<int>(GoesEnergyBins::Emin03), "Emin03");
    _colorMode.addOption(static_cast<int>(ColorMethod::ByFluxValue), "By Flux Value");
    _colorMode.addOption(static_cast<int>(ColorMethod::Uniform), "Uniform");

    _scalingMethod.addOption(static_cast<int>(ScalingMethod::Flux), "Flux");
    _scalingMethod.addOption(static_cast<int>(ScalingMethod::RFlux), "Radius * Flux");
    _scalingMethod.addOption(static_cast<int>(ScalingMethod::R2Flux), "Radius^2 * Flux");
    _scalingMethod.addOption(
        static_cast<int>(ScalingMethod::Log10RFlux), "log10(r) * Flux"
    );
    _scalingMethod.addOption(static_cast<int>(ScalingMethod::LnRFlux), "ln(r) * Flux");

    _nodeskipMethod.addOption(static_cast<int>(NodeSkipMethod::Uniform), "Uniform");
    _nodeskipMethod.addOption(static_cast<int>(NodeSkipMethod::Flux), "Flux");
    _nodeskipMethod.addOption(static_cast<int>(NodeSkipMethod::Radius), "Radius");

    if (p.energyBin.has_value()) {
        _goesEnergyBins.setValue(p.energyBin.value());
    }
    else { // default int 1 == Emin03 == MeV>100
        LINFO("Assuming default value 1, meaning Emin03");
        _goesEnergyBins.setValue(1);
    }
}

void RenderableFluxNodes::initialize() {
    populateStartTimes();
    loadNodeData(_goesEnergyBins.option().value);
    computeSequenceEndTime();
}

void RenderableFluxNodes::initializeGL() {
    // Setup shader program
    _shaderProgram = global::renderEngine->buildRenderProgram(
        "Fluxnodes",
        absPath("${MODULE_SPACE}/shaders/fluxnodes_vs.glsl"),
        absPath("${MODULE_SPACE}/shaders/fluxnodes_fs.glsl")
    );

    _uniformCache.streamColor = _shaderProgram->uniformLocation("streamColor");
    _uniformCache.nodeSize = _shaderProgram->uniformLocation("nodeSize");
    _uniformCache.thresholdFlux = _shaderProgram->uniformLocation("thresholdFlux");

    ghoul::opengl::updateUniformLocations(*_shaderProgram, _uniformCache, UniformNames);

    glGenVertexArrays(1, &_vertexArrayObject);
    glGenBuffers(1, &_vertexPositionBuffer);
    glGenBuffers(1, &_vertexColorBuffer);
    glGenBuffers(1, &_vertexFilteringBuffer);

    // Needed for alpha transparency
    setRenderBin(Renderable::RenderBin::PreDeferredTransparent);
    setupProperties();
}

void RenderableFluxNodes::definePropertyCallbackFunctions() {
    // Add Property Callback Functions
    _goesEnergyBins.onChange([this] {
        loadNodeData(_goesEnergyBins.option().value);
    });
    _colorTablePath.onChange([this]() {
        _transferFunction->setPath(_colorTablePath);
    });
}

void RenderableFluxNodes::loadNodeData(int energybinOption) {
    LDEBUG("Loading in binary files directly from sync folder");

    std::string energybin;
    switch (energybinOption) {
        case 0:
            energybin = "_emin01";
            break;
        case 1:
            energybin = "_emin03";
            break;
    }

    std::string file = _binarySourceFolderPath.string() + "\\positions" + energybin;
    std::string file2 = _binarySourceFolderPath.string() + "\\fluxes" + energybin;
    std::string file3 = _binarySourceFolderPath.string() + "\\radiuses" + energybin;

    std::ifstream fileStream(file, std::ifstream::binary);
    std::ifstream fileStream2(file2, std::ifstream::binary);
    std::ifstream fileStream3(file3, std::ifstream::binary);

    if (!fileStream.good()) {
        LERROR(fmt::format("Could not read file '{}'", file));
        return;
    }

    uint32_t nNodesPerTimestep = 0;
    fileStream.read(reinterpret_cast<char*>(&nNodesPerTimestep), sizeof(uint32_t));

    uint32_t nTimeSteps = 0;
    fileStream.read(reinterpret_cast<char*>(&nTimeSteps), sizeof(uint32_t));
    _nStates = nTimeSteps;

    if (_nStates != _startTimes.size()) {
        LERROR(
            "Number of states, _nStates, and number of start times, _startTimes, "
            "do not match"
        );
        return;
    }

    _statesColor.clear();
    _statesPos.clear();
    _statesRadius.clear();

    for (unsigned int i = 0; i < _nStates; ++i) {
        _vertexPositions.resize(nNodesPerTimestep);
        fileStream.read(reinterpret_cast<char*>(
            _vertexPositions.data()), nNodesPerTimestep * sizeof(glm::vec3)
        );

        _statesPos.push_back(_vertexPositions);
        _vertexPositions.clear();
    }
    for (unsigned int i = 0; i < _nStates; ++i) {
        _vertexColor.resize(nNodesPerTimestep);
        fileStream2.read(reinterpret_cast<char*>(
            _vertexColor.data()), nNodesPerTimestep * sizeof(float)
        );

        _statesColor.push_back(_vertexColor);
        _vertexColor.clear();
    }
    for (unsigned int i = 0; i < _nStates; ++i) {
        _vertexRadius.resize(nNodesPerTimestep);
        fileStream3.read(reinterpret_cast<char*>(
            _vertexRadius.data()), nNodesPerTimestep * sizeof(float)
        );

        _statesRadius.push_back(_vertexRadius);
        _vertexRadius.clear();
    }
}

void RenderableFluxNodes::setupProperties() {
    addProperty(_goesEnergyBins);

    addPropertySubOwner(_styleGroup);
    addPropertySubOwner(_streamGroup);
    addPropertySubOwner(_nodesAmountGroup);
    addPropertySubOwner(_earthdistGroup);
    addPropertySubOwner(_cameraPerspectiveGroup);

    _cameraPerspectiveGroup.addProperty(_cameraPerspectiveEnabled);
    _cameraPerspectiveGroup.addProperty(_perspectiveDistanceFactor);
    _minMaxNodeSize.setViewOption(properties::Property::ViewOptions::MinMaxRange);
    _cameraPerspectiveGroup.addProperty(_minMaxNodeSize);

    _earthdistGroup.addProperty(_distanceThreshold);
    _earthdistGroup.addProperty(_proximityNodesSize);
    _earthdistGroup.addProperty(_pulseEnabled);
    _earthdistGroup.addProperty(_gaussianPulseEnabled);

    _nodesAmountGroup.addProperty(_nodeskipMethod);
    _nodesAmountGroup.addProperty(_amountofNodes);
    _nodesAmountGroup.addProperty(_defaultNodeSkip);
    _nodesAmountGroup.addProperty(_earthNodeSkip);
    _nodesAmountGroup.addProperty(_nodeSize);
    _nodesAmountGroup.addProperty(_fluxNodeskipThreshold);
    _nodesAmountGroup.addProperty(_radiusNodeSkipThreshold);
    _nodesAmountGroup.addProperty(_maxNodeDistanceSize);

    _streamGroup.addProperty(_thresholdFlux);
    _streamGroup.addProperty(_filteringLower);
    _streamGroup.addProperty(_filteringUpper);
    _streamGroup.addProperty(_domainZ);

    _styleGroup.addProperty(_drawingCircles);
    _styleGroup.addProperty(_drawingHollow);
    _styleGroup.addProperty(_gaussianAlphaFilter);
    _styleGroup.addProperty(_colorMode);
    _styleGroup.addProperty(_scalingMethod);
    _colorTableRange.setViewOption(properties::Property::ViewOptions::MinMaxRange);
    _styleGroup.addProperty(_colorTableRange);
    _styleGroup.addProperty(_colorTablePath);
    _styleGroup.addProperty(_streamColor);
    _styleGroup.addProperty(_fluxColorAlpha);

    definePropertyCallbackFunctions();
}

void RenderableFluxNodes::deinitializeGL() {
    glDeleteVertexArrays(1, &_vertexArrayObject);
    _vertexArrayObject = 0;

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

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

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

    if (_shaderProgram) {
        global::renderEngine->removeRenderProgram(_shaderProgram.get());
        _shaderProgram = nullptr;
    }
}

bool RenderableFluxNodes::isReady() const {
    return _shaderProgram != nullptr;
}

void RenderableFluxNodes::populateStartTimes() {
    std::string timeFile;
    for (const std::string& filePath : _binarySourceFiles) {
        if (filePath.substr(filePath.find_last_of(".") + 1) == "csv") {
            timeFile = filePath;
            break;
        }
        else if (filePath.substr(filePath.find_last_of(".") + 1) == "dat") {
            timeFile = filePath;
            break;
        }
        else if (filePath.substr(filePath.find_last_of(".") + 1) == "txt") {
            timeFile = filePath;
            break;
        }
        //if no file extention but word "time" in file name
        else if (filePath.find("time") != std::string::npos &&
                    filePath.find(".") == std::string::npos)
        {
            timeFile = filePath;
            break;
        }
    }

    if (timeFile.empty()) {
        LERROR(
            "Could not find a metadata file with time steps, such as a csv, dat, txt or "
            "no file extention with 'time' in filename"
        );
    }

    // time filestream
    std::ifstream tfs(timeFile);
    if (!tfs.is_open()) {
        throw ghoul::RuntimeError("Could not open file");
    }

    std::string line;
    // gets only first line to "remove" header
    std::getline(tfs, line);
    std::stringstream s;
    s << line;

    int nColumns = 0;
    std::string columnName;
    // loops through the names/columns in first line/header
    while (s >> columnName) {
        ++nColumns;
    }
    while (std::getline(tfs, line)) {   // for each line of data
        std::istringstream iss(line);
        for (int i = 0; i < nColumns; ++i) {    // for each column in line
            std::string columnValue;
            iss >> columnValue;
            if (i != nColumns - 1) {    // last column
                continue;
            }
            if (columnValue.length() == 23) {
                // Ensure the separators are correct
                columnValue.replace(4, 1, "-");
                columnValue.replace(7, 1, "-");
                columnValue.replace(13, 1, ":");
                columnValue.replace(16, 1, ":");
                columnValue.replace(19, 1, ".");
                const double triggerTime = Time::convertTime(columnValue);
                _startTimes.push_back(triggerTime);
            }
            else {
                LERROR(fmt::format("Error in file formating. Last column in ",
                    "file '{}' is not on UTC ISO8601 format", timeFile
                ));
            }
        }
    }
}

void RenderableFluxNodes::updateActiveTriggerTimeIndex(double currentTime) {
    auto iter = std::upper_bound(_startTimes.begin(), _startTimes.end(), currentTime);
    if (iter != _startTimes.end()) {
        if (iter != _startTimes.begin()) {
            std::ptrdiff_t idx = std::distance(_startTimes.begin(), iter);
            _activeTriggerTimeIndex = static_cast<int>(idx) - 1;
        }
        else {
            _activeTriggerTimeIndex = 0;
        }
    }
    else {
        _activeTriggerTimeIndex = static_cast<int>(_nStates) - 1;
    }
}
void RenderableFluxNodes::render(const RenderData& data, RendererTasks&) {
    if (_activeTriggerTimeIndex == -1) {
        return;
    }
    _shaderProgram->activate();

    // Calculate Model View MatrixProjection
    const glm::dmat4 rotMat = glm::dmat4(data.modelTransform.rotation);
    const glm::dmat4 modelMat =
        glm::translate(glm::dmat4(1.0), data.modelTransform.translation) *
        rotMat * glm::scale(glm::dmat4(1.0), data.modelTransform.scale);
    const glm::dmat4 modelViewMat = data.camera.combinedViewMatrix() * modelMat;

    _shaderProgram->setUniform("modelViewProjection",
        data.camera.sgctInternal.projectionMatrix() * glm::mat4(modelViewMat)
    );

    SceneGraphNode* earthNode = sceneGraphNode("Earth");
    if (!earthNode) {
        LWARNING("Could not find scene graph node 'Earth'");
    }
    glm::vec3 earthPos = earthNode->worldPosition() * data.modelTransform.rotation;

    _shaderProgram->setUniform(_uniformCache.streamColor, _streamColor);
    _shaderProgram->setUniform(_uniformCache.nodeSize, _nodeSize);
    _shaderProgram->setUniform(_uniformCache.thresholdFlux, _thresholdFlux);
    _shaderProgram->setUniform(_uniformCache.colorMode, _colorMode);
    _shaderProgram->setUniform(_uniformCache.filterLower, _filteringLower);
    _shaderProgram->setUniform(_uniformCache.filterUpper, _filteringUpper);
    _shaderProgram->setUniform(_uniformCache.scalingMode, _scalingMethod);
    _shaderProgram->setUniform(_uniformCache.colorTableRange, _colorTableRange);
    _shaderProgram->setUniform(_uniformCache.domainLimZ, _domainZ);
    _shaderProgram->setUniform(_uniformCache.nodeSkip, _amountofNodes);
    _shaderProgram->setUniform(_uniformCache.nodeSkipDefault, _defaultNodeSkip);
    _shaderProgram->setUniform(_uniformCache.nodeSkipEarth, _earthNodeSkip);
    _shaderProgram->setUniform(_uniformCache.nodeSkipMethod, _nodeskipMethod);
    _shaderProgram->setUniform(
        _uniformCache.nodeSkipFluxThreshold,
        _fluxNodeskipThreshold
    );
    _shaderProgram->setUniform(
        _uniformCache.nodeSkipRadiusThreshold,
        _radiusNodeSkipThreshold
    );
    _shaderProgram->setUniform(_uniformCache.fluxColorAlpha, _fluxColorAlpha);

    _shaderProgram->setUniform(_uniformCache.earthPos, earthPos);
    _shaderProgram->setUniform(_uniformCache.distanceThreshold, _distanceThreshold);
    _shaderProgram->setUniform(_uniformCache.proximityNodesSize, _proximityNodesSize);
    _shaderProgram->setUniform(
        _uniformCache.time,
        global::windowDelegate->applicationTime()
    );
    _shaderProgram->setUniform(
        _uniformCache.maxNodeDistanceSize,
        _maxNodeDistanceSize
    );
    _shaderProgram->setUniform(
        _uniformCache.usingCameraPerspective,
        _cameraPerspectiveEnabled
    );
    _shaderProgram->setUniform(_uniformCache.drawCircles, _drawingCircles);
    _shaderProgram->setUniform(_uniformCache.drawHollow, _drawingHollow);
    _shaderProgram->setUniform(_uniformCache.useGaussian, _gaussianAlphaFilter);

    _shaderProgram->setUniform(
        _uniformCache.perspectiveDistanceFactor,
        _perspectiveDistanceFactor
    );
    _shaderProgram->setUniform(_uniformCache.minMaxNodeSize, _minMaxNodeSize);
    _shaderProgram->setUniform(_uniformCache.usingPulse, _pulseEnabled);
    _shaderProgram->setUniform(
        _uniformCache.usingGaussianPulse,
        _gaussianPulseEnabled
    );

    glm::vec3 cameraPos = data.camera.positionVec3() * data.modelTransform.rotation;

    _shaderProgram->setUniform("cameraPos", cameraPos);

    if (_colorMode == static_cast<int>(ColorMethod::ByFluxValue)) {
        ghoul::opengl::TextureUnit textureUnit;
        textureUnit.activate();
        _transferFunction->bind(); // Calls update internally
        _shaderProgram->setUniform("colorTable", textureUnit);
    }

    glBindVertexArray(_vertexArrayObject);

    glDrawArrays(
        GL_POINTS,
        0,
        static_cast<GLsizei>(_vertexPositions.size())
    );

    glBindVertexArray(0);
    _shaderProgram->deactivate();
}

void RenderableFluxNodes::computeSequenceEndTime() {
    if (_nStates > 1) {
        const double lastTriggerTime = _startTimes[_nStates - 1];
        const double sequenceDuration = lastTriggerTime - _startTimes[0];
        const double averageStateDuration = sequenceDuration /
            (static_cast<double>(_nStates) - 1.0);
        _sequenceEndTime = lastTriggerTime + averageStateDuration;
    }
    else if (_nStates == 1) {
        // If there's just one state it should never disappear!
        _sequenceEndTime = std::numeric_limits<double>::max();
    }
    else {
        LERROR("No states were found. The position file include this data");
    }
}

void RenderableFluxNodes::update(const UpdateData& data) {
    if (!_enabled) {
        return;
    }
    if (_shaderProgram->isDirty()) {
        _shaderProgram->rebuildFromFile();
    }
    bool needsUpdate = true;
    //Everything below is for updating depending on time
    const double currentTime = data.time.j2000Seconds();
    const bool isInInterval = (currentTime >= _startTimes[0]) &&
            (currentTime < _sequenceEndTime);
    //const bool isInInterval = true;
    if (isInInterval) {
        const size_t nextIdx = _activeTriggerTimeIndex + 1;
        if (
            // true => we were not in an interval the previous frame but now we are
            _activeTriggerTimeIndex == -1 ||
            // true => We stepped back to a time represented by another state
            currentTime < _startTimes[_activeTriggerTimeIndex] ||
            // true => We stepped forward to a time represented by another state
            (nextIdx < _nStates && currentTime >= _startTimes[nextIdx]))
        {
            updateActiveTriggerTimeIndex(currentTime);
        } // else {we're still in same state as previous frame (no changes needed)}
    }
    else {
        _activeTriggerTimeIndex = -1;
        needsUpdate = false;
    }

    if (needsUpdate && !_statesPos[_activeTriggerTimeIndex].empty()) {
        _vertexPositions = _statesPos[_activeTriggerTimeIndex];
        _vertexColor = _statesColor[_activeTriggerTimeIndex];
        _vertexRadius = _statesRadius[_activeTriggerTimeIndex];
        needsUpdate = false;
        updatePositionBuffer();
        updateVertexColorBuffer();
        updateVertexFilteringBuffer();
    }

    if (_shaderProgram->isDirty()) {
        _shaderProgram->rebuildFromFile();
        ghoul::opengl::updateUniformLocations(
            *_shaderProgram,
            _uniformCache,
            UniformNames
        );
    }
}

void RenderableFluxNodes::updatePositionBuffer() {
    glBindVertexArray(_vertexArrayObject);
    glBindBuffer(GL_ARRAY_BUFFER, _vertexPositionBuffer);

    glBufferData(
        GL_ARRAY_BUFFER,
        _vertexPositions.size() * sizeof(glm::vec3),
        _vertexPositions.data(),
        GL_STATIC_DRAW
    );

    glEnableVertexAttribArray(0);
    glEnable(GL_PROGRAM_POINT_SIZE);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

void RenderableFluxNodes::updateVertexColorBuffer() {
    glBindVertexArray(_vertexArrayObject);
    glBindBuffer(GL_ARRAY_BUFFER, _vertexColorBuffer);

    glBufferData(
        GL_ARRAY_BUFFER,
        _vertexColor.size() * sizeof(float),
        _vertexColor.data(),
        GL_STATIC_DRAW
    );

    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 0, nullptr);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

void RenderableFluxNodes::updateVertexFilteringBuffer() {
    glBindVertexArray(_vertexArrayObject);
    glBindBuffer(GL_ARRAY_BUFFER, _vertexFilteringBuffer);

    glBufferData(
        GL_ARRAY_BUFFER,
        _vertexRadius.size() * sizeof(float),
        _vertexRadius.data(),
        GL_STATIC_DRAW
    );

    glEnableVertexAttribArray(2);
    glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 0, nullptr);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}
} // namespace openspace
