#include "config.h"

#include <memory>

#include <torrent/download/resource_manager.h>
#include <torrent/download/choke_group.h>
#include <torrent/download/choke_queue.h>
#include <torrent/utils/log.h>
#include <torrent/utils/option_strings.h>

#include "ui/root.h"
#include "rpc/parse.h"
#include "rpc/parse_commands.h"

#include "globals.h"
#include "control.h"
#include "command_helpers.h"

// For cg_d_group.
#include "core/download.h"

#define LT_LOG_SUBSYSTEM(log_fmt, ...)                                  \
  lt_log_print_subsystem(torrent::LOG_TORRENT_INFO, "choke_queue", log_fmt, __VA_ARGS__);

// A hack to allow testing of the new choke_group API without the
// working parts present.
#define USE_CHOKE_GROUP 0

#if USE_CHOKE_GROUP

int64_t
cg_get_index(const torrent::Object& raw_args) {
  const torrent::Object& arg = (raw_args.is_list() && !raw_args.as_list().empty()) ? raw_args.as_list().front() : raw_args;

  int64_t index = 0;

  if (arg.is_string()) {
    if (!rpc::parse_whole_value_nothrow(arg.as_string().c_str(), &index))
      return torrent::resource_manager()->group_index_of(arg.as_string());

  } else {
    index = arg.as_value();
  }

  if (index < 0)
    index = (int64_t)torrent::resource_manager()->group_size() + index;

  return std::min<uint64_t>(index, torrent::resource_manager()->group_size());
}

torrent::choke_group*
cg_get_group(const torrent::Object& raw_args) {
  return torrent::resource_manager()->group_at(cg_get_index(raw_args));
}

int64_t
cg_d_group(core::Download* download) {
  return torrent::resource_manager()->entry_at(download->main()).group();
}

const std::string&
cg_d_group_name(core::Download* download) {
  return torrent::resource_manager()->group_at(torrent::resource_manager()->entry_at(download->main()).group())->name();
}

void
cg_d_group_set(core::Download* download, const torrent::Object& arg) {
  torrent::resource_manager()->set_group(torrent::resource_manager()->find_throw(download->main()), cg_get_index(arg));
}

torrent::Object
apply_cg_list() {
  torrent::Object::list_type result;

  for (torrent::ResourceManager::group_iterator
         itr = torrent::resource_manager()->group_begin(),
         last = torrent::resource_manager()->group_end(); itr != last; itr++)
    result.push_back((*itr)->name());

  return torrent::Object::from_list(result);
}

torrent::Object
apply_cg_insert(const std::string& arg) {
  int64_t dummy;

  if (rpc::parse_whole_value_nothrow(arg.c_str(), &dummy))
    throw torrent::input_error("Cannot use a value string as choke group name.");

  torrent::resource_manager()->push_group(arg);

  return torrent::Object();
}

torrent::Object
apply_cg_all_update_balance(bool is_up) {
  LT_LOG_SUBSYSTEM("apply update balance: resource_manager is_up:%i", (int)is_up);

  for (torrent::ResourceManager::group_iterator
         itr = torrent::resource_manager()->group_begin(),
         last = torrent::resource_manager()->group_end(); itr != last; itr++) {
    if (is_up)
      itr->up_queue()->balance();
    else
      itr->down_queue()->balance();
  }

  return torrent::Object();
}

//
// The hacked version:
//
#else

std::vector<std::unique_ptr<torrent::choke_group>> cg_list_hack;

int64_t
cg_get_index(const torrent::Object& raw_args) {
  const torrent::Object& arg = (raw_args.is_list() && !raw_args.as_list().empty()) ? raw_args.as_list().front() : raw_args;

  int64_t index = 0;

  if (arg.is_string()) {
    if (!rpc::parse_whole_value_nothrow(arg.as_string().c_str(), &index)) {
      auto itr = std::find_if(cg_list_hack.begin(), cg_list_hack.end(), [&arg](const auto& cg) { return arg.as_string() == cg->name(); });

      if (itr == cg_list_hack.end())
        throw torrent::input_error("Choke group not found.");

      return std::distance(cg_list_hack.begin(), itr);
    }

  } else {
    index = arg.as_value();
  }

  if (index < 0)
    index = (int64_t)cg_list_hack.size() + index;

  if ((size_t)index >= cg_list_hack.size())
    throw torrent::input_error("Choke group not found.");

  return index;
}

