#include "modules/sni/host.hpp"

#include <spdlog/spdlog.h>

#include "util/scope_guard.hpp"

namespace waybar::modules::SNI {

static const unsigned RETRY_DELAY_MS = 200;
static const unsigned MAX_RETRIES = 10;

Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar,
           const std::function<void(std::unique_ptr<Item>&)>& on_add,
           const std::function<void(std::unique_ptr<Item>&)>& on_remove,
           const std::function<void()>& on_update)
    : bus_name_("org.kde.StatusNotifierHost-" + std::to_string(getpid()) + "-" +
                std::to_string(id)),
      object_path_("/StatusNotifierHost/" + std::to_string(id)),
      bus_name_id_(Gio::DBus::own_name(Gio::DBus::BusType::BUS_TYPE_SESSION, bus_name_,
                                       sigc::mem_fun(*this, &Host::busAcquired))),
      config_(config),
      bar_(bar),
      on_add_(on_add),
      on_remove_(on_remove),
      on_update_(on_update) {}

Host::~Host() {
  retry_connection_.disconnect();
  if (bus_name_id_ > 0) {
    Gio::DBus::unown_name(bus_name_id_);
    bus_name_id_ = 0;
  }
  if (watcher_id_ > 0) {
    Gio::DBus::unwatch_name(watcher_id_);
    watcher_id_ = 0;
  }
  g_cancellable_cancel(cancellable_);
  g_clear_object(&cancellable_);
  g_clear_object(&watcher_);
}

void Host::busAcquired(const Glib::RefPtr<Gio::DBus::Connection>& conn, Glib::ustring name) {
  watcher_id_ = Gio::DBus::watch_name(conn, "org.kde.StatusNotifierWatcher",
                                      sigc::mem_fun(*this, &Host::nameAppeared),
                                      sigc::mem_fun(*this, &Host::nameVanished));
}

void Host::nameAppeared(const Glib::RefPtr<Gio::DBus::Connection>& conn, const Glib::ustring name,
                        const Glib::ustring& name_owner) {
  if (cancellable_ != nullptr) {
    // TODO
    return;
  }
  cancellable_ = g_cancellable_new();
  sn_watcher_proxy_new(conn->gobj(), G_DBUS_PROXY_FLAGS_NONE, "org.kde.StatusNotifierWatcher",
                       "/StatusNotifierWatcher", cancellable_, &Host::proxyReady, this);
}

void Host::nameVanished(const Glib::RefPtr<Gio::DBus::Connection>& conn, const Glib::ustring name) {
  retry_connection_.disconnect();
  retry_count_ = 0;
  g_cancellable_cancel(cancellable_);
  g_clear_object(&cancellable_);
  g_clear_object(&watcher_);
  clearItems();
}

void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) {
  GError* error = nullptr;
  waybar::util::ScopeGuard error_deleter([&error]() {
    if (error != nullptr) {
      g_error_free(error);
    }
  });
  SnWatcher* watcher = sn_watcher_proxy_new_finish(res, &error);
  if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
    spdlog::error("Host: {}", error->message);
    return;
  }
  auto host = static_cast<SNI::Host*>(data);
  if (error != nullptr) {
    spdlog::error("Host: {}", error->message);
    g_clear_object(&host->cancellable_);
    if (host->retry_count_ >= MAX_RETRIES) {
      spdlog::warn("Host: giving up on watcher proxy creation after {} retries", host->retry_count_);
      return;
    }
    host->retry_count_ += 1;
    // Store the timeout connection so it is disconnected in ~Host, avoiding a
    // use-after-free if the Host is destroyed before the retry fires.
    host->retry_connection_ = Glib::signal_timeout().connect(
        [host]() {
          if (host->watcher_ != nullptr) {
            return false;
          }
          auto conn = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::BUS_TYPE_SESSION);
          host->nameAppeared(conn, "org.kde.StatusNotifierWatcher", "");
          return false;
        },
        RETRY_DELAY_MS);
    return;
  }
  host->retry_count_ = 0;
  host->watcher_ = watcher;
  sn_watcher_call_register_host(host->watcher_, host->object_path_.c_str(), host->cancellable_,
                                &Host::registerHost, data);
}

