// SPDX-FileCopyrightText: Deutsches Elektronen-Synchrotron DESY, MSK, ChimeraTK Project <chimeratk-support@desy.de>
// SPDX-License-Identifier: LGPL-3.0-or-later
#pragma once

#include "ApplicationModule.h"
#include "Module.h"
#include "Utilities.h"
#include "VariableGroup.h"
#include "VariableNetworkNode.h"

#include <boost/smart_ptr/shared_ptr.hpp>

#include <string>

namespace ChimeraTK {

  /********************************************************************************************************************/

  /** Adds features required for inversion of control to an accessor. This is
   * needed for both the ArrayAccessor and the ScalarAccessor classes, thus it
   * uses a CRTP. */
  template<typename Derived>
  class InversionOfControlAccessor {
   public:
    /** Unregister at its owner when deleting */
    ~InversionOfControlAccessor();

    /** Change meta data (name, unit, description and optionally tags). This
     * function may only be used on Application-type nodes. If the optional
     * argument tags is omitted, the tags will not be changed. To clear the
     *  tags, an empty set can be passed. */
    void setMetaData(const std::string& name, const std::string& unit, const std::string& description);
    void setMetaData(const std::string& name, const std::string& unit, const std::string& description,
        const std::unordered_set<std::string>& tags);

    /** Add a tag. Valid names for tags only contain alpha-numeric characters
     * (i.e. no spaces and no special characters). */
    void addTag(const std::string& tag) { _node.addTag(tag); }

    /** Add multiple tags. Valid names for tags only contain alpha-numeric
     * characters (i.e. no spaces and no special characters). */
    void addTags(const std::unordered_set<std::string>& tags);

    /** Return set of tags. */
    const std::unordered_set<std::string>& getTags();

    /** Convert into VariableNetworkNode */
    explicit operator VariableNetworkNode() { return _node; }
    explicit operator VariableNetworkNode() const { return _node; }

    /** Replace with other accessor */
    void replace(Derived&& other);

    /** Return the owning module */
    [[nodiscard]] EntityOwner* getOwner() const { return _node.getOwningModule(); }

    [[nodiscard]] Model::ProcessVariableProxy getModel() const { return _node.getModel(); }

   protected:
    /// complete the description with the full description from the owner
    [[nodiscard]] std::string completeDescription(EntityOwner* owner, const std::string& description) const;

    InversionOfControlAccessor(Module* owner, const std::string& name, VariableDirection direction, std::string unit,
        size_t nElements, UpdateMode mode, const std::string& description, const std::type_info* valueType,
        const std::unordered_set<std::string>& tags = {});

    /** Default constructor creates a dysfunctional accessor (to be assigned with a real accessor later) */
    InversionOfControlAccessor() = default;

    VariableNetworkNode _node;
  };

  /********************************************************************************************************************/
  /********************************************************************************************************************/

  template<typename Derived>
  InversionOfControlAccessor<Derived>::~InversionOfControlAccessor() {
    if(getOwner() != nullptr) {
      if(static_cast<Derived*>(this)->_impl != nullptr) {
        auto* entity = getOwner();

        if(entity != nullptr) {
          auto* owner = dynamic_cast<Module*>(entity);
          while(owner->getOwner() != nullptr) {
            owner = dynamic_cast<Module*>(owner->getOwner());
          }

          auto* application = dynamic_cast<Application*>(owner);
          assert(application != nullptr);

          if(application->getLifeCycleState() == LifeCycleState::run) {
            try {
              throw ChimeraTK::logic_error(
                  "Variable has been destroyed with active connections while application is still running");
            }
            catch(ChimeraTK::logic_error&) {
              std::terminate();
            }
          }
        }
      }
      getOwner()->unregisterAccessor(_node);
    }
    if(getModel().isValid()) {
      try {
        getModel().removeNode(_node);
      }
      catch(ChimeraTK::logic_error& e) {
        std::cerr << "ChimeraTK::logic_error caught: " << e.what() << std::endl;
        std::terminate();
      }
    }
  }

  /********************************************************************************************************************/

  template<typename Derived>
  void InversionOfControlAccessor<Derived>::setMetaData(
      const std::string& name, const std::string& unit, const std::string& description) {
    _node.setMetaData(name, unit, completeDescription(getOwner(), description));
  }

  /********************************************************************************************************************/

