/*
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1
 * of the License, or (at your option) any later version.
 *
 * This program 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 Lesser General Public License for more details.
 */

#ifndef SH4LT_WRITER_H_
#define SH4LT_WRITER_H_

#include <memory>
#include <string>

#include "./ipcs/sysv-sem.hpp"
#include "./ipcs/sysv-shm.hpp"
#include "./ipcs/unix-socket-protocol.hpp"
#include "./ipcs/unix-socket-server.hpp"
#include "./logger/logger.hpp"
#include "./metadata.hpp"
#include "./shtype/shtype.hpp"
#include "./time.hpp"
#include "./utils/safe-bool-idiom.hpp"

namespace sh4lt {

class OneWriteAccess;
class Writer : public SafeBoolIdiom {
  friend OneWriteAccess;

 public:
  /**
   * Construct a Writer object.
   *
   * \param   data_desr             The ShType description for the frames to be transmitted.
   * \param   memsize               Initial size of the shared memory. Note the shared memory
   *                                can be resized at each frame.
   * \param   log                   Shared pointer holding a Log object where to write internal
   *                                logs.
   * \param   on_client_connect     Callback to be triggered when a follower connects.
   * \param   on_client_disconnect  Callback to be triggered when a follower disconnects.
   * \param   unix_permission       Permission to apply to the internal Unix socket, shared memory
   *                                and semaphore.
   */
  Writer(const ShType& data_descr,
         size_t memsize,
         logger::Logger::ptr log,
         UnixSocketProtocol::ServerSide::onClientConnect on_client_connect = nullptr,
         UnixSocketProtocol::ServerSide::onClientDisconnect on_client_disconnect = nullptr,
         mode_t unix_permission = UnixSocketServer::kDefaultPermission);

  ~Writer();
  Writer(Writer&&) = delete;
  Writer() = delete;
  Writer(const Writer&) = delete;
  auto operator=(const Writer&) -> Writer& = delete;
  auto operator=(Writer&&) -> Writer& = delete;

  /**
   * Get currently allocated size of the shared memory used by the writer.
   *
   */
  [[nodiscard]] auto alloc_size() const -> size_t;

  /**
   * Get current number of followers.
   * \return the number of followers
   */
  [[nodiscard]] inline auto num_of_followers() const -> size_t { return srv_->num_of_clients(); };

  /**
   * Copy a frame of data to the sh4lt.
   *
   * \param data  Pointer to the begining of the frame.
   * \param size  Size of the frame to copy.
   * \param buffer_date Playhead date for the beginning of the buffer, in nanoseconds, or -1.
   * \param buffer_duration Buffer duration, in nanoseconds, or -1.
   * \param speed Buffer date clock speed.
   *
   * \return Success of the copy to the shared memory
   *
   */
  auto copy_to_shm(const void* data,
                   size_t size,
                   int64_t buffer_date,
                   int64_t buffer_duration,
                   float speed = 1.0) -> bool;

  /**
   * Copy a frame of data to the sh4lt, with metadata update. Metadata should be provided only when
   * it has been updated. Note that invocation of copy_to_shm_with_metadata with metadata set to
   * nullptr has the same behaviour as copy_to_shm.
   *
   * \param data  Pointer to the begining of the frame.
   * \param size  Size of the frame to copy.
   * \param metadata Pointer to the metadata
   * \param buffer_date Playhead date for the beginning of the buffer, in nanoseconds, or -1.
   * \param buffer_duration Buffer duration, in nanoseconds, or -1.
   * \param speed Buffer date clock speed.
   *
   * \return Success of the copy to the shared memory
   *
   */
  auto copy_to_shm_with_metadata(const void* data,
                                 size_t size,
                                 const void* metadata,
                                 size_t metadata_size,
                                 int64_t buffer_date,
                                 int64_t buffer_duration,
                                 float speed = 1.0) -> bool;

  /**
   * Provide direct access to the memory with lock. The locked/unlocked state of the shared
   * memory is synchronized with the life of the returned value.
   *
   * \return OneWriteAccess object in a unique pointer. Its destruction release the lock.
   *
   */
  auto get_one_write_access() -> std::unique_ptr<OneWriteAccess>;
  /**
   * Provide lock and resize simultaneously.
   * Same as the get_one_write_access method, but allows to resize the shared memory before
   * the new access
   *
   * \param new_size New size to be allocated.
   *
   * \return OneWriteAccess object in a unique pointer. Its destruction release the lock.
   *
   */
  auto get_one_write_access_resize(size_t new_size) -> std::unique_ptr<OneWriteAccess>;

  /**
   * Provide lock to the memory.
   * Pointer version of get_one_write_access.
   *
   * \return Pointer to the create OneWriteAccess.
   *
   */
  auto get_one_write_access_ptr() -> OneWriteAccess*;

