/*****************************************************************************************
 *                                                                                       *
 * 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/space/speckloader.h>

#include <ghoul/fmt.h>
#include <ghoul/filesystem/cachemanager.h>
#include <ghoul/filesystem/file.h>
#include <ghoul/filesystem/filesystem.h>
#include <ghoul/logging/logmanager.h>
#include <ghoul/misc/assert.h>
#include <cctype>
#include <fstream>
#include <functional>
#include <string_view>

namespace {
    constexpr int8_t DataCacheFileVersion = 10;
    constexpr int8_t LabelCacheFileVersion = 11;
    constexpr int8_t ColorCacheFileVersion = 10;

    bool startsWith(std::string lhs, std::string_view rhs) noexcept {
        for (size_t i = 0; i < lhs.size(); i++) {
            lhs[i] = static_cast<char>(tolower(lhs[i]));
        }
        return (rhs.size() <= lhs.size()) && (lhs.substr(0, rhs.size()) == rhs);
    }

    void strip(std::string& line) noexcept {
        // 1. Remove all spaces from the beginning
        // 2. Remove #
        // 3. Remove all spaces from the new beginning
        // 4. Remove all spaces from the end

        while (!line.empty() && (line[0] == ' ' || line[0] == '\t')) {
            line = line.substr(1);
        }

        if (!line.empty() && line[0] == '#') {
            line = line.substr(1);
        }

        while (!line.empty() && (line[0] == ' ' || line[0] == '\t')) {
            line = line.substr(1);
        }

        while (!line.empty() && (line.back() == ' ' || line.back() == '\t')) {
            line = line.substr(0, line.size() - 1);
        }
    }

    template <typename T, typename U>
    void checkSize(U value, std::string_view message) {
        if (value > std::numeric_limits<U>::max()) {
            throw ghoul::RuntimeError(fmt::format("Error saving file: {}", message));
        }
    }

    template <typename T>
    using LoadCacheFunc = std::function<std::optional<T>(std::filesystem::path)>;

    template <typename T>
    using SaveCacheFunc = std::function<void(const T&, std::filesystem::path)>;

    template <typename T>
    using LoadSpeckFunc = std::function<T(
        std::filesystem::path, openspace::speck::SkipAllZeroLines)>;



    template <typename T>
    T internalLoadFileWithCache(std::filesystem::path speckPath,
                                     openspace::speck::SkipAllZeroLines skipAllZeroLines,
                                     LoadSpeckFunc<T> loadSpeckFunction,
                                     LoadCacheFunc<T> loadCacheFunction,
                                     SaveCacheFunc<T> saveCacheFunction)
    {
        static_assert(
            std::is_same_v<T, openspace::speck::Dataset> ||
            std::is_same_v<T, openspace::speck::Labelset> ||
            std::is_same_v<T, openspace::speck::ColorMap>
        );

        std::filesystem::path cached = FileSys.cacheManager()->cachedFilename(speckPath);

        if (std::filesystem::exists(cached)) {
            LINFOC(
                "SpeckLoader",
                fmt::format("Cached file {} used for file {}", cached, speckPath)
            );

            std::optional<T> dataset = loadCacheFunction(cached);
            if (dataset.has_value()) {
                // We could load the cache file and we are now done with this
                return *dataset;
            }
            else {
                FileSys.cacheManager()->removeCacheFile(cached);
            }
        }
        LINFOC("SpeckLoader", fmt::format("Loading file {}", speckPath));
        T dataset = loadSpeckFunction(speckPath, skipAllZeroLines);

        if (!dataset.entries.empty()) {
            LINFOC("SpeckLoader", "Saving cache");
            saveCacheFunction(dataset, cached);
        }
        return dataset;
    }
} // namespace

namespace openspace::speck {

namespace data {

Dataset loadFile(std::filesystem::path path, SkipAllZeroLines skipAllZeroLines) {
    ghoul_assert(std::filesystem::exists(path), "File must exist");

    std::ifstream file(path);
    if (!file.good()) {
        throw ghoul::RuntimeError(fmt::format("Failed to open speck file {}", path));
    }

    Dataset res;

    int nDataValues = 0;
    int currentLineNumber = 0;

    std::string line;
    // First phase: Loading the header information
    while (std::getline(file, line)) {
        currentLineNumber++;

        // Guard against wrong line endings (copying files from Windows to Mac) causes
        // lines to have a final \r
        if (!line.empty() && line.back() == '\r') {
            line = line.substr(0, line.length() - 1);
        }

        // Ignore empty line or commented-out lines
        if (line.empty() || line[0] == '#') {
            continue;
        }

        strip(line);

        // If the first character is a digit, we have left the preamble and are in the
        // data section of the file
        if (std::isdigit(line[0]) || line[0] == '-') {
            break;
        }


        if (startsWith(line, "datavar")) {
            // each datavar line is following the form:
            // datavar <idx> <description>
            // with <idx> being the index of the data variable

            std::stringstream str(line);
            std::string dummy;
            Dataset::Variable v;
            str >> dummy >> v.index >> v.name;

            nDataValues += 1;
            res.variables.push_back(v);
            continue;
        }

        if (startsWith(line, "texturevar")) {
            // each texturevar line is following the form:
            // texturevar <idx>
            // where <idx> is the data value index where the texture index is stored
            if (res.textureDataIndex != -1) {
                throw ghoul::RuntimeError(fmt::format(
                    "Error loading speck file {}: Texturevar defined twice", path
                ));
            }

            std::stringstream str(line);
            std::string dummy;
            str >> dummy >> res.textureDataIndex;

            continue;
        }

        if (startsWith(line, "polyorivar")) {
            // each polyorivar line is following the form:
            // texturevar <idx>
            // where <idx> is the data value index where the orientation index storage
            // starts. There are 6 values stored in total, xyz + uvw

            if (res.orientationDataIndex != -1) {
                throw ghoul::RuntimeError(fmt::format(
                    "Error loading speck file {}: Orientation index defined twice", path
                ));
            }

            std::stringstream str(line);
            std::string dummy;
            str >> dummy >> res.orientationDataIndex;

            // Ok.. this is kind of weird.  Speck unfortunately doesn't tell us in the
            // specification how many values a datavar has. Usually this is 1 value per
            // datavar, unless it is a polygon orientation thing. Now, the datavar name
            // for these can be anything (have seen 'orientation' and 'ori' before, so we
            // can't really check by name for these or we will miss some if they are
            // mispelled or whatever. So we have to go the roundabout way of adding the
            // 5 remaining values (the 6th nDataValue was already added in the
            // corresponding 'datavar' section) here
            nDataValues += 5;

            continue;
        }

        if (startsWith(line, "texture")) {
            // each texture line is following one of two forms:
            // 1:   texture -M 1 halo.sgi
            // 2:   texture 1 M1.sgi
            // The parameter in #1 is currently being ignored

            std::stringstream str(line);

            std::string dummy;
            str >> dummy;

            if (line.find('-') != std::string::npos) {
                str >> dummy;
            }

            Dataset::Texture texture;
            str >> texture.index >> texture.file;

            for (const Dataset::Texture& t : res.textures) {
                if (t.index == texture.index) {
                    throw ghoul::RuntimeError(fmt::format(
                        "Error loading speck file {}: Texture index '{}' defined twice",
                        path, texture.index
                    ));
                }
            }

            res.textures.push_back(texture);
            continue;
        }

        if (startsWith(line, "maxcomment")) {
            // ignoring this comment as we don't need it
            continue;
        }

        // If we get this far, we had an illegal header as it wasn't an empty line and
        // didn't start with either '#' denoting a comment line, and didn't start with
        // either the 'datavar', 'texturevar', 'polyorivar', or 'texture' keywords
        throw ghoul::RuntimeError(fmt::format(
            "Error in line {} while reading the header information of file {}. Line is "
            "neither a comment line, nor starts with one of the supported keywords for "
            "SPECK files",
            currentLineNumber, path
        ));
    }

    std::sort(
        res.variables.begin(), res.variables.end(),
        [](const Dataset::Variable& lhs, const Dataset::Variable& rhs) {
            return lhs.index < rhs.index;
        }
    );

    std::sort(
        res.textures.begin(), res.textures.end(),
        [](const Dataset::Texture& lhs, const Dataset::Texture& rhs) {
            return lhs.index < rhs.index;
        }
    );

    // For the first line, we already loaded it and rejected it above, so if we do another
    // std::getline, we'd miss the first data value line
    bool isFirst = true;
    while (isFirst || std::getline(file, line)) {
        currentLineNumber++;
        isFirst = false;

        // Ignore empty line or commented-out lines
        if (line.empty() || line[0] == '#') {
            continue;
        }

        // Guard against wrong line endings (copying files from Windows to Mac) causes
        // lines to have a final \r
        if (line.back() == '\r') {
            line = line.substr(0, line.length() - 1);
        }

        strip(line);

        if (line.empty()) {
            continue;
        }

        // If the first character is a digit, we have left the preamble and are in the
        // data section of the file
        if (!std::isdigit(line[0]) && line[0] != '-') {
            throw ghoul::RuntimeError(fmt::format(
                "Error loading speck file {}: Header information and datasegment "
                "intermixed", path
            ));
        }

        bool allZero = true;

        std::stringstream str(line);
        Dataset::Entry entry;
        str >> entry.position.x >> entry.position.y >> entry.position.z;
        allZero &= (entry.position == glm::vec3(0.0));

        if (!str.good()) {
            // Need to subtract one of the line number here as we increase the current
            // line count in the beginning of the while loop we are currently in
            throw ghoul::RuntimeError(fmt::format(
                "Error loading position information out of data line {} in file {}. "
                "Value was not a number",
                currentLineNumber - 1, path
            ));
        }

        entry.data.resize(nDataValues);
        std::stringstream valueStream;
        for (int i = 0; i < nDataValues; i += 1) {
            std::string value;
            str >> value;
            if (value == "nan" || value == "NaN") {
                entry.data[i] = std::numeric_limits<float>::quiet_NaN();
            }
            else {
                valueStream.clear();
                valueStream.str(value);
                valueStream >> entry.data[i];

                allZero &= (entry.data[i] == 0.0);
                if (valueStream.fail()) {
                    // Need to subtract one of the line number here as we increase the
                    // current line count in the beginning of the while loop we are
                    // currently in
                    throw ghoul::RuntimeError(fmt::format(
                        "Error loading data value {} out of data line {} in file {}. "
                        "Value was not a number",
                        i, currentLineNumber - 1, path
                    ));
                }
            }
        }

        if (skipAllZeroLines && allZero) {
            continue;
        }

        std::string rest;
        std::getline(str, rest);
        if (!rest.empty()) {

            strip(rest);
            entry.comment = rest;
        }

        res.entries.push_back(std::move(entry));
    }

#ifdef _DEBUG
    if (!res.entries.empty()) {
        size_t nValues = res.entries[0].data.size();
        ghoul_assert(nDataValues == nValues, "nDataValues calculation went wrong");
        for (const Dataset::Entry& e : res.entries) {
            ghoul_assert(
                e.data.size() == nDataValues,
                "Row had different number of data values"
            );
        }
    }
#endif

    return res;
}

std::optional<Dataset> loadCachedFile(std::filesystem::path path) {
    std::ifstream file(path, std::ios::binary);
    if (!file.good()) {
        return std::nullopt;
    }

    Dataset result;

    int8_t fileVersion;
    file.read(reinterpret_cast<char*>(&fileVersion), sizeof(int8_t));
    if (fileVersion != DataCacheFileVersion) {
        // Incompatible version and we won't be able to read the file
        return std::nullopt;
    }

    //
    // Read variables
    uint16_t nVariables;
    file.read(reinterpret_cast<char*>(&nVariables), sizeof(uint16_t));
    result.variables.resize(nVariables);
    for (int i = 0; i < nVariables; i += 1) {
        Dataset::Variable var;

        int16_t idx;
        file.read(reinterpret_cast<char*>(&idx), sizeof(int16_t));
        var.index = idx;

        uint16_t len;
        file.read(reinterpret_cast<char*>(&len), sizeof(uint16_t));
        var.name.resize(len);
        file.read(var.name.data(), len);

        result.variables[i] = std::move(var);
    }

    //
    // Read textures
    uint16_t nTextures;
    file.read(reinterpret_cast<char*>(&nTextures), sizeof(uint16_t));
    result.textures.resize(nTextures);
    for (int i = 0; i < nTextures; i += 1) {
        Dataset::Texture tex;

        int16_t idx;
        file.read(reinterpret_cast<char*>(&idx), sizeof(int16_t));
        tex.index = idx;

        uint16_t len;
        file.read(reinterpret_cast<char*>(&len), sizeof(uint16_t));
        tex.file.resize(len);
        file.read(tex.file.data(), len);

        result.textures[i] = std::move(tex);
    }

    //
    // Read indices
    int16_t texDataIdx;
    file.read(reinterpret_cast<char*>(&texDataIdx), sizeof(int16_t));
    result.textureDataIndex = texDataIdx;

    int16_t oriDataIdx;
    file.read(reinterpret_cast<char*>(&oriDataIdx), sizeof(int16_t));
    result.orientationDataIndex = oriDataIdx;

    //
    // Read entries
    uint64_t nEntries;
    file.read(reinterpret_cast<char*>(&nEntries), sizeof(uint64_t));
    result.entries.reserve(nEntries);
    for (uint64_t i = 0; i < nEntries; i += 1) {
        Dataset::Entry e;
        file.read(reinterpret_cast<char*>(&e.position.x), sizeof(float));
        file.read(reinterpret_cast<char*>(&e.position.y), sizeof(float));
        file.read(reinterpret_cast<char*>(&e.position.z), sizeof(float));

        uint16_t nValues;
        file.read(reinterpret_cast<char*>(&nValues), sizeof(uint16_t));
        e.data.resize(nValues);
        file.read(reinterpret_cast<char*>(e.data.data()), nValues * sizeof(float));

        uint16_t len;
        file.read(reinterpret_cast<char*>(&len), sizeof(uint16_t));
        if (len > 0) {
            std::string comment;
            comment.resize(len);
            file.read(comment.data(), len);
            e.comment = std::move(comment);
        }

        result.entries.push_back(std::move(e));
    }

    return result;
}

void saveCachedFile(const Dataset& dataset, std::filesystem::path path) {
    std::ofstream file(path, std::ofstream::binary);

    file.write(reinterpret_cast<const char*>(&DataCacheFileVersion), sizeof(int8_t));

    //
    // Store variables
    checkSize<uint16_t>(dataset.variables.size(), "Too many variables");
    uint16_t nVariables = static_cast<uint16_t>(dataset.variables.size());
    file.write(reinterpret_cast<const char*>(&nVariables), sizeof(uint16_t));
    for (const Dataset::Variable& var : dataset.variables) {
        checkSize<int16_t>(var.index, "Variable index too large");
        int16_t idx = static_cast<int16_t>(var.index);
        file.write(reinterpret_cast<const char*>(&idx), sizeof(int16_t));

        checkSize<uint16_t>(var.name.size(), "Variable name too long");
        uint16_t len = static_cast<uint16_t>(var.name.size());
        file.write(reinterpret_cast<const char*>(&len), sizeof(uint16_t));
        file.write(var.name.data(), len);
    }

    //
    // Store textures
    checkSize<uint16_t>(dataset.textures.size(), "Too many textures");
    uint16_t nTextures = static_cast<uint16_t>(dataset.textures.size());
    file.write(reinterpret_cast<const char*>(&nTextures), sizeof(uint16_t));
    for (const Dataset::Texture& tex : dataset.textures) {
        checkSize<int16_t>(tex.index, "Texture index too large");
        int16_t idx = static_cast<int16_t>(tex.index);
        file.write(reinterpret_cast<const char*>(&idx), sizeof(int16_t));


        checkSize<uint16_t>(tex.file.size(), "Texture file too long");
        uint16_t len = static_cast<uint16_t>(tex.file.size());
        file.write(reinterpret_cast<const char*>(&len), sizeof(uint16_t));
        file.write(tex.file.data(), len);
    }

    //
    // Store indices
    checkSize<int16_t>(dataset.textureDataIndex, "Texture index too large");
    int16_t texIdx = static_cast<int16_t>(dataset.textureDataIndex);
    file.write(reinterpret_cast<const char*>(&texIdx), sizeof(int16_t));

    checkSize<int16_t>(dataset.orientationDataIndex, "Orientation index too large");
    int16_t orientationIdx = static_cast<int16_t>(dataset.orientationDataIndex);
    file.write(reinterpret_cast<const char*>(&orientationIdx), sizeof(int16_t));

    //
    // Store entries
    checkSize<uint64_t>(dataset.entries.size(), "Too many entries");
    uint64_t nEntries = static_cast<uint64_t>(dataset.entries.size());
    file.write(reinterpret_cast<const char*>(&nEntries), sizeof(uint64_t));
    for (const Dataset::Entry& e : dataset.entries) {
        file.write(reinterpret_cast<const char*>(&e.position.x), sizeof(float));
        file.write(reinterpret_cast<const char*>(&e.position.y), sizeof(float));
        file.write(reinterpret_cast<const char*>(&e.position.z), sizeof(float));

        checkSize<uint16_t>(e.data.size(), "Too many data variables");
        uint16_t nValues = static_cast<uint16_t>(e.data.size());
        file.write(reinterpret_cast<const char*>(&nValues), sizeof(uint16_t));
        file.write(
            reinterpret_cast<const char*>(e.data.data()),
            e.data.size() * sizeof(float)
        );

        if (e.comment.has_value()) {
            checkSize<uint16_t>(e.comment->size(), "Comment too long");
        }
        uint16_t commentLen = e.comment.has_value() ?
            static_cast<uint16_t>(e.comment->size()) :
            0;
        file.write(reinterpret_cast<const char*>(&commentLen), sizeof(uint16_t));
        if (e.comment.has_value()) {
            file.write(e.comment->data(), e.comment->size());
        }
    }
}

Dataset loadFileWithCache(std::filesystem::path speckPath,
                               SkipAllZeroLines skipAllZeroLines)
{
    return internalLoadFileWithCache<Dataset>(
        speckPath,
        skipAllZeroLines,
        &loadFile,
        &loadCachedFile,
        &saveCachedFile
    );
}

} // namespace data

namespace label {

Labelset loadFile(std::filesystem::path path, SkipAllZeroLines) {
    ghoul_assert(std::filesystem::exists(path), "File must exist");

    std::ifstream file(path);
    if (!file.good()) {
        throw ghoul::RuntimeError(fmt::format("Failed to open speck file {}", path));
    }

    Labelset res;

    std::string line;
    // First phase: Loading the header information
    while (std::getline(file, line)) {
        // Ignore empty line or commented-out lines
        if (line.empty() || line[0] == '#') {
            continue;
        }

        // Guard against wrong line endings (copying files from Windows to Mac) causes
        // lines to have a final \r
        if (line.back() == '\r') {
            line = line.substr(0, line.length() - 1);
        }

        strip(line);

        // If the first character is a digit, we have left the preamble and are in the
        // data section of the file
        if (std::isdigit(line[0]) || line[0] == '-') {
            break;
        }

        if (startsWith(line, "textcolor")) {
            // each textcolor line is following the form:
            // textcolor <idx>
            // with <idx> being the index of the color into some configuration file (not
            // really sure how these configuration files work, but they don't seem to be
            // included in the speck file)
            if (res.textColorIndex != -1) {
                throw ghoul::RuntimeError(fmt::format(
                    "Error loading label file {}: Textcolor defined twice", path
                ));
            }


            std::stringstream str(line);
            std::string dummy;
            str >> dummy >> res.textColorIndex;
            continue;
        }
    }

    // For the first line, we already loaded it and rejected it above, so if we do another
    // std::getline, we'd miss the first data value line
    bool isFirst = true;
    while (isFirst || std::getline(file, line)) {
        isFirst = false;

        // Ignore empty line or commented-out lines
        if (line.empty() || line[0] == '#') {
            continue;
        }

        // Guard against wrong line endings (copying files from Windows to Mac) causes
        // lines to have a final \r
        if (line.back() == '\r') {
            line = line.substr(0, line.length() - 1);
        }

        strip(line);

        if (line.empty()) {
            continue;
        }

        // If the first character is a digit, we have left the preamble and are in the
        // data section of the file
        if (!std::isdigit(line[0]) && line[0] != '-') {
            throw ghoul::RuntimeError(fmt::format(
                "Error loading label file {}: Header information and datasegment "
                "intermixed", path
            ));
        }

        // Each line looks like this:
        // <x> <y> <z> text <label> # potential comment
        // so we want to get the position, remove the 'text' text and the potential
        // comment at the end
        std::stringstream str(line);
        Labelset::Entry entry;
        str >> entry.position.x >> entry.position.y >> entry.position.z;

        std::string rest;
        std::getline(str, rest);
        strip(rest);

        if (startsWith(rest, "id")) {
            // optional arument with identifier
            // Remove the 'id' text
            rest = rest.substr(std::string_view("id ").size());
            size_t index = rest.find("text");
            entry.identifier = rest.substr(0, index - 1);

            // update the rest, remove the identifier
            rest = rest.substr(index);
        }
        if (!startsWith(rest, "text")) {
            throw ghoul::RuntimeError(fmt::format(
                "Error loading label file {}: File contains an unsupported value "
                "between positions and text label", path
            ));
        }

        // Remove the 'text' text
        rest = rest.substr(std::string_view("text ").size());

        // Remove the trailing comment
        for (size_t i = 0; i < rest.size(); i += 1) {
            if (rest[i] == '#') {
                rest = rest.substr(0, i);
                break;
            }
        }

        strip(rest);

        entry.text = rest;
        if (!rest.empty()) {
            res.entries.push_back(std::move(entry));
        }
    }

    return res;
}

std::optional<Labelset> loadCachedFile(std::filesystem::path path) {
    std::ifstream file(path, std::ios::binary);
    if (!file.good()) {
        return std::nullopt;
    }

    int8_t fileVersion;
    file.read(reinterpret_cast<char*>(&fileVersion), sizeof(int8_t));
    if (fileVersion != LabelCacheFileVersion) {
        // Incompatible version and we won't be able to read the file
        return std::nullopt;
    }

    Labelset result;

    int16_t textColorIdx;
    file.read(reinterpret_cast<char*>(&textColorIdx), sizeof(int16_t));
    result.textColorIndex = textColorIdx;

    uint32_t nEntries;
    file.read(reinterpret_cast<char*>(&nEntries), sizeof(uint32_t));
    result.entries.reserve(nEntries);
    for (unsigned int i = 0; i < nEntries; i += 1) {
        Labelset::Entry e;
        file.read(reinterpret_cast<char*>(&e.position.x), sizeof(float));
        file.read(reinterpret_cast<char*>(&e.position.y), sizeof(float));
        file.read(reinterpret_cast<char*>(&e.position.z), sizeof(float));

        // Identifier
        uint8_t idLen;
        file.read(reinterpret_cast<char*>(&idLen), sizeof(uint8_t));
        e.identifier.resize(idLen);
        file.read(e.identifier.data(), idLen);

        // Text
        uint16_t len;
        file.read(reinterpret_cast<char*>(&len), sizeof(uint16_t));
        e.text.resize(len);
        file.read(e.text.data(), len);

        result.entries.push_back(e);
    }

    return result;
}

void saveCachedFile(const Labelset& labelset, std::filesystem::path path) {
    std::ofstream file(path, std::ofstream::binary);

    file.write(reinterpret_cast<const char*>(&LabelCacheFileVersion), sizeof(int8_t));

    //
    // Storage text color
    checkSize<int16_t>(labelset.textColorIndex, "Too high text color");
    int16_t textColorIdx = static_cast<int16_t>(labelset.textColorIndex);
    file.write(reinterpret_cast<const char*>(&textColorIdx), sizeof(int16_t));

    //
    // Storage text lines
    checkSize<uint32_t>(labelset.entries.size(), "Too many entries");
    uint32_t nEntries = static_cast<uint32_t>(labelset.entries.size());
    file.write(reinterpret_cast<const char*>(&nEntries), sizeof(uint32_t));
    for (const Labelset::Entry& e : labelset.entries) {
        file.write(reinterpret_cast<const char*>(&e.position.x), sizeof(float));
        file.write(reinterpret_cast<const char*>(&e.position.y), sizeof(float));
        file.write(reinterpret_cast<const char*>(&e.position.z), sizeof(float));

        // Identifier
        checkSize<uint8_t>(e.identifier.size(), "Identifier too long");
        uint8_t idLen = static_cast<uint8_t>(e.identifier.size());
        file.write(reinterpret_cast<const char*>(&idLen), sizeof(uint8_t));
        file.write(e.identifier.data(), idLen);

        // Text
        checkSize<uint16_t>(e.text.size(), "Text too long");
        uint16_t len = static_cast<uint16_t>(e.text.size());
        file.write(reinterpret_cast<const char*>(&len), sizeof(uint16_t));
        file.write(e.text.data(), len);
    }
}

Labelset loadFileWithCache(std::filesystem::path speckPath,
                                SkipAllZeroLines skipAllZeroLines)
{
    return internalLoadFileWithCache<Labelset>(
        speckPath,
        skipAllZeroLines,
        &loadFile,
        &loadCachedFile,
        &saveCachedFile
    );
}

} // namespace label

namespace color {

ColorMap loadFile(std::filesystem::path path, SkipAllZeroLines) {
    ghoul_assert(std::filesystem::exists(path), "File must exist");

    std::ifstream file(path);
    if (!file.good()) {
        throw ghoul::RuntimeError(fmt::format("Failed to open speck file {}", path));
    }

    ColorMap res;
    int nColorLines = -1;

    // The beginning of the speck file has a header that either contains comments
    // (signaled by a preceding '#') or information about the structure of the file
    // (signaled by the keywords 'datavar', 'texturevar', and 'texture')
    std::string line;
    while (std::getline(file, line)) {
        // Ignore empty line or commented-out lines
        if (line.empty() || line[0] == '#') {
            continue;
        }

        // Guard against wrong line endings (copying files from Windows to Mac) causes
        // lines to have a final \r
        if (line.back() == '\r') {
            line = line.substr(0, line.length() - 1);
        }

        strip(line);

        std::stringstream str(line);
        if (nColorLines == -1) {
            // This is the first time we get this far, it will have to be the first number
            // meaning that it is the number of color values

            str >> nColorLines;
            res.entries.reserve(nColorLines);
        }
        else {
            // We have already read the number of color lines, so we are in the process of
            // reading the individual value lines

            glm::vec4 color;
            str >> color.x >> color.y >> color.z >> color.w;
            res.entries.push_back(std::move(color));
        }
    }

    if (nColorLines != static_cast<int>(res.entries.size())) {
        LWARNINGC("SpeckLoader", fmt::format(
            "While loading color map, the expected number of color values '{}' was "
            "different from the actual number of color values '{}'",
            nColorLines, res.entries.size()
        ));
    }

    return res;
}

std::optional<ColorMap> loadCachedFile(std::filesystem::path path) {
    std::ifstream file(path, std::ios::binary);
    if (!file.good()) {
        return std::nullopt;
    }

    int8_t fileVersion;
    file.read(reinterpret_cast<char*>(&fileVersion), sizeof(int8_t));
    if (fileVersion != ColorCacheFileVersion) {
        // Incompatible version and we won't be able to read the file
        return std::nullopt;
    }

    ColorMap result;

    uint32_t nColors;
    file.read(reinterpret_cast<char*>(&nColors), sizeof(uint32_t));
    result.entries.reserve(nColors);
    for (unsigned int i = 0; i < nColors; i += 1) {
        glm::vec4 color;
        file.read(reinterpret_cast<char*>(&color.x), sizeof(float));
        file.read(reinterpret_cast<char*>(&color.y), sizeof(float));
        file.read(reinterpret_cast<char*>(&color.z), sizeof(float));
        file.read(reinterpret_cast<char*>(&color.w), sizeof(float));
        result.entries.push_back(color);
    }
    return result;
}

void saveCachedFile(const ColorMap& colorMap, std::filesystem::path path) {
    std::ofstream file(path, std::ofstream::binary);

    file.write(reinterpret_cast<const char*>(&ColorCacheFileVersion), sizeof(int8_t));

    uint32_t nColors = static_cast<uint32_t>(colorMap.entries.size());
    file.write(reinterpret_cast<const char*>(&nColors), sizeof(uint32_t));
    for (const glm::vec4& color : colorMap.entries) {
        file.write(reinterpret_cast<const char*>(&color.x), sizeof(float));
        file.write(reinterpret_cast<const char*>(&color.y), sizeof(float));
        file.write(reinterpret_cast<const char*>(&color.z), sizeof(float));
        file.write(reinterpret_cast<const char*>(&color.w), sizeof(float));
    }
}

ColorMap loadFileWithCache(std::filesystem::path path, SkipAllZeroLines skipAllZeroLines)
{
    return internalLoadFileWithCache<ColorMap>(
        path,
        skipAllZeroLines,
        &loadFile,
        &loadCachedFile,
        &saveCachedFile
    );
}

} // namespace color

int Dataset::index(std::string_view variableName) const {
    for (const Dataset::Variable& v : variables) {
        if (v.name == variableName) {
            return v.index;
        }
    }
    return -1;
}

bool Dataset::normalizeVariable(std::string_view variableName) {
    std::optional<int> idx;
    for (const Dataset::Variable& var : variables) {
        if (var.name == variableName) {
            idx = var.index;
            break;
        }
    }

    if (!idx.has_value()) {
        // We didn't find the variable that was specified
        return false;
    }

    float minValue = std::numeric_limits<float>::max();
    float maxValue = -std::numeric_limits<float>::max();
    for (Dataset::Entry& e : entries) {
        minValue = std::min(minValue, e.data[*idx]);
        maxValue = std::max(maxValue, e.data[*idx]);
    }

    for (Dataset::Entry& e : entries) {
        e.data[*idx] = (e.data[*idx] - minValue) / (maxValue - minValue);
    }

    return true;
}

} // namespace openspace::speck