  template<typename Derived>
  void InversionOfControlAccessor<Derived>::setMetaData(const std::string& name, const std::string& unit,
      const std::string& description, const std::unordered_set<std::string>& tags) {
    _node.setMetaData(name, unit, completeDescription(getOwner(), description), tags);
  }

  /********************************************************************************************************************/

  template<typename Derived>
  void InversionOfControlAccessor<Derived>::addTags(const std::unordered_set<std::string>& tags) {
    for(const auto& tag : tags) {
      _node.addTag(tag);
    }
  }

  /********************************************************************************************************************/

  template<typename Derived>
  const std::unordered_set<std::string>& InversionOfControlAccessor<Derived>::getTags() {
    return _node.getTags();
  }

  /********************************************************************************************************************/

  template<typename Derived>
  void InversionOfControlAccessor<Derived>::replace(Derived&& other) {
    if(static_cast<Derived*>(this)->_impl != nullptr || other._impl != nullptr) {
      try {
        throw ChimeraTK::logic_error(
            "Variable has been destroyed with active connections while application is still running");
      }
      catch(ChimeraTK::logic_error& ex) {
        std::terminate();
      }
    }

    // remove accessor from owning module
    if(getOwner() != nullptr) {
      getOwner()->unregisterAccessor(_node);
    }

    // remove node from model
    if(getModel().isValid()) {
      getModel().removeNode(_node);
    }

    // transfer the node
    _node = std::move(other._node);
    other._node = VariableNetworkNode(); // Make sure the destructor of other sees an invalid node

    // update the app accessor pointer in the node
    if(_node.getType() == NodeType::Application) {
      _node.setAppAccessorPointer(static_cast<Derived*>(this));
    }
    else {
      assert(_node.getType() == NodeType::invalid);
    }
    // Note: the accessor is registered by the VariableNetworkNode, so we don't have to re-register.
  }

  /********************************************************************************************************************/

  template<typename Derived>
  std::string InversionOfControlAccessor<Derived>::completeDescription(
      EntityOwner* owner, const std::string& description) const {
    auto ownerDescription = owner->getFullDescription();
    if(ownerDescription.empty()) {
      return description;
    }
    if(description.empty()) {
      return ownerDescription;
    }
    return ownerDescription + " - " + description;
  }

  /********************************************************************************************************************/

  template<typename Derived>
  InversionOfControlAccessor<Derived>::InversionOfControlAccessor(Module* owner, const std::string& name,
      VariableDirection direction, std::string unit, size_t nElements, UpdateMode mode, const std::string& description,
      const std::type_info* valueType, const std::unordered_set<std::string>& tags)
  : _node(owner, static_cast<Derived*>(this), ChimeraTK::Utilities::raiseIftrailingSlash(name, false), direction, unit,
        nElements, mode, completeDescription(owner, description), valueType, tags) {
    static_assert(std::is_base_of<InversionOfControlAccessor<Derived>, Derived>::value,
        "InversionOfControlAccessor<> must be used in a curiously recurring template pattern!");

    /// @todo FIXME eliminate dynamic_cast and the "lambda trick" by changing owner pointer type
    auto addToOnwer = [&](auto& owner_casted) {
      auto model = owner_casted.getModel();
      if(!model.isValid()) {
        // this happens e.g. for default-constructed owners and their sub-modules
        return;
      }
      auto neighbourDir = model.visit(
          Model::returnDirectory, Model::getNeighbourDirectory, Model::returnFirstHit(Model::DirectoryProxy{}));

      auto dir = neighbourDir.addDirectoryRecursive(Utilities::getPathName(name));
      auto var = dir.addVariable(Utilities::getUnqualifiedName(name));

      model.addVariable(var, _node);
    };
    auto* owner_am = dynamic_cast<ApplicationModule*>(owner);
    auto* owner_vg = dynamic_cast<VariableGroup*>(owner);
    if(owner_am) {
      addToOnwer(*owner_am);
    }
    else if(owner_vg) {
      addToOnwer(*owner_vg);
    }
    else {
      throw ChimeraTK::logic_error("Trying to add " + name + " to " + owner->getQualifiedName() +
          " which is neither an ApplicationModule nor a VariableGroup, but a " +
          boost::core::demangled_name(typeid(owner)));
    }

    owner->registerAccessor(_node);
  }

  /********************************************************************************************************************/

} // namespace ChimeraTK