/*****************************************************************************************
 *                                                                                       *
 * 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/globebrowsing/src/layer.h>

#include <openspace/documentation/documentation.h>
#include <openspace/documentation/verifier.h>
#include <modules/globebrowsing/src/layergroup.h>
#include <modules/globebrowsing/src/layermanager.h>
#include <modules/globebrowsing/src/tileindex.h>
#include <modules/globebrowsing/src/tiletextureinitdata.h>
#include <ghoul/logging/logmanager.h>
#include <ghoul/misc/profiling.h>

namespace openspace::globebrowsing {

namespace {
    constexpr const char* _loggerCat = "Layer";

    constexpr const char* KeyIdentifier = "Identifier";
    constexpr const char* KeyName = "Name";
    constexpr const char* KeyDesc = "Description";
    constexpr const char* KeyLayerGroupID = "LayerGroupID";
    constexpr const char* KeySettings = "Settings";
    constexpr const char* KeyAdjustment = "Adjustment";
    constexpr const char* KeyPadTiles = "PadTiles";

    constexpr const char* KeyOpacity = "Opacity";
    constexpr const char* KeyGamma = "Gamma";
    constexpr const char* KeyMultiplier = "Multiplier";
    constexpr const char* KeyOffset = "Offset";


    constexpr openspace::properties::Property::PropertyInfo TypeInfo = {
        "Type",
        "Type",
        "The type of this Layer. This value is a read-only property and thus cannot be "
        "changed."
    };

    constexpr openspace::properties::Property::PropertyInfo BlendModeInfo = {
        "BlendMode",
        "Blend Mode",
        "This value specifies the blend mode that is applied to this layer. The blend "
        "mode determines how this layer is added to the underlying layers beneath."
    };

    constexpr openspace::properties::Property::PropertyInfo EnabledInfo = {
        "Enabled",
        "Enabled",
        "If this value is enabled, the layer will be used for the final composition of "
        "the planet. If this value is disabled, the layer will be ignored in the "
        "composition."
    };

    constexpr openspace::properties::Property::PropertyInfo ResetInfo = {
        "Reset",
        "Reset",
        "If this value is triggered, this layer will be reset. This will delete the "
        "local cache for this layer and will trigger a fresh load of all tiles."
    };

    constexpr openspace::properties::Property::PropertyInfo RemoveInfo = {
        "Remove",
        "Remove",
        "If this value is triggered, a script will be executed that will remove this "
        "layer before the next frame."
    };

    constexpr openspace::properties::Property::PropertyInfo ColorInfo = {
        "Color",
        "Color",
        "If the 'Type' of this layer is a solid color, this value determines what this "
        "solid color is."
    };
} // namespace

documentation::Documentation Layer::Documentation() {
    using namespace documentation;
    return {
        "Layer",
        "globebrowsing_layer",
        {
            {
                KeyIdentifier,
                new StringVerifier,
                Optional::No,
                "The unique identifier for this layer. May not contain '.' or spaces."
            },
            {
                KeyName,
                new StringVerifier,
                Optional::Yes,
                "A human-readable name for the user interface. If this is omitted, the "
                "identifier is used instead."
            },
            {
                KeyDesc,
                new StringVerifier,
                Optional::Yes,
                "A human-readable description of the layer to be used in informational "
                "texts presented to the user."
            },
            {
                "Type",
                new StringInListVerifier({
                    "DefaultTileLayer", "SingleImageTileLayer", "SizeReferenceTileLayer",
                    "TemporalTileLayer", "TileIndexTileLayer", "ByIndexTileLayer",
                    "ByLevelTileLayer", "SolidColor"
                }),
                Optional::Yes,
                "Specifies the type of layer that is to be added. If this value is not "
                "specified, the layer is a DefaultTileLayer."
            },
            {
                EnabledInfo.identifier,
                new BoolVerifier,
                Optional::Yes,
                "Determine whether the layer is enabled or not. If this value is not "
                "specified, the layer is disabled."
            },
            {
                KeyPadTiles,
                new BoolVerifier,
                Optional::Yes,
                "Determines whether the downloaded tiles should have a padding added to "
                "the borders."
            },
            {
                KeySettings,
                new TableVerifier({
                    {
                        KeyOpacity,
                        new DoubleInRangeVerifier(0.0, 1.0),
                        Optional::Yes,
                        "The opacity value of the layer."
                    },
                    {
                        KeyGamma,
                        new DoubleVerifier,
                        Optional::Yes,
                        "The gamma value that is applied to each pixel of the layer."
                    },
                    {
                        KeyMultiplier,
                        new DoubleVerifier,
                        Optional::Yes,
                        "The multiplicative factor that is applied to each pixel of the "
                        "layer."
                    },
                    {
                        KeyOffset,
                        new DoubleVerifier,
                        Optional::Yes,
                        "An additive offset that is applied to each pixel of the layer."
                    }
                }),
                Optional::Yes,
                "Specifies the render settings that should be applied to this layer."
            },
            {
                KeyAdjustment,
                new ReferencingVerifier("globebrowsing_layeradjustment"),
                Optional::Yes,
                ""
            },
            {
                BlendModeInfo.identifier,
                new StringInListVerifier({
                    "Normal", "Multiply", "Add", "Subtract", "Color"
                }),
                Optional::Yes,
                "Sets the blend mode of this layer to determine how it interacts with "
                "other layers on top of this."
            },
            {
                "Fallback",
                new ReferencingVerifier("globebrowsing_layer"),
                Optional::Yes,
                "If the primary layer creation fails, this layer is used as a fallback"
            }
        }
    };
}

Layer::Layer(layergroupid::GroupID id, const ghoul::Dictionary& layerDict,
             LayerGroup& parent)
    : properties::PropertyOwner({
        layerDict.value<std::string>(KeyIdentifier),
        layerDict.hasKey(KeyName) ? layerDict.value<std::string>(KeyName) : "",
        layerDict.hasKey(KeyDesc) ? layerDict.value<std::string>(KeyDesc) : ""
    })
    , _parent(parent)
    , _typeOption(TypeInfo, properties::OptionProperty::DisplayType::Dropdown)
    , _blendModeOption(BlendModeInfo, properties::OptionProperty::DisplayType::Dropdown)
    , _enabled(EnabledInfo, false)
    , _reset(ResetInfo)
    , _remove(RemoveInfo)
    , _solidColor(ColorInfo, glm::vec3(1.f), glm::vec3(0.f), glm::vec3(1.f))
    , _layerGroupId(id)
{
    documentation::testSpecificationAndThrow(Documentation(), layerDict, "Layer");

    layergroupid::TypeID typeID;
    if (layerDict.hasKeyAndValue<std::string>("Type")) {
        const std::string& typeString = layerDict.value<std::string>("Type");
        typeID = ghoul::from_string<layergroupid::TypeID>(typeString);
    }
    else {
        typeID = layergroupid::TypeID::DefaultTileLayer;
    }
    if (typeID == layergroupid::TypeID::Unknown) {
        throw ghoul::RuntimeError("Unknown layer type!");
    }

    initializeBasedOnType(typeID, layerDict);

    if (layerDict.hasKeyAndValue<bool>(EnabledInfo.identifier)) {
        _enabled = layerDict.value<bool>(EnabledInfo.identifier);
    }

    bool padTiles = true;
    if (layerDict.hasKeyAndValue<bool>(KeyPadTiles)) {
        padTiles = layerDict.value<bool>(KeyPadTiles);
    }

    TileTextureInitData initData = tileTextureInitData(_layerGroupId, padTiles);
    _padTilePixelStartOffset = initData.tilePixelStartOffset;
    _padTilePixelSizeDifference = initData.tilePixelSizeDifference;

    if (layerDict.hasKeyAndValue<ghoul::Dictionary>(KeySettings)) {
        ghoul::Dictionary dict = layerDict.value<ghoul::Dictionary>(KeySettings);
        if (dict.hasKeyAndValue<float>(KeyOpacity)) {
            _renderSettings.opacity = dict.value<float>(KeyOpacity);
        }

        if (dict.hasKeyAndValue<float>(KeyGamma)) {
            _renderSettings.gamma = dict.value<float>(KeyGamma);
        }

        if (dict.hasKeyAndValue<float>(KeyMultiplier)) {
            _renderSettings.multiplier = dict.value<float>(KeyMultiplier);
        }

        if (dict.hasKeyAndValue<float>(KeyOffset)) {
            _renderSettings.offset = dict.value<float>(KeyOffset);
        }
    }
    if (layerDict.hasKeyAndValue<ghoul::Dictionary>(KeyAdjustment)) {
        _layerAdjustment.setValuesFromDictionary(
            layerDict.value<ghoul::Dictionary>(KeyAdjustment)
        );
    }

    // Add options to option properties
    for (int i = 0; i < layergroupid::NUM_LAYER_TYPES; ++i) {
        _typeOption.addOption(i, layergroupid::LAYER_TYPE_NAMES[i]);
    }
    _typeOption.setValue(static_cast<int>(typeID));
    _type = static_cast<layergroupid::TypeID>(_typeOption.value());

    for (int i = 0; i < layergroupid::NUM_BLEND_MODES; ++i) {
        _blendModeOption.addOption(i, layergroupid::BLEND_MODE_NAMES[i]);
    }

    // Initialize blend mode
    if (layerDict.hasKeyAndValue<std::string>(BlendModeInfo.identifier)) {
        using namespace layergroupid;
        std::string blendMode = layerDict.value<std::string>(BlendModeInfo.identifier);
        BlendModeID blendModeID = ghoul::from_string<BlendModeID>(blendMode);
        _blendModeOption = static_cast<int>(blendModeID);
    }
    else {
        _blendModeOption = static_cast<int>(layergroupid::BlendModeID::Normal);
    }

    // On change callbacks definitions
    _enabled.onChange([&]() {
        if (_onChangeCallback) {
            _onChangeCallback(this);
        }
    });

    _reset.onChange([&]() {
        if (_tileProvider) {
            tileprovider::reset(*_tileProvider);
        }
    });

    _remove.onChange([&]() {
        if (_tileProvider) {
            tileprovider::reset(*_tileProvider);
            _parent.deleteLayer(identifier());
        }
    });

    _typeOption.onChange([&]() {
        switch (type()) {
            // Intentional fall through. Same for all tile layers
            case layergroupid::TypeID::DefaultTileLayer:
            case layergroupid::TypeID::SingleImageTileLayer:
            case layergroupid::TypeID::SizeReferenceTileLayer:
            case layergroupid::TypeID::TemporalTileLayer:
            case layergroupid::TypeID::TileIndexTileLayer:
            case layergroupid::TypeID::ByIndexTileLayer:
            case layergroupid::TypeID::ByLevelTileLayer:
                if (_tileProvider) {
                    removePropertySubOwner(*_tileProvider);
                }
                break;
            case layergroupid::TypeID::SolidColor:
                removeProperty(_solidColor);
                break;
            default:
                break;
        }

        _type = static_cast<layergroupid::TypeID>(_typeOption.value());
        initializeBasedOnType(type(), {});
        addVisibleProperties();
        if (_onChangeCallback) {
            _onChangeCallback(this);
        }
    });

    _blendModeOption.onChange([&]() {
        if (_onChangeCallback) {
            _onChangeCallback(this);
        }
    });

    _layerAdjustment.onChange([&]() {
        if (_onChangeCallback) {
            _onChangeCallback(this);
        }
    });

    _typeOption.setReadOnly(true);

    // Add the properties
    addProperty(_typeOption);
    addProperty(_blendModeOption);
    addProperty(_enabled);
    addProperty(_reset);
    addProperty(_remove);

    _solidColor.setViewOption(properties::Property::ViewOptions::Color);

    addVisibleProperties();

    addPropertySubOwner(_renderSettings);
    addPropertySubOwner(_layerAdjustment);
}

void Layer::initialize() {
    ZoneScoped

    if (_tileProvider) {
        tileprovider::initialize(*_tileProvider);
    }
}

void Layer::deinitialize() {
    if (_tileProvider) {
        tileprovider::deinitialize(*_tileProvider);
    }
}

ChunkTilePile Layer::chunkTilePile(const TileIndex& tileIndex, int pileSize) const {
    ZoneScoped

    if (_tileProvider) {
        return tileprovider::chunkTilePile(*_tileProvider, tileIndex, pileSize);
    }
    else {
        ChunkTilePile chunkTilePile;
        std::fill(chunkTilePile.begin(), chunkTilePile.end(), std::nullopt);
        for (int i = 0; i < pileSize; ++i) {
            chunkTilePile[i] = ChunkTile {
                Tile(), TileUvTransform { { 0, 0 }, { 1, 1 } }
            };
        }
        return chunkTilePile;
    }
}

Tile::Status Layer::tileStatus(const TileIndex& index) const {
    return _tileProvider ?
        tileprovider::tileStatus(*_tileProvider, index) :
        Tile::Status::Unavailable;
}

layergroupid::TypeID Layer::type() const {
    return _type;
}

layergroupid::BlendModeID Layer::blendMode() const {
    return static_cast<layergroupid::BlendModeID>(_blendModeOption.value());
}

TileDepthTransform Layer::depthTransform() const {
    return _tileProvider ?
        tileprovider::depthTransform(*_tileProvider) :
        TileDepthTransform{ 1.f, 0.f };
}

void Layer::setEnabled(bool enabled) {
    _enabled = enabled;
}

bool Layer::enabled() const {
    return _enabled;
}

tileprovider::TileProvider* Layer::tileProvider() const {
    return _tileProvider.get();
}

glm::vec3 Layer::solidColor() const {
    return _solidColor;
}

const LayerRenderSettings& Layer::renderSettings() const {
    return _renderSettings;
}

const LayerAdjustment& Layer::layerAdjustment() const {
    return _layerAdjustment;
}

void Layer::onChange(std::function<void(Layer*)> callback) {
    _onChangeCallback = std::move(callback);
}

int Layer::update() {
    ZoneScoped

    if (_tileProvider) {
        return tileprovider::update(*_tileProvider);
    }
    else {
        return 0;
    }
}

glm::ivec2 Layer::tilePixelStartOffset() const {
    return _padTilePixelStartOffset;
}

glm::ivec2 Layer::tilePixelSizeDifference() const {
    return _padTilePixelSizeDifference;
}

glm::vec2 Layer::tileUvToTextureSamplePosition(const TileUvTransform& uvTransform,
                                               const glm::vec2& tileUV,
                                               const glm::uvec2& resolution)
{
    glm::vec2 uv = uvTransform.uvOffset + uvTransform.uvScale * tileUV;

    const glm::vec2 sourceSize = glm::vec2(resolution) +
                                 glm::vec2(_padTilePixelSizeDifference);
    const glm::vec2 currentSize = glm::vec2(resolution);
    const glm::vec2 sourceToCurrentSize = currentSize / sourceSize;
    return sourceToCurrentSize * (uv - glm::vec2(_padTilePixelStartOffset) / sourceSize);
}

void Layer::initializeBasedOnType(layergroupid::TypeID id, ghoul::Dictionary initDict) {
    switch (id) {
        // Intentional fall through. Same for all tile layers
        case layergroupid::TypeID::DefaultTileLayer:
        case layergroupid::TypeID::SingleImageTileLayer:
        case layergroupid::TypeID::SizeReferenceTileLayer:
        case layergroupid::TypeID::TemporalTileLayer:
        case layergroupid::TypeID::TileIndexTileLayer:
        case layergroupid::TypeID::ByIndexTileLayer:
        case layergroupid::TypeID::ByLevelTileLayer: {
            // We add the id to the dictionary since it needs to be known by
            // the tile provider
            initDict.setValue(KeyLayerGroupID, _layerGroupId);
            if (initDict.hasKeyAndValue<std::string>(KeyName)) {
                std::string name = initDict.value<std::string>(KeyName);
                LDEBUG("Initializing tile provider for layer: '" + name + "'");
            }
            _tileProvider = std::unique_ptr<tileprovider::TileProvider>(
                tileprovider::createFromDictionary(id, std::move(initDict))
            );
            break;
        }
        case layergroupid::TypeID::SolidColor: {
            if (initDict.hasKeyAndValue<glm::vec3>(ColorInfo.identifier)) {
                _solidColor = initDict.value<glm::vec3>(ColorInfo.identifier);
            }
            break;
        }
        default:
            throw ghoul::MissingCaseException();
    }
}

void Layer::addVisibleProperties() {
    switch (type()) {
        // Intentional fall through. Same for all tile layers
        case layergroupid::TypeID::DefaultTileLayer:
        case layergroupid::TypeID::SingleImageTileLayer:
        case layergroupid::TypeID::SizeReferenceTileLayer:
        case layergroupid::TypeID::TemporalTileLayer:
        case layergroupid::TypeID::TileIndexTileLayer:
        case layergroupid::TypeID::ByIndexTileLayer:
        case layergroupid::TypeID::ByLevelTileLayer: {
            if (_tileProvider) {
                addPropertySubOwner(*_tileProvider);
            }
            break;
        }
        case layergroupid::TypeID::SolidColor: {
            addProperty(_solidColor);
            break;
        }
        default:
            break;
    }
}

} // namespace openspace::globebrowsing
