#include "config.h"

#include "core/manager.h"

#include <cstdio>
#include <cstring>
#include <fstream>
#include <sstream>
#include <unistd.h>
#include <sys/select.h>
#include <fnmatch.h>
#include <torrent/utils/resume.h>
#include <torrent/object.h>
#include <torrent/exceptions.h>
#include <torrent/object_stream.h>
#include <torrent/throttle.h>
#include <torrent/net/http_stack.h>
#include <torrent/net/socket_address.h>
#include <torrent/runtime/network_config.h>
#include <torrent/runtime/network_manager.h>
#include <torrent/utils/log.h>
#include <torrent/utils/string_manip.h>

#include "rpc/parse_commands.h"
#include "utils/directory.h"
#include "utils/base64.h"
#include "utils/file_status_cache.h"

#include "globals.h"
#include "control.h"
#include "core/download.h"
#include "core/download_factory.h"
#include "core/http_queue.h"
#include "core/view.h"

#include <torrent/runtime/client_config.h>

namespace core {

const int Manager::create_start;
const int Manager::create_tied;
const int Manager::create_quiet;
const int Manager::create_raw_data;

void
Manager::push_log(const char* msg) {
  m_log_important->lock_and_push_log(msg, strlen(msg), 0);
  m_log_complete->lock_and_push_log(msg, strlen(msg), 0);
}

Manager::Manager()
  : m_log_important(torrent::log_open_log_buffer("important")),
    m_log_complete(torrent::log_open_log_buffer("complete")) {

  m_download_list     = std::make_unique<DownloadList>();
  m_file_status_cache = std::make_unique<FileStatusCache>();
  m_http_queue        = std::make_unique<HttpQueue>();

  torrent::Throttle* unthrottled = torrent::Throttle::create_throttle();
  unthrottled->set_max_rate(0);
  m_throttles["NULL"] = std::make_pair(unthrottled, unthrottled);
}

Manager::~Manager() {
  torrent::Throttle::destroy_throttle(m_throttles["NULL"].first);
}

bool
Manager::is_download_shutdown_completed() {
  for (const auto& download : *download_list()) {
    // TODO: Why is this being checked?
    // if (download->tracker_controller().is_active())
    //   return false;

    if (download->tracker_controller().has_active_trackers_not_dht_scrape_disownable())
      return false;
  }

  return true;
}

void
Manager::set_hashing_view(View* v) {
  if (v == nullptr || m_hashingView != nullptr)
    throw torrent::internal_error("Manager::set_hashing_view(...) received nullptr or is already set.");

  m_hashingView = v;
  m_hashingView->signal_changed().push_back(std::bind(&Manager::receive_hashing_changed, this));
}

ThrottlePair
Manager::get_throttle(const std::string& name) {
  auto itr       = m_throttles.find(name);
  auto throttles = (itr == m_throttles.end() ? ThrottlePair(nullptr, nullptr) : itr->second);

  if (throttles.first == nullptr)
    throttles.first = torrent::up_throttle_global();

  if (throttles.second == nullptr)
    throttles.second = torrent::down_throttle_global();

  return throttles;
}

int64_t
Manager::retrieve_throttle_value(const torrent::Object::string_type& name, bool rate, bool up) {
  ThrottleMap::iterator itr = throttles().find(name);

  if (itr == throttles().end()) {
    return (int64_t)-1;
  } else {
    torrent::Throttle* throttle = up ? itr->second.first : itr->second.second;

    // check whether the actual up/down throttle exist (one of the pair can be missing)
    if (throttle == nullptr)
      return (int64_t)-1;

    int64_t throttle_max = (int64_t)throttle->max_rate();

    if (rate) {

      if (throttle_max > 0)
        return (int64_t)throttle->rate()->rate();
      else
        return (int64_t)-1;

    } else {
      return throttle_max;
    }

  }
}

void
Manager::set_magnet_path(const std::string& path) {
  if (path.empty())
    m_magnet_path.clear();
  else if (path.back() == '/')
    m_magnet_path = path;
  else
    m_magnet_path = path + '/';
}

void
Manager::cleanup() {
  // Need to disconnect log signals? Not really since we won't receive
  // any more.

  m_download_list->clear();

  torrent::cleanup();
}

void
Manager::shutdown(bool force) {
  if (!force) {
    for (const auto& d : *m_download_list)
      m_download_list->pause_default(d.get());
  } else {
    for (const auto& d : *m_download_list)
      m_download_list->close_quick(d);
  }
}

void
Manager::receive_http_failed(std::string msg) {
  push_log_std("Http download error: \"" + msg + "\"");
}

bool
is_data_uri(const std::string& uri) {
  return std::strncmp(uri.c_str(), "data:", 5) == 0;
}

std::string
decode_data_uri(const std::string& uri) {
  const auto pos = uri.find("base64,", 5);
  if (pos == std::string::npos)
    throw torrent::input_error("Invalid data uri: not base64 encoded.");
  const auto start = pos + 7;
  if (start >= uri.size())
    throw torrent::input_error("Empty base64.");
  return utils::decode_base64(uri.substr(start));
}

void
Manager::try_create_download(const std::string& uri, int flags, const command_list_type& commands) {
  // If the path was attempted loaded before, skip it.
  if ((flags & create_tied) &&
      !(flags & create_raw_data) &&
      !is_network_uri(uri) &&
      !is_magnet_uri(uri) &&
      !is_data_uri(uri) &&
      !file_status_cache()->insert(uri))
    return;

  // Adding download.
  DownloadFactory* f = new DownloadFactory(this);

  f->variables()["tied_to_file"] = (int64_t)(bool)(flags & create_tied);
  f->commands().insert(f->commands().end(), commands.begin(), commands.end());

  f->set_start(flags & create_start);
  f->set_print_log(!(flags & create_quiet));
  f->slot_finished([f]() { delete f; });

  if (is_data_uri(uri)) {
    // Allow the use of data URIs, primarily for JSON-RPC which
    // doesn't have a defined mechanism for binary data
    f->load_raw_data(decode_data_uri(uri));
    f->variables()["tied_to_file"] = (int64_t)false;
  } else if (flags & create_raw_data) {
    f->load_raw_data(uri);
  } else {
    f->load(uri);
  }

  f->commit();
}

void
Manager::try_create_download_from_meta_download(torrent::Object* bencode, const std::string& metafile) {
  DownloadFactory* f = new DownloadFactory(this);

  f->variables()["tied_to_file"] = (int64_t)true;
  f->variables()["tied_file"] = metafile;

  torrent::Object& meta = bencode->get_key("rtorrent_meta_download");
  torrent::Object::list_type& commands = meta.get_key_list("commands");
  for (const auto& command : commands)
    f->commands().insert(f->commands().end(), command.as_string());

  f->set_start(meta.get_key_value("start"));
  f->set_print_log(meta.get_key_value("print_log"));
  f->slot_finished([f]() { delete f; });

  // Bit of a waste to create the bencode repesentation here
  // only to have the DownloadFactory decode it.
  std::stringstream s;
  s.imbue(std::locale::classic());
  s << *bencode;
  f->load_raw_data(s.str());
  f->commit();
}

utils::Directory
path_expand_transform(std::string path, const utils::directory_entry& entry) {
  return path + entry.s_name;
}

namespace {

// Consider rewritting such that m_seq is replaced by first/last.
template <typename Sequence>
class split_iterator_t {
public:
  typedef typename Sequence::const_iterator const_iterator;
  typedef typename Sequence::value_type     value_type;

