#include "components/eventloop.hpp"
#include "components/types.hpp"
#include "utils/string.hpp"
#include "utils/time.hpp"
#include "x11/color.hpp"

POLYBAR_NS

/**
 * Deconstruct eventloop
 */
eventloop::~eventloop() noexcept {
  for (auto&& block : m_modules) {
    for (auto&& module : block.second) {
      auto module_name = module->name();
      auto cleanup_ms = time_util::measure([&module] {
        module->stop();
        module.reset();
      });
      m_log.trace("eventloop: Deconstruction of %s took %lu microsec.", module_name, cleanup_ms);
    }
  }
}

/**
 * Enqueue event
 */
bool eventloop::enqueue(const entry_t& i) {
  bool enqueued;

  if (!(enqueued = m_queue.enqueue(i))) {
    m_log.warn("Failed to queue event (%d)", i.type);
  }

  return enqueued;
}

/**
 * Start module threads and wait for events on the queue
 *
 * @param timeframe Time to wait for subsequent events
 * @param limit Maximum amount of subsequent events to swallow within timeframe
 */
void eventloop::run(std::chrono::duration<double, std::milli> timeframe, int limit) {
  m_log.info("Starting event loop", timeframe.count(), limit);
  m_running = true;

  m_log.trace("eventloop: timeframe: %d, limit: %d", timeframe.count(), limit);

  start_modules();

  while (m_running) {
    entry_t evt, next{static_cast<int>(event_type::NONE)};
    m_queue.wait_dequeue(evt);

    if (!m_running) {
      break;
    }

    if (match_event(evt, event_type::UPDATE)) {
      int swallowed = 0;
      while (swallowed++ < limit && m_queue.wait_dequeue_timed(next, timeframe)) {
        if (match_event(next, event_type::QUIT)) {
          evt = next;
          break;
        } else if (compare_events(evt, next)) {
          m_log.trace_x("eventloop: Swallowing event within timeframe");
          evt = next;
        } else {
          break;
        }
      }
    }

    forward_event(evt);

    if (match_event(next, event_type::NONE)) {
      continue;
    }
    if (compare_events(evt, next)) {
      continue;
    }

    forward_event(next);
  }

  m_log.trace("eventloop: Loop ended");
}

/**
 * Stop main loop by enqueuing a QUIT event
 */
void eventloop::stop() {
  m_log.info("Stopping event loop");
  m_running = false;
  enqueue({static_cast<int>(event_type::QUIT)});
}

/**
 * Set callback handler for UPDATE events
 */
void eventloop::set_update_cb(callback<>&& cb) {
  m_update_cb = forward<decltype(cb)>(cb);
}

/**
 * Set callback handler for raw INPUT events
 */
void eventloop::set_input_db(callback<string>&& cb) {
  m_unrecognized_input_cb = forward<decltype(cb)>(cb);
}

/**
 * Add module to alignment block
 */
void eventloop::add_module(const alignment pos, module_t&& module) {
  auto it = m_modules.lower_bound(pos);

  if (it != m_modules.end() && !(m_modules.key_comp()(pos, it->first))) {
    it->second.emplace_back(forward<module_t>(module));
  } else {
    vector<module_t> vec;
    vec.emplace_back(forward<module_t>(module));
    m_modules.insert(it, modulemap_t::value_type(pos, move(vec)));
  }
}

/**
 * Get reference to module map
 */
modulemap_t& eventloop::modules() {
  return m_modules;
}

/**
 * Start module threads
 */
void eventloop::start_modules() {
  for (auto&& block : m_modules) {
    for (auto&& module : block.second) {
      try {
        m_log.info("Starting %s", module->name());
        module->start();
      } catch (const application_error& err) {
        m_log.err("Failed to start '%s' (reason: %s)", module->name(), err.what());
      }
    }
  }
}

/**
 * Test if event matches given type
 */
bool eventloop::match_event(entry_t evt, event_type type) {
  return static_cast<int>(type) == evt.type;
}

/**
 * Compare given events
 */
bool eventloop::compare_events(entry_t evt, entry_t evt2) {
  return evt.type == evt2.type;
}

/**
 * Forward event to handler based on type
 */
void eventloop::forward_event(entry_t evt) {
  if (evt.type == static_cast<int>(event_type::UPDATE)) {
    on_update();
  } else if (evt.type == static_cast<int>(event_type::INPUT)) {
    on_input(string{evt.data});
  } else if (evt.type == static_cast<int>(event_type::CHECK)) {
    on_check();
  } else if (evt.type == static_cast<int>(event_type::QUIT)) {
    on_quit();
  } else {
    m_log.warn("Unknown event type for enqueued event (%d)", evt.type);
  }
}

/**
 * Handler for enqueued UPDATE events
 */
void eventloop::on_update() {
  m_log.trace("eventloop: Received UPDATE event");

  if (m_update_cb) {
    m_update_cb();
  } else {
    m_log.warn("No callback to handle update");
  }
}

/**
 * Handler for enqueued INPUT events
 */
void eventloop::on_input(char* input) {
  m_log.trace("eventloop: Received INPUT event");

  if (!m_modulelock.try_lock()) {
    return;
  }

  std::lock_guard<std::mutex> guard(m_modulelock, std::adopt_lock);

  for (auto&& block : m_modules) {
    for (auto&& module : block.second) {
      if (!module->receive_events()) {
        continue;
      }
      if (module->handle_event(input)) {
        return;
      }
    }
  }

  if (m_unrecognized_input_cb) {
    m_unrecognized_input_cb(input);
  } else {
    m_log.warn("No callback to handle unrecognized input");
  }
}

/**
 * Handler for enqueued CHECK events
 */
void eventloop::on_check() {
  if (!m_running) {
    return;
  }

  for (auto&& block : m_modules) {
    for (auto&& module : block.second) {
      if (module->running()) {
        return;
      }
    }
  }

  m_log.warn("No running modules...");
  stop();
}

/**
 * Handler for enqueued QUIT events
 */
void eventloop::on_quit() {
  m_log.trace("eventloop: Received QUIT event");
  m_running = false;
}

POLYBAR_NS_END