/*
 * This file is part of ArmarX.
 *
 * ArmarX is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * ArmarX is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 * @package    robdekon
 * @author     Christoph Pohl ( christoph dot pohl at kit dot edu )
 * @date       07.09.22
 * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
 *             GNU General Public License
 */

#include "RemoteGuiVisitors.h"

#include <SimoxUtility/math/convert/quat_to_rpy.h>
#include <SimoxUtility/math/convert/rpy_to_quat.h>

#include <ArmarXCore/util/CPPUtility/Iterator.h>

#include <ArmarXGui/libraries/RemoteGui/RemoteGui.h>

#include <RobotAPI/libraries/aron/converter/eigen/EigenConverter.h>
#include <RobotAPI/libraries/aron/core/data/variant/All.h>
#include <RobotAPI/libraries/aron_component_config/VariantHelperFactory.h>

#include "Util.h"

#define INPUT_GUARD(i)                                                                             \
    ARMARX_TRACE;                                                                                  \
    ARMARX_CHECK_NOT_NULL(i);                                                                      \
    if (in_list_)                                                                                  \
        return;

namespace armarx::aron::component_config
{

    void
    MakeConfigGuiVisitor::visitObjectOnEnter(DataInput& dict, TypeInput& /*unused*/)
    {
        INPUT_GUARD(dict);

        if (dict->getPath().hasElement())
        {
            std::string name = pathToName(dict);
            group_hierarchy_.emplace_back(std::make_shared<RemoteGui::detail::GroupBoxBuilder>(
                RemoteGui::makeGroupBox(name)));
        }
        else
        {
            group_hierarchy_.push_back(builder_);
        }
    }

    void
    MakeConfigGuiVisitor::visitObjectOnExit(DataInput& dict, TypeInput& /*j*/)
    {
        INPUT_GUARD(dict);
        auto builder = *group_hierarchy_.back();
        group_hierarchy_.pop_back();
        if (!group_hierarchy_.empty())
        {
            group_hierarchy_.back()->addChild(builder);
        }
    }

    void
    MakeConfigGuiVisitor::visitInt(DataInput& i, TypeInput& /*unused*/)
    {
        INPUT_GUARD(i);
        auto value = data::Int::DynamicCastAndCheck(i);
        const auto& name = pathToName(i);
        auto group = RemoteGui::makeHBoxLayout(name + "_layout");
        group.addChild(RemoteGui::makeLabel(name + "_label").value(i->getPath().getLastElement()));
        group.addHSpacer();
        group.addChild(RemoteGui::makeIntSpinBox(name)
                           .value(value->getValue())
                           .min(-1000)
                           .max(1000)
                           .toolTip(name));
        group_hierarchy_.back()->addChild(group);
    }

    void
    MakeConfigGuiVisitor::visitFloat(DataInput& f, TypeInput& /*unused*/)
    {
        INPUT_GUARD(f);
        auto value = data::Float::DynamicCastAndCheck(f);
        const auto& name = pathToName(f);
        auto group = RemoteGui::makeHBoxLayout(name + "_layout");
        group.addChild(RemoteGui::makeLabel(name + "_label").value(f->getPath().getLastElement()));
        group.addHSpacer();
        group.addChild(RemoteGui::makeFloatSpinBox(name)
                           .value(value->getValue())
                           .min(-1000)
                           .max(1000)
                           .toolTip(name));
        group_hierarchy_.back()->addChild(group);
    }

    void
    MakeConfigGuiVisitor::visitDouble(DataInput& d, TypeInput& /*unused*/)
    {
        INPUT_GUARD(d);
        auto value = data::Double::DynamicCastAndCheck(d);
        const auto& name = pathToName(d);
        auto group = RemoteGui::makeHBoxLayout(name + "_layout");
        group.addChild(RemoteGui::makeLabel(name + "_label").value(d->getPath().getLastElement()));
        group.addHSpacer();
        group.addChild(RemoteGui::makeFloatSpinBox(name)
                           .value(value->getValue())
                           .min(-1000)
                           .max(1000)
                           .toolTip(name));
        group_hierarchy_.back()->addChild(group);
    }

