#pragma once

#include "hebi.h"

#include <array>
#include <functional>
#include <memory>
#include <mutex>
#include <vector>

#include "util.hpp"

namespace hebi {

class LogFile;
class Group;
class GroupCommand;
class GroupFeedback;
class GroupInfo;

namespace util {

class MobileIO;

} // namespace util

/**
 * \brief Definition of a callback function for GroupFeedback returned from a
 * Group of modules.
 */
using GroupFeedbackHandler = std::function<void(const GroupFeedback&)>;

enum InfoExtraFields : uint64_t {
  EthernetInfo = HebiInfoExtraFieldsEthernetInfo, // IP address, subnet, etc.
  UserData = HebiInfoExtraFieldsUserData, // user data bytes and floats
  FirmwareInfo = HebiInfoExtraFieldsFirmwareInfo, // mechanical/electrical revs, etc
  RuntimeData = HebiInfoExtraFieldsRuntimeData, // time on, time commanded
};

class UserState {
  friend class Group;

private:
  std::array<bool, 9> has_state_bits_;
  std::array<double, 9> state_values_;
public:
  // Creates a user state with all values empty
  UserState() = default;
  // Creates a user state with all values specified
  UserState(double v1, double v2, double v3, double v4, double v5, double v6, double v7,
            double v8, double v9) :
            state_values_({v1, v2, v3, v4, v5, v6, v7, v8, v9}) {
    // Has state for all fields
    has_state_bits_.fill(true);
  }

  /**
   * \brief Does the UserState have the specific value set?
   *
   * \param number The relevant user state value, 1-9.
   * \return true if the value is set, false otherwise
   */
  bool hasValue(size_t number) const {
    return (number >= 1 && number <= 9) ? (has_state_bits_[number - 1]) : false;
  }

  /**
   * \brief Get the specific UserState value.
   *
   * \param number The relevant user state value, 1-9.
   * \return The stored value of this entry.  Note this may be garbage if not set.
   */
  double getValue(size_t number) const {
    return (number >= 1 && number <= 9) ? (state_values_[number - 1]) : 0;
  }

  /**
   * \brief Set the specific value in the UserState.
   *
   * \param number The relevant user state value, 1-9.
   * \param value The value to set this user state element to.
   */
  void setValue(size_t number, double value) {
    if (number < 1 || number > 9)
      return;
    has_state_bits_[number - 1] = true;
    state_values_[number - 1] = value;
  }

  /**
   * \brief Clears the specific value set in the UserState.
   *
   * \param number The relevant user state value, 1-9.
   */
  void clearValue(size_t number) {
    if (number < 1 || number > 9)
      return;
    has_state_bits_[number - 1] = false;
  }
};

/**
 * \brief Represents a group of physical HEBI modules, and allows Command,
 * Feedback, and Info objects to be sent to and received from the hardware.
 */
class Group final {
  friend class util::MobileIO;

private:
  /**
   * C-style group object
   */
  HebiGroupPtr internal_;

  /**
   * The number of modules in this group.
   */
  const int number_of_modules_;

  /**
   * Protects access to the group feedback handler vector.
   */
  std::mutex handler_lock_;

  /**
   * A list of handler functions that are called when the internal C API
   * feedback callback is made.
   */
  std::vector<GroupFeedbackHandler> handlers_;

#ifndef DOXYGEN_OMIT_INTERNAL
  /**
   * Intermediary to convert C-style function callbacks to C++ style, and
   * change callback parameter types.
   */
  friend void callbackWrapper(HebiGroupFeedbackPtr group_feedback, void* user_data);
#endif // DOXYGEN_OMIT_INTERNAL

  /**
   * Calls any attached C++ handler functions.  Should only be called from the
   * internal C thread via the callbackWrapper translation function.
   */
  void callAttachedHandlers(HebiGroupFeedbackPtr group_feedback);

public:
  /**
   * \brief The default timeout for any send-with-acknowledgement or request
   * operation is 500 ms.
   */
  static const int32_t DEFAULT_TIMEOUT_MS = 500;

#ifndef DOXYGEN_OMIT_INTERNAL
  /**
   * Creates a group from the underlying C-style group object. This should
   * only be called to create groups from the lookup class, not from user
   * code!
   */
  Group(HebiGroupPtr group, float initial_feedback_frequency = 0.0f, int32_t initial_command_lifetime = 0);
#endif // DOXYGEN_OMIT_INTERNAL

  /**
   * \brief Destructor cleans up group.
   */
  ~Group() noexcept; /* annotating specified destructor as noexcept is best-practice */

  /**
   * \brief Returns the number of modules in the group
   */
  int size() const;

  /**
   * \brief Sets the command lifetime for the modules in this group.
   *
   * This parameter defines how long a module will execute a command set
   * point sent to it.  Note the commands from other systems/applications
   * are ignored during this time.  A value of '0' indicates commands last
   * forever, and there is no lockout behavior.
   *
   * See docs.hebi.us for more information.
   */
  bool setCommandLifetimeMs(int32_t ms);

  /**
   * \brief Returns the command lifetime for the modules in this group.
   *
   * See setCommandLifetimeMs for more information.
  */
  int32_t getCommandLifetimeMs() const;

  /**
   * \brief Send a command to the given group without requesting an
   * acknowledgement.
   *
   * Appropriate for high-frequency applications.
   *
   * \param group_command The GroupCommand object containing information to be
   * sent to the group.
   *
   * \returns true if the command was successfully sent, false otherwise.
   */
  bool sendCommand(const GroupCommand& group_command);