torrent::choke_group*
cg_get_group(const torrent::Object& raw_args) {
  int64_t index = cg_get_index(raw_args);

  if ((size_t)index >= cg_list_hack.size())
    throw torrent::input_error("Choke group not found.");

  return cg_list_hack.at(index).get();
}

int64_t cg_d_group(core::Download* download) { return download->group(); }
const std::string& cg_d_group_name(core::Download* download) {
  return cg_list_hack.at(download->group())->name();
}
void    cg_d_group_set(core::Download* download, const torrent::Object& arg) { download->set_group(cg_get_index(arg)); }

torrent::Object
apply_cg_list() {
  torrent::Object::list_type result;

  for (const auto& itr : cg_list_hack)
    result.push_back(itr->name());

  return torrent::Object::from_list(result);
}

int
cg_get_can_unchoke(torrent::choke_queue* cq) {
  return cq->max_unchoked_signed() - (int)cq->size_unchoked();
}

torrent::Object
apply_cg_insert(const std::string& arg) {
  int64_t dummy;

  if (rpc::parse_whole_value_nothrow(arg.c_str(), &dummy))
    throw torrent::input_error("Cannot use a value string as choke group name.");

  if (arg.empty() || std::any_of(cg_list_hack.begin(), cg_list_hack.end(), [&arg](const auto& cg) { return arg == cg->name(); }))
    throw torrent::input_error("Duplicate name for choke group.");

  cg_list_hack.push_back(std::make_unique<torrent::choke_group>());
  cg_list_hack.back()->set_name(arg);

  cg_list_hack.back()->up_queue()->set_heuristics(torrent::HEURISTICS_UPLOAD_LEECH);
  cg_list_hack.back()->down_queue()->set_heuristics(torrent::HEURISTICS_DOWNLOAD_LEECH);

  return torrent::Object();
}

torrent::Object
apply_cg_index_of(const std::string& arg) {
  auto itr = std::find_if(cg_list_hack.begin(), cg_list_hack.end(), [&arg](const auto& cg) { return arg == cg->name(); });

  if (itr == cg_list_hack.end())
    throw torrent::input_error("Choke group not found.");

  return std::distance(cg_list_hack.begin(), itr);
}

torrent::Object
apply_cg_all_update_balance(bool is_up) {
  LT_LOG_SUBSYSTEM("apply update balance: hack is_up:%i", (int)is_up);

  for (const auto& itr : cg_list_hack) {
    if (is_up)
      itr->up_queue()->balance();
    else
      itr->down_queue()->balance();
  }

  return torrent::Object();
}

//
// End of choke group hack.
//
#endif


torrent::Object
apply_cg_max_set(const torrent::Object::list_type& args, bool is_up) {
  if (args.size() != 2)
    throw torrent::input_error("Incorrect number of arguments.");

  int64_t second_arg = 0;
  rpc::parse_whole_value(args.back().as_string().c_str(), &second_arg);

  if (is_up)
    cg_get_group(args.front())->up_queue()->set_max_unchoked(second_arg);
  else
    cg_get_group(args.front())->down_queue()->set_max_unchoked(second_arg);

  return torrent::Object();
}

torrent::Object
apply_cg_heuristics_set(const torrent::Object::list_type& args, bool is_up) {
  if (args.size() != 2)
    throw torrent::input_error("Incorrect number of arguments.");

  int t = torrent::option_find_string(is_up ? torrent::OPTION_CHOKE_HEURISTICS_UPLOAD : torrent::OPTION_CHOKE_HEURISTICS_DOWNLOAD,
                                      args.back().as_string().c_str());

  if (is_up)
    cg_get_group(args.front())->up_queue()->set_heuristics((torrent::choke_queue::heuristics_enum)t);
  else
    cg_get_group(args.front())->down_queue()->set_heuristics((torrent::choke_queue::heuristics_enum)t);

  return torrent::Object();
}

torrent::Object
apply_cg_tracker_mode_set(const torrent::Object::list_type& args) {
  if (args.size() != 2)
    throw torrent::input_error("Incorrect number of arguments.");

  int t = torrent::option_find_string(torrent::OPTION_TRACKER_MODE, args.back().as_string().c_str());

  cg_get_group(args.front())->set_tracker_mode((torrent::choke_group::tracker_mode_enum)t);

  return torrent::Object();
}

#define CG_GROUP_AT()          std::bind(&cg_get_group, std::placeholders::_2)
#define CHOKE_GROUP(direction) std::bind(direction, CG_GROUP_AT())