    void
    MakeConfigGuiVisitor::visitBool(DataInput& b, TypeInput& /*unused*/)
    {
        INPUT_GUARD(b);
        auto value = data::Bool::DynamicCastAndCheck(b);
        const auto& name = pathToName(b);
        auto group = RemoteGui::makeHBoxLayout(name + "_layout");
        group.addChild(RemoteGui::makeLabel(name + "_label").value(b->getPath().getLastElement()));
        group.addHSpacer();
        group.addChild(
            RemoteGui::makeCheckBox(name).label("").value(value->getValue()).toolTip(name));
        group_hierarchy_.back()->addChild(group);
    }

    void
    MakeConfigGuiVisitor::visitString(DataInput& string, TypeInput& /*unused*/)
    {
        INPUT_GUARD(string);
        auto value = data::String::DynamicCastAndCheck(string);
        const auto& name = pathToName(string);
        auto group = RemoteGui::makeHBoxLayout(name + "_layout");
        group.addChild(
            RemoteGui::makeLabel(name + "_label").value(string->getPath().getLastElement()));
        group.addHSpacer();
        group.addChild(RemoteGui::makeLineEdit(name).value(value->getValue()).toolTip(name));
        group_hierarchy_.back()->addChild(group);
    }

    MakeConfigGuiVisitor::MakeConfigGuiVisitor(const std::string& name) :
        builder_(std::make_unique<RemoteGui::detail::GroupBoxBuilder>(name))
    {
    }

    RemoteGui::detail::GroupBoxBuilder
    MakeConfigGuiVisitor::getGroupBoxBuilder() const
    {
        return *builder_;
    }

    void
    MakeConfigGuiVisitor::visitListOnEnter(DataInput& o, TypeInput& t)
    {
        in_list_ = true;
        auto group = RemoteGui::makeSimpleGridLayout(pathToName(o) + "_grid").cols(20);
        auto data = data::List::DynamicCastAndCheck(o);
        auto type = type::List::DynamicCast(t)->getAcceptedType()->getDescriptor();
        if (std::find(implementedListDescriptors.begin(), implementedListDescriptors.end(), type) ==
            implementedListDescriptors.end())
        {
            return;
        }
        for (const auto& el : data->getElements())
        {
            group.addChild(RemoteGui::makeLineEdit(pathToName(el))
                               .value(factories::VariantHelper::make(type)->to_string(el)),
                           10);
            group.addHSpacer(8);
            group.addChild(RemoteGui::makeButton(pathToName(el) + "_button")
                               .label("-")
                               .toolTip("Remove List Element"),
                           2);
        }
        group_hierarchy_.back()->addChild(
            RemoteGui::makeGroupBox(pathToName(o) + "_grp")
                .label(o->getPath().getLastElement())
                .collapsed(true)
                .addChild(group)
                .addChild(RemoteGui::makeButton(pathToName(o) + "_add")
                              .label("+")
                              .toolTip("Add new list entry.")));
    }

    void
    MakeConfigGuiVisitor::visitListOnExit(DataInput& /*unused*/, TypeInput& /*unused*/)
    {
        in_list_ = false;
    }