  /**
   * Provide lock and resize simultaneously.
   * Pointer version of get_one_write_access_resize
   *
   * \return Pointer to the create OneWriteAccess.
   *
   */
  auto get_one_write_access_ptr_resize(size_t new_size) -> OneWriteAccess*;

  /**
   * Wait for the the readers to finish reading the last buffer.
   *
   * \param timeout_nsec The maximum time to wait in nanoseconds.
   *
   * \return True if readers has finished before timeout, false otherwise.
   *
   */
  auto wait_readers_for(int64_t timeout_nsec) -> bool;

 private:
  std::string path_{};
  std::string metadata_path_{};
  Metadata metadata_header_{};
  UnixSocketProtocol::onConnectData connect_data_;
  UnixSocketProtocol::ServerSide proto_;
  std::unique_ptr<UnixSocketServer> srv_;
  std::unique_ptr<sysVShm> shm_;
  std::unique_ptr<sysVSem> sem_;
  logger::Logger::ptr log_;
  size_t alloc_size_;
  size_t metadata_size_{0};
  size_t total_size_;  // shm + metadata header + metadata
  Time time_{};
  mode_t perms_;

  bool is_valid_{true};
  [[nodiscard]] auto is_valid() const -> bool final { return is_valid_; }
};

// see check-sh4lt
class OneWriteAccess {
  friend Writer;

 public:
  /**
   * Get the pointer to the sh4lt memory.
   *
   * \return The pointer to the sh4lt memory
   */
  auto get_mem() -> void* { return mem_; };

  /**
   * Get the pointer to the sh4lt metadata memory.
   *
   * \param shm_size the memory size use for the sh4lt. This must be the same value as the size
   * provided at notification.
   *
   * \return The pointer to the sh4lt metadata memory
   */
  auto get_metadata_mem(size_t shm_size) -> void*;

  /**
   * Resize the sh4lt with reallocation of a new uninitialized shared memory space.
   *
   * \note This reinitialize the memory. You should apply writes after resizing.
   *
   * \param newsize Expected new size of the sh4lt memory.
   *
   * \return Allocated size, or 0 if resize failed. The allocated size includes also metadata header
   * and metadata. It is computed as `shm_size + metadata_header_size + metadata_size`;
   */
  auto shm_resize(size_t shm_size) -> size_t;

  /**
   * Resize the sh4lt with reallocation of a new shared memory space for both data and metadata.
   *
   * \note This reinitialize the memory. You should apply writes after resizing.
   *
   * \param newsize Expected new size of the sh4lt memory.
   *
   * \return Allocated size, or 0 if resize failed. The allocated size includes metadata header and
   * metadata. It is computed as `shm_size + metadata_header_size + metadata_size`;

   */
  auto shm_resize_with_metadata(size_t shm_size, size_t metadata_size) -> size_t;

  /**
   * Resize the sh4lt metadata.
   *
   * \note This reinitialize the memory. You should apply writes after resizing.
   *
   * \param metadata_size Expected new size of the sh4lt memory.
   *
   * \return Allocated size, or 0 if resize failed. The allocated size includes metadata header and
   * metadata. It is computed as `shm_size + metadata_header_size + metadata_size`;
   */
  auto shm_metadata_resize(size_t metadata_size) -> size_t;

  /**
   * Include notification of metadata updates to clients. Must be called before `notify_clients`.
   *
   * \param do_notify true if metadata must be notified, false otherwise
   **/
  auto set_metadata_notification(bool do_notify) -> void;

  /**
   * Notify sh4lt clients.
   *
   * \note This method must be called only once.
   *
   * \param size Size of the frame to be available for the clients.
   * \param buffer_date Playhead date for the beginning of the buffer, in nanoseconds, or -1.
   * \param buffer_duration Buffer duration, in nanoseconds, or -1.
   * \param speed buffer_date clock speed.
   *
   * \return Number of notified clients.
   *
   */
  auto notify_clients(size_t size,
                      int64_t buffer_date,
                      int64_t buffer_duration,
                      float speed = 1.0) -> short;

  ~OneWriteAccess() = default;
  OneWriteAccess() = delete;
  OneWriteAccess(OneWriteAccess&&) = delete;
  OneWriteAccess(const OneWriteAccess&) = delete;
  auto operator=(const OneWriteAccess&) -> OneWriteAccess& = delete;
  auto operator=(OneWriteAccess&&) -> OneWriteAccess& = default;

 private:
  OneWriteAccess(
      Writer* writer, sysVSem* sem, void* mem, UnixSocketServer* srv, logger::Logger::ptr log);
  Writer* writer_;
  WriteLock wlock_;
  void* mem_;
  UnixSocketServer* srv_;
  logger::Logger::ptr log_;
  bool has_notified_{false};
  bool notify_metadata_{false};
};

}  // namespace sh4lt
#endif
