/*
 * 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.
 */

#include "./writer.hpp"

#include <cstring>     // memcpy
#include <functional>  // std::hash

#include "./ipcs/reader.hpp"

namespace sh4lt {

Writer::Writer(const ShType& data_descr,
               size_t memsize,
               logger::Logger::ptr log,
               UnixSocketProtocol::ServerSide::onClientConnect on_client_connect,
               UnixSocketProtocol::ServerSide::onClientDisconnect on_client_disconnect,
               mode_t unix_permission)
    : path_(ShType::get_path(data_descr.label(), data_descr.group())),
      connect_data_(memsize, ShType::serialize(data_descr)),
      proto_(on_client_connect, on_client_disconnect, [this]() { return connect_data_; }),
      srv_(std::make_unique<UnixSocketServer>(
          path_,
          &proto_,
          log.get(),
          [&](int) { sem_->cancel_commited_reader(); },
          unix_permission)),
      shm_(std::make_unique<sysVShm>(ftok(path_.c_str(), 'n'),
                                     memsize + sizeof(Metadata),
                                     log.get(),
                                     /*owner = */ true,
                                     unix_permission)),
      sem_(std::make_unique<sysVSem>(
          ftok(path_.c_str(), 'm'), log.get(), /*owner = */ true, unix_permission)),
      log_(log),
      alloc_size_(memsize),
      total_size_(alloc_size_ + sizeof(Metadata)),
      perms_(unix_permission) {
  if (!data_descr) {
    log_->error("Shtype is not valid");
    for (const std::string& it : data_descr.msgs()) {
      log_->error(it.c_str());
    }
    is_valid_ = false;
    return;
  }
  if (path_.empty()) {
    log_->error(ShType::get_log_for_path_issue().c_str());
    return;
  }
  if (!(*srv_.get()) || !(*shm_.get()) || !(*sem_.get())) {
    sem_.reset();
    shm_.reset();
    srv_.reset();

    // checking if a server is responding with at this sh4lt path
    bool can_read = false;
    {
      log_->debug("checking if a sh4lt having the same path is already active");
      Reader inspector(path_, nullptr, nullptr, nullptr, log_.get());
      can_read = static_cast<bool>(inspector);
    }
    if (!can_read) {
      log_->debug("writer detected a dead Sh4lt, will clean and retry");
      force_sockserv_cleaning(path_, log.get());
      srv_ = std::make_unique<UnixSocketServer>(
          path_, &proto_, log.get(), [&](int) { sem_->cancel_commited_reader(); }, unix_permission);
      force_shm_cleaning(ftok(path_.c_str(), 'n'), log.get());
      shm_ = std::make_unique<sysVShm>(ftok(path_.c_str(), 'n'),
                                       memsize + sizeof(metadata_header_),
                                       log.get(),
                                       /* owner = */ true,
                                       unix_permission);
      force_semaphore_cleaning(ftok(path_.c_str(), 'm'), log.get());
      sem_ = std::make_unique<sysVSem>(
          ftok(path_.c_str(), 'm'), log.get(), /* owner = */ true, unix_permission);
      is_valid_ = (*srv_.get()) && (*shm_.get()) && (*sem_.get());
    } else {
      log_->error("an other writer is using the same path");
      is_valid_ = false;
    }
  }
  if (!is_valid_) {
    log_->warning("writer failled initialization");
    return;
  }
  // init metadata header
  auto* header = reinterpret_cast<Metadata*>(static_cast<char*>(shm_->get_mem()) + memsize);
  header->is_dirty = false;
  header->size = metadata_size_;
  // start serving
  srv_->start_serving();
  log_->debug("writer initialized");
}

Writer::~Writer() { srv_.reset(); }

bool Writer::copy_to_shm(
    const void* data, size_t size, int64_t buffer_date, int64_t buffer_duration, float speed) {
  return copy_to_shm_with_metadata(data, size, nullptr, 0, buffer_date, buffer_duration, speed);
}

bool Writer::copy_to_shm_with_metadata(const void* data,
                                       size_t shm_size,
                                       const void* metadata,
                                       size_t metadata_size,
                                       int64_t buffer_date,
                                       int64_t buffer_duration,
                                       float speed) {
  auto time = time_.update(buffer_date, buffer_duration, speed);
  auto total_size =
      shm_size + sizeof(Metadata) + (nullptr == metadata ? metadata_size_ : metadata_size);
  bool res = true;
  if (nullptr == sem_) {
    log_->warning("semaphore is not initialized");
    return false;
  }

  if (!(*sem_)) {
    log_->warning("semaphore was not correctly initialized");
    return false;
  }
  {
    WriteLock wlock(sem_.get());
    // resize shm if needed
    if (total_size > total_size_) {
      log_->debug("resizing sh4lt (%) from % bytes to % bytes",
                  path_,
                  std::to_string(total_size_),
                  std::to_string(total_size),
                  std::to_string(shm_size));
      shm_.reset();
      shm_ = std::make_unique<sysVShm>(ftok(path_.c_str(), 'n'),
                                       total_size,
                                       log_.get(),
                                       /*owner = */ true,
                                       perms_);
      // reinit metadata memory, old metadata gets lost here
      auto* header = reinterpret_cast<Metadata*>(static_cast<char*>(shm_->get_mem()) + shm_size);
      header->is_dirty = false;
      header->size = metadata_size_;
      if (!shm_) {
        log_->error("resizing shared memory failed");
        return false;
      }
      connect_data_.shm_size_ = shm_size;
      alloc_size_ = shm_size;
      total_size_ = total_size;
    }
    // lock readers
    auto num_readers = srv_->notify_update(shm_size, &time, metadata ? true : false);
    if (0 < num_readers) {
      wlock.commit_readers(num_readers);
    }
    // copy to shared memory
    auto dest = shm_->get_mem();
    if (dest != std::memcpy(dest, data, shm_size)) {
      log_->warning("issue with copy of data to shared memory");
      res = false;
    }
    // handle metadata
    Metadata* header = reinterpret_cast<Metadata*>(static_cast<char*>(dest) + shm_size);
    if (nullptr != metadata) {
      header->size = metadata_size;
      header->is_dirty = true;
      metadata_size_ = metadata_size;
      auto* mtdest = static_cast<char*>(dest) + shm_size + sizeof(Metadata);
      if (mtdest != std::memcpy(mtdest, metadata, metadata_size)) {
        log_->warning("issue with copy of metadata to shared memory");
        res = false;
      }
    } else {
      header->is_dirty = false;
    }
  }  // release wlock & lock
  return res;
}

auto Writer::get_one_write_access() -> std::unique_ptr<OneWriteAccess> {
  return std::unique_ptr<OneWriteAccess>(
      new OneWriteAccess(this, sem_.get(), shm_->get_mem(), srv_.get(), log_));
}

auto Writer::get_one_write_access_ptr() -> OneWriteAccess* {
  return new OneWriteAccess(this, sem_.get(), shm_->get_mem(), srv_.get(), log_);
}

auto Writer::get_one_write_access_resize(size_t new_size) -> std::unique_ptr<OneWriteAccess> {
  auto res = std::unique_ptr<OneWriteAccess>(
      new OneWriteAccess(this, sem_.get(), shm_->get_mem(), srv_.get(), log_));
  if (new_size == alloc_size_) return res;
  if (0 == res->shm_resize(new_size)) {
    res.reset();
    return {};
  }
  return res;
}

auto Writer::get_one_write_access_ptr_resize(size_t new_size) -> OneWriteAccess* {
  auto res = new OneWriteAccess(this, sem_.get(), shm_->get_mem(), srv_.get(), log_);
  if (new_size == alloc_size_) return res;
  if (0 == res->shm_resize(new_size)) {
    delete res;
    return {};
  }
  return res;
}

auto Writer::alloc_size() const -> size_t { return alloc_size_; }

bool Writer::wait_readers_for(int64_t timeout_nsec) {
  timespec delay;
  delay.tv_sec = timeout_nsec / 1000000000;
  delay.tv_nsec = timeout_nsec % 1000000000;
  return static_cast<bool>(TimedWriteLock(sem_.get(), &delay));
}

OneWriteAccess::OneWriteAccess(
    Writer* writer, sysVSem* sem, void* mem, UnixSocketServer* srv, logger::Logger::ptr log)
    : writer_(writer), wlock_(sem), mem_(mem), srv_(srv), log_(log) {}

auto OneWriteAccess::shm_resize_with_metadata(size_t shm_size, size_t metadata_size) -> size_t {
  auto total_size = shm_size + sizeof(Metadata) + metadata_size;
  writer_->shm_.reset();
  writer_->shm_ = std::make_unique<sysVShm>(ftok(writer_->path_.c_str(), 'n'),
                                            total_size,
                                            log_.get(),
                                            /*owner = */ true,
                                            writer_->perms_);
  if (!writer_->shm_) return 0;
  mem_ = writer_->shm_->get_mem();
  writer_->connect_data_.shm_size_ = shm_size;
  writer_->metadata_size_ = metadata_size;
  writer_->alloc_size_ = shm_size;
  writer_->total_size_ = total_size;
  auto* header = reinterpret_cast<Metadata*>(static_cast<char*>(mem_) + shm_size);
  header->is_dirty = false;
  header->size = metadata_size;
  return writer_->total_size_;
}

auto OneWriteAccess::shm_resize(size_t shm_size) -> size_t {
  return shm_resize_with_metadata(shm_size, writer_->metadata_size_);
}

auto OneWriteAccess::shm_metadata_resize(size_t metadata_size) -> size_t {
  return shm_resize_with_metadata(writer_->alloc_size_, metadata_size);
}

auto OneWriteAccess::set_metadata_notification(bool do_notify) -> void {
  notify_metadata_ = do_notify;
}

auto OneWriteAccess::get_metadata_mem(size_t size) -> void* {
  return Metadata::get_mem(mem_, size);
}

auto OneWriteAccess::notify_clients(size_t size,
                                    int64_t buffer_date,
                                    int64_t buffer_duration,
                                    float speed) -> short {
  if (has_notified_) {
    log_->warning(
        "one notification only is expected per OneWriteAccess instance, "
        "ignoring current invocation");
    return 0;
  }
  has_notified_ = true;
  // update metadata header if needed
  auto* header = reinterpret_cast<Metadata*>(static_cast<char*>(mem_) + writer_->alloc_size_);
  if (notify_metadata_) {
    header->is_dirty = true;
    header->size = writer_->metadata_size_;
  } else {
    header->is_dirty = false;
  }
  // update time info
  auto time_info = writer_->time_.update(buffer_date, buffer_duration, speed);
  // inform readers
  short num_readers = srv_->notify_update(size, &time_info, notify_metadata_);
  if (0 < num_readers) {
    wlock_.commit_readers(num_readers);
  }
  return num_readers;
}

}  // namespace sh4lt