    void
    MakeConfigGuiVisitor::visitIntEnum(DataInput& o, TypeInput& t)
    {
        INPUT_GUARD(o);
        auto data = data::Int::DynamicCastAndCheck(o);
        auto type = type::IntEnum::DynamicCast(t);
        const auto& name = pathToName(o);
        auto group = RemoteGui::makeHBoxLayout(name + "_layout");
        group.addChild(RemoteGui::makeLabel(name + "_label").value(o->getPath().getLastElement()));
        group.addHSpacer();
        group.addChild(RemoteGui::makeComboBox(name)
                           .options(type->getAcceptedValueNames())
                           .value(type->getValueName(data->getValue()))
                           .toolTip(name));
        group_hierarchy_.back()->addChild(group);
    }
    void
    MakeConfigGuiVisitor::visitMatrix(const std::shared_ptr<data::Variant>& q,
                                      const std::shared_ptr<type::Variant>& t)
    {
        auto data = data::NDArray::DynamicCastAndCheck(q);
        auto type = type::Matrix::DynamicCast(t);
        const auto& cols = type->getCols();
        const auto& rows = type->getRows();
        const auto& name = pathToName(q);
        auto group = RemoteGui::makeHBoxLayout(name + "_layout");
        group.addChild(RemoteGui::makeLabel(name + "_label").value(q->getPath().getLastElement()));
        group.addHSpacer();

        if (cols == 4 && rows == 4)
        {
            // Poses
            const auto& matrix = aron::converter::AronEigenConverter::ConvertToMatrix4f(data);
            group.addChild(RemoteGui::makePosRPYSpinBoxes(name).value(matrix).toolTip(name));
        }
        else if ((cols == 3 and rows == 1) or (rows == 3 and cols == 1))
        {
            // Positions
            const auto& vector = aron::converter::AronEigenConverter::ConvertToVector3f(data);
            group.addChild(RemoteGui::makeVector3fSpinBoxes(name).value(vector).toolTip(name));
        }
        group_hierarchy_.back()->addChild(group);
    }

    void
    MakeConfigGuiVisitor::visitQuaternion(const std::shared_ptr<data::Variant>& q,
                                          const std::shared_ptr<type::Variant>& t)
    {
        INPUT_GUARD(q);
        auto data = data::NDArray::DynamicCastAndCheck(q);
        auto type = type::Quaternion::DynamicCast(t);
        const auto& quat = simox::math::quat_to_rpy(
            aron::converter::AronEigenConverter::ConvertToQuaternionf(data));
        const auto& name = pathToName(q);
        auto group = RemoteGui::makeHBoxLayout(name + "_layout");
        group.addChild(RemoteGui::makeLabel(name + "_label").value(q->getPath().getLastElement()));
        group.addHSpacer();
        group.addChild(RemoteGui::makeVector3fSpinBoxes(name).value(quat).min(-M_PI).max(M_PI).steps(601).decimals(2).toolTip(name));
        group_hierarchy_.back()->addChild(group);
    }

    void
    MakeConfigGuiVisitor::visitDictOnEnter(const std::shared_ptr<data::Variant>& o,
                                           const std::shared_ptr<type::Variant>& t)
    {
        in_list_ = true;
        auto group = RemoteGui::makeSimpleGridLayout(pathToName(o) + "_grid").cols(20);
        auto data = data::Dict::DynamicCastAndCheck(o);
        auto type = type::Dict::DynamicCast(t)->getAcceptedType()->getDescriptor();
        if (std::find(implementedListDescriptors.begin(), implementedListDescriptors.end(), type) ==
            implementedListDescriptors.end())
        {
            return;
        }
        for (const auto& el : data->getElements())
        {
            group.addChild(RemoteGui::makeLineEdit(pathToName(el.second) + "_lbl").value(el.first),
                           5);
            group.addHSpacer(2);
            group.addChild(RemoteGui::makeLineEdit(pathToName(el.second))
                               .value(factories::VariantHelper::make(type)->to_string(el.second)),
                           5);
            group.addHSpacer(6);
            group.addChild(RemoteGui::makeButton(pathToName(el.second) + "_button")
                               .label("-")
                               .toolTip("Remove List Element"),
                           2);
        }
        group_hierarchy_.back()->addChild(
            RemoteGui::makeGroupBox(pathToName(o) + "_grp")
                .label(o->getPath().getLastElement())
                .collapsed(true)
                .addChild(group)
                .addChild(RemoteGui::makeButton(pathToName(o) + "_add")
                              .label("+")
                              .toolTip("Add new dict entry.")));
    }

