#include "modules/custom_graph.hpp"

#include <spdlog/spdlog.h>

#include <algorithm>
#include <cmath>
#include <stdexcept>
#include <string>

#include "util/scope_guard.hpp"

waybar::modules::CustomGraph::CustomGraph(const std::string& name, const std::string& id,
                                          const Json::Value& config, const std::string& output_name)
    : AGraph(config, "custom-graph-" + name, id),
      name_(name),
      output_name_(output_name),
      id_(id),
      tooltip_format_enabled_{config_["tooltip-format"].isString()},
      percentage_(0),
      fp_(nullptr),
      pid_(-1) {
  if (config.isNull()) {
    spdlog::warn("There is no configuration for 'custom-graph/{}', element will be hidden", name);
  }
  dp.emit();
  if (!config_["signal"].empty() && config_["interval"].empty() &&
      config_["restart-interval"].empty()) {
    waitingWorker();
  } else if (interval_.count() > 0) {
    delayWorker();
  } else if (config_["exec"].isString()) {
    continuousWorker();
  }
}

waybar::modules::CustomGraph::~CustomGraph() {
  if (pid_ != -1) {
    killpg(pid_, SIGTERM);
    waitpid(pid_, NULL, 0);
    pid_ = -1;
  }
}

void waybar::modules::CustomGraph::delayWorker() {
  thread_ = [this] {
    for (int i : this->pid_children_) {
      int status;
      waitpid(i, &status, 0);
    }

    this->pid_children_.clear();

    bool can_update = true;
    if (config_["exec-if"].isString()) {
      output_ = util::command::execNoRead(config_["exec-if"].asString());
      if (output_.exit_code != 0) {
        can_update = false;
        dp.emit();
      }
    }
    if (can_update) {
      if (config_["exec"].isString()) {
        output_ = util::command::exec(config_["exec"].asString(), output_name_);
      }
      dp.emit();
    }
    thread_.sleep_for(interval_);
  };
}

void waybar::modules::CustomGraph::continuousWorker() {
  auto cmd = config_["exec"].asString();
  pid_ = -1;
  fp_ = util::command::open(cmd, pid_, output_name_);
  if (!fp_) {
    throw std::runtime_error("Unable to open " + cmd);
  }
  thread_ = [this, cmd] {
    char* buff = nullptr;
    waybar::util::ScopeGuard buff_deleter([&buff]() {
      if (buff) {
        free(buff);
      }
    });
    size_t len = 0;
    if (getline(&buff, &len, fp_) == -1) {
      int exit_code = 1;
      if (fp_) {
        exit_code = WEXITSTATUS(util::command::close(fp_, pid_));
        fp_ = nullptr;
      }
      if (exit_code != 0) {
        output_ = {exit_code, ""};
        dp.emit();
        spdlog::error("{} stopped unexpectedly, is it endless?", name_);
      }
      if (config_["restart-interval"].isUInt()) {
        pid_ = -1;
        thread_.sleep_for(std::chrono::seconds(config_["restart-interval"].asUInt()));
        fp_ = util::command::open(cmd, pid_, output_name_);
        if (!fp_) {
          throw std::runtime_error("Unable to open " + cmd);
        }
      } else {
        thread_.stop();
        return;
      }
    } else {
      std::string output = buff;

      // Remove last newline
      if (!output.empty() && output[output.length() - 1] == '\n') {
        output.erase(output.length() - 1);
      }
      output_ = {0, output};
      dp.emit();
    }
  };
}

void waybar::modules::CustomGraph::waitingWorker() {
  thread_ = [this] {
    bool can_update = true;
    if (config_["exec-if"].isString()) {
      output_ = util::command::execNoRead(config_["exec-if"].asString());
      if (output_.exit_code != 0) {
        can_update = false;
        dp.emit();
      }
    }
    if (can_update) {
      if (config_["exec"].isString()) {
        output_ = util::command::exec(config_["exec"].asString(), output_name_);
      }
      dp.emit();
    }
    thread_.sleep();
  };
}

void waybar::modules::CustomGraph::refresh(int sig) {
  if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) {
    thread_.wake_up();
  }
}

void waybar::modules::CustomGraph::handleEvent() {
  if (!config_["exec-on-event"].isBool() || config_["exec-on-event"].asBool()) {
    thread_.wake_up();
  }
}

bool waybar::modules::CustomGraph::handleScroll(GdkEventScroll* e) {
  auto ret = AGraph::handleScroll(e);
  handleEvent();
  return ret;
}

bool waybar::modules::CustomGraph::handleToggle(GdkEventButton* const& e) {
  auto ret = AGraph::handleToggle(e);
  handleEvent();
  return ret;
}