/*

<cg_index> -> '0'..'(choke_group.size)'
           -> '-1'..'-(choke_group.size)'
           -> '<group_name>'

(choke_group.list) -> List of group names.
(choke_group.size) -> Number of groups.

(choke_group.insert,"group_name")

Adds a new group with default settings, use index '-1' to accessing it
immediately afterwards.

(choke_group.index_of,"group_name") -> <group_index>

Throws if the group name was not found.

(choke_group.general.size,<cg_index>) -> <size>

Number of torrents in this group.

(choke_group.tracker.mode,<cg_index>) -> "tracker_mode"
(choke_group.tracker.mode.set,<cg_index>,"tracker_mode")

Decide on how aggressive a tracker should be, see
'strings.tracker_mode' for list of available options

(choke_group.up.rate,<cg_index>) -> <bytes/second>
(choke_group.down.rate,<cg_index>) -> <bytes/second>

Upload / download rate for the aggregate of all torrents in this
particular group.

(choke_group.up.max,<cg_index>) -> <max_upload_slots>
(choke_group.up.max.unlimited,<cg_index>) -> <max_upload_slots>
(choke_group.up.max.set,<cg_index>, <max_upload_slots>)
(choke_group.down.max,<cg_index>) -> <max_download_slots>
(choke_group.down.max.unlimited,<cg_index>) -> <max_download_slots>
(choke_group.down.max.set,<cg_index>, <max_download_slots)

Number of unchoked upload / download peers regulated on a group basis.

(choke_group.up.total,<cg_index>) -> <number of queued and unchoked interested peers>
(choke_group.up.queued,<cg_index>) -> <number of queued interested peers>
(choke_group.up.unchoked,<cg_index>) -> <number of unchoked uploads>
(choke_group.down.total,<cg_index>) -> <number of queued and unchoked interested peers>
(choke_group.down.queued,<cg_index>) -> <number of queued interested peers>
(choke_group.down.unchoked,<cg_index>) -> <number of unchoked uploads>

(choke_group.up.heuristics,<cg_index>) -> "heuristics"
(choke_group.up.heuristics.set,<cg_index>,"heuristics")
(choke_group.down.heuristics,<cg_index>) -> "heuristics"
(choke_group.down.heuristics.set,<cg_index>,"heuristics")

Heuristics are used for deciding what peers to choke and unchoke, see
'strings.choke_heuristics{,_download,_upload}' for a list of available
options.

(d.group) -> <choke_group_index>
(d.group.name) -> "choke_group_name"
(d.group.set,<cg_index>)

 */