    void
    MakeConfigGuiVisitor::visitDictOnExit(const std::shared_ptr<data::Variant>&,
                                          const std::shared_ptr<type::Variant>&)
    {
        in_list_ = false;
    }


    GetValueFromMapVisitor::GetValueFromMapVisitor(armarx::RemoteGui::TabProxy* proxy) :
        proxy_(std::experimental::make_observer(proxy))
    {
    }

    void
    GetValueFromMapVisitor::visitInt(DataInput& i, TypeInput&)
    {
        INPUT_GUARD(i);
        auto value = data::Int::DynamicCastAndCheck(i);
        const std::string name = pathToName(i);
        auto gui_value = proxy_->getValue<int>(name);
        if (value->getValue() != gui_value.get())
        {
            if (proxy_->hasValueChanged(name))
            {
                value->setValue(gui_value.get());
                i = value;
            }
            else
            {
                proxy_->setValue(value->getValue(), name);
            }
        }
    }

    void
    GetValueFromMapVisitor::visitFloat(DataInput& f, TypeInput&)
    {
        INPUT_GUARD(f);
        auto value = data::Float::DynamicCastAndCheck(f);
        const std::string name = pathToName(f);
        auto gui_value = proxy_->getValue<float>(name);
        if (value->getValue() != gui_value.get())
        {
            if (proxy_->hasValueChanged(name))
            {
                value->setValue(gui_value.get());
                f = value;
            }
            else
            {
                proxy_->setValue(value->getValue(), name);
            }
        }
    }

    void
    GetValueFromMapVisitor::visitDouble(DataInput& d, TypeInput&)
    {
        INPUT_GUARD(d);
        auto value = data::Double::DynamicCastAndCheck(d);
        const std::string name = pathToName(d);
        // TODO: does double work here?
        auto gui_value = proxy_->getValue<float>(name);
        if (value->getValue() != gui_value.get())
        {
            if (proxy_->hasValueChanged(name))
            {
                value->setValue(gui_value.get());
                d = value;
            }
            else
            {
                proxy_->setValue(static_cast<float>(value->getValue()), name);
            }
        }
    }

    void
    GetValueFromMapVisitor::visitBool(DataInput& b, TypeInput&)
    {
        INPUT_GUARD(b);
        auto value = data::Bool::DynamicCastAndCheck(b);
        const std::string name = pathToName(b);
        auto gui_value = proxy_->getValue<bool>(name);
        if (value->getValue() != gui_value.get())
        {
            if (proxy_->hasValueChanged(name))
            {
                value->setValue(gui_value.get());
                b = value;
            }
            else
            {
                proxy_->setValue(value->getValue(), name);
            }
        }
    }

    void
    GetValueFromMapVisitor::visitString(DataInput& string, TypeInput&)
    {
        INPUT_GUARD(string);
        auto value = data::String::DynamicCastAndCheck(string);
        const std::string name = pathToName(string);
        auto gui_value = proxy_->getValue<std::string>(name);
        if (value->getValue() != gui_value.get())
        {
            if (proxy_->hasValueChanged(name))
            {
                value->setValue(gui_value.get());
                string = value;
            }
            else
            {
                proxy_->setValue(value->getValue(), name);
            }
        }
    }

    type::Descriptor
    GetValueFromMapVisitor::getDescriptor(DataInput& o, TypeInput& t)
    {
        return data::ConstTypedVariantVisitor::GetDescriptor(o, t);
    }

    GetValueFromMapVisitor::MapElements
    GetValueFromMapVisitor::getObjectElements(DataInput& o, TypeInput& t)
    {
        return component_config::getObjectElements(o, t);
    }

    GetValueFromMapVisitor::MapElements
    GetValueFromMapVisitor::getDictElements(DataInput& o, TypeInput& t)
    {
        return component_config::getDictElements(o, t);
    }