auto waybar::modules::CustomGraph::update() -> void {
  // Hide label if output is empty
  if ((config_["exec"].isString() || config_["exec-if"].isString()) &&
      (output_.out.empty() || output_.exit_code != 0)) {
    event_box_.hide();
  } else {
    if (config_["return-type"].asString() == "json") {
      parseOutputJson();
    } else {
      parseOutputRaw();
    }

    try {
      addValue(percentage_);

      if (tooltipEnabled()) {
        if (tooltip_format_enabled_) {
          auto tooltip = config_["tooltip-format"].asString();
          tooltip = fmt::format(fmt::runtime(tooltip), fmt::arg("text", text_),
                                fmt::arg("alt", alt_), fmt::arg("percentage", percentage_));
          graph_.set_tooltip_markup(tooltip);
        } else {
          if (graph_.get_tooltip_markup() != tooltip_) {
            graph_.set_tooltip_markup(tooltip_);
          }
        }
      }
      auto style = graph_.get_style_context();
      auto classes = style->list_classes();
      for (auto const& c : classes) {
        if (c == id_) continue;
        style->remove_class(c);
      }
      for (auto const& c : class_) {
        style->add_class(c);
      }
      style->add_class("flat");
      style->add_class(MODULE_CLASS);
      event_box_.show();
    } catch (const fmt::format_error& e) {
      if (std::strcmp(e.what(), "cannot switch from manual to automatic argument indexing") != 0)
        throw;

      throw fmt::format_error(
          "mixing manual and automatic argument indexing is no longer supported; "
          "try replacing \"{}\" with \"{text}\" in your format specifier");
    }
  }
  // Call parent update
  AGraph::update();
}

void waybar::modules::CustomGraph::parseOutputRaw() {
  std::istringstream output(output_.out);
  std::string line;
  int i = 0;
  while (getline(output, line)) {
    Glib::ustring validated_line = line;
    if (!validated_line.validate()) {
      validated_line = validated_line.make_valid();
    }

    if (i == 0) {
      if (config_["escape"].isBool() && config_["escape"].asBool()) {
        text_ = Glib::Markup::escape_text(validated_line);
        tooltip_ = Glib::Markup::escape_text(validated_line);
      } else {
        text_ = validated_line;
        tooltip_ = validated_line;
      }
      class_.clear();
      // Derive the graph value from the first line's leading numeric value,
      // since the raw (i3blocks) format has no dedicated percentage field.
      std::string value = validated_line;
      if (!value.empty() && value.back() == '%') {
        value.pop_back();
      }
      try {
        double parsed = std::stod(value);
        if (std::isnan(parsed)) {
          percentage_ = 0;
        } else {
          percentage_ = static_cast<int>(std::clamp(std::lround(parsed), 0L, 100L));
        }
      } catch (const std::exception&) {
        percentage_ = 0;
      }
    } else if (i == 1) {
      if (config_["escape"].isBool() && config_["escape"].asBool()) {
        tooltip_ = Glib::Markup::escape_text(validated_line);
      } else {
        tooltip_ = validated_line;
      }
    } else if (i == 2) {
      class_.push_back(validated_line);
    } else {
      break;
    }
    i++;
  }
}

void waybar::modules::CustomGraph::parseOutputJson() {
  std::istringstream output(output_.out);
  std::string line;
  class_.clear();
  while (getline(output, line)) {
    auto parsed = parser_.parse(line);
    if (config_["escape"].isBool() && config_["escape"].asBool()) {
      text_ = Glib::Markup::escape_text(parsed["text"].asString());
    } else {
      text_ = parsed["text"].asString();
    }
    if (config_["escape"].isBool() && config_["escape"].asBool()) {
      alt_ = Glib::Markup::escape_text(parsed["alt"].asString());
    } else {
      alt_ = parsed["alt"].asString();
    }
    if (config_["escape"].isBool() && config_["escape"].asBool()) {
      tooltip_ = Glib::Markup::escape_text(parsed["tooltip"].asString());
    } else {
      tooltip_ = parsed["tooltip"].asString();
    }
    if (parsed["class"].isString()) {
      class_.push_back(parsed["class"].asString());
    } else if (parsed["class"].isArray()) {
      for (auto const& c : parsed["class"]) {
        class_.push_back(c.asString());
      }
    }
    if (!parsed["percentage"].asString().empty() && parsed["percentage"].isNumeric()) {
      percentage_ = (int)lround(parsed["percentage"].asFloat());
    } else {
      percentage_ = 0;
    }
    break;
  }
}