void
initialize_command_groups() {
  CMD_ANY         ("choke_group.list",                std::bind(&apply_cg_list));
  CMD_ANY_STRING  ("choke_group.insert",              std::bind(&apply_cg_insert, std::placeholders::_2));

#if USE_CHOKE_GROUP
  CMD_ANY         ("choke_group.size",                std::bind(&torrent::ResourceManager::group_size, torrent::resource_manager()));
  CMD_ANY_STRING  ("choke_group.index_of",            std::bind(&torrent::ResourceManager::group_index_of, torrent::resource_manager(), std::placeholders::_2));
#else
  apply_cg_insert("default");

  CMD_ANY         ("choke_group.size",                [](auto, auto) { return (int64_t)cg_list_hack.size(); });
  CMD_ANY_STRING  ("choke_group.index_of",            std::bind(&apply_cg_index_of, std::placeholders::_2));
#endif

  // Commands specific for a group. Supports as the first argument the
  // name, the index or a negative index.
  CMD_ANY         ("choke_group.general.size",        std::bind(&torrent::choke_group::size, CG_GROUP_AT()));

  CMD_ANY         ("choke_group.tracker.mode",        [](auto, auto arg) { return torrent::option_to_str_or_throw(torrent::OPTION_TRACKER_MODE, cg_get_group(arg)->tracker_mode()); });
  CMD_ANY_LIST    ("choke_group.tracker.mode.set",    [](auto, auto arg) { return apply_cg_tracker_mode_set(arg); });

  CMD_ANY         ("choke_group.all.up.update_balance",   std::bind(&apply_cg_all_update_balance, true));
  CMD_ANY         ("choke_group.all.down.update_balance", std::bind(&apply_cg_all_update_balance, false));

  CMD_ANY         ("choke_group.up.rate",             std::bind(&torrent::choke_group::up_rate, CG_GROUP_AT()));
  CMD_ANY         ("choke_group.down.rate",           std::bind(&torrent::choke_group::down_rate, CG_GROUP_AT()));

  CMD_ANY         ("choke_group.up.max.unlimited",    std::bind(&torrent::choke_queue::is_unlimited, CHOKE_GROUP(&torrent::choke_group::up_queue)));
  CMD_ANY         ("choke_group.up.max",              std::bind(&torrent::choke_queue::max_unchoked_signed, CHOKE_GROUP(&torrent::choke_group::up_queue)));
  CMD_ANY_LIST    ("choke_group.up.max.set",          std::bind(&apply_cg_max_set, std::placeholders::_2, true));

  CMD_ANY         ("choke_group.up.total",            std::bind(&torrent::choke_queue::size_total, CHOKE_GROUP(&torrent::choke_group::up_queue)));
  CMD_ANY         ("choke_group.up.queued",           std::bind(&torrent::choke_queue::size_queued, CHOKE_GROUP(&torrent::choke_group::up_queue)));
  CMD_ANY         ("choke_group.up.unchoked",         std::bind(&torrent::choke_queue::size_unchoked, CHOKE_GROUP(&torrent::choke_group::up_queue)));
  CMD_ANY         ("choke_group.up.heuristics",       [](auto, auto arg) { return torrent::option_to_str_or_throw(torrent::OPTION_CHOKE_HEURISTICS, cg_get_group(arg)->up_queue()->heuristics()); });
  CMD_ANY_LIST    ("choke_group.up.heuristics.set",   [](auto, auto arg) { return apply_cg_heuristics_set(arg, true); });

  CMD_ANY         ("choke_group.down.max.unlimited",  std::bind(&torrent::choke_queue::is_unlimited, CHOKE_GROUP(&torrent::choke_group::down_queue)));
  CMD_ANY         ("choke_group.down.max",            std::bind(&torrent::choke_queue::max_unchoked_signed, CHOKE_GROUP(&torrent::choke_group::down_queue)));
  CMD_ANY_LIST    ("choke_group.down.max.set",        std::bind(&apply_cg_max_set, std::placeholders::_2, false));

  CMD_ANY         ("choke_group.down.total",          std::bind(&torrent::choke_queue::size_total, CHOKE_GROUP(&torrent::choke_group::down_queue)));
  CMD_ANY         ("choke_group.down.queued",         std::bind(&torrent::choke_queue::size_queued, CHOKE_GROUP(&torrent::choke_group::down_queue)));
  CMD_ANY         ("choke_group.down.unchoked",       std::bind(&torrent::choke_queue::size_unchoked, CHOKE_GROUP(&torrent::choke_group::down_queue)));
  CMD_ANY         ("choke_group.down.heuristics",     [](auto, auto arg) { return torrent::option_to_str_or_throw(torrent::OPTION_CHOKE_HEURISTICS, cg_get_group(arg)->down_queue()->heuristics()); });
  CMD_ANY_LIST    ("choke_group.down.heuristics.set", [](auto, auto arg) { return apply_cg_heuristics_set(arg, false); });

  rpc::rpc.mark_safe("choke_group.list");
  rpc::rpc.mark_safe("choke_group.size");
  rpc::rpc.mark_safe("choke_group.index_of");
  rpc::rpc.mark_safe("choke_group.general.size");
  rpc::rpc.mark_safe("choke_group.tracker.mode");
  rpc::rpc.mark_safe("choke_group.up.rate");
  rpc::rpc.mark_safe("choke_group.down.rate");
  rpc::rpc.mark_safe("choke_group.up.max");
  rpc::rpc.mark_safe("choke_group.up.max.unlimited");
  rpc::rpc.mark_safe("choke_group.up.total");
  rpc::rpc.mark_safe("choke_group.up.queued");
  rpc::rpc.mark_safe("choke_group.up.unchoked");
  rpc::rpc.mark_safe("choke_group.up.heuristics");
  rpc::rpc.mark_safe("choke_group.down.max");
  rpc::rpc.mark_safe("choke_group.down.max.unlimited");
  rpc::rpc.mark_safe("choke_group.down.total");
  rpc::rpc.mark_safe("choke_group.down.queued");
  rpc::rpc.mark_safe("choke_group.down.unchoked");
  rpc::rpc.mark_safe("choke_group.down.heuristics");
}