    GetValueFromMapVisitor::ListElements
    GetValueFromMapVisitor::getListElements(DataInput& o, TypeInput& t)
    {
        return component_config::getListElements(o, t);
    }

    GetValueFromMapVisitor::PairElements
    GetValueFromMapVisitor::getPairElements(DataInput& o, TypeInput& t)
    {
        return component_config::getPairElements(o, t);
    }

    GetValueFromMapVisitor::TupleElements
    GetValueFromMapVisitor::getTupleElements(DataInput& o, TypeInput& t)
    {
        return component_config::getTupleElements(o, t);
    }

    void
    GetValueFromMapVisitor::visitIntEnum(DataInput& o, TypeInput& t)
    {
        INPUT_GUARD(o);
        auto data = data::Int::DynamicCastAndCheck(o);
        auto type = type::IntEnum::DynamicCastAndCheck(t);
        std::string str;
        const std::string name = pathToName(o);
        proxy_->getValue(str, pathToName(o));
        if (data->getValue() != type->getValue(str))
        {
            if (proxy_->hasValueChanged(name))
            {
                data->getValue() = type->getValue(str);
                o = data;
            }
            else
            {
                proxy_->setValue(type->getValueName(data->getValue()), name);
            }
        }
    }

    void
    GetValueFromMapVisitor::visitListOnEnter(DataInput& o, TypeInput& t)
    {
        in_list_ = true;
        auto data = data::List::DynamicCastAndCheck(o);
        auto type = type::List::DynamicCast(t)->getAcceptedType()->getDescriptor();
        if (std::find(implementedListDescriptors.begin(), implementedListDescriptors.end(), type) ==
            implementedListDescriptors.end())
        {
            return;
        }
        const auto& elements = data->getElements();
        for (const auto& [idx, el] : armarx::MakeIndexedContainer(elements))
        {
            if (proxy_->getButtonClicked(pathToName(el) + "_button"))
            {
                data->removeElement(idx);
                tab_rebuild_required_ = true;
            }
            auto gui_value = proxy_->getValue<std::string>(pathToName(el)).get();
            factories::VariantHelper::make(type)->set_value_from_string(el, gui_value);
        }
        if (proxy_->getButtonClicked(pathToName(o) + "_add"))
        {
            data->addElement(factories::VariantHelper::make(type)->from_string(
                "", o->getPath().withIndex(data->childrenSize())));
            tab_rebuild_required_ = true;
        }
    }

    void
    GetValueFromMapVisitor::visitListOnExit(DataInput&, TypeInput&)
    {
        in_list_ = false;
    }

    bool
    GetValueFromMapVisitor::tabRebuildRequired() const
    {
        return tab_rebuild_required_;
    }
    void
    GetValueFromMapVisitor::visitMatrix(std::shared_ptr<data::Variant>& o,
                                        const std::shared_ptr<type::Variant>& t)
    {
        INPUT_GUARD(o);
        auto value = data::NDArray::DynamicCastAndCheck(o);
        auto type = type::Matrix::DynamicCastAndCheck(t);
        const std::string name = pathToName(o);
        const auto& cols = type->getCols();
        const auto& rows = type->getRows();
        // TODO: does double work here?
        if (cols == 4 && rows == 4)
        {
            auto gui_value = proxy_->getValue<Eigen::Matrix4f>(name);
            auto config_value = converter::AronEigenConverter::ConvertToMatrix4f(value);

            if (config_value != gui_value.get())
            {
                if (proxy_->hasValueChanged(name))
                {
                    auto variant = converter::AronEigenConverter::ConvertFromMatrix(gui_value.get());
                    value->setData(gui_value.get().size() * sizeof(float), variant->getData());
                }
                else
                {
                    proxy_->setValue(config_value, name);
                }
            }
        } else if ((cols == 3 and rows == 1) or (cols == 1 and rows == 3))
        {
            auto gui_value = proxy_->getValue<Eigen::Vector3f>(name);
            auto config_value = converter::AronEigenConverter::ConvertToVector3f(value);

            if (config_value != gui_value.get())
            {
                if (proxy_->hasValueChanged(name))
                {
                    auto variant = converter::AronEigenConverter::ConvertFromMatrix(gui_value.get());
                    value->setData(gui_value.get().size() * sizeof(float), variant->getData());
                }
                else
                {
                    proxy_->setValue(config_value, name);
                }
            }
        }

    }
    void
    GetValueFromMapVisitor::visitQuaternion(std::shared_ptr<data::Variant>& o,
                                            const std::shared_ptr<type::Variant>& t)
    {
        INPUT_GUARD(o);
        auto value = data::NDArray::DynamicCastAndCheck(o);
        const std::string name = pathToName(o);
        // TODO: does double work here?
        auto gui_value = proxy_->getValue<Eigen::Vector3f>(name);
        const auto& quat = simox::math::quat_to_rpy(converter::AronEigenConverter::ConvertToQuaternionf(value));
        if (quat != gui_value.get())
        {
            if (proxy_->hasValueChanged(name))
            {
                auto variant = converter::AronEigenConverter::ConvertFromQuaternion(simox::math::rpy_to_quat(gui_value.get()));
                value->setData(4 * sizeof(float), variant->getData());
            }
            else
            {
                proxy_->setValue(quat, name);
            }
        }
    }

