/*
 * VariableNetworkNode.h
 *
 *  Created on: Jun 23, 2016
 *      Author: Martin Hierholzer
 */

#ifndef CHIMERATK_VARIABLE_NETWORK_NODE_H
#define CHIMERATK_VARIABLE_NETWORK_NODE_H

#include <iostream>
#include <unordered_map>
#include <unordered_set>

#include <assert.h>

#include <boost/shared_ptr.hpp>

#include <ChimeraTK/NDRegisterAccessorAbstractor.h>

#include "ConstantAccessor.h"
#include "Flags.h"
#include "VersionNumberUpdatingRegisterDecorator.h"
#include "Visitor.h"

namespace ChimeraTK {

class VariableNetwork;
class AccessorBase;
class EntityOwner;
struct VariableNetworkNode_data;

/** Pseudo type to identify nodes which can have arbitrary types */
class AnyType {};

/** Class describing a node of a variable network */
class VariableNetworkNode {

public:
  /** Copy-constructor: Just copy the pointer to the data storage object */
  VariableNetworkNode(const VariableNetworkNode &other);

  /** Copy by assignment operator: Just copy the pointer to the data storage
   * object */
  VariableNetworkNode &operator=(const VariableNetworkNode &rightHandSide);

  /** Constructor for an Application node */
  VariableNetworkNode(EntityOwner *owner,
                      ChimeraTK::TransferElementAbstractor *accessorBridge,
                      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 = {});

  /** Constructor for a Device node */
  VariableNetworkNode(const std::string &name, const std::string &deviceAlias,
                      const std::string &registerName, UpdateMode mode,
                      VariableDirection direction,
                      const std::type_info &valTyp = typeid(AnyType),
                      size_t nElements = 0);

  /** Constructor for a ControlSystem node */
  VariableNetworkNode(std::string publicName, VariableDirection direction,
                      const std::type_info &valTyp = typeid(AnyType),
                      size_t nElements = 0);

  /** Constructor for a TriggerReceiver node triggering the data transfer of
   * another network. The additional dummy argument is only there to
   * discriminate the signature from the copy constructor and will be ignored.
   */
  VariableNetworkNode(VariableNetworkNode &nodeToTrigger, int);

  /** Constructor to wrap a VariableNetworkNode_data pointer */
  VariableNetworkNode(boost::shared_ptr<VariableNetworkNode_data> pdata);

  /** Default constructor for an invalid node */
  VariableNetworkNode();

  /** Factory function for a constant (a constructor cannot be templated) */
  template <typename UserType>
  static VariableNetworkNode makeConstant(bool makeFeeder, UserType value = 0,
                                          size_t length = 1);

  /** 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);

  /** Set the owner network of this node. If an owner network is already set, an
   * assertion will be raised */
  void setOwner(VariableNetwork *network);

  /** Clear the owner network of this node. */
  void clearOwner();

  /** Set the value type for this node. Only possible of the current value type
   * is undecided (i.e. AnyType). */
  void setValueType(const std::type_info &newType) const;

  /** Set the direction for this node. Only possible if current direction is
   * VariableDirection::feeding and the node type is NodeType::ControlSystem. */
  void setDirection(VariableDirection newDirection) const;

  /** Function checking if the node requires a fixed implementation */
  bool hasImplementation() const;

  /** Compare two nodes */
  bool operator==(const VariableNetworkNode &other) const;
  bool operator!=(const VariableNetworkNode &other) const;
  bool operator<(const VariableNetworkNode &other) const;

  /** Connect two nodes */
  VariableNetworkNode operator>>(VariableNetworkNode other);

  /** Add a trigger */
  VariableNetworkNode operator[](VariableNetworkNode trigger);

  /** Check for presence of an external trigger */
  bool hasExternalTrigger() const;

  /** Return the external trigger node. if no external trigger is present, an
   * assertion will be raised. */
  VariableNetworkNode getExternalTrigger();

  /** Print node information to std::cout */
  void dump(std::ostream &stream = std::cout) const;

  /** Check if the node already has an owner */
  bool hasOwner() const;

  /** Add a tag. This function may only be used on Application-type nodes. Valid
   * names for tags only contain
   *  alpha-numeric characters (i.e. no spaces and no special characters). @todo
   * enforce this!*/
  void addTag(const std::string &tag);

  /** Getter for the properties */
  NodeType getType() const;
  UpdateMode getMode() const;
  VariableDirection getDirection() const;
  const std::type_info &getValueType() const;
  std::string getName() const;
  std::string getQualifiedName() const;
  const std::string &getUnit() const;
  const std::string &getDescription() const;
  VariableNetwork &getOwner() const;
  VariableNetworkNode getNodeToTrigger();
  const std::string &getPublicName() const;
  const std::string &getDeviceAlias() const;
  const std::string &getRegisterName() const;
  const std::unordered_set<std::string> &getTags() const;
  void setNumberOfElements(size_t nElements);
  size_t getNumberOfElements() const;
  ChimeraTK::TransferElementAbstractor &getAppAccessorNoType();

  void setPublicName(const std::string &name) const;

  template <typename UserType>
  ChimeraTK::NDRegisterAccessorAbstractor<UserType> &getAppAccessor() const;

