/*****************************************************************************************
 *                                                                                       *
 * 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/server/include/topics/flightcontrollertopic.h>

#include <modules/server/include/connection.h>
#include <modules/server/include/jsonconverters.h>
#include <openspace/engine/globals.h>
#include <openspace/interaction/inputstate.h>
#include <openspace/interaction/websocketcamerastates.h>
#include <openspace/interaction/websocketinputstate.h>
#include <openspace/interaction/navigationhandler.h>
#include <openspace/interaction/orbitalnavigator.h>
#include <openspace/rendering/renderable.h>
#include <openspace/rendering/renderengine.h>
#include <openspace/scene/scenegraphnode.h>
#include <openspace/scene/scene.h>
#include <openspace/scripting/scriptengine.h>
#include <openspace/util/timemanager.h>
#include <ghoul/filesystem/file.h>
#include <ghoul/filesystem/filesystem.h>
#include <ghoul/fmt.h>
#include <ghoul/logging/logmanager.h>
#include <ghoul/lua/ghoul_lua.h>
#include <iterator>
#include <unordered_map>

namespace {
    constexpr const char* BasePathToken = "${BASE}";

    enum class Command {
        Connect = 0,
        Disconnect,
        InputState,
        UpdateView,
        Autopilot,
        Friction,
        Lua
    };

    using AxisType = openspace::interaction::WebsocketCameraStates::AxisType;

    constexpr const char* _loggerCat = "FlightControllerTopic";
    constexpr const char* TypeKey = "type";
    constexpr const char* ValuesKey = "values";

    // Disconnect JSON keys
    constexpr const char* SuccessKey = "success";

    // Connect JSON keys
    constexpr const char* FocusNodesKey = "focusNodes";
    constexpr const char* AllNodesKey = "allNodes";
    constexpr const char* InterestingTimesKey = "interestingTimes";

    // Change focus JSON keys
    constexpr const char* FocusKey = "focus";
    constexpr const char* AnchorKey = "anchor";
    constexpr const char* AimKey = "aim";
    constexpr const char* ResetVelocitiesKey = "resetVelocities";
    constexpr const char* RetargetAnchorKey = "retargetAnchor";
    constexpr const char* RetargetAimKey = "retargetAim";
    constexpr const char* SceneNodeName = "identifier";
    constexpr const char* SceneNodeEnabled = "enabled";

    constexpr const char* RenderableKey = "renderable";
    constexpr const char* RenderableEnabled = "Enabled";

    // Autopilot JSON keys
    constexpr const char* AutopilotEngagedKey = "engaged";
    constexpr const char* AutopilotInputKey = "autopilotInput";

    constexpr const char* FlightControllerType = "flightcontroller";

    // Friction JSON Keys
    constexpr const char* FrictionEngagedKey = "engaged";
    constexpr const char* FrictionPropertyUri =
        "NavigationHandler.OrbitalNavigator.Friction";
    constexpr const char* FrictionRotationKey = "rotation";
    constexpr const char* FrictionZoomKey = "zoom";
    constexpr const char* FrictionRollKey = "roll";
    constexpr const char* RotationalFriction = "Friction.RotationalFriction";
    constexpr const char* ZoomFriction = "Friction.ZoomFriction";
    constexpr const char* RollFriction = "Friction.RollFriction";

    // Friction Lua Keys
    constexpr const char* LuaKey = "lua";
    constexpr const char* LuaScript = "script";

    constexpr const char* OrbitX = "orbitX";
    constexpr const char* OrbitY = "orbitY";
    constexpr const char* ZoomIn = "zoomIn";
    constexpr const char* ZoomOut = "zoomOut";
    constexpr const char* LocalRollX = "localRollX";
    constexpr const char* LocalRollY = "localRollY";
    constexpr const char* GlobalRollX = "globalRollX";
    constexpr const char* GlobalRollY = "globalRollY";
    constexpr const char* PanX = "panX";
    constexpr const char* PanY = "panY";

    constexpr const char* Connect = "connect";
    constexpr const char* Disconnect = "disconnect";
    constexpr const char* InputState = "inputState";
    constexpr const char* UpdateView = "updateView";
    constexpr const char* Autopilot = "autopilot";
    constexpr const char* Friction = "friction";
    constexpr const char* Lua = "lua";

    const static std::unordered_map<std::string, AxisType> AxisIndexMap ({
        { OrbitX, AxisType::OrbitX },
        { OrbitY, AxisType::OrbitY },
        { ZoomIn, AxisType::ZoomIn },
        { ZoomOut, AxisType::ZoomOut },
        { LocalRollX, AxisType::LocalRollX },
        { LocalRollY, AxisType::LocalRollY },
        { GlobalRollX, AxisType::GlobalRollX },
        { GlobalRollY, AxisType::GlobalRollY },
        { PanX, AxisType::PanX },
        { PanY, AxisType::PanY }
    });

    const static std::unordered_map<std::string, Command> CommandMap ({
        { Connect, Command::Connect },
        { Disconnect, Command::Disconnect },
        { InputState, Command::InputState },
        { UpdateView, Command::UpdateView },
        { Autopilot, Command::Autopilot },
        { Friction, Command::Friction },
        { Lua, Command::Lua }
    });

    const int Axes = 10;

} // namespace

using nlohmann::json;

namespace openspace {

FlightControllerTopic::FlightControllerTopic() {
    for (auto it = AxisIndexMap.begin(); it != AxisIndexMap.end(); ++it) {
        global::navigationHandler.setWebsocketAxisMapping(
            int(std::distance(AxisIndexMap.begin(), it)),
            it->second
        );
    }

    // Add WebsocketInputState to global states
    global::websocketInputStates[_topicId] = &_inputState;
}

FlightControllerTopic::~FlightControllerTopic() {
    // Reset global websocketInputStates
    global::websocketInputStates.erase(_topicId);
    global::websocketInputStates = interaction::WebsocketInputStates();
}

bool FlightControllerTopic::isDone() const {
    if (_isDone) {
        disengageAutopilot();
    }
    return _isDone;
}

void FlightControllerTopic::handleJson(const nlohmann::json& json) {
    auto it = CommandMap.find(json[TypeKey]);
    if (it == CommandMap.end()) {
        LWARNING(
            fmt::format("Poorly formatted JSON command: no '{}' in payload", TypeKey)
        );
        return;
    }

    switch (it->second) {
        case Command::Connect:
            connect();
            break;
        case Command::Disconnect:
            disconnect();
            break;
        case Command::InputState:
            processInputState(json);
            break;
        case Command::UpdateView:
            updateView(json);
            break;
        case Command::Autopilot:
            handleAutopilot(json[Autopilot]);
            break;
        case Command::Friction:
            setFriction(json[Friction]);
            break;
        case Command::Lua:
            processLua(json[Lua]);
            break;
        default:
            LWARNING(fmt::format("Unrecognized action: {}", it->first));
            break;
    }
}

void FlightControllerTopic::connect() {
    _isDone = false;
    std::fill(_inputState.axes.begin(), _inputState.axes.end(), 0.f);
    _payload[TypeKey] = Connect;
    setFocusNodes();
    setInterestingTimes();
    _payload[Connect][FocusNodesKey] = _focusNodes;
    _payload[Connect][AllNodesKey] = _allNodes;
    _payload[Connect][InterestingTimesKey] = _interestingTimes;
    _connection->sendJson(wrappedPayload(_payload));
}

void FlightControllerTopic::setFocusNodes() {
    // Get all scene nodes
    std::vector<SceneGraphNode*> nodes =
        global::renderEngine.scene()->allSceneGraphNodes();

    // Remove all nodes with no renderable
    nodes.erase(
        std::remove_if(
            nodes.begin(),
           nodes.end(),
           [](SceneGraphNode* node) { return node->renderable() == nullptr; }
        ),
        nodes.end()
    );

    // Sort them alphabetically
    std::sort(
        nodes.begin(),
        nodes.end(),
        [](SceneGraphNode* lhs, SceneGraphNode* rhs) {
            return lhs->guiName() < rhs->guiName();
        }
    );

    // Add to interesting nodes list and all nodes list
    for (SceneGraphNode* n : nodes) {
        // Set whether it's enabled
        const std::vector<std::string>& tags = n->tags();
        const auto it = std::find(tags.begin(), tags.end(), "GUI.Interesting");
        if (it != tags.end()) {
            _focusNodes[n->guiName()][SceneNodeName] = n->identifier();
            _focusNodes[n->guiName()][SceneNodeEnabled] = n->renderable()->isEnabled();
        }
        _allNodes[n->guiName()][SceneNodeName] = n->identifier();
        _allNodes[n->guiName()][SceneNodeEnabled] = n->renderable()->isEnabled();
    }
}

void FlightControllerTopic::setInterestingTimes() {
    std::vector<Scene::InterestingTime> times =
        global::renderEngine.scene()->interestingTimes();

    std::sort(
        times.begin(),
        times.end(),
        [](Scene::InterestingTime lhs, Scene::InterestingTime rhs) {
            return lhs.name < rhs.name;
        }
    );

    for (const Scene::InterestingTime& t : times) {
        _interestingTimes[t.name] = t.time;
    }
}

void FlightControllerTopic::updateView(const nlohmann::json& json) const {

    if (json.find(RenderableKey) != json.end()) {
        setRenderableEnabled(json);
    }
    else {
        changeFocus(json);
    }
}

void FlightControllerTopic::changeFocus(const nlohmann::json& json) const {
    if (json.find(FocusKey) == json.end()) {
        const std::string j = json;
        LWARNING(
            fmt::format("Could not find {} key in JSON. JSON was:\n{}", FocusKey, j)
        );
        if (json.find(AimKey) == json.end()) {
            LWARNING(
                fmt::format("Could not find {} key in JSON. JSON was:\n{}", AimKey, j)
            );
            if (json.find(AnchorKey) == json.end()) {
                LWARNING(fmt::format(
                    "Could not find {} key in JSON. JSON was:\n{}", AnchorKey, j
                ));
                return;
            }
        }
    }

    const std::string focus = json.find(FocusKey) != json.end() ? json[FocusKey] : "";
    const std::string aim = json.find(AimKey) != json.end() ? json[AimKey] : "";
    const std::string anchor = json.find(AnchorKey) != json.end() ? json[AnchorKey] : "";

    const bool resetVelocities = json[ResetVelocitiesKey];
    const bool retargetAnchor = json[RetargetAnchorKey];
    const bool retargetAim = json[RetargetAimKey];

    Scene* scene = global::renderEngine.scene();
    const SceneGraphNode* focusNode = scene->sceneGraphNode(focus);
    const SceneGraphNode* aimNode = scene->sceneGraphNode(aim);
    const SceneGraphNode* anchorNode = scene->sceneGraphNode(anchor);
    if (focusNode) {
        global::navigationHandler.orbitalNavigator().setFocusNode(
            focusNode,
            resetVelocities
        );
    }
    else {
        if (aimNode) {
            global::navigationHandler.orbitalNavigator().setAimNode(aim);
        }
        if (anchorNode) {
            global::navigationHandler.orbitalNavigator().setAnchorNode(anchor);
        }
    }

    if (retargetAnchor) {
        global::navigationHandler.orbitalNavigator().startRetargetAnchor();
    }
    if (retargetAim) {
        global::navigationHandler.orbitalNavigator().startRetargetAim();
    }
}

void FlightControllerTopic::setRenderableEnabled(const nlohmann::json& json) const {
    if (json[RenderableKey].find(SceneNodeName) == json[RenderableKey].end()) {
        const std::string j = json;
        LWARNING(
            fmt::format("Could not find {} key in JSON. JSON was:\n{}", FocusKey, j)
        );
        return;
    }

    const std::string name = json[RenderableKey][SceneNodeName];
    const bool enabled = json[RenderableKey][SceneNodeEnabled];

    const SceneGraphNode* node = global::renderEngine.scene()->sceneGraphNode(name);
    if (node && node->renderable() != nullptr) {
        node->renderable()->property(RenderableEnabled)->set(enabled);
    }
}

void FlightControllerTopic::disconnect() {
    // Reset global websocketInputStates
    global::websocketInputStates.erase(_topicId);
    global::websocketInputStates = interaction::WebsocketInputStates();

    // Update FlightController
    nlohmann::json j;
    j[TypeKey] = Disconnect;
    j[Disconnect][SuccessKey] = true;
    _connection->sendJson(wrappedPayload(j));

    _isDone = true;
}

void FlightControllerTopic::setFriction(bool all) const {
    setFriction(all, all, all);
}

void FlightControllerTopic::setFriction(bool roll, bool rotation, bool zoom) const {
    const interaction::OrbitalNavigator& navigator =
        global::navigationHandler.orbitalNavigator();

    navigator.property(RollFriction)->set(roll);
    navigator.property(RotationalFriction)->set(rotation);
    navigator.property(ZoomFriction)->set(zoom);

    // Update FlightController
    nlohmann::json j;
    j[TypeKey] = Friction;
    j[Friction][FrictionRollKey] = roll;
    j[Friction][FrictionRotationKey] = rotation;
    j[Friction][FrictionZoomKey] = zoom;
    _connection->sendJson(wrappedPayload(j));
}

void FlightControllerTopic::setFriction(const nlohmann::json& json) const {
    setFriction(json[FrictionRollKey], json[FrictionRotationKey], json[FrictionZoomKey]);
}

void FlightControllerTopic::disengageAutopilot() const {
    setFriction(true);
}

void FlightControllerTopic::engageAutopilot(const nlohmann::json &json) {
    // Disable/enable friction
    setFriction(false);
    auto input = json[AutopilotInputKey][ValuesKey];

    std::fill(_inputState.axes.begin(), _inputState.axes.end(), 0.f);
    _inputState.isConnected = true;

    for (auto it = input.begin(); it != input.end(); ++it) {
        const auto mapIt = AxisIndexMap.find(it.key());
        if (mapIt == AxisIndexMap.end()) {
            if (it.key() != TypeKey || CommandMap.find(it.value()) == CommandMap.end()) {
                LWARNING(fmt::format(
                    "No axis, button, or command named {} (value: {})",
                    it.key(), static_cast<int>(it.value())
                ));
            }
            continue;
        }
        _inputState.axes[std::distance(AxisIndexMap.begin(), mapIt)] = float(it.value());
    }
}

void FlightControllerTopic::handleAutopilot(const nlohmann::json &json) {
    const bool engaged = json[AutopilotEngagedKey];

    if (engaged) {
        engageAutopilot(json);
    }
    else {
        disengageAutopilot();
    }
    _autopilotEngaged = engaged;

    nlohmann::json j;
    j[TypeKey] = Autopilot;
    j[Autopilot][AutopilotEngagedKey] = _autopilotEngaged;

    _connection->sendJson(wrappedPayload(j));
}

void FlightControllerTopic::processInputState(const nlohmann::json& json) {
    std::fill(_inputState.axes.begin(), _inputState.axes.end(), 0.f);
    _inputState.isConnected = true;

    // Get "inputState" object from "payload"
    auto input = json[InputState][ValuesKey];

    for (auto it = input.begin(); it != input.end(); ++it) {
        const auto mapIt = AxisIndexMap.find(it.key());
        if (mapIt == AxisIndexMap.end()) {
            if (it.key() != TypeKey || CommandMap.find(it.value()) == CommandMap.end()) {
                LWARNING(fmt::format(
                    "No axis, button, or command named {} (value: {})",
                    it.key() , static_cast<int>(it.value())
                ));
            }
            continue;
        }
        _inputState.axes[std::distance(AxisIndexMap.begin(), mapIt)] = float(it.value());
    }
}

void FlightControllerTopic::processLua(const nlohmann::json &json) {
    const std::string script = json[LuaScript];
    global::scriptEngine.queueScript(
        script,
        openspace::scripting::ScriptEngine::RemoteScripting::Yes
    );
}

} // namespace openspace