    void
    GetValueFromMapVisitor::visitDictOnEnter(std::shared_ptr<data::Variant>& o,
                                             const std::shared_ptr<type::Variant>& t)
    {
        in_list_ = true;
        auto data = data::Dict::DynamicCastAndCheck(o);
        auto type = type::Dict::DynamicCast(t)->getAcceptedType()->getDescriptor();
        if (std::find(implementedListDescriptors.begin(), implementedListDescriptors.end(), type) ==
            implementedListDescriptors.end())
        {
            return;
        }
        const auto& elements = data->getElements();
        std::map<std::string, std::string> changed_labels;
        for (const auto& [idx, el] : elements)
        {
            const std::string name = pathToName(el);
            if (proxy_->getButtonClicked(name + "_button"))
            {
                data->removeElement(idx);
                tab_rebuild_required_ = true;
            }
            auto gui_value = proxy_->getValue<std::string>(name).get();
            auto gui_key = proxy_->getValue<std::string>(name + "_lbl").get();
            auto config_value = factories::VariantHelper::make(type)->to_string(el);
            if (gui_value != config_value)
            {
                if (proxy_->hasValueChanged(name))
                {
                    factories::VariantHelper::make(type)->set_value_from_string(el, gui_value);
                }
                else
                {
                    proxy_->setValue(config_value, name);
                }
            }
            if (gui_key != idx)
            {
                if (proxy_->hasValueChanged(name + "_lbl"))
                {
                    changed_labels.emplace(idx, gui_key);
                }
                else
                {
                    proxy_->setValue(idx, name + "_lbl");
                }
            }
        }
        // replace changed keys in map
        for (const auto& [old_label, new_label] : changed_labels)
        {
            auto element = data->getElement(old_label);
            data->removeElement(old_label);
            auto variantHelper = factories::VariantHelper::make(type);
            data->addElement(new_label,
                             variantHelper->from_string(
                                 variantHelper->to_string(element),
                                 o->getPath().withDetachedLastElement().withElement(new_label)));
            tab_rebuild_required_ = true;
        }

        if (proxy_->getButtonClicked(pathToName(o) + "_add"))
        {
            data->addElement("defaultKey",
                             factories::VariantHelper::make(type)->from_string(
                                 "", o->getPath().withElement("defaultKey")));
            tab_rebuild_required_ = true;
        }
    }

    void
    GetValueFromMapVisitor::visitDictOnExit(std::shared_ptr<data::Variant>&,
                                            const std::shared_ptr<type::Variant>&)
    {
        in_list_ = false;
    }
} // namespace armarx::aron::component_config

#undef INPUT_GUARD