/**
* File:   environment.cpp
* Author: Alexander Ksenofontov <aksenofo@yahoo.ru>
*
* Created on August 17, 2016, 16:13 PM
*/

#include "environment.h"
#include <libtorrent/session.hpp>
#include <libtorrent/add_torrent_params.hpp>
#include <libtorrent/torrent_handle.hpp>
#include <libtorrent/alert_types.hpp>
#include <libtorrent/bencode.hpp>
#include <libtorrent/torrent_status.hpp>
#include <libtorrent/torrent_info.hpp>
#include "libtorrent/announce_entry.hpp"
#include "libtorrent/bdecode.hpp"
#include "libtorrent/magnet_uri.hpp"

using namespace libtorrent;
namespace lt = libtorrent;

Environment::Environment(int listen_port)
: thread_(nullptr)
, stop_(false){
	lt::settings_pack pack;

	pack.set_int(lt::settings_pack::alert_mask
		, lt::alert::error_notification
		| lt::alert::storage_notification
		| lt::alert::status_notification
		| lt::alert::tracker_notification
		| lt::alert::progress_notification
	);
	pack.set_bool(settings_pack::allow_multiple_connections_per_ip, true);

	// if the port value is equal 0 - than use default
	if (listen_port) {
		std::string bind_to_interface;
		if (bind_to_interface.empty()) bind_to_interface = "0.0.0.0";
		char iface_str[100];
		snprintf(iface_str, sizeof(iface_str), "%s:%d", bind_to_interface.c_str()
			, listen_port);
		pack.set_str(settings_pack::listen_interfaces, iface_str);
	}

	torrent_session_ = new lt::session(pack);

	thread_ = new std::thread(&Environment::start, this);

}

bool Environment::find(const libtorrent::sha1_hash& hash, std::mutex** mtx, 
	                   std::condition_variable** cv, tor_info** ti,
	                   std::chrono::steady_clock::time_point** last_time_activity) {
	auto it(ids_.find(hash));
	if (it != ids_.end()) {
		auto& sync_pair(std::get<2>(it->second));
		*ti = std::get<1>(it->second).get();
		*mtx = &sync_pair->first;
		*cv = &sync_pair->second;
		if (last_time_activity) {
			*last_time_activity = &std::get<0>(it->second);
		}
		return true;
	}
	else
		return false;
}

void Environment::fill_tor_status(const libtorrent::sha1_hash& hash, tor_info& ti) {
	lt::torrent_handle handle = torrent_session_->find_torrent(hash);
	if (handle.is_valid()) {
		ti.progress_ = handle.status().progress;
	}
}

void Environment::start() {
	std::mutex* mtx; 
	std::condition_variable* cv; 
	tor_info* ti;
	std::chrono::steady_clock::time_point* last_time_activity;
	while (!stop_) {
		std::vector<lt::alert*> alerts;
		torrent_session_->pop_alerts(&alerts);
		for (lt::alert const* a : alerts) {
			if (lt::alert_cast<lt::torrent_added_alert>(a)) {
				std::unique_lock<std::mutex> lk(ids_mutex_);
				auto hash(lt::alert_cast<lt::torrent_added_alert>(a)->handle.info_hash());
				if (find(hash, &mtx, &cv, &ti, &last_time_activity)) {
					std::unique_lock<std::mutex> lk(*mtx);
					*last_time_activity = std::chrono::steady_clock::now();
					ti->added_ = true;
					fill_tor_status(hash, *ti);
					cv->notify_one();
				}
			}
			else if (lt::alert_cast<lt::save_resume_data_alert>(a)) {
				auto v(lt::alert_cast<lt::save_resume_data_alert>(a));
				std::unique_lock<std::mutex> lk(ids_mutex_);
				auto it(ids_.find(v->handle.info_hash()));
				if(it != ids_.end()) {
					auto& sync_pair(std::get<2>(it->second));
					std::unique_lock<std::mutex> lk(sync_pair->first);
					sync_pair->second.notify_one();
				}
			}
			else if (lt::alert_cast<lt::torrent_finished_alert>(a)) {
				std::unique_lock<std::mutex> lk(ids_mutex_);
				auto hash(lt::alert_cast<lt::torrent_finished_alert>(a)->handle.info_hash());
				if (find(hash, &mtx, &cv, &ti, &last_time_activity)) {
					std::unique_lock<std::mutex> lk(*mtx);
					ti->done_ = true;
					fill_tor_status(hash, *ti);
					cv->notify_one();
				}
			}
			else if (lt::alert_cast<lt::block_finished_alert>(a)) {
				std::unique_lock<std::mutex> lk(ids_mutex_);
				auto hash(lt::alert_cast<lt::block_finished_alert>(a)->handle.info_hash());
				if (find(hash, &mtx, &cv, &ti, &last_time_activity)) {
					std::unique_lock<std::mutex> lk(*mtx);
					*last_time_activity = std::chrono::steady_clock::now();
					fill_tor_status(hash, *ti);
					cv->notify_one();
				}
			}
		}

		// Verify if timeout was expired
		std::unique_lock<std::mutex> lk(ids_mutex_);
		std::for_each(ids_.begin(), ids_.end(), [&](std::map<libtorrent::sha1_hash, info_t>::value_type& pa) {
			// Already terminated
			if (std::get<1>(pa.second)->abort_timeout_)
				return;

			auto& sync_pair(std::get<2>(pa.second));
			std::unique_lock<std::mutex> lk(sync_pair->first);
			if(std::chrono::steady_clock::now() - std::get<0>(pa.second) > std::chrono::milliseconds(torrent_to_http_switch_timeout_milisec)) {
				std::get<1>(pa.second)->abort_timeout_ = true;
				ti->done_ = true;
				sync_pair->second.notify_one();
			}
		});

		if(!stop_)
			std::this_thread::sleep_for(std::chrono::milliseconds(500));
	}

	// Send to all
	std::for_each(ids_.begin(), ids_.end(), [&](std::map<libtorrent::sha1_hash, info_t>::value_type& pa) {
		auto& sync_pair(std::get<2>(pa.second));
		std::unique_lock<std::mutex> lk(sync_pair->first);
		std::get<1>(pa.second)->abort_ = true;
		ti->done_ = true;
		sync_pair->second.notify_one();
	});

	ids_.clear();
}

sha1_hash Environment::add(const libtorrent::add_torrent_params& atp) {
	std::unique_lock<std::mutex> lk(ids_mutex_);

	sha1_hash rc(atp.ti->info_hash());
	if (ids_.find(rc) != ids_.end()) {
		// The hash is already regisred
		throw std::runtime_error("Duplicated torrent file.");
	}
	torrent_session_->async_add_torrent(atp);
	auto tup(std::make_tuple(
		std::chrono::steady_clock::now(),
		std::shared_ptr<tor_info>(new tor_info),
		std::shared_ptr<std::pair<std::mutex, std::condition_variable>>(new std::pair<std::mutex, std::condition_variable>)
	));

	ids_.insert(std::make_pair(rc, tup));

	return rc;
}

// Remove torrent session
void Environment::remove(const sha1_hash& hash) {
	std::unique_lock<std::mutex> lk(ids_mutex_);
	auto h(torrent_session_->find_torrent(hash));
	if (h.is_valid())
		torrent_session_->remove_torrent(h, 0);
	
	auto it(ids_.find(hash));
	if (it != ids_.end())
		ids_.erase(it);
}

// Call destructor 
Environment::~Environment() {
	if (thread_) {
		stop_.exchange(true);
		thread_->join();
		thread_ = nullptr;
	}
	delete torrent_session_;
}
