#include <fcntl.h>
#include <spdlog/spdlog.h>
#include <sys/types.h>
#include <sys/wait.h>

#include <csignal>
#include <list>
#include <mutex>

#include "bar.hpp"
#include "client.hpp"
#include "util/SafeSignal.hpp"

std::mutex reap_mtx;
std::list<pid_t> reap;

static int signal_pipe_write_fd;

// Write a single signal to `signal_pipe_write_fd`.
// This function is set as a signal handler, so it must be async-signal-safe.
static void writeSignalToPipe(int signum) {
  ssize_t amt = write(signal_pipe_write_fd, &signum, sizeof(int));

  // There's not much we can safely do inside of a signal handler.
  // Let's just ignore any errors.
  (void)amt;
}

// This initializes `signal_pipe_write_fd`, and sets up signal handlers.
//
// This function will run forever, emitting every `SIGUSR1`, `SIGUSR2`,
// `SIGINT`, `SIGCHLD`, and `SIGRTMIN + 1`...`SIGRTMAX` signal received
// to `signal_handler`.
static void catchSignals(waybar::SafeSignal<int>& signal_handler) {
  int fd[2];
  if (pipe(fd) != 0) {
    spdlog::error("Failed to create signal pipe: {}", strerror(errno));
    return;
  }

  int signal_pipe_read_fd = fd[0];
  signal_pipe_write_fd = fd[1];

  // This pipe should be able to buffer ~thousands of signals. If it fills up,
  // we'll drop signals instead of blocking.

  // We can't allow the write end to block because we'll be writing to it in a
  // signal handler, which could interrupt the loop that's reading from it and
  // deadlock.

  fcntl(signal_pipe_write_fd, F_SETFL, O_NONBLOCK);

  std::signal(SIGUSR1, writeSignalToPipe);
  std::signal(SIGUSR2, writeSignalToPipe);
  std::signal(SIGINT, writeSignalToPipe);
  std::signal(SIGCHLD, writeSignalToPipe);

  for (int sig = SIGRTMIN + 1; sig <= SIGRTMAX; ++sig) {
    std::signal(sig, writeSignalToPipe);
  }

  while (true) {
    int signum;
    ssize_t amt = read(signal_pipe_read_fd, &signum, sizeof(int));
    if (amt < 0) {
      spdlog::error("read from signal pipe failed with error {}, closing thread", strerror(errno));
      break;
    }

    if (amt != sizeof(int)) {
      continue;
    }

    signal_handler.emit(signum);
  }
}

waybar::util::KillSignalAction getActionForBar(waybar::Bar* bar, int signal) {
  switch (signal) {
    case SIGUSR1:
      return bar->getOnSigusr1Action();
    case SIGUSR2:
      return bar->getOnSigusr2Action();
    default:
      return waybar::util::KillSignalAction::NOOP;
  }
}

void handleUserSignal(int signal, bool& reload) {
  int i = 0;
  for (auto& bar : waybar::Client::inst()->bars) {
    switch (getActionForBar(bar.get(), signal)) {
      case waybar::util::KillSignalAction::HIDE:
        spdlog::debug("Visibility 'hide' for bar ", i);
        bar->hide();
        break;
      case waybar::util::KillSignalAction::SHOW:
        spdlog::debug("Visibility 'show' for bar ", i);
        bar->show();
        break;
      case waybar::util::KillSignalAction::TOGGLE:
        spdlog::debug("Visibility 'toggle' for bar ", i);
        bar->toggle();
        break;
      case waybar::util::KillSignalAction::RELOAD:
        spdlog::info("Reloading...");
        reload = true;
        waybar::Client::inst()->reset();
        return;
      case waybar::util::KillSignalAction::NOOP:
        break;
    }
    i++;
  }
}

// Must be called on the main thread.
//
// If this signal should restart or close the bar, this function will write
// `true` or `false`, respectively, into `reload`.
static void handleSignalMainThread(int signum, bool& reload) {
  if (signum >= SIGRTMIN + 1 && signum <= SIGRTMAX) {
    for (auto& bar : waybar::Client::inst()->bars) {
      bar->handleSignal(signum);
    }
    return;
  }

  switch (signum) {
    case SIGUSR1:
      handleUserSignal(SIGUSR1, reload);
      break;
    case SIGUSR2:
      handleUserSignal(SIGUSR2, reload);
      break;
    case SIGINT:
      spdlog::info("Quitting.");
      reload = false;
      waybar::Client::inst()->reset();
      break;
    case SIGCHLD:
      spdlog::debug("Received SIGCHLD in signalThread");
      {
        std::lock_guard<std::mutex> lock(reap_mtx);
        for (auto it = reap.begin(); it != reap.end();) {
          if (waitpid(*it, nullptr, WNOHANG) == *it) {
            spdlog::debug("Reaped child with PID: {}", *it);
            it = reap.erase(it);
          } else {
            ++it;
          }
        }
      }
      break;
    default:
      spdlog::debug("Received signal with number {}, but not handling", signum);
      break;
  }
}

int main(int argc, char* argv[]) {
  try {
    auto* client = waybar::Client::inst();

    bool reload;

    waybar::SafeSignal<int> posix_signal_received;
    posix_signal_received.connect([&](int signum) { handleSignalMainThread(signum, reload); });

    std::thread signal_thread([&]() { catchSignals(posix_signal_received); });

    // Every `std::thread` must be joined or detached.
    // This thread should run forever, so detach it.
    signal_thread.detach();

    auto ret = 0;
    do {
      reload = false;
      ret = client->main(argc, argv);
    } while (reload);

    std::signal(SIGUSR1, SIG_IGN);
    std::signal(SIGUSR2, SIG_IGN);
    std::signal(SIGINT, SIG_IGN);

    delete client;
    return ret;
  } catch (const std::exception& e) {
    spdlog::error("{}", e.what());
    return 1;
  } catch (const Glib::Exception& e) {
    spdlog::error("{}", static_cast<std::string>(e.what()));
    return 1;
  }
}