void Host::registerHost(GObject* src, GAsyncResult* res, gpointer data) {
  GError* error = nullptr;
  waybar::util::ScopeGuard error_deleter([&error]() {
    if (error != nullptr) {
      g_error_free(error);
    }
  });
  sn_watcher_call_register_host_finish(SN_WATCHER(src), res, &error);
  if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
    spdlog::error("Host: {}", error->message);
    return;
  }
  auto host = static_cast<SNI::Host*>(data);
  if (error != nullptr) {
    spdlog::error("Host: {}", error->message);
    return;
  }
  g_signal_connect(host->watcher_, "item-registered", G_CALLBACK(&Host::itemRegistered), data);
  g_signal_connect(host->watcher_, "item-unregistered", G_CALLBACK(&Host::itemUnregistered), data);
  auto items = sn_watcher_dup_registered_items(host->watcher_);
  if (items != nullptr) {
    for (uint32_t i = 0; items[i] != nullptr; i += 1) {
      host->addRegisteredItem(items[i]);
    }
  }
  g_strfreev(items);
}

void Host::itemRegistered(SnWatcher* watcher, const gchar* service, gpointer data) {
  auto host = static_cast<SNI::Host*>(data);
  host->addRegisteredItem(service);
}

void Host::itemUnregistered(SnWatcher* watcher, const gchar* service, gpointer data) {
  auto host = static_cast<SNI::Host*>(data);
  auto [bus_name, object_path] = host->getBusNameAndObjectPath(service);
  for (auto it = host->items_.begin(); it != host->items_.end(); ++it) {
    if ((*it)->bus_name == bus_name && (*it)->object_path == object_path) {
      host->removeItem(it);
      break;
    }
  }
}

void Host::itemReady(Item& item) {
  auto it = std::find_if(items_.begin(), items_.end(),
                         [&item](const auto& candidate) { return candidate.get() == &item; });
  if (it != items_.end() && (*it)->isReady()) {
    on_add_(*it);
  }
}

void Host::itemInvalidated(Item& item) {
  auto it = std::find_if(items_.begin(), items_.end(),
                         [&item](const auto& candidate) { return candidate.get() == &item; });
  if (it != items_.end()) {
    removeItem(it);
  }
}

void Host::removeItem(std::vector<std::unique_ptr<Item>>::iterator it) {
  if ((*it)->isReady()) {
    on_remove_(*it);
  }
  items_.erase(it);
}

void Host::clearItems() {
  bool removed_ready_item = false;
  for (auto& item : items_) {
    if (item->isReady()) {
      on_remove_(item);
      removed_ready_item = true;
    }
  }
  bool had_items = !items_.empty();
  items_.clear();
  if (had_items && !removed_ready_item) {
    on_update_();
  }
}

std::tuple<std::string, std::string> Host::getBusNameAndObjectPath(const std::string service) {
  auto it = service.find('/');
  if (it != std::string::npos) {
    return {service.substr(0, it), service.substr(it)};
  }
  return {service, "/StatusNotifierItem"};
}

void Host::addRegisteredItem(const std::string& service) {
  std::string bus_name, object_path;
  std::tie(bus_name, object_path) = getBusNameAndObjectPath(service);
  auto it = std::find_if(items_.begin(), items_.end(), [&bus_name, &object_path](const auto& item) {
    return bus_name == item->bus_name && object_path == item->object_path;
  });
  if (it == items_.end()) {
    items_.emplace_back(std::make_unique<Item>(
      bus_name, object_path, config_, bar_,
      [this](Item& item) { itemReady(item); },
      [this](Item& item) { itemInvalidated(item); },
      on_update_));
  }
}

}  // namespace waybar::modules::SNI