  split_iterator_t() {}

  split_iterator_t(const Sequence& seq, value_type delim) :
    m_seq(&seq),
    m_delim(delim),
    m_pos(seq.begin()),
    m_next(std::find(seq.begin(), seq.end(), delim)) {
  }

  Sequence operator * () { return Sequence(m_pos, m_next); }

  split_iterator_t& operator ++ () {
    m_pos = m_next;

    if (m_pos == m_seq->end())
      return *this;

    m_pos++;
    m_next = std::find(m_pos, m_seq->end(), m_delim);

    return *this;
  }

  bool operator == (const split_iterator_t&) const { return m_pos == m_seq->end(); }
  bool operator != (const split_iterator_t&) const { return m_pos != m_seq->end(); }

private:
  const Sequence* m_seq;
  value_type      m_delim;
  const_iterator  m_pos;
  const_iterator  m_next;
};

template <typename Sequence>
inline split_iterator_t<Sequence>
split_iterator(const Sequence& seq, typename Sequence::value_type delim) {
  return split_iterator_t<Sequence>(seq, delim);
}

template <typename Sequence>
inline split_iterator_t<Sequence>
split_iterator(const Sequence&) {
  return split_iterator_t<Sequence>();
}

}

// Move this somewhere better.
void
path_expand(std::vector<std::string>* paths, const std::string& pattern) {
  std::vector<utils::Directory> currentCache;
  std::vector<utils::Directory> nextCache;

  auto first = split_iterator(pattern, '/');
  auto last  = split_iterator(pattern);

  if (first == last)
    return;

  // Check for initial '/' that indicates the root.
  if ((*first).empty()) {
    currentCache.push_back(utils::Directory("/"));
    ++first;
  } else if (torrent::utils::trim_spaces_str(*first) == "~") {
    currentCache.push_back(utils::Directory("~"));
    ++first;
  } else {
    currentCache.push_back(utils::Directory("."));
  }

  // Might be an idea to use depth-first search instead.

  for (; first != last; ++first) {
    const std::string& pattern = *first;

    if (pattern.empty())
      continue;

    // Special case for ".."?

    for (auto& itr : currentCache) {
      // Only include filenames starting with '.' if the pattern
      // starts with the same.
      itr.update((pattern[0] != '.') ? utils::Directory::update_hide_dot : 0);
      itr.erase(std::remove_if(itr.begin(), itr.end(), [&pattern](const utils::directory_entry& entry) { return fnmatch(pattern.c_str(), entry.s_name.c_str(), 0) != 0; }), itr.end());

      for (const auto& cache : itr)
        nextCache.push_back(path_expand_transform(itr.path() + (itr.path() == "/" ? "" : "/"), cache));
    }

    currentCache.clear();
    currentCache.swap(nextCache);
  }

  for (const auto& cache : currentCache)
    paths->push_back(cache.path());
}

bool
manager_equal_tied(const std::string& path, Download* download) {
  return path == rpc::call_command_string("d.tied_to_file", rpc::make_target(download));
}

void
Manager::try_create_download_expand(const std::string& uri, int flags, command_list_type commands) {
  if (flags & create_raw_data) {
    try_create_download(uri, flags, commands);
    return;
  }

  std::vector<std::string> paths;
  paths.reserve(256);

  path_expand(&paths, uri);

  if (!paths.empty())
    for (auto& path : paths)
      try_create_download(path, flags, commands);

  else
    try_create_download(uri, flags, commands);
}

// DownloadList's hashing related functions don't actually start the
// hashing, it only reacts to events. This functions checks the
// hashing view and starts hashing if nessesary.
void
Manager::receive_hashing_changed() {
  bool foundHashing = std::any_of(m_hashingView->begin_visible(), m_hashingView->end_visible(),
                                   std::mem_fn(&Download::is_hash_checking));

  // Try quick hashing all those with hashing == initial, set them to
  // something else when failed.
  for (View::iterator itr = m_hashingView->begin_visible(), last = m_hashingView->end_visible(); itr != last; ++itr) {
    if ((*itr)->is_hash_checked())
      throw torrent::internal_error("core::Manager::receive_hashing_changed() (*itr)->is_hash_checked().");

    if ((*itr)->is_hash_checking() || (*itr)->is_hash_failed())
      continue;

    bool tryQuick =
      rpc::call_command_value("d.hashing", rpc::make_target(*itr)) == Download::variable_hashing_initial &&
      (*itr)->download()->file_list()->bitfield()->empty();

    if (!tryQuick && foundHashing)
      continue;

    try {
      m_download_list->open_throw(itr->get());

      // Since the bitfield is allocated on loading of resume load or
      // hash start, and unallocated on close, we know that if it it
      // not empty then we have already loaded any existing resume
      // data.
      if ((*itr)->download()->file_list()->bitfield()->empty())
        torrent::resume_load_progress(*(*itr)->download(), (*itr)->download()->bencode()->get_key("libtorrent_resume"));

      if (tryQuick) {
        if ((*itr)->download()->hash_check(true))
          continue;

        (*itr)->download()->hash_stop();

        if (foundHashing) {
          rpc::call_command_set_value("d.hashing.set", Download::variable_hashing_rehash, rpc::make_target(*itr));
          continue;
        }
      }

      (*itr)->download()->hash_check(false);
      foundHashing = true;

    } catch (torrent::local_error& e) {
      if (tryQuick) {
        // Make sure we don't repeat the quick hashing.
        rpc::call_command_set_value("d.hashing.set", Download::variable_hashing_rehash, rpc::make_target(*itr));

      } else {
        (*itr)->set_hash_failed(true);
        (*itr)->set_message("Hashing failed: " + std::string(e.what()));
        lt_log_print(torrent::LOG_TORRENT_ERROR, "Hashing failed: %s", e.what());
      }
    }
  }
}

}