  template <typename UserType>
  void setAppAccessorImplementation(
      boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>> impl) const;

  template <typename UserType>
  boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>>
  getConstAccessor() const;

  /** Return the unique ID of this node (will change every time the application
   * is started). */
  const void *getUniqueId() const { return pdata.get(); }

  /** Change pointer to the accessor. May only be used for application nodes. */
  void setAppAccessorPointer(ChimeraTK::TransferElementAbstractor *accessor);

  EntityOwner *getOwningModule() const;

  void setOwningModule(EntityOwner *newOwner) const;

  void accept(Visitor<VariableNetworkNode> &visitor) const;

  // protected:  @todo make protected again (with proper interface extension)

  boost::shared_ptr<VariableNetworkNode_data> pdata;
};

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

/** We use a pimpl pattern so copied instances of VariableNetworkNode refer to
 * the same instance of the data structure and thus stay consistent all the
 * time. */
struct VariableNetworkNode_data {

  VariableNetworkNode_data() {}

  /** Type of the node (Application, Device, ControlSystem, Trigger) */
  NodeType type{NodeType::invalid};

  /** Update mode: poll or push */
  UpdateMode mode{UpdateMode::invalid};

  /** Node direction: feeding or consuming */
  VariableDirection direction{VariableDirection::invalid, false};

  /** Value type of this node. If the type_info is the typeid of AnyType, the
   * actual type can be decided when making the connections. */
  const std::type_info *valueType{&typeid(AnyType)};

  /** Engineering unit. If equal to ChimeraTK::TransferElement::unitNotSet, no
   * unit has been defined (and any unit is allowed). */
  std::string unit{ChimeraTK::TransferElement::unitNotSet};

  /** Description */
  std::string description{""};

  /** The network this node belongs to */
  VariableNetwork *network{nullptr};

  /** Pointer to implementation if type == Constant */
  boost::shared_ptr<ChimeraTK::TransferElement> constNode;

  /** Pointer to implementation if type == Application */
  ChimeraTK::TransferElementAbstractor *appNode{nullptr};

  /** Pointer to network which should be triggered by this node */
  VariableNetworkNode nodeToTrigger{nullptr};

  /** Pointer to the network providing the external trigger. May only be used
   * for feeding nodes with an update mode poll. When enabled, the update mode
   * will be converted into push. */
  VariableNetworkNode externalTrigger{nullptr};

  /** Public name if type == ControlSystem */
  std::string publicName;

  /** Accessor name if type == Application */
  std::string name;
  std::string qualifiedName;

  /** Device information if type == Device */
  std::string deviceAlias;
  std::string registerName;

  /** Number of elements in the variable. 0 means not yet decided. */
  size_t nElements{0};

  /** Set of tags  if type == Application */
  std::unordered_set<std::string> tags;

  /** Map to store triggered versions of this node. The map key is the trigger
   * node and the value is the node with the respective trigger added. */
  std::map<VariableNetworkNode, VariableNetworkNode> nodeWithTrigger;

  /** Pointer to the module owning this node */
  EntityOwner *owningModule{nullptr};
};

/*********************************************************************************************************************/
/*** Implementations
 * *************************************************************************************************/
/*********************************************************************************************************************/

template <typename UserType>
VariableNetworkNode VariableNetworkNode::makeConstant(bool makeFeeder,
                                                      UserType value,
                                                      size_t length) {
  VariableNetworkNode node;
  node.pdata = boost::make_shared<VariableNetworkNode_data>();
  node.pdata->constNode.reset(new ConstantAccessor<UserType>(value, length));
  node.pdata->type = NodeType::Constant;
  node.pdata->valueType = &typeid(UserType);
  node.pdata->nElements = length;
  node.pdata->name = "*UNNAMED CONSTANT*";
  if (makeFeeder) {
    node.pdata->direction = {VariableDirection::feeding, false};
    node.pdata->mode = UpdateMode::push;
  } else {
    node.pdata->direction = {VariableDirection::consuming, false};
    node.pdata->mode = UpdateMode::poll;
  }
  return node;
}

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

template <typename UserType>
ChimeraTK::NDRegisterAccessorAbstractor<UserType> &
VariableNetworkNode::getAppAccessor() const {
  assert(typeid(UserType) == getValueType());
  assert(pdata->type == NodeType::Application);
  auto accessor =
      static_cast<ChimeraTK::NDRegisterAccessorAbstractor<UserType> *>(
          pdata->appNode);
  assert(accessor != nullptr);
  return *accessor;
}

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

template <typename UserType>
boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>>
VariableNetworkNode::getConstAccessor() const {
  return boost::dynamic_pointer_cast<ChimeraTK::NDRegisterAccessor<UserType>>(
      pdata->constNode);
}

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

template <typename UserType>
void VariableNetworkNode::setAppAccessorImplementation(
    boost::shared_ptr<NDRegisterAccessor<UserType>> impl) const {
  auto decorated =
      boost::make_shared<VersionNumberUpdatingRegisterDecorator<UserType>>(
          impl, getOwningModule());
  getAppAccessor<UserType>().replace(decorated);
}

} /* namespace ChimeraTK */

#endif /* CHIMERATK_VARIABLE_NETWORK_NODE_H */