  /**
   * \brief Send a command to the given group, requesting an acknowledgement
   * of transmission to be sent back.
   *
   * \param group_command The GroupCommand object containing information to be
   * sent to the group.
   * \param timeout_ms Indicates how many milliseconds to wait for a response
   * after sending a packet.  For typical networks, '15' ms is a value that
   * can be reasonably expected to encompass the time required for a
   * round-trip transmission.
   *
   * \returns true if an acknowledgement was successfully received
   * (guaranteeing the group received this command), or a negative number for
   * an error otherwise.
   *
   * Note: A non-true return does not indicate a specific failure, and may
   * result from an error while sending or simply a timeout/dropped response
   * packet after a successful transmission.
   */
  bool sendCommandWithAcknowledgement(const GroupCommand& group_command, int32_t timeout_ms = DEFAULT_TIMEOUT_MS);

  /**
   * \brief Requests feedback from the group.
   *
   * Sends a background request to the modules in the group; if/when all modules
   * return feedback, any associated handler functions are called. This returned
   * feedback is also stored to be returned by the next call to
   * getNextFeedback (any previously returned data is discarded).
   *
   * \returns @c true if feedback was request was successfully sent, otherwise @c false on failure
   * (i.e., connection error).
   */
  bool sendFeedbackRequest();

  /**
   * \brief Returns the most recently stored feedback from a sent feedback
   * request, or returns the next one received (up to the requested timeout).
   *
   * Note that a feedback request can be sent either with the
   * sendFeedbackRequest function, or by setting a background feedback
   * frequency with setFeedbackFrequencyHz.
   *
   * Warning: other data in the provided 'Feedback' object is erased!
   *
   * \param feedback On success, the group feedback read from the group are written
   * into this structure.
   * \param timeout_ms Indicates how many milliseconds to wait for feedback.
   * For typical networks, '15' ms is a value that can be reasonably expected to
   * allow for a round trip transmission after the last 'send feedback request'
   * call.
   *
   * \returns @c true if feedback was returned, otherwise @c false on failure (i.e., connection
   * error or timeout waiting for response).
   */
  bool getNextFeedback(GroupFeedback& feedback, int32_t timeout_ms = DEFAULT_TIMEOUT_MS);

  /**
   * \brief Request info from the group, and store it in the passed-in info
   * object.
   *
   * \returns true if the request was successful within the specified timeout;
   * in this case 'info' has been updated. Otherwise, returns false and does
   * not update 'info'.
   */
  bool requestInfo(GroupInfo& info, int32_t timeout_ms = DEFAULT_TIMEOUT_MS);

  /**
   * \brief Request info from the group, and store it in the passed-in info
   * object. Include extra fields
   *
   * \returns true if the request was successful within the specified timeout;
   * in this case 'info' has been updated. Otherwise, returns false and does
   * not update 'info'.
   */
  bool requestInfoExtra(GroupInfo& info, InfoExtraFields extra_fields, int32_t timeout_ms = DEFAULT_TIMEOUT_MS);

  /**
   * \brief Starts log (stopping any active log).
   *
   * \param dir The relative or absolute path to the directory to log in. To
   * use the current directory, just use an empty string.
   *
   * \returns the path to the log file, otherwise an empty string on failure
   */
  std::string startLog(const std::string& dir) const;

  /**
   * \brief Starts log (stopping any active log).
   *
   * \param dir The relative or absolute path to the directory to log in. To
   * use the current directory, just use an empty string.
   *
   * \param file The name of the file within the directory
   *
   * \returns the path to the log file, otherwise an empty string on failure
   */
  std::string startLog(const std::string& dir, const std::string& file) const;

  /**
   * \brief Stops any active log.
   *
   * \returns shared pointer to the created log file.
   * If the file was not successfully created, this will return null.
   */
  std::shared_ptr<LogFile> stopLog() const;

  /**
   * \brief Adds a message directly to the log file without sending anything to the group.
   * The message can have 9 values that are read later.
   *
   * \return @c true on success, or @c false if something went wrong (e.g., not currently
   * logging)
   */
  bool logUserState(const UserState& state) const;

  /**
   * \brief Sets the frequency of the internal feedback request + callback thread.
   *
   * \returns @c true if the frequency successfully was set, or @c false
   * if the parameter was outside the accepted range (less than zero or faster
   * than supported maximum).
   */
  bool setFeedbackFrequencyHz(float frequency);
  /**
   * \brief Gets the frequency of the internal feedback request + callback thread.
   *
   * \returns The current feedback request loop frequency (in Hz).
   */
  float getFeedbackFrequencyHz() const;
  /**
   * \brief Adds a handler function to be called by the internal feedback request
   * thread.
   */
  void addFeedbackHandler(GroupFeedbackHandler handler);
  /**
   * \brief Removes all feedback handlers presently added.
   */
  void clearFeedbackHandlers();

  /**
   * \brief Creates an imitation group of provided size
   *
   * \param size the number of modules in the group
   *
   * \returns A shared pointer to the created imitation group
   */
  static std::shared_ptr<Group> createImitation(size_t size);

private:
  /**
   * Disable copy and move constructors and assignment operators
   */
  HEBI_DISABLE_COPY_MOVE(Group)
};

} // namespace hebi
