/*

Copyright (c) 2016-2018, Pavel Pimenov
Copyright (c) 2016-2018, Pavel Pimenov
Copyright (c) 2003-2022, Arvid Norberg
Copyright (c) 2003, Daniel Wallin
Copyright (c) 2004, Magnus Jonsson
Copyright (c) 2008, Andrew Resch
Copyright (c) 2015, Mikhail Titov
Copyright (c) 2015-2020, Steven Siloti
Copyright (c) 2016-2021, Alden Torres
Copyright (c) 2016-2017, Andrei Kurushin
Copyright (c) 2016, Jonathan McDougall
Copyright (c) 2016-2018, 2022, Pave Pimenov
Copyright (c) 2017, 2020, AllSeeingEyeTolledEweSew
Copyright (c) 2017, Falcosc
Copyright (c) 2017, ximply
Copyright (c) 2018, Fernando Rodriguez
Copyright (c) 2018, airium
Copyright (c) 2018, d-komarov
Copyright (c) 2020, Paul-Louis Ageneau
Copyright (c) 2020, Rosen Penev
Copyright (c) 2020, Viktor Elofsson
Copyright (c) 2021, AdvenT
Copyright (c) 2021, Joris CARRIER
Copyright (c) 2021, Mark Scott
Copyright (c) 2021, thrnz
Copyright (c) 2024, Elyas EL IDRISSI
Copyright (c) 2025, Vladimir Golovnev (glassez)
All rights reserved.

You may use, distribute and modify this code under the terms of the BSD license,
see LICENSE file.
*/

#include "libtorrent/config.hpp"

#include <cstdarg> // for va_list
#include <ctime>
#include <algorithm>

#include <set>
#include <map>
#include <vector>
#include <cctype>
#include <memory>
#include <numeric>
#include <limits> // for numeric_limits
#include <cstdio> // for snprintf
#include <functional>

#include "libtorrent/aux_/torrent.hpp"

#ifdef TORRENT_SSL_PEERS
#include "libtorrent/aux_/ssl_stream.hpp"
#include "libtorrent/aux_/ssl.hpp"
#endif // TORRENT_SSL_PEERS

#include "libtorrent/torrent_handle.hpp"
#include "libtorrent/announce_entry.hpp"
#include "libtorrent/torrent_info.hpp"
#include "libtorrent/aux_/parse_url.hpp"
#include "libtorrent/bencode.hpp"
#include "libtorrent/hasher.hpp"
#include "libtorrent/entry.hpp"
#include "libtorrent/aux_/peer.hpp"
#include "libtorrent/aux_/peer_connection.hpp"
#include "libtorrent/aux_/bt_peer_connection.hpp"
#include "libtorrent/aux_/web_peer_connection.hpp"
#include "libtorrent/peer_connection_handle.hpp"
#include "libtorrent/peer_id.hpp"
#include "libtorrent/identify_client.hpp"
#include "libtorrent/alert_types.hpp"
#include "libtorrent/extensions.hpp"
#include "libtorrent/aux_/session_interface.hpp"
#include "libtorrent/aux_/instantiate_connection.hpp"
#include "libtorrent/assert.hpp"
#include "libtorrent/kademlia/dht_tracker.hpp"
#include "libtorrent/peer_info.hpp"
#include "libtorrent/aux_/http_connection.hpp"
#include "libtorrent/aux_/random.hpp"
#include "libtorrent/peer_class.hpp" // for peer_class
#include "libtorrent/aux_/socket_io.hpp" // for read_*_endpoint
#include "libtorrent/ip_filter.hpp"
#include "libtorrent/aux_/request_blocks.hpp"
#include "libtorrent/performance_counters.hpp" // for counters
#include "libtorrent/aux_/resolver_interface.hpp"
#include "libtorrent/aux_/alloca.hpp"
#include "libtorrent/aux_/resolve_links.hpp"
#include "libtorrent/aux_/resolve_duplicate_filenames.hpp"
#include "libtorrent/aux_/file_progress.hpp"
#include "libtorrent/aux_/has_block.hpp"
#include "libtorrent/aux_/alert_manager.hpp"
#include "libtorrent/disk_interface.hpp"
#include "libtorrent/aux_/ip_helpers.hpp" // for is_ip_address
#include "libtorrent/download_priority.hpp"
#include "libtorrent/hex.hpp" // to_hex
#include "libtorrent/aux_/range.hpp"
#include "libtorrent/aux_/merkle.hpp"
#include "libtorrent/mmap_disk_io.hpp" // for hasher_thread_divisor
#include "libtorrent/aux_/numeric_cast.hpp"
#include "libtorrent/aux_/path.hpp"
#include "libtorrent/aux_/generate_peer_id.hpp"
#include "libtorrent/aux_/announce_entry.hpp"
#include "libtorrent/aux_/ssl.hpp"
#include "libtorrent/aux_/apply_pad_files.hpp"
#include "libtorrent/aux_/tracker_list.hpp"

#ifndef TORRENT_DISABLE_LOGGING
#include "libtorrent/aux_/session_impl.hpp" // for tracker_logger
#endif

#include "libtorrent/aux_/torrent_impl.hpp"

using namespace std::placeholders;

namespace libtorrent::aux {
namespace {

bool is_downloading_state(int const st)
{
	switch (st)
	{
		case torrent_status::checking_files:
		case torrent_status::checking_resume_data:
			return false;
		case torrent_status::downloading_metadata:
		case torrent_status::downloading:
		case torrent_status::finished:
		case torrent_status::seeding:
			return true;
		default:
			// unexpected state
			TORRENT_ASSERT_FAIL_VAL(st);
			return false;
	}
}

// convert file priorities into piece priorities
aux::vector<download_priority_t, piece_index_t> file_to_piece_prio(
	file_storage const& fs
	, aux::vector<download_priority_t, file_index_t> const& file_prios)
{
	aux::vector<download_priority_t, piece_index_t> pieces(aux::numeric_cast<std::size_t>(
		fs.num_pieces()), dont_download);

	for (auto const i : fs.file_range())
	{
		std::int64_t const size = fs.file_size(i);
		if (size == 0) continue;

		// pad files always have priority 0
		download_priority_t const file_prio
			= fs.pad_file_at(i) ? dont_download
			: i >= file_prios.end_index() ? default_priority
			: file_prios[i];

		if (file_prio == dont_download)
		{
			// the pieces already start out as priority 0, no need to update
			// the pieces vector in this case
			continue;
		}

		// mark all pieces of the file with this file's priority
		// but only if the priority is higher than the pieces
		// already set (to avoid problems with overlapping pieces)
		// if one piece spans several files, we might
		// come here several times with the same start_piece, end_piece
		for (piece_index_t const p : file_piece_range_inclusive(fs, i))
			pieces[p] = std::max(pieces[p], file_prio);
	}
	return pieces;
}
} // anonymous namespace

	web_seed_t::web_seed_t(web_seed_entry const& wse)
		: web_seed_entry(wse)
	{
		peer_info.web_seed = true;
	}

	web_seed_t::web_seed_t(std::string const& url_
		, std::string const& auth_
		, web_seed_entry::headers_t const& extra_headers_)
		: web_seed_entry(url_, auth_, extra_headers_)
	{
		peer_info.web_seed = true;
	}

	torrent_hot_members::torrent_hot_members(aux::session_interface& ses
		, add_torrent_params const& p, bool const session_paused)
		: m_ses(ses)
		, m_complete(0xffffff)
		, m_upload_mode(bool(p.flags & torrent_flags::upload_mode))
		, m_connections_initialized(false)
		, m_abort(false)
		, m_paused(bool(p.flags & torrent_flags::paused))
		, m_session_paused(session_paused)
#ifndef TORRENT_DISABLE_SHARE_MODE
		, m_share_mode(bool(p.flags & torrent_flags::share_mode))
#endif
		, m_have_all(false)
		, m_graceful_pause_mode(false)
		, m_state_subscription(bool(p.flags & torrent_flags::update_subscribe))
		, m_max_connections(0xffffff)
		, m_state(torrent_status::checking_resume_data)
	{}

	torrent::torrent(
		aux::session_interface& ses
		, bool const session_paused
		, add_torrent_params&& p)
		: torrent_hot_members(ses, p, session_paused)
		, m_total_uploaded(p.total_uploaded)
		, m_total_downloaded(p.total_downloaded)
		, m_tracker_timer(ses.get_context())
		, m_inactivity_timer(ses.get_context())
		, m_trackerid(p.trackerid)
		, m_save_path(complete(p.save_path))
		, m_part_file_dir(p.part_file_dir)
		, m_stats_counters(ses.stats_counters())
		, m_added_time(p.added_time ? p.added_time : aux::posix_time())
		, m_completed_time(p.completed_time)
		, m_last_seen_complete(p.last_seen_complete)
		, m_swarm_last_seen_complete(p.last_seen_complete)
		, m_info_hash(p.info_hashes)
		, m_comment(p.comment)
		, m_created_by(p.created_by)
		, m_creation_date(p.creation_date)
		, m_error_file(torrent_status::error_file_none)
		, m_sequence_number(-1)
		, m_peer_id(aux::generate_peer_id(settings()))
		, m_announce_to_trackers(!(p.flags & torrent_flags::paused))
		, m_announce_to_lsd(!(p.flags & torrent_flags::paused))
		, m_has_incoming(false)
		, m_files_checked(false)
		, m_storage_mode(p.storage_mode)
		, m_announcing(false)
		, m_added(false)
		, m_sequential_download(p.flags & torrent_flags::sequential_download)
		, m_auto_sequential(false)
		, m_seed_mode(false)
#ifndef TORRENT_DISABLE_SUPERSEEDING
		, m_super_seeding(p.flags & torrent_flags::super_seeding)
#endif
		, m_stop_when_ready(p.flags & torrent_flags::stop_when_ready)
		, m_enable_dht(!bool(p.flags & torrent_flags::disable_dht))
		, m_enable_lsd(!bool(p.flags & torrent_flags::disable_lsd))
		, m_i2p(bool(p.flags & torrent_flags::i2p_torrent))
		, m_disable_v1_hashes(false)
		, m_max_uploads((1 << 24) - 1)
		, m_num_uploads(0)
		, m_enable_pex(!bool(p.flags & torrent_flags::disable_pex))
		, m_apply_ip_filter(bool(p.flags & torrent_flags::apply_ip_filter))
		, m_pending_active_change(false)
#if TORRENT_ABI_VERSION < 4
		, m_v2_piece_layers_validated(false)
#endif
		, m_connect_boost_counter(static_cast<std::uint8_t>(settings().get_int(settings_pack::torrent_connect_boost)))
		, m_incomplete(0xffffff)
		, m_announce_to_dht(!(p.flags & torrent_flags::paused))
		, m_ssl_torrent(false)
		, m_deleted(false)
		, m_last_download(aux::from_time_t(p.last_download))
		, m_last_upload(aux::from_time_t(p.last_upload))
		, m_userdata(p.userdata)
		, m_auto_managed(bool(p.flags & torrent_flags::auto_managed))
		, m_current_gauge_state(static_cast<std::uint32_t>(no_gauge_state))
		, m_moving_storage(false)
		, m_inactive(false)
		, m_downloaded(0xffffff)
		, m_progress_ppm(0)
		, m_torrent_initialized(false)
		, m_outstanding_file_priority(false)
		, m_complete_sent(false)
	{
		TORRENT_ASSERT_PRECOND(!is_complete(m_part_file_dir));
		// This is stored in 2 bits
		TORRENT_ASSERT_PRECOND(static_cast<std::uint8_t>(p.storage_mode) < 4);

		if (p.flags & torrent_flags::need_save_resume)
		{
			m_need_save_resume_data |= torrent_handle::if_metadata_changed;
		}

		// we cannot log in the constructor, because it relies on shared_from_this
		// being initialized, which happens after the constructor returns.

#if TORRENT_USE_UNC_PATHS
		m_save_path = canonicalize_path(m_save_path);
		m_part_file_dir = canonicalize_path(m_part_file_dir);
#endif

		if (!m_apply_ip_filter)
		{
			inc_stats_counter(counters::non_filter_torrents);
		}

#if TORRENT_ABI_VERSION < 4
		if (!m_torrent_file)
			m_torrent_file = (p.ti ? p.ti : std::make_shared<torrent_info>(m_info_hash));
#else
		if (!m_torrent_file)
			m_torrent_file = (p.ti ? p.ti : std::make_shared<torrent_info const>(m_info_hash));
#endif
		if (m_torrent_file->is_valid())
		{
			std::lock_guard<std::mutex> l(m_torrent_file_mutex);
			m_torrent_file_external = m_torrent_file;
		}

#if TORRENT_USE_I2P
		if (m_torrent_file->is_i2p())
			m_i2p = true;
#endif

		if (m_torrent_file->is_valid())
		{
			// merkle trees are loaded from add_torrent_params below, in load_merkle_trees()
			error_code ec = initialize_merkle_trees();
			if (ec) throw system_error(ec);
			m_size_on_disk = m_torrent_file->layout().size_on_disk();
		}

		// --- WEB SEEDS ---

		std::vector<web_seed_t> ws;
#if TORRENT_ABI_VERSION < 4
		// if override web seed flag is set, don't load any web seeds from the
		// torrent file.
		if (!(p.flags & torrent_flags::deprecated_override_web_seeds))
		{
			for (auto const& e : m_torrent_file->internal_web_seeds())
				ws.emplace_back(e);
		}
#endif

		// add web seeds from add_torrent_params
		bool const multi_file = m_torrent_file->is_valid()
				&& m_torrent_file->num_files() > 1;

		for (auto const& u : p.url_seeds)
		{
			ws.emplace_back(u);

			// correct URLs to end with a "/" for multi-file torrents
			if (multi_file)
				ensure_trailing_slash(ws.back().url);
#if TORRENT_ABI_VERSION < 4
			if (!m_torrent_file->is_valid())
				m_torrent_file->add_url_seed(ws.back().url);
#endif
		}

		aux::random_shuffle(ws);
		for (auto& w : ws) m_web_seeds.emplace_back(std::move(w));

		// --- TRACKERS ---

#if TORRENT_ABI_VERSION < 4
		// if override trackers flag is set, don't load trackers from torrent file
		if (!(p.flags & torrent_flags::deprecated_override_trackers))
		{
			m_trackers.replace(m_torrent_file->internal_trackers());
		}
#endif

		int tier = 0;
		auto tier_iter = p.tracker_tiers.begin();
		for (auto const& url : p.trackers)
		{
			if (url.empty()) continue;

			aux::announce_entry e(url);
			if (tier_iter != p.tracker_tiers.end())
				tier = *tier_iter++;

			e.fail_limit = 0;
			e.source = lt::announce_entry::source_magnet_link;
			e.tier = std::uint8_t(tier);

			if (!m_trackers.add_tracker(e))
				continue;

#if TORRENT_ABI_VERSION < 4
			// add the tracker to the m_torrent_file here so that the trackers
			// will be preserved via create_torrent() when passing in just the
			// torrent_info object.
			if (!m_torrent_file->is_valid())
				m_torrent_file->add_tracker(e.url, e.tier, lt::announce_entry::tracker_source(e.source));
#endif
		}

		if (settings().get_bool(settings_pack::prefer_udp_trackers))
			m_trackers.prioritize_udp_trackers();

		if (m_torrent_file->is_valid())
		{
			// setting file- or piece priorities for seed mode makes no sense. If a
			// torrent ends up in seed mode by accident, it can be very confusing,
			// so assume the seed mode flag is not intended and don't enable it in
			// that case. Also, if the resume data says we're missing a piece, we
			// can't be in seed-mode.
			m_seed_mode = (p.flags & torrent_flags::seed_mode)
				&& std::find(p.file_priorities.begin(), p.file_priorities.end(), dont_download) == p.file_priorities.end()
				&& std::find(p.piece_priorities.begin(), p.piece_priorities.end(), dont_download) == p.piece_priorities.end()
				&& std::find(p.have_pieces.begin(), p.have_pieces.end(), false) == p.have_pieces.end();

			m_connections_initialized = true;
		}
		else
		{
			if (!p.name.empty()) m_name = std::make_unique<std::string>(p.name);
		}

		TORRENT_ASSERT(is_single_thread());
		m_file_priority.assign(p.file_priorities.begin(), p.file_priorities.end());

		if (m_seed_mode)
		{
			m_verified.resize(m_torrent_file->num_pieces(), false);
			m_verifying.resize(m_torrent_file->num_pieces(), false);
		}

		m_total_uploaded = p.total_uploaded;
		m_total_downloaded = p.total_downloaded;

		// the number of seconds this torrent has spent in started, finished and
		// seeding state so far, respectively.
		m_active_timer.set_total(seconds32(std::max(0, std::min(0x3fffffff, p.active_time))));
		m_finished_timer.set_total(seconds32(std::max(0, std::min(0x3fffffff, p.finished_time))));
		m_seeding_timer.set_total(seconds32(std::max(0, std::min(0x3fffffff, p.seeding_time))));

		if (m_completed_time != 0 && m_completed_time < m_added_time)
			m_completed_time = m_added_time;

		// --- V2 HASHES ---

		if (m_torrent_file->is_valid() && m_torrent_file->info_hashes().has_v2())
		{
			if (!p.merkle_trees.empty())
				load_merkle_trees(
					std::move(p.merkle_trees)
					, std::move(p.merkle_tree_mask)
					, std::move(p.verified_leaf_hashes));

			// we really don't want to store extra copies of the trees
			TORRENT_ASSERT(p.merkle_trees.empty());
		}

		if (valid_metadata())
		{
			inc_stats_counter(counters::num_total_pieces_added
				, m_torrent_file->num_pieces());
		}

		// TODO: 3 we could probably get away with just saving a few fields here
		m_add_torrent_params = std::make_unique<add_torrent_params>(std::move(p));

		// seed m_active_timer/m_seeding_timer/m_finished_timer's live state
		// to match reality as constructed (m_paused/m_seed_mode etc. are
		// all settled by now); every subsequent transition maintains it
		// from here via update_state_timers()
		update_state_timers();
	}

	void torrent::load_merkle_trees(
		aux::vector<std::vector<sha256_hash>, file_index_t> trees_import
		, aux::vector<bitfield, file_index_t> mask
		, aux::vector<bitfield, file_index_t> verified)
	{
		auto const& fs = m_torrent_file->layout();

		bitfield const empty_verified;
		for (file_index_t i{0}; i < fs.end_file(); ++i)
		{
			if (fs.pad_file_at(i) || fs.file_size(i) == 0)
				continue;

			if (i >= trees_import.end_index()) break;
			bitfield const& verified_bitmask = (i >= verified.end_index()) ? empty_verified : verified[i];
			if (i < mask.end_index() && !mask[i].empty())
			{
				m_merkle_trees[i].load_sparse_tree(trees_import[i], mask[i], verified_bitmask);
			}
			else
			{
				m_merkle_trees[i].load_tree(trees_import[i], verified_bitmask);
			}
		}
	}

	void torrent::inc_stats_counter(int c, int value)
	{ m_ses.stats_counters().inc_stats_counter(c, value); }

	int torrent::current_stats_state() const
	{
		if (m_abort || !m_added)
			return counters::num_checking_torrents + no_gauge_state;

		if (has_error()) return counters::num_error_torrents;
		if (m_paused || m_graceful_pause_mode)
		{
			if (!is_auto_managed()) return counters::num_stopped_torrents;
			if (is_seed()) return counters::num_queued_seeding_torrents;
			return counters::num_queued_download_torrents;
		}
		if (state() == torrent_status::checking_files
#if TORRENT_ABI_VERSION == 1
			|| state() == torrent_status::queued_for_checking
#endif
			)
			return counters::num_checking_torrents;
		else if (is_seed()) return counters::num_seeding_torrents;
		else if (is_upload_only()) return counters::num_upload_only_torrents;
		return counters::num_downloading_torrents;
	}

	void torrent::update_gauge()
	{
		int const new_gauge_state = current_stats_state() - counters::num_checking_torrents;
		TORRENT_ASSERT(new_gauge_state >= 0);
		TORRENT_ASSERT(new_gauge_state <= no_gauge_state);

		if (new_gauge_state == int(m_current_gauge_state)) return;

		if (m_current_gauge_state != no_gauge_state)
			inc_stats_counter(m_current_gauge_state + counters::num_checking_torrents, -1);
		if (new_gauge_state != no_gauge_state)
			inc_stats_counter(new_gauge_state + counters::num_checking_torrents, 1);

		TORRENT_ASSERT(new_gauge_state >= 0);
		TORRENT_ASSERT(new_gauge_state <= no_gauge_state);
		m_current_gauge_state = static_cast<std::uint32_t>(new_gauge_state);
	}

	void torrent::leave_seed_mode(seed_mode_t const checking)
	{
		if (!m_seed_mode) return;

		if (checking == seed_mode_t::check_files)
		{
			// this means the user promised we had all the
			// files, but it turned out we didn't. This is
			// an error.

			// TODO: 2 post alert

#ifndef TORRENT_DISABLE_LOGGING
			debug_log("*** FAILED SEED MODE, rechecking");
#endif
		}

#ifndef TORRENT_DISABLE_LOGGING
		debug_log("*** LEAVING SEED MODE (%s)"
			, checking == seed_mode_t::skip_checking ? "as seed" : "as non-seed");
#endif
		m_seed_mode = false;
		// m_seed_mode is one of is_seed()'s inputs, and set_have_all() below
		// only reacts to m_have_all changing, so flush/re-anchor the timers
		// here explicitly
		update_state_timers();
		// seed is false if we turned out not
		// to be a seed after all
		if (checking == seed_mode_t::check_files
			&& state() != torrent_status::checking_resume_data)
		{
			set_have_all(false);
			set_state(torrent_status::downloading);
			force_recheck();
		}
		m_num_verified = 0;
		m_verified.clear();
		m_verifying.clear();

		set_need_save_resume(torrent_handle::if_state_changed);
	}

	void torrent::verified(piece_index_t const piece)
	{
		TORRENT_ASSERT(!m_verified.get_bit(piece));
		++m_num_verified;
		m_verified.set_bit(piece);
		set_need_save_resume(torrent_handle::if_download_progress);
	}

	void torrent::start()
	{
		TORRENT_ASSERT(is_single_thread());
		TORRENT_ASSERT(m_was_started == false);
#if TORRENT_USE_ASSERTS
		m_was_started = true;
#endif

		update_want_tick();

		// Some of these calls may log to the torrent debug log, which requires a
		// call to get_handle(), which requires the torrent object to be fully
		// constructed, as it relies on get_shared_from_this()
		if (m_add_torrent_params)
		{
			add_torrent_params const& p = *m_add_torrent_params;

#if TORRENT_ABI_VERSION == 1
			if (p.internal_resume_data_error
				&& m_ses.alerts().should_post<fastresume_rejected_alert>())
			{
				m_ses.alerts().emplace_alert<fastresume_rejected_alert>(get_handle()
					, p.internal_resume_data_error, ""
					, operation_t::unknown);
			}
#endif


			set_max_uploads(p.max_uploads, false);
			set_max_connections(p.max_connections, false);
			set_limit_impl(p.upload_limit, peer_connection::upload_channel, false);
			set_limit_impl(p.download_limit, peer_connection::download_channel, false);

			for (auto const& peer : p.peers)
			{
				add_peer(peer, peer_info::resume_data);
			}

			if (!p.peers.empty())
			{
				do_connect_boost();
			}

			if (!p.root_certificate.empty())
			{
				m_ssl_torrent = true;
#ifdef TORRENT_SSL_PEERS
				init_ssl(p.root_certificate);
#endif
			}

#ifndef TORRENT_DISABLE_LOGGING
			if (should_log() && !p.peers.empty())
			{
				std::string str;
				for (auto const& peer : p.peers)
				{
					str += peer.address().to_string();
					str += ' ';
				}
				debug_log("add_torrent add_peer() [ %s] connect-candidates: %d"
					, str.c_str(), m_peer_list
					? m_peer_list->num_connect_candidates() : -1);
			}
#endif
		}

#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			debug_log("creating torrent: %s max-uploads: %d max-connections: %d "
				"upload-limit: %d download-limit: %d flags: %s%s%s%s%s%s%s%s%s%s%s%s "
				"save-path: %s part-file-path: %s"
				, torrent_file().name().c_str()
				, int(m_max_uploads)
				, int(m_max_connections)
				, upload_limit()
				, download_limit()
				, m_seed_mode ? "seed-mode " : ""
				, m_upload_mode ? "upload-mode " : ""
#ifndef TORRENT_DISABLE_SHARE_MODE
				, m_share_mode ? "share-mode " : ""
#else
				, ""
#endif
				, m_apply_ip_filter ? "apply-ip-filter " : ""
				, m_paused ? "paused " : ""
				, m_auto_managed ? "auto-managed " : ""
				, m_state_subscription ? "update-subscribe " : ""
#ifndef TORRENT_DISABLE_SUPERSEEDING
				, m_super_seeding ? "super-seeding " : ""
#else
				, ""
#endif
				, m_sequential_download ? "sequential-download " : ""
				, (m_add_torrent_params && m_add_torrent_params->flags & torrent_flags::deprecated_override_trackers)
					? "override-trackers "  : ""
				, (m_add_torrent_params && m_add_torrent_params->flags & torrent_flags::deprecated_override_web_seeds)
					? "override-web-seeds " : ""
				, (m_ssl_torrent ? "ssl-torrent " : "")
				, m_save_path.c_str()
				, m_part_file_dir.c_str()
				);
		}
#endif

		update_gauge();

		update_want_peers();
		update_want_scrape();
		update_state_list();

		if (m_torrent_file->is_valid())
		{
			init();
		}
		else
		{
			// we need to start announcing since we don't have any
			// metadata. To receive peers to ask for it.
			set_state(torrent_status::downloading_metadata);
			start_announcing();
		}

#if TORRENT_USE_INVARIANT_CHECKS
		check_invariant();
#endif
	}

	void torrent::set_apply_ip_filter(bool const b)
	{
		if (b == bool(m_apply_ip_filter)) return;
		if (b)
		{
			inc_stats_counter(counters::non_filter_torrents, -1);
		}
		else
		{
			inc_stats_counter(counters::non_filter_torrents);
		}

		set_need_save_resume(torrent_handle::if_config_changed);

		m_apply_ip_filter = b;
		ip_filter_updated();
		state_updated();
	}

	void torrent::set_ip_filter(std::shared_ptr<const ip_filter> ipf)
	{
		m_ip_filter = std::move(ipf);
		if (!m_apply_ip_filter) return;
		ip_filter_updated();
	}

#ifndef TORRENT_DISABLE_DHT
	bool torrent::should_announce_dht() const
	{
		TORRENT_ASSERT(is_single_thread());
		if (!m_enable_dht) return false;
		if (!m_ses.announce_dht()) return false;

#if TORRENT_USE_I2P
		// i2p torrents don't announced on the DHT
		// unless we allow mixed swarms
		if (is_i2p() && !settings().get_bool(settings_pack::allow_i2p_mixed))
			return false;
#endif

		if (!m_ses.dht()) return false;
		if (m_torrent_file->is_valid() && !m_files_checked) return false;
		if (!m_announce_to_dht) return false;
		if (m_paused) return false;

		// don't announce private torrents
		if (m_torrent_file->is_valid() && m_torrent_file->priv()) return false;
		if (m_trackers.empty()) return true;
		if (!settings().get_bool(settings_pack::use_dht_as_fallback)) return true;

		return !m_trackers.any_verified();
	}

#endif

	torrent::~torrent()
	{
		// TODO: 3 assert there are no outstanding async operations on this
		// torrent

#if TORRENT_USE_ASSERTS
		// by the time a torrent is destroyed it must already have been
		// removed from every session_impl::m_torrent_lists entry, otherwise
		// the list is left with a dangling pointer.
		for (auto const& l : m_links)
		{
			TORRENT_ASSERT(!l.in_list());
		}
#endif

		// The invariant can't be maintained here, since the torrent
		// is being destructed, all weak references to it have been
		// reset, which means that all its peers already have an
		// invalidated torrent pointer (so it cannot be verified to be correct)

		// i.e. the invariant can only be maintained if all connections have
		// been closed by the time the torrent is destructed. And they are
		// supposed to be closed. So we can still do the invariant check.

		// however, the torrent object may be destructed from the main
		// thread when shutting down, if the disk cache has references to it.
		// this means that the invariant check that this is called from the
		// network thread cannot be maintained

		TORRENT_ASSERT(m_peer_class == peer_class_t{0});
		TORRENT_ASSERT(m_connections.empty());
		// just in case, make sure the session accounting is kept right
		for (auto* p : m_connections)
			m_ses.close_connection(p);
	}

	void torrent::read_piece(piece_index_t const piece)
	{
		error_code ec;
		if (m_abort || m_deleted)
		{
			ec.assign(boost::system::errc::operation_canceled, generic_category());
		}
		else if (!valid_metadata())
		{
			ec.assign(errors::no_metadata, libtorrent_category());
		}
		else if (!user_have_piece(piece))
		{
			ec.assign(errors::invalid_piece_index, libtorrent_category());
		}

		if (ec)
		{
			m_ses.alerts().emplace_alert<read_piece_alert>(get_handle(), piece, ec);
			return;
		}

		const int piece_size = piece_size_for_req(piece);
		const int blocks_in_piece = (piece_size + block_size() - 1) / block_size();

		TORRENT_ASSERT(blocks_in_piece > 0);
		TORRENT_ASSERT(piece_size > 0);

		if (blocks_in_piece == 0)
		{
			// this shouldn't actually happen
			boost::shared_array<char> buf;
			m_ses.alerts().emplace_alert<read_piece_alert>(
				get_handle(), piece, buf, 0);
			return;
		}

		std::shared_ptr<read_piece_struct> rp = std::make_shared<read_piece_struct>();
		rp->piece_data.reset(new (std::nothrow) char[std::size_t(piece_size)]);
		if (!rp->piece_data)
		{
			m_ses.alerts().emplace_alert<read_piece_alert>(
				get_handle(), piece, error_code(boost::system::errc::not_enough_memory, generic_category()));
			return;
		}
		rp->blocks_left = blocks_in_piece;
		rp->fail = false;

		disk_job_flags_t flags{};
		auto const read_mode = settings().get_int(settings_pack::disk_io_read_mode);
		if (read_mode == settings_pack::disable_os_cache)
			flags |= disk_interface::volatile_read;

		peer_request r;
		r.piece = piece;
		r.start = 0;
		auto self = shared_from_this();
		for (int i = 0; i < blocks_in_piece; ++i, r.start += block_size())
		{
			r.length = std::min(piece_size - r.start, block_size());
			m_ses.disk_thread().async_read(m_storage, r
				, [self, r, rp](disk_buffer_holder block, storage_error const& se) mutable
				{ self->on_disk_read_complete(std::move(block), se, r, rp); }
				, flags);
		}
		m_ses.deferred_submit_jobs();
	}

#ifndef TORRENT_DISABLE_SHARE_MODE
	void torrent::send_share_mode()
	{
#ifndef TORRENT_DISABLE_EXTENSIONS
		for (auto* const pc : m_connections)
		{
			TORRENT_INCREMENT(m_iterating_connections);
			if (pc->type() != connection_type::bittorrent) continue;
			auto* p = static_cast<bt_peer_connection*>(pc);
			p->write_share_mode();
		}
#endif
	}
#endif // TORRENT_DISABLE_SHARE_MODE

	void torrent::send_upload_only()
	{
#ifndef TORRENT_DISABLE_EXTENSIONS

#ifndef TORRENT_DISABLE_SHARE_MODE
		if (share_mode()) return;
#endif
#ifndef TORRENT_DISABLE_SUPERSEEDING
		if (super_seeding()) return;
#endif

		// if we send upload-only, the other end is very likely to disconnect
		// us, at least if it's a seed. If we don't want to close redundant
		// connections, don't sent upload-only
		if (!settings().get_bool(settings_pack::close_redundant_connections)) return;

		// if we're super seeding, we don't want to make peers
		// think that we only have a single piece and is upload
		// only, since they might disconnect immediately when
		// they have downloaded a single piece, although we'll
		// make another piece available
		bool const upload_only_enabled = is_upload_only()
#ifndef TORRENT_DISABLE_SUPERSEEDING
			&& !super_seeding()
#endif
			;

		for (auto* p : m_connections)
		{
			TORRENT_INCREMENT(m_iterating_connections);

			p->send_not_interested();
			p->send_upload_only(upload_only_enabled);
		}
#endif // TORRENT_DISABLE_EXTENSIONS
	}

	torrent_flags_t torrent::flags() const
	{
		torrent_flags_t ret = torrent_flags_t{};
		if (m_seed_mode)
			ret |= torrent_flags::seed_mode;
		if (m_upload_mode)
			ret |= torrent_flags::upload_mode;
#ifndef TORRENT_DISABLE_SHARE_MODE
		if (m_share_mode)
			ret |= torrent_flags::share_mode;
#endif
		if (m_apply_ip_filter)
			ret |= torrent_flags::apply_ip_filter;
		if (is_torrent_paused())
			ret |= torrent_flags::paused;
		if (m_auto_managed)
			ret |= torrent_flags::auto_managed;
#ifndef TORRENT_DISABLE_SUPERSEEDING
		if (m_super_seeding)
			ret |= torrent_flags::super_seeding;
#endif
		if (m_sequential_download)
			ret |= torrent_flags::sequential_download;
		if (m_stop_when_ready)
			ret |= torrent_flags::stop_when_ready;
		if (!m_enable_dht)
			ret |= torrent_flags::disable_dht;
		if (!m_enable_lsd)
			ret |= torrent_flags::disable_lsd;
		if (!m_enable_pex)
			ret |= torrent_flags::disable_pex;
		if (m_i2p)
			ret |= torrent_flags::i2p_torrent;
		if (m_disable_v1_hashes)
			ret |= torrent_flags::disable_v1_hashes;
		return ret;
	}

	void torrent::set_flags(torrent_flags_t const flags
		, torrent_flags_t const mask)
	{
		if (mask & torrent_flags::i2p_torrent)
		{
			m_i2p = bool(flags & torrent_flags::i2p_torrent);
		}
		if ((mask & torrent_flags::seed_mode)
			&& !(flags & torrent_flags::seed_mode))
		{
			leave_seed_mode(seed_mode_t::check_files);
		}
		if (mask & torrent_flags::upload_mode)
			set_upload_mode(bool(flags & torrent_flags::upload_mode));
#ifndef TORRENT_DISABLE_SHARE_MODE
		if (mask & torrent_flags::share_mode)
			set_share_mode(bool(flags & torrent_flags::share_mode));
#endif
		if (mask & torrent_flags::apply_ip_filter)
			set_apply_ip_filter(bool(flags & torrent_flags::apply_ip_filter));
		if (mask & torrent_flags::paused)
		{
			if (flags & torrent_flags::paused)
				pause(torrent_handle::graceful_pause);
			else
				resume();
		}
		if (mask & torrent_flags::auto_managed)
			auto_managed(bool(flags & torrent_flags::auto_managed));
#ifndef TORRENT_DISABLE_SUPERSEEDING
		if (mask & torrent_flags::super_seeding)
			set_super_seeding(bool(flags & torrent_flags::super_seeding));
#endif
		if (mask & torrent_flags::sequential_download)
			set_sequential_download(bool(flags & torrent_flags::sequential_download));
		if (mask & torrent_flags::stop_when_ready)
			stop_when_ready(bool(flags & torrent_flags::stop_when_ready));
		if (mask & torrent_flags::disable_dht)
		{
			bool const new_value = !bool(flags & torrent_flags::disable_dht);
			if (m_enable_dht != new_value) set_need_save_resume(torrent_handle::if_config_changed);
			m_enable_dht = new_value;
		}
		if (mask & torrent_flags::disable_lsd)
		{
			bool const new_value = !bool(flags & torrent_flags::disable_lsd);
			if (m_enable_lsd != new_value) set_need_save_resume(torrent_handle::if_config_changed);
			m_enable_lsd = new_value;
		}
		if (mask & torrent_flags::disable_pex)
		{
			bool const new_value = !bool(flags & torrent_flags::disable_pex);
			if (bool(m_enable_pex) != new_value)
				set_need_save_resume(torrent_handle::if_config_changed);
			m_enable_pex = new_value;
		}
	}

#ifndef TORRENT_DISABLE_SHARE_MODE
	void torrent::set_share_mode(bool const s)
	{
		if (s == bool(m_share_mode)) return;

		m_share_mode = s;
		set_need_save_resume(torrent_handle::if_config_changed);
#ifndef TORRENT_DISABLE_LOGGING
		debug_log("*** set-share-mode: %d", s);
#endif
		if (m_share_mode)
		{
			std::size_t const num_files = valid_metadata()
				? std::size_t(m_torrent_file->num_files())
				: m_file_priority.size();
			// in share mode, all pieces have their priorities initialized to
			// dont_download
			prioritize_files(aux::vector<download_priority_t, file_index_t>(num_files, dont_download));
		}
	}
#endif // TORRENT_DISABLE_SHARE_MODE

	void torrent::set_upload_mode(bool b)
	{
		if (b == bool(m_upload_mode)) return;

		m_upload_mode = b;
#ifndef TORRENT_DISABLE_LOGGING
		debug_log("*** set-upload-mode: %d", b);
#endif

		set_need_save_resume(torrent_handle::if_state_changed);
		update_gauge();
		state_updated();
		send_upload_only();

		if (m_upload_mode)
		{
			// clear request queues of all peers
			for (auto* p : m_connections)
			{
				TORRENT_INCREMENT(m_iterating_connections);
				// we may want to disconnect other upload-only peers
				if (p->upload_only())
					p->update_interest();
				p->cancel_all_requests();
			}
			// this is used to try leaving upload only mode periodically
			m_upload_mode_time = aux::time_now32();
		}
		else if (m_peer_list)
		{
			// reset last_connected, to force fast reconnect after leaving upload mode
			for (auto* pe : *m_peer_list)
			{
				pe->last_connected = 0;
			}

			// send_block_requests on all peers
			for (auto* p : m_connections)
			{
				TORRENT_INCREMENT(m_iterating_connections);
				// we may be interested now, or no longer interested
				p->update_interest();
				p->send_block_requests();
			}
		}
	}

	void torrent::need_peer_list()
	{
		if (m_peer_list) return;
		m_peer_list = std::make_unique<peer_list>(m_ses.get_peer_allocator());
	}

	void torrent::handle_exception()
	{
		try
		{
			throw;
		}
		catch (system_error const& err)
		{
#ifndef TORRENT_DISABLE_LOGGING
			if (should_log())
			{
				debug_log("torrent exception: (%d) %s: %s"
					, err.code().value(), err.code().message().c_str()
					, err.what());
			}
#endif
			set_error(err.code(), torrent_status::error_file_exception);
		}
		catch (std::exception const& err)
		{
			TORRENT_UNUSED(err);
			set_error(error_code(), torrent_status::error_file_exception);
#ifndef TORRENT_DISABLE_LOGGING
			if (should_log())
			{
				debug_log("torrent exception: %s", err.what());
			}
#endif
		}
		catch (...)
		{
			set_error(error_code(), torrent_status::error_file_exception);
#ifndef TORRENT_DISABLE_LOGGING
			if (should_log())
			{
				debug_log("torrent exception: unknown");
			}
#endif
		}
	}

	void torrent::handle_disk_error(string_view job_name
		, storage_error const& error
		, peer_connection* c
		, disk_class rw)
	{
		TORRENT_UNUSED(job_name);
		TORRENT_ASSERT(is_single_thread());
		TORRENT_ASSERT(error);

#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			debug_log("disk error: (%d) %s [%*s : %s] in file: %s"
				, error.ec.value(), error.ec.message().c_str()
				, int(job_name.size()), job_name.data()
				, operation_name(error.operation)
				, resolve_filename(error.file()).c_str());
		}
#endif

		if (error.ec == boost::system::errc::not_enough_memory)
		{
			if (alerts().should_post<file_error_alert>())
				alerts().emplace_alert<file_error_alert>(error.ec
					, resolve_filename(error.file()), error.operation, get_handle());
			if (c) c->disconnect(errors::no_memory, error.operation);
			return;
		}

		if (error.ec == boost::asio::error::operation_aborted) return;

		// notify the user of the error
		if (alerts().should_post<file_error_alert>())
			alerts().emplace_alert<file_error_alert>(error.ec
				, resolve_filename(error.file()), error.operation, get_handle());

		// if a write operation failed, and future writes are likely to
		// fail, while reads may succeed, just set the torrent to upload mode
		// if we make an incorrect assumption here, it's not the end of the
		// world, if we ever issue a read request and it fails as well, we
		// won't get in here and we'll actually end up pausing the torrent
		if (rw == disk_class::write
			&& (error.ec == boost::system::errc::read_only_file_system
			|| error.ec == boost::system::errc::permission_denied
			|| error.ec == boost::system::errc::operation_not_permitted
			|| error.ec == boost::system::errc::no_space_on_device
			|| error.ec == boost::system::errc::file_too_large))
		{
			// if we failed to write, stop downloading and just
			// keep seeding.
			// TODO: 1 make this depend on the error and on the filesystem the
			// files are being downloaded to. If the error is no_space_left_on_device
			// and the filesystem doesn't support sparse files, only zero the priorities
			// of the pieces that are at the tails of all files, leaving everything
			// up to the highest written piece in each file
			set_upload_mode(true);
			return;
		}

		// put the torrent in an error-state
		set_error(error.ec, error.file());

		// if the error appears to be more serious than a full disk, just pause the torrent
		pause();
	}

	void torrent::handle_inconsistent_hashes(piece_index_t const piece)
	{
		auto const file_slices = torrent_file().map_block(piece, 0, 0);
		file_index_t const file = file_slices.empty() ? torrent_status::error_file_none : file_slices[0].file_index;
		set_error(errors::torrent_inconsistent_hashes, file);
		// if this is a hybrid torrent, we may have marked some more pieces
		// as "have" but not yet validated them against the v2 hashes. At
		// this point, just assume we have no pieces
		m_picker.reset();
		m_hash_picker.reset();
		m_file_progress.clear();
		set_have_all(false);
		update_gauge();
		pause();
	}

	void torrent::on_piece_fail_sync(piece_index_t const piece, piece_block) try
	{
		if (m_abort) return;

		// the user may have called force_recheck, which clears
		// the piece picker
		if (has_picker())
		{
			// unlock the piece and restore it, as if no block was
			// ever downloaded for it.
			m_picker->restore_piece(piece);
		}

		update_gauge();
		// some peers that previously was no longer interesting may
		// now have become interesting, since we lack this one piece now.
		for (auto i = begin(); i != end();)
		{
			peer_connection* p = *i;
			// update_interest may disconnect the peer and
			// invalidate the iterator
			++i;
			// no need to do anything with peers that
			// already are interested. Gaining a piece may
			// only make uninteresting peers interesting again.
			if (p->is_interesting()) continue;
			p->update_interest();
			if (!m_abort)
			{
				if (request_a_block(*this, *p))
					inc_stats_counter(counters::hash_fail_piece_picks);
				p->send_block_requests();
			}
		}
	}
	catch (...) { handle_exception(); }

	void torrent::on_disk_read_complete(disk_buffer_holder buffer
		, storage_error const& se
		, peer_request const&  r, std::shared_ptr<read_piece_struct> rp) try
	{
		// hold a reference until this function returns
		TORRENT_ASSERT(is_single_thread());

		--rp->blocks_left;
		if (se)
		{
			rp->fail = true;
			rp->error = se.ec;
			handle_disk_error("read", se);
		}
		else
		{
			std::memcpy(rp->piece_data.get() + r.start, buffer.data(), aux::numeric_cast<std::size_t>(r.length));
		}

		if (rp->blocks_left == 0)
		{
			int size = piece_size_for_req(r.piece);
			if (rp->fail)
			{
				m_ses.alerts().emplace_alert<read_piece_alert>(
					get_handle(), r.piece, rp->error);
			}
			else
			{
				m_ses.alerts().emplace_alert<read_piece_alert>(
					get_handle(), r.piece, rp->piece_data, size);
			}
		}
	}
	catch (...) { handle_exception(); }

	storage_mode_t torrent::storage_mode() const
	{ return storage_mode_t(m_storage_mode); }

	void torrent::clear_peers()
	{
		disconnect_all(error_code(), operation_t::unknown);
		if (m_peer_list) m_peer_list->clear();
	}

	void torrent::set_sequential_range(piece_index_t first_piece, piece_index_t last_piece)
	{
		if (!has_picker()) {
			if (!valid_metadata() || !m_connections_initialized) return;
			else if (!m_have_all
				|| settings().get_int(settings_pack::suggest_mode)
				== settings_pack::suggest_read_cache)
				need_picker();
			else return;
		}
		if (first_piece > m_torrent_file->last_piece() || last_piece > m_torrent_file->last_piece()) return;
		TORRENT_ASSERT_PRECOND(first_piece <= m_torrent_file->last_piece() && last_piece <= m_torrent_file->last_piece());
		m_sequential_download = true;
		m_picker->set_sequential_range(first_piece, last_piece);
	}

	void torrent::set_sequential_range(piece_index_t first_piece)
	{
		if (!has_picker()) {
			if (!valid_metadata() || !m_connections_initialized) return;
			else if (!m_have_all
				|| settings().get_int(settings_pack::suggest_mode)
				== settings_pack::suggest_read_cache)
				need_picker();
			else return;
		}
		// find the last file in piece `first_piece`
		auto const f = m_torrent_file->layout().last_file_index_at_piece(first_piece);
		// find the last piece in file `f`
		auto const last_piece = m_torrent_file->layout().last_piece_index_at_file(f);
		TORRENT_ASSERT(first_piece <= last_piece);
		set_sequential_range(first_piece, last_piece);
	}

	void torrent::need_picker()
	{
		if (m_picker) return;

		TORRENT_ASSERT(valid_metadata());
		TORRENT_ASSERT(m_connections_initialized);

		INVARIANT_CHECK;

		// if we have all pieces we should not have a picker
		// unless we're in suggest mode
		TORRENT_ASSERT(!m_have_all
			|| settings().get_int(settings_pack::suggest_mode)
			== settings_pack::suggest_read_cache);

		auto pp = std::make_unique<piece_picker>(m_torrent_file->total_size()
			, m_torrent_file->piece_length());

		if (m_have_all) pp->we_have_all();

		// initialize the file progress too
		if (m_file_progress.empty())
			m_file_progress.init(*pp, m_torrent_file->layout());


		if (!m_file_priority.empty())
		{
			auto const pieces = file_to_piece_prio(m_torrent_file->layout(), m_file_priority);
			piece_index_t index(0);
			for (auto prio : pieces)
			{
				TORRENT_ASSERT(prio <= top_priority);
				pp->set_piece_priority(index, prio);
				++index;
			}
		}

		m_picker = std::move(pp);

		update_gauge();

		for (auto* const p : m_connections)
		{
			TORRENT_INCREMENT(m_iterating_connections);
			if (p->is_disconnecting()) continue;
			peer_has(p->get_bitfield(), p);
		}
	}

	void torrent::need_hash_picker()
	{
		if (m_hash_picker) return;

		TORRENT_ASSERT(valid_metadata());
		TORRENT_ASSERT(m_connections_initialized);

		//INVARIANT_CHECK;

		m_hash_picker = std::make_unique<hash_picker>(m_torrent_file->layout()
			, m_merkle_trees);
	}

	struct piece_refcount
	{
		piece_refcount(piece_picker& p, piece_index_t piece)
			: m_picker(p)
			, m_piece(piece)
		{
			m_picker.inc_refcount(m_piece, nullptr);
		}

		piece_refcount(piece_refcount const&) = delete;
		piece_refcount& operator=(piece_refcount const&) = delete;

		void disarm() { m_armed = false; }

		~piece_refcount()
		{
			if (m_armed)
				m_picker.dec_refcount(m_piece, nullptr);
		}

	private:
		piece_picker& m_picker;
		piece_index_t m_piece;
		bool m_armed = true;
	};

	void torrent::add_piece_async(piece_index_t const piece
		, std::vector<char> data, add_piece_flags_t const flags)
	{
		TORRENT_ASSERT(is_single_thread());

		TORRENT_ASSERT_PRECOND(piece < torrent_file().end_piece());
		TORRENT_ASSERT_PRECOND(piece >= piece_index_t{0});
		TORRENT_ASSERT_PRECOND(data.size() == std::size_t(piece_size_for_req(piece)));

		// make sure the piece index is correct
		if (piece >= torrent_file().end_piece())
			return;

		// make sure the piece size is correct
		if (data.size() != std::size_t(piece_size_for_req(piece)))
			return;

		add_piece(piece, data.data(), flags);
	}

	// TODO: 3 there's some duplication between this function and
	// peer_connection::incoming_piece(). is there a way to merge something?
	void torrent::add_piece(piece_index_t const piece, char const* data
		, add_piece_flags_t const flags)
	{
		TORRENT_ASSERT(is_single_thread());

		// make sure the piece index is correct
		if (piece >= torrent_file().end_piece())
			return;

		int const piece_size = piece_size_for_req(piece);
		int const blocks_in_piece = (piece_size + block_size() - 1) / block_size();

		if (m_deleted) return;

		// avoid crash trying to access the picker when there is none
		if (m_have_all && !has_picker()) return;

		// we don't support clobbering the piece picker while checking the
		// files. We may end up having the same piece multiple times
		TORRENT_ASSERT_PRECOND(state() != torrent_status::checking_files
			&& state() != torrent_status::checking_resume_data);
		if (state() == torrent_status::checking_files
			|| state() == torrent_status::checking_resume_data)
			return;

		need_picker();

		if (picker().have_piece(piece)
			&& !(flags & torrent_handle::overwrite_existing))
			return;

		peer_request p;
		p.piece = piece;
		p.start = 0;
		piece_refcount refcount{picker(), piece};
		auto self = shared_from_this();
		for (int i = 0; i < blocks_in_piece; ++i, p.start += block_size())
		{
			piece_block const block(piece, i);

			bool const finished = picker().is_finished(block);

			// if this block is already finished, only resume if we have the
			// flag set to overwrite existing data.
			if (!(flags & torrent_handle::overwrite_existing) && finished)
				continue;

			bool const downloaded = picker().is_downloaded(block);

			// however, if the block is downloaded by not written to disk yet,
			// we can't (easily) replace it. We would have to synchronize with
			// the disk in a clear_piece() call. Instead, just ignore such
			// blocks.
			if (downloaded && !finished)
				continue;

			p.length = std::min(piece_size - p.start, block_size());

			m_stats_counters.inc_stats_counter(counters::queued_write_bytes, p.length);

			disk_job_flags_t dflags{};

			auto const write_mode = settings().get_int(settings_pack::disk_io_write_mode);
			if (write_mode == settings_pack::disable_os_cache)
				dflags |= disk_interface::flush_piece | disk_interface::volatile_read;

#ifndef TORRENT_DISABLE_LOGGING
			debug_log("*** add_piece [ piece: %d | block: %d ]"
				, static_cast<int>(piece), i);
#endif
			m_ses.disk_thread().async_write(m_storage, p, data + p.start, nullptr
				, [self, p](storage_error const& error) { self->on_disk_write_complete(error, p); }
				, dflags);

			bool const was_finished = picker().is_piece_finished(p.piece);
			bool const multi = picker().num_peers(block) > 1;

			picker().mark_as_downloading(block, nullptr);
			picker().mark_as_writing(block, nullptr);

			if (multi) cancel_block(block);

			// did we just finish the piece?
			// this means all blocks are either written
			// to disk or are in the disk write cache
			if (picker().is_piece_finished(p.piece) && !was_finished)
			{
				verify_piece(p.piece);
			}
		}
		m_ses.deferred_submit_jobs();
		// if we don't have a picker anymore, we don't need to (and shouldn't)
		// decrement the refcount
		if (!m_picker)
			refcount.disarm();
	}

	void torrent::on_disk_write_complete(storage_error const& error
		, peer_request const& p) try
	{
		TORRENT_ASSERT(is_single_thread());

		m_stats_counters.inc_stats_counter(counters::queued_write_bytes, -p.length);

//		std::fprintf(stderr, "torrent::on_disk_write_complete ret:%d piece:%d block:%d\n"
//			, j->ret, j->piece, j->offset/0x4000);

		INVARIANT_CHECK;
		if (m_abort) return;
		piece_block const block_finished(p.piece, p.start / block_size());

		if (error)
		{
			handle_disk_error("write", error);
			return;
		}

		if (!has_picker()) return;

		// if we already have this block, just ignore it.
		// this can happen if the same block is passed in through
		// add_piece() multiple times
		if (picker().is_finished(block_finished)) return;

		picker().mark_as_finished(block_finished, nullptr);
		maybe_done_flushing();

		if (alerts().should_post<block_finished_alert>())
		{
			alerts().emplace_alert<block_finished_alert>(get_handle(),
				tcp::endpoint(), peer_id(), block_finished.block_index
				, block_finished.piece_index);
		}
	}
	catch (...) { handle_exception(); }

	peer_request torrent::to_req(piece_block const& p) const
	{
		return to_req(p, piece_size_for_req(p.piece_index));
	}

	// piece_size must be the value of piece_size_for_req(p.piece_index). This
	// overload exists so callers in hot paths can cache piece_size_for_req()
	// across multiple to_req() calls for the same piece, since
	// piece_size_for_req() is O(log files) for v2-only torrents.
	peer_request torrent::to_req(piece_block const& p, int const piece_size) const
	{
		TORRENT_ASSERT(piece_size == piece_size_for_req(p.piece_index));
		int const block_offset = p.block_index * block_size();
		int const block = std::min(piece_size - block_offset, block_size());
		TORRENT_ASSERT(block > 0);
		TORRENT_ASSERT(block <= block_size());

		peer_request r;
		r.piece = p.piece_index;
		r.start = block_offset;
		r.length = block;
		return r;
	}

	std::string torrent::name() const
	{
		if (valid_metadata()) return m_torrent_file->name();
		if (m_name) return *m_name;
		return "";
	}

	aux::allocation_slot torrent::name_idx(aux::stack_allocator& a)
	{
		return m_name_idx.copy_string(a, [this]() -> std::string {
			if (valid_metadata()) return m_torrent_file->name();
			if (m_name) return *m_name;
			if (m_info_hash.has_v2())
				return aux::to_hex(m_info_hash.v2);
			else
				return aux::to_hex(m_info_hash.v1);
		});
	}

#ifndef TORRENT_DISABLE_EXTENSIONS

	void torrent::add_extension(std::shared_ptr<torrent_plugin> ext)
	{
		m_extensions.push_back(std::move(ext));
		auto& ext_ref = m_extensions.back();

		for (auto* p : m_connections)
		{
			TORRENT_INCREMENT(m_iterating_connections);
			std::shared_ptr<peer_plugin> pp(ext_ref->new_connection(peer_connection_handle(p->self())));
			if (pp) p->add_extension(std::move(pp));
		}

		// if files are checked for this torrent, call the extension
		// to let it initialize itself
		if (m_connections_initialized)
			ext_ref->on_files_checked();
	}

	void torrent::remove_extension(std::shared_ptr<torrent_plugin> ext)
	{
		auto const i = std::find(m_extensions.begin(), m_extensions.end(), ext);
		if (i == m_extensions.end()) return;
		m_extensions.erase(i);
	}

	void torrent::add_extension_fun(std::function<std::shared_ptr<torrent_plugin>(torrent_handle const&, client_data_t)> const& ext
		, client_data_t userdata)
	{
		std::shared_ptr<torrent_plugin> tp(ext(get_handle(), userdata));
		if (!tp) return;

		add_extension(std::move(tp));
	}

#endif

#ifdef TORRENT_SSL_PEERS
	bool torrent::verify_peer_cert(bool const preverified, ssl::verify_context& ctx)
	{
		// if the cert wasn't signed by the correct CA, fail the verification
		if (!preverified) return false;

		std::string expected = m_torrent_file->name();
#ifndef TORRENT_DISABLE_LOGGING
		std::string names;
		bool match = false;
#endif

#ifdef TORRENT_USE_OPENSSL
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#pragma clang diagnostic ignored "-Wused-but-marked-unused"
#pragma clang diagnostic ignored "-Wold-style-cast"
#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant"
#endif
		// we're only interested in checking the certificate at the end of the chain.
		// any certificate that isn't the leaf (i.e. the one presented by the peer)
		// should be accepted automatically, given preverified is true. The leaf certificate
		// need to be verified to make sure its DN matches the info-hash
		int depth = X509_STORE_CTX_get_error_depth(ctx.native_handle());
		if (depth > 0) return true;

		X509* cert = X509_STORE_CTX_get_current_cert(ctx.native_handle());

		// Go through the alternate names in the certificate looking for matching DNS entries
		auto* gens = static_cast<GENERAL_NAMES*>(
			X509_get_ext_d2i(cert, NID_subject_alt_name, nullptr, nullptr));

		for (int i = 0; i < sk_GENERAL_NAME_num(gens); ++i)
		{
			GENERAL_NAME* gen = sk_GENERAL_NAME_value(gens, i);
			if (gen->type != GEN_DNS) continue;
			ASN1_IA5STRING* domain = gen->d.dNSName;
			if (ASN1_STRING_type(domain) != V_ASN1_IA5STRING || !ASN1_STRING_get0_data(domain) || !ASN1_STRING_length(domain)) continue;
			string_view const torrent_name(
				reinterpret_cast<char const*>(ASN1_STRING_get0_data(domain)),
				aux::numeric_cast<std::size_t>(ASN1_STRING_length(domain))
			);

#ifndef TORRENT_DISABLE_LOGGING
			if (i > 1) names += " | n: ";
			names.append(torrent_name.data(), torrent_name.size());
#endif
			if (torrent_name == "*"_sv || torrent_name == expected)
			{
#ifndef TORRENT_DISABLE_LOGGING
				match = true;
				// if we're logging, keep looping over all names,
				// for completeness of the log
				continue;
#else
				return true;
#endif
			}
		}

		// no match in the alternate names, so try the common names. We should only
		// use the "most specific" common name, which is the last one in the list.
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
		X509_NAME const* name = X509_get_subject_name(cert);
#else
		X509_NAME* name = X509_get_subject_name(cert);
#endif
		int i = -1;
		ASN1_STRING const* common_name = nullptr;
		while ((i = X509_NAME_get_index_by_NID(name, NID_commonName, i)) >= 0)
		{
			X509_NAME_ENTRY const* name_entry = X509_NAME_get_entry(name, i);
			common_name = X509_NAME_ENTRY_get_data(name_entry);
		}
		if (common_name && ASN1_STRING_get0_data(common_name) && ASN1_STRING_length(common_name))
		{
			string_view const torrent_name(
				reinterpret_cast<char const*>(ASN1_STRING_get0_data(common_name)),
				aux::numeric_cast<std::size_t>(ASN1_STRING_length(common_name))
			);

#ifndef TORRENT_DISABLE_LOGGING
			if (!names.empty()) names += " | n: ";
			names.append(torrent_name.data(), torrent_name.size());
#endif
			if (torrent_name == "*"_sv || torrent_name == expected)
			{
#ifdef TORRENT_DISABLE_LOGGING
				return true;
#else
				match = true;
#endif

			}
		}
#ifdef __clang__
#pragma clang diagnostic pop
#endif

#elif defined TORRENT_USE_GNUTLS
		gnutls_x509_crt_t cert = ctx.native_handle();

		// We don't use gnutls_x509_crt_check_hostname()
		// as it doesn't handle wildcards the way we need here

		char buf[256];
		unsigned int seq = 0;
		while(true) {
			size_t len = sizeof(buf);
			int ret = gnutls_x509_crt_get_subject_alt_name(cert, seq, buf, &len, nullptr);
			if(ret == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) break;
			if(ret == GNUTLS_E_SUCCESS)
			{
				string_view const torrent_name(buf, len);
#ifndef TORRENT_DISABLE_LOGGING
				if (!names.empty()) names += " | n: ";
				names.append(torrent_name.data(), torrent_name.size());
#endif
				if (torrent_name == "*"_sv || torrent_name == expected)
				{
#ifndef TORRENT_DISABLE_LOGGING
					match = true;
					continue;
#else
					return true;
#endif
				}
			}
			++seq;
		}

		// no match in the alternate names, so try the common name
		size_t len = sizeof(buf);
		int ret = gnutls_x509_crt_get_dn_by_oid(cert, GNUTLS_OID_X520_COMMON_NAME, 0, 0, buf, &len);
		if(ret == GNUTLS_E_SUCCESS)
		{
			string_view const torrent_name(buf, len);
#ifndef TORRENT_DISABLE_LOGGING
			if (!names.empty()) names += " | n: ";
			names.append(torrent_name.data(), torrent_name.size());
#endif
			if (torrent_name == "*"_sv || torrent_name == expected)
			{
#ifdef TORRENT_DISABLE_LOGGING
				return true;
#else
				match = true;
#endif
			}
		}
#endif // TORRENT_USE_GNUTLS

#ifndef TORRENT_DISABLE_LOGGING
		debug_log("<== incoming SSL CONNECTION [ n: %s | match: %s ]"
			, names.c_str(), match?"yes":"no");
		return match;
#else
		return false;
#endif
	}

	void torrent::init_ssl(string_view cert)
	{
		// create the SSL context for this torrent. We need to
		// inject the root certificate, and no other, to
		// verify other peers against
		std::unique_ptr<ssl::context> ctx(std::make_unique<ssl::context>(ssl::context::tls));

		ctx->set_options(ssl::context::default_workarounds | ssl::context::single_dh_use
			| ssl::no_legacy_tls_versions);

		error_code ec;
		ctx->set_verify_mode(ssl::context::verify_peer
			| ssl::context::verify_fail_if_no_peer_cert
			| ssl::context::verify_client_once, ec);
		if (ec)
		{
			set_error(ec, torrent_status::error_file_ssl_ctx);
			pause();
			return;
		}

		// the verification function verifies the distinguished name
		// of a peer certificate to make sure it matches the info-hash
		// of the torrent, or that it's a "star-cert"
		ctx->set_verify_callback(
				std::bind(&torrent::verify_peer_cert, this, _1, _2)
				, ec);
		if (ec)
		{
			set_error(ec, torrent_status::error_file_ssl_ctx);
			pause();
			return;
		}

		// set the root certificate as trust
		ssl::set_trust_certificate(ctx->native_handle(), cert, ec);
		if (ec)
		{
			set_error(ec, torrent_status::error_file_ssl_ctx);
			pause();
			return;
		}

#if 0
		char filename[100];
		std::snprintf(filename, sizeof(filename), "/tmp/%u.pem", random());
		FILE* f = fopen(filename, "w+");
		fwrite(cert.c_str(), cert.size(), 1, f);
		fclose(f);
		ctx->load_verify_file(filename);
#endif

		// if all went well, set the torrent ssl context to this one
		m_ssl_ctx = std::move(ctx);
		// tell the client we need a cert for this torrent
		alerts().emplace_alert<torrent_need_cert_alert>(get_handle());
	}
#endif // TORRENT_SSL_PEERS

	void torrent::construct_storage()
	{
		// lock in the v2-only mode at storage-construction time so that a later
		// change to the disable_v1_hashes setting doesn't cause a mismatch
		// between the storage (set up as v2-only) and the engine's block-sizing.
		m_disable_v1_hashes = (m_add_torrent_params
			&& bool(m_add_torrent_params->flags & torrent_flags::disable_v1_hashes))
			&& m_torrent_file->v2();
		storage_params params{
			m_torrent_file->files_impl(),
			m_renamed_files,
			m_save_path,
			m_part_file_dir,
			static_cast<storage_mode_t>(m_storage_mode),
			m_file_priority,
			m_info_hash.get_best(),
			m_torrent_file->v1() && !m_disable_v1_hashes,
			m_torrent_file->v2()
		};

		// the shared_from_this() will create an intentional
		// cycle of ownership, se the hpp file for description.
		m_storage = m_ses.disk_thread().new_torrent(params, shared_from_this());
	}

	peer_connection* torrent::find_lowest_ranking_peer() const
	{
		auto lowest_rank = end();
		for (auto i = begin(); i != end(); ++i)
		{
			// disconnecting peers don't count
			if ((*i)->is_disconnecting()) continue;
			if (lowest_rank == end() || (*lowest_rank)->peer_rank() > (*i)->peer_rank())
				lowest_rank = i;
		}

		if (lowest_rank == end()) return nullptr;
		return *lowest_rank;
	}

	// this may not be called from a constructor because of the call to
	// shared_from_this(). It's either called when we start() the torrent, or at a
	// later time if it's a magnet link, once the metadata is downloaded
	void torrent::init()
	{
		INVARIANT_CHECK;

		TORRENT_ASSERT(is_single_thread());

#ifndef TORRENT_DISABLE_LOGGING
		debug_log("init torrent: %s", torrent_file().name().c_str());
#endif

		TORRENT_ASSERT(valid_metadata());
		TORRENT_ASSERT(m_torrent_file->num_files() > 0);
		TORRENT_ASSERT(m_torrent_file->total_size() >= 0);

		if (int(m_file_priority.size()) > m_torrent_file->num_files())
			m_file_priority.resize(m_torrent_file->num_files());
		else if (m_add_torrent_params->flags & torrent_flags::default_dont_download)
			m_file_priority.resize(m_torrent_file->num_files(), dont_download);

		std::string_view cert = !m_add_torrent_params->root_certificate.empty()
			? m_add_torrent_params->root_certificate
			: m_torrent_file->ssl_cert();
		if (!is_ssl_torrent() && !cert.empty())
		{
			m_ssl_torrent = true;
#ifdef TORRENT_SSL_PEERS
			init_ssl(cert);
#endif
		}

		if (m_torrent_file->num_pieces() > piece_picker::max_pieces)
		{
			set_error(errors::too_many_pieces_in_torrent, torrent_status::error_file_none);
			pause();
			return;
		}

		if (m_torrent_file->num_pieces() == 0)
		{
			set_error(errors::torrent_invalid_length, torrent_status::error_file_none);
			pause();
			return;
		}

		int const blocks_per_piece
			= (m_torrent_file->piece_length() + default_block_size - 1) / default_block_size;
		if (blocks_per_piece > piece_picker::max_blocks_per_piece)
		{
			set_error(errors::invalid_piece_size, torrent_status::error_file_none);
			pause();
			return;
		}

		// --- MAPPED FILES ---
		file_storage const& fs = m_torrent_file->layout();
		if (m_add_torrent_params)
		{
			m_renamed_files.import_filenames(m_torrent_file->layout()
				, m_add_torrent_params->renamed_files);
		}

		construct_storage();

#ifndef TORRENT_DISABLE_SHARE_MODE
		if (m_share_mode && valid_metadata())
		{
			// in share mode, all pieces have their priorities initialized to 0
			m_file_priority.clear();
			m_file_priority.resize(m_torrent_file->num_files(), dont_download);
		}
#endif

		// it's important to initialize the peers early, because this is what will
		// fix up their have-bitmasks to have the correct size
		// TODO: 2 add a unit test where we don't have metadata, connect to a peer
		// that sends a bitfield that's too large, then we get the metadata
		if (!m_connections_initialized)
		{
			m_connections_initialized = true;
			// all peer connections have to initialize themselves now that the metadata
			// is available
			// copy the peer list since peers may disconnect and invalidate
			// m_connections as we initialize them
			for (auto* const c : m_connections)
			{
				auto pc = c->self();
				if (pc->is_disconnecting()) continue;
				pc->on_metadata_impl();
				if (pc->is_disconnecting()) continue;
				pc->init();
			}
		}

		// in case file priorities were passed in via the add_torrent_params
		// and also in the case of share mode, we need to update the priorities
		// this has to be applied before piece priority
		if (!m_file_priority.empty())
		{
			// m_file_priority was loaded from the resume data, this doesn't
			// alter any state that needs to be saved in the resume data
			auto const ns = m_need_save_resume_data;
			update_piece_priorities(m_file_priority);
			m_need_save_resume_data = ns;
		}

		if (m_add_torrent_params)
		{
			piece_index_t idx(0);
			if (m_add_torrent_params->piece_priorities.size() > std::size_t(m_torrent_file->num_pieces()))
				m_add_torrent_params->piece_priorities.resize(std::size_t(m_torrent_file->num_pieces()));

			for (auto prio : m_add_torrent_params->piece_priorities)
			{
				if (has_picker() || prio != default_priority)
				{
					need_picker();
					m_picker->set_piece_priority(idx, prio);
				}
				++idx;
			}
			update_gauge();
		}

		if (m_seed_mode)
		{
			set_have_all(true);
			update_gauge();
			update_state_list();
			update_want_tick();
		}
		else
		{
			need_picker();

			TORRENT_ASSERT(block_size() > 0);

			m_padding_bytes = 0;
			std::vector<piece_index_t> have_pieces;

			aux::apply_pad_files(fs, [&](piece_index_t const piece, int const bytes)
			{
				m_padding_bytes += bytes;
				if (bytes == fs.piece_size(piece))
					have_pieces.push_back(piece);
				m_picker->set_pad_bytes(piece, bytes);
			});

			for (auto i : have_pieces)
			{
				// we may have this piece already, if we picked it up from
				// resume data.
				if (m_picker->have_piece(i)) continue;
//				picker().piece_passed(i);
//				TORRENT_ASSERT(picker().have_piece(i));
				we_have(i);
			}
		}

		set_state(torrent_status::checking_resume_data);

		aux::vector<std::string, file_index_t> links;
#ifndef TORRENT_DISABLE_MUTABLE_TORRENTS
		if (!m_torrent_file->similar_torrents().empty()
			|| !m_torrent_file->collections().empty())
		{
			resolve_links res(m_torrent_file);

			for (auto const& ih : m_torrent_file->similar_torrents())
			{
				std::shared_ptr<torrent> t = m_ses.find_torrent(info_hash_t(ih)).lock();
				if (!t) continue;

				// Only attempt to reuse files from torrents that are seeding.
				// TODO: this could be optimized by looking up which files are
				// complete and just look at those
				if (!t->is_seed()) continue;

				auto ti = t->get_torrent_file();
				res.match(*ti, filenames(ti->layout(), t->m_renamed_files), t->save_path());
			}
			for (auto const& c : m_torrent_file->collections())
			{
				std::vector<std::shared_ptr<torrent>> ts = m_ses.find_collection(c);

				for (auto const& t : ts)
				{
					// Only attempt to reuse files from torrents that are seeding.
					// TODO: this could be optimized by looking up which files are
					// complete and just look at those
					if (!t->is_seed()) continue;

					auto ti = t->get_torrent_file();
					res.match(*ti, filenames(ti->layout(), t->m_renamed_files), t->save_path());
				}
			}

			links = std::move(res).get_links();
		}
#endif // TORRENT_DISABLE_MUTABLE_TORRENTS

#if TORRENT_USE_ASSERTS
		TORRENT_ASSERT(m_outstanding_check_files == false);
		m_outstanding_check_files = true;
#endif

		if (!m_add_torrent_params || !(m_add_torrent_params->flags & torrent_flags::no_verify_files))
		{
			m_ses.disk_thread().async_check_files(
				m_storage, m_add_torrent_params ? m_add_torrent_params.get() : nullptr
				, std::move(links), [self = shared_from_this()](status_t st, storage_error const& error)
				{ self->on_resume_data_checked(st, error); });
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("init, async_check_files");
#endif
			m_ses.deferred_submit_jobs();
		}
		else
		{
			on_resume_data_checked({}, storage_error{});
		}

		update_want_peers();
		update_want_tick();

		// this will remove the piece picker, if we're done with it
		maybe_done_flushing();

		m_torrent_initialized = true;
	}

	bt_peer_connection* torrent::find_introducer(tcp::endpoint const& ep) const
	{
#ifndef TORRENT_DISABLE_EXTENSIONS
		for (auto* pe : m_connections)
		{
			TORRENT_INCREMENT(m_iterating_connections);
			if (pe->type() != connection_type::bittorrent) continue;
			auto* p = static_cast<bt_peer_connection*>(pe);
			if (!p->supports_holepunch()) continue;
			if (p->was_introduced_by(ep)) return p;
		}
#else
		TORRENT_UNUSED(ep);
#endif
		return nullptr;
	}

	bt_peer_connection* torrent::find_peer(tcp::endpoint const& ep) const
	{
		for (auto* p : m_connections)
		{
			TORRENT_INCREMENT(m_iterating_connections);
			if (p->type() != connection_type::bittorrent) continue;
			if (p->remote() == ep) return static_cast<bt_peer_connection*>(p);
		}
		return nullptr;
	}

	peer_connection* torrent::find_peer(peer_id const& pid)
	{
		for (auto* p : m_connections)
		{
			if (p->pid() == pid) return p;
		}
		return nullptr;
	}

	bool torrent::is_self_connection(peer_id const& pid) const
	{
		return m_outgoing_pids.count(pid) > 0;
	}

	void torrent::on_resume_data_checked(status_t const status
		, storage_error const& error) try
	{
#if TORRENT_USE_ASSERTS
		TORRENT_ASSERT(m_outstanding_check_files);
		m_outstanding_check_files = false;
#endif

		// when applying some of the resume data to the torrent, we will
		// trigger calls that set m_need_save_resume_data, even though we're
		// just applying the state of the resume data we loaded with. We don't
		// want anything in this function to affect the state of
		// m_need_save_resume_data, so we save it in a local variable and reset
		// it at the end of the function.
		auto const need_save_resume_data = m_need_save_resume_data;

		TORRENT_ASSERT(is_single_thread());

		if (m_abort) return;

		if (status & disk_status::oversized_file)
		{
			if (m_ses.alerts().should_post<oversized_file_alert>())
				m_ses.alerts().emplace_alert<oversized_file_alert>(get_handle());
		}

		if (status & disk_status::fatal_disk_error)
		{
			TORRENT_ASSERT(m_outstanding_check_files == false);
			handle_disk_error("check_resume_data", error);
			auto_managed(false);
			pause();
		}

		state_updated();

		if (m_add_torrent_params)
		{
			// --- PEERS ---

			for (auto const& p : m_add_torrent_params->peers)
			{
				add_peer(p , peer_info::resume_data);
			}

#ifndef TORRENT_DISABLE_LOGGING
			if (should_log() && !m_add_torrent_params->peers.empty())
			{
				std::string str;
				for (auto const& peer : m_add_torrent_params->peers)
				{
					str += peer.address().to_string();
					str += ' ';
				}
				debug_log("resume-checked add_peer() [ %s] connect-candidates: %d"
					, str.c_str(), m_peer_list
					? m_peer_list->num_connect_candidates() : -1);
			}
#endif

			for (auto const& p : m_add_torrent_params->banned_peers)
			{
				torrent_peer* peer = add_peer(p, peer_info::resume_data);
				if (peer) ban_peer(peer);
			}

			if (!m_add_torrent_params->peers.empty()
				|| !m_add_torrent_params->banned_peers.empty())
			{
				update_want_peers();
			}

#ifndef TORRENT_DISABLE_LOGGING
			if (m_peer_list && m_peer_list->num_peers() > 0)
				debug_log("resume added peers (total peers: %d)"
					, m_peer_list->num_peers());
#endif
		}

		bool const has_error_status
			= (status & disk_status::fatal_disk_error)
			|| (status & disk_status::need_full_check)
			|| (status & disk_status::file_exist);

		// only report this error if the user actually provided resume data
		// (i.e. m_add_torrent_params->have_pieces)
		if ((error || has_error_status)
			&& m_add_torrent_params
			&& aux::contains_resume_data(*m_add_torrent_params)
			&& m_ses.alerts().should_post<fastresume_rejected_alert>())
		{
			m_ses.alerts().emplace_alert<fastresume_rejected_alert>(get_handle()
				, error.ec
				, resolve_filename(error.file())
				, error.operation);
		}

#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			if (has_error_status || error)
			{
				debug_log("fastresume data rejected: ret: %d (%d) op: %s file: %d %s"
					, int(static_cast<std::uint8_t>(status)), error.ec.value()
					, operation_name(error.operation)
					, static_cast<int>(error.file())
					, error.ec.message().c_str());
			}
			else
			{
				debug_log("fastresume data accepted");
			}
		}
#endif

		bool should_start_full_check = has_error_status;

		// if we got a partial pieces bitfield, it means we were in the middle of
		// checking this torrent. pick it up where we left off
		if (!has_error_status
			&& m_add_torrent_params
			&& !m_add_torrent_params->have_pieces.empty()
			&& m_add_torrent_params->have_pieces.size() < m_torrent_file->num_pieces())
		{
			m_checking_piece = m_num_checked_pieces
				= m_add_torrent_params->have_pieces.end_index();
			should_start_full_check = true;
		}

		// if ret != 0, it means we need a full check. We don't necessarily need
		// that when the resume data check fails. For instance, if the resume data
		// is incorrect, but we don't have any files, we skip the check and initialize
		// the storage to not have anything.
		if (!has_error_status)
		{
			// there are either no files for this torrent
			// or the resume_data was accepted

			if (m_seed_mode)
			{
				set_have_all(true);
				update_gauge();
				update_state_list();

				if (!error && m_add_torrent_params)
				{
					int const num_pieces2 = std::min(m_add_torrent_params->verified_pieces.size()
						, torrent_file().num_pieces());
					for (piece_index_t i{0}; i < piece_index_t(num_pieces2); ++i)
					{
						if (!m_add_torrent_params->verified_pieces[i]) continue;
						m_verified.set_bit(i);
					}
				}
			}
			else if (!error && m_add_torrent_params)
			{
				// --- PIECES ---

				int const num_pieces = std::min(m_add_torrent_params->have_pieces.size()
					, torrent_file().num_pieces());
				for (piece_index_t i{0}; i < piece_index_t(num_pieces); ++i)
				{
					if (!m_add_torrent_params->have_pieces[i]) continue;
					need_picker();
					m_picker->piece_flushed(i);
					inc_stats_counter(counters::num_piece_passed);
					update_gauge();
					we_have(i, true);
				}

				// --- UNFINISHED PIECES ---

				int const num_blocks_per_piece = torrent_file().blocks_per_piece();

				for (auto const& p : m_add_torrent_params->unfinished_pieces)
				{
					piece_index_t const piece = p.first;
					bitfield const& blocks = p.second;

					if (piece < piece_index_t(0) || piece >= torrent_file().end_piece())
					{
						continue;
					}

					if (have_piece(piece))
						continue;

					// being in seed mode and missing a piece is not compatible.
					// Leave seed mode if that happens
					if (m_seed_mode) leave_seed_mode(seed_mode_t::skip_checking);

					if (has_picker() && m_picker->have_piece(piece))
					{
						m_picker->we_dont_have(piece);
						update_gauge();
					}

					need_picker();

					const int num_bits = std::min(num_blocks_per_piece, int(blocks.size()));
					for (int k = 0; k < num_bits; ++k)
					{
						if (blocks.get_bit(k))
						{
							m_picker->mark_as_finished(piece_block(piece, k), nullptr);
						}
					}
					if (m_picker->is_piece_finished(piece))
					{
						verify_piece(piece);
					}
				}
			}
		}
		else
		{
			m_seed_mode = false;
			// either the fastresume data was rejected or there are
			// some files
			set_have_all(false);
			update_gauge();
			update_state_list();
		}

		if (should_start_full_check)
		{
			stop_announcing();
			set_state(torrent_status::checking_files);
			if (should_check_files()) start_checking();

			// start the checking right away (potentially)
			m_ses.trigger_auto_manage();
		}
		else
		{
			files_checked();
		}

		// this will remove the piece picker, if we're done with it
		maybe_done_flushing();
		TORRENT_ASSERT(m_outstanding_check_files == false);
		m_add_torrent_params.reset();

		// restore m_need_save_resume_data to its state when we entered this
		// function.
		m_need_save_resume_data = need_save_resume_data;
	}
	catch (...) { handle_exception(); }

	void torrent::force_recheck()
	{
		INVARIANT_CHECK;

		if (!valid_metadata()) return;

		// if the torrent is already queued to check its files
		// don't do anything
		if (should_check_files()
			|| m_state == torrent_status::checking_resume_data)
			return;

		clear_error();

		disconnect_all(errors::stopping_torrent, operation_t::bittorrent);
		stop_announcing();

		// we're checking everything anyway, no point in assuming we are a seed
		// now.
		leave_seed_mode(seed_mode_t::skip_checking);

		// forget that we have any pieces
		set_have_all(false);

		// removing the piece picker will clear the user priorities
		// instead, just clear which pieces we have
		if (m_picker)
		{
			m_picker->resize(m_torrent_file->total_size(), m_torrent_file->piece_length());

			m_file_progress.clear();
			m_file_progress.init(picker(), m_torrent_file->layout());
		}

		// resizing the picker above may have changed is_finished(), which
		// set_have_all() above didn't catch if m_have_all was already false
		update_state_timers();

		// assume that we don't have anything
		m_files_checked = false;

		update_gauge();
		update_want_tick();
		set_state(torrent_status::checking_resume_data);

		set_queue_position(last_pos);

		TORRENT_ASSERT(m_outstanding_check_files == false);
		m_add_torrent_params.reset();

		// this will clear the stat cache, to make us actually query the
		// filesystem for files again
		m_ses.disk_thread().async_release_files(m_storage);

		m_ses.disk_thread().async_check_files(m_storage, nullptr
			, {}, [self = shared_from_this()](status_t st, storage_error const& error)
			{ self->on_force_recheck(st, error); });
		m_ses.deferred_submit_jobs();
	}

	void torrent::on_force_recheck(status_t const status, storage_error const& error) try
	{
		TORRENT_ASSERT(is_single_thread());

		// hold a reference until this function returns
		state_updated();

		if (m_abort) return;

		if (status & disk_status::oversized_file)
		{
			if (m_ses.alerts().should_post<oversized_file_alert>())
				m_ses.alerts().emplace_alert<oversized_file_alert>(get_handle());
		}

		if (error)
		{
			handle_disk_error("force_recheck", error);
			return;
		}
#ifndef TORRENT_DISABLE_LOGGING
		debug_log("on_force_recheck: status: %x", static_cast<std::uint8_t>(status));
#endif
		bool const has_error_status
			= (status & disk_status::fatal_disk_error)
			|| (status & disk_status::need_full_check)
			|| (status & disk_status::file_exist);
		if (!has_error_status)
		{
			// if there are no files, just start
			files_checked();
		}
		else
		{
			m_progress_ppm = 0;
			m_checking_piece = piece_index_t(0);
			m_num_checked_pieces = piece_index_t(0);

#ifndef TORRENT_DISABLE_LOGGING
			debug_log("on_force_recheck: starting from 0");
#endif
			set_state(torrent_status::checking_files);
			if (m_auto_managed) pause(torrent_handle::graceful_pause);
			if (should_check_files()) start_checking();
			else m_ses.trigger_auto_manage();
		}
	}
	catch (...) { handle_exception(); }

	void torrent::start_checking() try
	{
		TORRENT_ASSERT(should_check_files());

		int num_outstanding = settings().get_int(settings_pack::checking_mem_usage) * block_size()
			/ m_torrent_file->piece_length();
		// if we only keep a single read operation in-flight at a time, we suffer
		// significant performance degradation. Always keep at least 4 jobs
		// outstanding per hasher thread
		int const min_outstanding
			= std::max(1, settings().get_int(settings_pack::hashing_threads)) * 2;
		if (num_outstanding < min_outstanding) num_outstanding = min_outstanding;

		// subtract the number of pieces we already have outstanding
		num_outstanding -= (static_cast<int>(m_checking_piece)
			- static_cast<int>(m_num_checked_pieces));
		if (num_outstanding <= 0) return;

		// we might already have some outstanding jobs, if we were paused and
		// resumed quickly, before the outstanding jobs completed
		if (m_checking_piece >= m_torrent_file->end_piece())
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("start_checking, checking_piece >= num_pieces. %d >= %d"
				, static_cast<int>(m_checking_piece), m_torrent_file->num_pieces());
#endif
			return;
		}

		for (int i = 0; i < num_outstanding; ++i)
		{
			if (has_picker())
			{
				// skip pieces we already have
				while (m_checking_piece < m_torrent_file->end_piece()
					&& m_picker->have_piece(m_checking_piece))
				{
					++m_checking_piece;
					++m_num_checked_pieces;
				}
			}

			if (m_checking_piece >= m_torrent_file->end_piece()) break;

			auto flags = disk_interface::sequential_access | disk_interface::volatile_read;
			if (torrent_file().info_hashes().has_v1() && !m_disable_v1_hashes)
				flags |= disk_interface::v1_hash;
			aux::vector<sha256_hash> hashes;
			if (torrent_file().info_hashes().has_v2())
				hashes.resize(torrent_file().layout().blocks_in_piece2(m_checking_piece));

			span<sha256_hash> v2_span(hashes);
			m_ses.disk_thread().async_hash(m_storage, m_checking_piece, v2_span, flags
				, [self = shared_from_this(), hashes1 = std::move(hashes)]
				(piece_index_t p, sha1_hash const& h, storage_error const& error) mutable
				{ self->on_piece_hashed(std::move(hashes1), p, h, error); });
			++m_checking_piece;
			if (m_checking_piece >= m_torrent_file->end_piece()) break;
		}
		m_ses.deferred_submit_jobs();
#ifndef TORRENT_DISABLE_LOGGING
		debug_log("start_checking, m_checking_piece: %d"
			, static_cast<int>(m_checking_piece));
#endif
	}
	catch (...) { handle_exception(); }

	// This is only used for checking of torrents. i.e. force-recheck or initial checking
	// of existing files
	void torrent::on_piece_hashed(aux::vector<sha256_hash> block_hashes
		, piece_index_t const piece, sha1_hash const& piece_hash
		, storage_error const& error) try
	{
		TORRENT_ASSERT(is_single_thread());
		INVARIANT_CHECK;

		if (m_abort) return;
		if (m_deleted) return;

		state_updated();

		++m_num_checked_pieces;

		if (error)
		{
			if (error.ec == boost::system::errc::no_such_file_or_directory
				|| error.ec == boost::asio::error::eof
				|| error.ec == lt::errors::file_too_short
#ifdef TORRENT_WINDOWS
				|| error.ec == error_code(ERROR_HANDLE_EOF, system_category())
#endif
				)
			{
				TORRENT_ASSERT(error.file() >= file_index_t(0));

				// skip this file by updating m_checking_piece to the first piece following it
				file_storage const& fl = m_torrent_file->layout();
				std::int64_t file_size = fl.file_size(error.file());
				piece_index_t last = fl.map_file(error.file(), file_size, 0).piece;
				if (m_checking_piece < last)
				{
					int diff = static_cast<int>(last) - static_cast<int>(m_checking_piece);
					m_num_checked_pieces = piece_index_t(static_cast<int>(m_num_checked_pieces) + diff);
					m_checking_piece = last;
				}
			}
			else
			{
				m_checking_piece = piece_index_t{0};
				m_num_checked_pieces = piece_index_t{0};
				if (m_ses.alerts().should_post<file_error_alert>())
					m_ses.alerts().emplace_alert<file_error_alert>(error.ec,
						resolve_filename(error.file()), error.operation, get_handle());

#ifndef TORRENT_DISABLE_LOGGING
				if (should_log())
				{
					debug_log("on_piece_hashed, fatal disk error: (%d) %s", error.ec.value()
						, error.ec.message().c_str());
				}
#endif
				auto_managed(false);
				pause();
				set_error(error.ec, error.file());

				// recalculate auto-managed torrents sooner
				// in order to start checking the next torrent
				m_ses.trigger_auto_manage();
				return;
			}
		}

		m_progress_ppm = std::uint32_t(std::int64_t(static_cast<int>(m_num_checked_pieces))
			* 1000000 / torrent_file().num_pieces());

		boost::tribool hash_passed[2]
			= { boost::indeterminate, boost::indeterminate };

		if (!settings().get_bool(settings_pack::disable_hash_checks))
		{
			if (torrent_file().info_hashes().has_v1() && !m_disable_v1_hashes)
				hash_passed[0] = piece_hash == m_torrent_file->hash_for_piece(piece);

			// if the v1 hash failed the check, don't add the v2 hashes to the
			// merkle tree. They are most likely invalid.
			if (torrent_file().info_hashes().has_v2() && !bool(hash_passed[0] == false))
			{
				hash_passed[1] = on_blocks_hashed(piece, block_hashes);
			}
		}
		else
		{
			hash_passed[0] = hash_passed[1] = true;
		}

#ifndef TORRENT_DISABLE_LOGGING
		std::stringstream hash;
		hash << piece_hash;
		debug_log("on_piece_hashed, piece: %d piece_hash: %s"
			, static_cast<int>(piece), hash.str().c_str());
#endif
		if ((hash_passed[0] && !hash_passed[1]) || (!hash_passed[0] && hash_passed[1]))
		{
			handle_inconsistent_hashes(piece);
			return;
		}
		else if (hash_passed[0] || hash_passed[1])
		{
			if (has_picker() || !m_have_all)
			{
				need_picker();
				m_picker->piece_flushed(piece);
				set_need_save_resume(torrent_handle::if_download_progress);
				update_gauge();
			}
			we_have(piece);
		}
		else if (!error
			&& boost::indeterminate(hash_passed[0])
			&& boost::indeterminate(hash_passed[1]))
		{
			// The data exists but we don't have the hashes needed to verify
			// it yet. This is a special case because we want to say we have
			// the piece once the hash is verified and not download the data
			// unless the hash check fails. To get this effect we setup the
			// piece's state in the piece picker so that it looks like a piece
			// which is finished but not hash checked.
			need_picker();
			int const blocks_in_piece = m_picker->blocks_in_piece(piece);
			for (int i = 0; i < blocks_in_piece; ++i)
				m_picker->mark_as_finished(piece_block(piece, i), nullptr);
		}

		if (m_checking_piece < m_torrent_file->end_piece() && has_picker())
		{
			// skip pieces we already have
			while (m_picker->have_piece(m_checking_piece))
			{
				++m_checking_piece;
				++m_num_checked_pieces;
				if (m_checking_piece >= m_torrent_file->end_piece())
				{
					// actually, we already have outstanding jobs for
					// the remaining pieces. We just need to wait for them
					// to finish
					break;
				}
			}
		}

		if (m_num_checked_pieces < m_torrent_file->end_piece())
		{
			// we paused the checking
			if (!should_check_files())
			{
#ifndef TORRENT_DISABLE_LOGGING
				debug_log("on_piece_hashed, checking paused");
#endif
				if (m_checking_piece == m_num_checked_pieces)
				{
					// we are paused, and we just completed the last outstanding job.
					// now we can be considered paused
					if (alerts().should_post<torrent_paused_alert>())
						alerts().emplace_alert<torrent_paused_alert>(get_handle());
				}
				return;
			}

			if (m_checking_piece >= m_torrent_file->end_piece())
				return;

			auto flags = disk_interface::sequential_access | disk_interface::volatile_read;

			if (torrent_file().info_hashes().has_v1() && !m_disable_v1_hashes)
				flags |= disk_interface::v1_hash;
			if (torrent_file().info_hashes().has_v2())
				block_hashes.resize(torrent_file().layout().blocks_in_piece2(m_checking_piece));

			span<sha256_hash> v2_span(block_hashes);
			m_ses.disk_thread().async_hash(m_storage, m_checking_piece, v2_span, flags
				, [self = shared_from_this(), hashes = std::move(block_hashes)]
				(piece_index_t p, sha1_hash const& h, storage_error const& e)
				{ self->on_piece_hashed(std::move(hashes), p, h, e); });
			++m_checking_piece;
			m_ses.deferred_submit_jobs();
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("on_piece_hashed, m_checking_piece: %d"
				, static_cast<int>(m_checking_piece));
#endif
			return;
		}

#ifndef TORRENT_DISABLE_LOGGING
		debug_log("on_piece_hashed, completed");
#endif

		auto const& pack = settings();
		auto get_int_setting = [&pack](int const name) {
			int const v = pack.get_int(name);
			if (v < 0) return std::numeric_limits<int>::max();
			return v;
		};
		int const limit = std::min({
			get_int_setting(settings_pack::active_downloads)
			, get_int_setting(settings_pack::active_seeds)
			, get_int_setting(settings_pack::active_limit)});
		int const num_torrents = m_ses.num_torrents();
		if (m_auto_managed && num_torrents > limit)
		{
			// if we're auto managed and we've reached one of the limits. Assume
			// we need to be paused until the auto managed logic runs again
			// (which is triggered further down)
			set_paused(true);
		}

		// we're done checking! (this should cause a call to trigger_auto_manage)
		files_checked();

		// reset the checking state
		m_checking_piece = piece_index_t(0);
		m_num_checked_pieces = piece_index_t(0);
	}
	catch (...) { handle_exception(); }

#if TORRENT_ABI_VERSION == 1
	void torrent::use_interface(std::string net_interfaces)
	{
		std::shared_ptr<settings_pack> p = std::make_shared<settings_pack>();
		p->set_str(settings_pack::outgoing_interfaces, std::move(net_interfaces));
		m_ses.apply_settings_pack(p);
	}
#endif

	void torrent::on_tracker_announce(error_code const& ec) try
	{
		COMPLETE_ASYNC("tracker::on_tracker_announce");
		TORRENT_ASSERT(is_single_thread());
		TORRENT_ASSERT(m_waiting_tracker > 0);
		--m_waiting_tracker;
		if (ec) return;
		if (m_abort) return;
		announce_with_tracker();
	}
	catch (...) { handle_exception(); }

	void torrent::lsd_announce()
	{
		if (m_abort) return;
		if (!m_enable_lsd) return;

		// if the files haven't been checked yet, we're
		// not ready for peers. Except, if we don't have metadata,
		// we need peers to download from
		if (!m_files_checked && valid_metadata()) return;

		if (!m_announce_to_lsd) return;

		// private torrents are never announced on LSD
		if (m_torrent_file->is_valid() && m_torrent_file->priv()) return;

#if TORRENT_USE_I2P
		// i2p torrents are also never announced on LSD
		// unless we allow mixed swarms
		if (is_i2p() && !settings().get_bool(settings_pack::allow_i2p_mixed))
			return;
#endif

		if (is_paused()) return;

		if (!m_ses.has_lsd()) return;

		// TODO: this pattern is repeated in a few places. Factor this into
		// a function and generalize the concept of a torrent having a
		// dedicated listen port
#ifdef TORRENT_SSL_PEERS
		int port = is_ssl_torrent() ? m_ses.ssl_listen_port() : m_ses.listen_port();
#else
		int port = m_ses.listen_port();
#endif

		// announce with the local discovery service
		m_torrent_file->info_hashes().for_each([&](sha1_hash const& ih, protocol_version)
		{
			m_ses.announce_lsd(ih, port);
		});
	}

#ifndef TORRENT_DISABLE_DHT

	void torrent::dht_announce()
	{
		TORRENT_ASSERT(is_single_thread());
		if (!m_ses.dht())
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("DHT: no dht initialized");
#endif
			return;
		}
		if (!should_announce_dht())
		{
#ifndef TORRENT_DISABLE_LOGGING
			if (should_log())
			{
#if TORRENT_USE_I2P
				if (is_i2p() && !settings().get_bool(settings_pack::allow_i2p_mixed))
					debug_log("DHT: i2p torrent (and mixed peers not allowed)");
#endif
				if (!m_ses.announce_dht())
					debug_log("DHT: no listen sockets");

				if (m_torrent_file->is_valid() && !m_files_checked)
					debug_log("DHT: files not checked, skipping DHT announce");

				if (!m_announce_to_dht)
					debug_log("DHT: queueing disabled DHT announce");

				if (m_paused)
					debug_log("DHT: torrent paused, no DHT announce");

				if (!m_enable_dht)
					debug_log("DHT: torrent has DHT disabled flag");

				if (m_torrent_file->is_valid() && m_torrent_file->priv())
					debug_log("DHT: private torrent, no DHT announce");

				if (settings().get_bool(settings_pack::use_dht_as_fallback)
					&& m_trackers.any_verified())
				{
					debug_log("DHT: only using DHT as fallback and there are working trackers");
				}
			}
#endif
			return;
		}

		TORRENT_ASSERT(!m_paused);

#ifndef TORRENT_DISABLE_LOGGING
		debug_log("START DHT announce");
		m_dht_start_time = aux::time_now();
#endif

		// if we're a seed, we tell the DHT for better scrape stats
		dht::announce_flags_t flags = is_seed() ? dht::announce::seed : dht::announce_flags_t{};

		// If this is an SSL torrent the announce needs to specify an SSL
		// listen port. DHT nodes only operate on non-SSL ports so SSL
		// torrents cannot use implied_port.
		// if we allow incoming uTP connections and don't overwrite
		// the announced port, set the implied_port argument
		// in the announce, this will make the DHT node use
		// our source port in the packet as our listen port, which is
		// likely more accurate when behind a NAT
		const auto announce_port = std::uint16_t(settings().get_int(settings_pack::announce_port));
		if (is_ssl_torrent())
		{
			flags |= dht::announce::ssl_torrent;
		}
		else if (!announce_port && settings().get_bool(settings_pack::enable_incoming_utp))
		{
			flags |= dht::announce::implied_port;
		}

		std::weak_ptr<torrent> self(shared_from_this());
		m_torrent_file->info_hashes().for_each([&](sha1_hash const& ih, protocol_version v)
		{
			m_ses.dht()->announce(ih, announce_port, flags
				, std::bind(&torrent::on_dht_announce_response_disp, self, v, _1));
		});
	}

	void torrent::on_dht_announce_response_disp(std::weak_ptr<torrent> const t
		, protocol_version const v, std::vector<tcp::endpoint> const& peers)
	{
		std::shared_ptr<torrent> tor = t.lock();
		if (!tor) return;
		tor->on_dht_announce_response(v, peers);
	}

	void torrent::on_dht_announce_response(protocol_version const v
		, std::vector<tcp::endpoint> const& peers) try
	{
		TORRENT_ASSERT(is_single_thread());

#ifndef TORRENT_DISABLE_LOGGING
		debug_log("END DHT announce (%d ms) (%d peers)"
			, int(total_milliseconds(clock_type::now() - m_dht_start_time))
			, int(peers.size()));
#endif

		if (m_abort) return;
		if (peers.empty()) return;

		if (m_ses.alerts().should_post<dht_reply_alert>())
		{
			m_ses.alerts().emplace_alert<dht_reply_alert>(
				get_handle(), int(peers.size()));
		}

#if TORRENT_USE_I2P
		if (is_i2p() && !settings().get_bool(settings_pack::allow_i2p_mixed)) return;
#endif

		if (torrent_file().priv()) return;

		for (auto const& p : peers)
			add_peer(p, peer_info::dht, v == protocol_version::V2 ? pex_lt_v2 : pex_flags_t(0));

#ifndef TORRENT_DISABLE_LOGGING
		if (should_log() && !peers.empty())
		{
			std::string str;
			for (auto const& peer : peers)
			{
				str += peer.address().to_string();
				str += ' ';
			}
			debug_log("DHT add_peer() [ %s] connect-candidates: %d"
				, str.c_str(), m_peer_list
				? m_peer_list->num_connect_candidates() : -1);
		}
#endif

		do_connect_boost();

		update_want_peers();
	}
	catch (...) { handle_exception(); }

#endif

#if TORRENT_USE_I2P
	bool torrent::i2p_compatible_tracker(aux::announce_entry const& ae) const
	{
		return ae.i2p || !is_i2p() || settings().get_bool(settings_pack::allow_i2p_mixed);
	}
#endif

	namespace {
		void refresh_endpoint_list(aux::session_interface& ses,
			bool const is_ssl,
			bool const complete_sent,
			aux::announce_entry& ae)
		{
#if TORRENT_USE_I2P
		if (ae.i2p)
		{
			if (ae.endpoints.size() > 1)
			{
				ae.endpoints.erase(ae.endpoints.begin() + 1, ae.endpoints.end());
			}
			else if (ae.endpoints.empty())
			{
				ae.endpoints.emplace_back(aux::listen_socket_handle(), complete_sent);
			}
			return;
		}
#endif

		auto const ver = ses.listen_socket_version();
		if (ver == ae.listen_socket_version)
			return;

		// update the endpoint list by adding entries for new listen sockets
		// and removing entries for non-existent ones
		std::size_t valid_endpoints = 0;
		ses.for_each_listen_socket([&](aux::listen_socket_handle const& s) {
			if (s.is_ssl() != is_ssl)
				return;
			for (auto& aep : ae.endpoints)
			{
				if (aep.socket != s) continue;
				if (&ae.endpoints[valid_endpoints] != &aep) std::swap(ae.endpoints[valid_endpoints], aep);
				valid_endpoints++;
				return;
			}

			ae.endpoints.emplace_back(s, complete_sent);
			std::swap(ae.endpoints[valid_endpoints], ae.endpoints.back());
			valid_endpoints++;
		});

#if TORRENT_USE_RTC
		if(auto pos = ae.url.find(':'); pos != string_view::npos)
		{
			string_view const protocol = string_view(ae.url).substr(0, pos);
			if (protocol == "ws" || protocol == "wss")
			{
				// WebSocket trackers will ignore the endpoint anyway
				valid_endpoints = std::min(valid_endpoints, std::size_t(1));
			}
		}
#endif

		TORRENT_ASSERT(valid_endpoints <= ae.endpoints.size());
		ae.endpoints.erase(ae.endpoints.begin() + int(valid_endpoints), ae.endpoints.end());
		ae.listen_socket_version = ver;
		}
	}

	namespace
	{
		struct announce_protocol_state
		{
			// the tier is kept as INT_MAX until we find the first
			// tracker that works, then it's set to that tracker's
			// tier.
			int tier = INT_MAX;

			// have we sent an announce in this tier yet?
			bool sent_announce = false;

			// have we finished sending announces on this listen socket?
			bool done = false;
		};

		struct announce_state
		{
			explicit announce_state(aux::listen_socket_handle s)
				: socket(std::move(s)) {}

			aux::listen_socket_handle socket;

			aux::array<announce_protocol_state, num_protocols, protocol_version> state;
		};
	}

	void torrent::announce_with_tracker(event_t e, bool const high_priority)
	{
		TORRENT_ASSERT(is_single_thread());
		TORRENT_ASSERT(e == event_t::stopped || state() != torrent_status::checking_files);
		INVARIANT_CHECK;

		if (m_trackers.empty())
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("*** announce: no trackers");
#endif
			return;
		}

		if (m_abort) e = event_t::stopped;

		// having stop_tracker_timeout <= 0 means that there is
		// no need to send any request to trackers or trigger any
		// related logic when the event is stopped
		if (e == event_t::stopped
			&& settings().get_int(settings_pack::stop_tracker_timeout) <= 0)
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("*** announce: event == stopped && stop_tracker_timeout <= 0");
#endif
			return;
		}

		// if we're not announcing to trackers, only allow
		// stopping
		if (e != event_t::stopped && !m_announce_to_trackers)
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("*** announce: event != stopped && !m_announce_to_trackers");
#endif
			return;
		}

		// if we're not allowing peers, there's no point in announcing
		if (e != event_t::stopped && m_paused)
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("*** announce: event != stopped && m_paused");
#endif
			return;
		}

		TORRENT_ASSERT(!m_paused || e == event_t::stopped);

		if (e == event_t::none && is_finished() && !is_seed())
			e = event_t::paused;

		tracker_request req = build_tracker_request(e);

// some older versions of clang had a bug where it would fire this warning here
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wmissing-braces"
#endif
		aux::array<bool const, num_protocols, protocol_version> const supports_protocol
		{ {
			m_info_hash.has_v1(),
			m_info_hash.has_v2()
		} };
#ifdef __clang__
#pragma clang diagnostic pop
#endif

		time_point32 const now = aux::time_now32();

		// each listen socket gets its own announce state
		// so that each one should get at least one announce
		std::vector<announce_state> listen_socket_states;

		bool const announce_to_all_tiers = settings().get_bool(settings_pack::announce_to_all_tiers);
		bool const announce_to_all_trackers = settings().get_bool(settings_pack::announce_to_all_trackers);
#ifndef TORRENT_DISABLE_LOGGING
		int idx = -1;
		if (should_log())
		{
			debug_log("*** announce: "
				"[ announce_to_all_tiers: %d announce_to_all_trackers: %d num_trackers: %d ]"
				, announce_to_all_tiers
				, announce_to_all_trackers
				, int(m_trackers.size()));
		}
#endif

		// TODO: it would be nice to raise the abstraction level of the
		// tracker_list or trackers a bit here
		for (auto& ae : m_trackers)
		{
#ifndef TORRENT_DISABLE_LOGGING
			++idx;
#endif
			refresh_endpoint_list(m_ses, is_ssl_torrent(), bool(m_complete_sent), ae);

			// if trackerid is not specified for tracker use default one, probably set explicitly
			req.trackerid = ae.trackerid.empty() ? m_trackerid : ae.trackerid;
			req.url = ae.url;

#if TORRENT_USE_I2P
			// if we don't allow mixing normal peers into this i2p torrent,
			// skip non-i2p trackers. update_tracker_timer() applies the
			// same skip so the timer doesn't keep firing for trackers we'll
			// never contact.
			if (!i2p_compatible_tracker(ae)) continue;
			// req is reused across iterations, so set the i2p bit to match
			// this tracker rather than only ever OR-ing it in.
			if (ae.i2p)
				req.kind |= tracker_request::i2p;
			else
				req.kind &= ~tracker_request::i2p;
#endif

			for (auto& aep : ae.endpoints)
			{
				// do not add code which continues to the next endpoint here!
				// listen_socket_states needs to be populated even if none of the endpoints
				// will be announcing for this tracker
				// otherwise the early bail out when neither announce_to_all_trackers
				// nor announce_to_all_tiers is set may be triggered prematurely

				auto aep_state_iter = std::find_if(listen_socket_states.begin(), listen_socket_states.end()
					, [&](announce_state const& s) { return s.socket == aep.socket; });
				if (aep_state_iter == listen_socket_states.end())
				{
					listen_socket_states.emplace_back(aep.socket);
					aep_state_iter = listen_socket_states.end() - 1;
				}
				announce_state& ep_state = *aep_state_iter;

				if (!aep.enabled) continue;

				for (protocol_version const ih : all_versions)
				{
					if (!supports_protocol[ih]) continue;

					auto& state = ep_state.state[ih];
					auto& a = aep.info_hashes[ih];

					// if we haven't sent an event=start to the tracker, there's no
					// point in sending an event=stopped
					if (!a.start_sent && req.event == event_t::stopped)
						continue;

					if (state.done) continue;

#ifndef TORRENT_DISABLE_LOGGING
					if (should_log())
					{
						debug_log("*** tracker:  (%d) [ep: %s ] \"%s\" [ "
							" i->tier: %d tier: %d working: %d limit: %d"
							" can: %d sent: %d ]"
							, idx, print_endpoint(aep.local_endpoint).c_str()
							, ae.url.c_str(), ae.tier, state.tier, a.is_working(), ae.fail_limit
							, a.can_announce(now, is_seed(), ae.fail_limit), state.sent_announce);
					}
#endif

					if (announce_to_all_tiers
						&& !announce_to_all_trackers
						&& state.sent_announce
						&& ae.tier <= state.tier
						&& state.tier != INT_MAX)
						continue;

					if (ae.tier > state.tier && state.sent_announce
						&& !announce_to_all_tiers) continue;
					if (a.is_working()) { state.tier = ae.tier; state.sent_announce = false; }
					if (!a.can_announce(now, is_seed(), ae.fail_limit))
					{
						// this counts
						if (a.is_working())
						{
							state.sent_announce = true;
							if (!announce_to_all_trackers
								&& !announce_to_all_tiers)
							{
								state.done = true;
							}
						}
						continue;
					}

					send_tracker_request(req, aep, a, ih, e, high_priority, now);

					state.sent_announce = true;
					if (a.is_working()
						&& !announce_to_all_trackers
						&& !announce_to_all_tiers)
					{
						state.done = true;
					}
				}
			}

			if (std::all_of(listen_socket_states.begin(), listen_socket_states.end()
				, [supports_protocol](announce_state const& s) {
					for (protocol_version const ih : all_versions)
					{
						if (supports_protocol[ih] && !s.state[ih].done)
							return false;
					}
					return true;
				}))
				break;
		}
		update_tracker_timer(now);
	}

	tracker_request torrent::build_tracker_request(event_t const e)
	{
		tracker_request req;
		if (settings().get_bool(settings_pack::apply_ip_filter_to_trackers) && m_apply_ip_filter)
		{
			req.filter = m_ip_filter;
		}

		req.private_torrent = m_torrent_file && m_torrent_file->priv();

		req.pid = m_peer_id;
		req.downloaded = m_stat.total_payload_download() - m_total_failed_bytes;
		req.uploaded = m_stat.total_payload_upload();
		req.corrupt = m_total_failed_bytes;
		req.left = bytes_left().value_or(16 * 1024);
#ifdef TORRENT_SSL_PEERS
		// if this torrent contains an SSL certificate, make sure
		// any SSL tracker presents a certificate signed by it
		req.ssl_ctx = m_ssl_ctx.get();
#endif

		req.redundant = m_total_redundant_bytes;
		// exclude redundant bytes if we should
		if (!settings().get_bool(settings_pack::report_true_downloaded))
		{
			req.downloaded -= m_total_redundant_bytes;

			// if the torrent is complete we know that all incoming pieces will be
			// marked redundant so add them to the redundant count
			// this is mainly needed to cover the case where a torrent has just completed
			// but still has partially downloaded pieces
			// if the incoming pieces are not accounted for it could cause the downloaded
			// amount to exceed the total size of the torrent which upsets some trackers
			if (is_seed())
			{
				for (auto const& c : m_connections)
				{
					TORRENT_INCREMENT(m_iterating_connections);
					auto const pbp = c->downloading_piece_progress();
					if (pbp.bytes_downloaded > 0)
					{
						req.downloaded -= pbp.bytes_downloaded;
						req.redundant += pbp.bytes_downloaded;
					}
				}
			}
		}
		if (req.downloaded < 0) req.downloaded = 0;

		req.event = e;

		// since sending our IPv4/v6 address to the tracker may be sensitive. Only
		// do that if we're not in anonymous mode and if it's a private torrent
		if (!settings().get_bool(settings_pack::anonymous_mode) && m_torrent_file
			&& m_torrent_file->priv())
		{
			m_ses.for_each_listen_socket([&](aux::listen_socket_handle const& s) {
				if (s.is_ssl() != is_ssl_torrent()) return;
				tcp::endpoint const ep = s.get_local_endpoint();
				if (ep.address().is_unspecified()) return;
				if (aux::is_v6(ep))
				{
					if (!aux::is_local(ep.address()) && !ep.address().is_loopback())
						req.ipv6.push_back(ep.address().to_v6());
				}
				else
				{
					if (!aux::is_local(ep.address()) && !ep.address().is_loopback())
						req.ipv4.push_back(ep.address().to_v4());
				}
			});
		}

		// if we are aborting. we don't want any new peers
		req.num_want =
			(req.event == event_t::stopped) ? 0 : settings().get_int(settings_pack::num_want);

		return req;
	}

	void torrent::send_tracker_request(tracker_request& req,
		aux::announce_endpoint& aep,
		aux::announce_infohash& a,
		protocol_version const ih,
		event_t const e,
		bool const high_priority,
		time_point32 const now)
	{
		req.event = e;
		if (req.event == event_t::none)
		{
			if (!a.start_sent)
				req.event = event_t::started;
			else if (!m_complete_sent && !a.complete_sent && is_seed())
			{
				req.event = event_t::completed;
			}
		}

		req.triggered_manually = a.triggered_manually;
		a.triggered_manually = false;

#if TORRENT_ABI_VERSION == 1
		req.auth = tracker_login();
#endif
		req.key = tracker_key();

		req.outgoing_socket = aep.socket;
		req.info_hash = m_torrent_file->info_hashes().get(ih);
		if (high_priority) req.kind |= tracker_request::high_priority;

#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			debug_log("==> TRACKER REQUEST \"%s\" event: %s abort: %d ssl: %p "
					  "port: %d ssl-port: %d fails: %d upd: %d ep: %s",
				req.url.c_str(),
				(req.event == event_t::stopped			? "stopped"
						: req.event == event_t::started ? "started"
														: ""),
				m_abort
#ifdef TORRENT_SSL_PEERS
				,
				static_cast<void*>(req.ssl_ctx)
#else
				,
				static_cast<void*>(nullptr)
#endif
					,
				m_ses.listen_port(),
				m_ses.ssl_listen_port(),
				a.fails,
				a.updating,
				print_endpoint(aep.local_endpoint).c_str());
		}

		// if we're not logging session logs, don't bother creating an
		// observer object just for logging
		if (m_abort && m_ses.should_log())
		{
			auto tl = std::make_shared<aux::tracker_logger>(m_ses);
			m_ses.queue_tracker_request(req, tl);
		}
		else
#endif
		{
			m_ses.queue_tracker_request(req, shared_from_this());
		}

		a.updating = true;
		a.next_announce = now;
		a.min_announce = now;

		if (m_ses.alerts().should_post<tracker_announce_alert>())
		{
			m_ses.alerts().emplace_alert<tracker_announce_alert>(
				get_handle(), aep.local_endpoint, req.url, ih, req.event);
		}
	}

	// unlike announce_with_tracker(), this only considers a single,
	// specific tracker, and does not defer to other trackers based on
	// tier or "is one tracker already working" bookkeeping. This is used
	// to force-announce a specific tracker (by index), since the whole
	// point of asking for a specific tracker is to reach that tracker,
	// regardless of the state of any other tracker in the list.
	void torrent::announce_tracker_now(
		aux::announce_entry& ae, event_t const e, bool const high_priority)
	{
		TORRENT_ASSERT(is_single_thread());

		if (m_abort) return;
		if (e != event_t::stopped && (!m_announce_to_trackers || m_paused)) return;

		tracker_request req = build_tracker_request(e);
		time_point32 const now = aux::time_now32();

		refresh_endpoint_list(m_ses, is_ssl_torrent(), bool(m_complete_sent), ae);
		req.trackerid = ae.trackerid.empty() ? m_trackerid : ae.trackerid;
		req.url = ae.url;

#if TORRENT_USE_I2P
		if (!i2p_compatible_tracker(ae)) return;
		if (ae.i2p) req.kind |= tracker_request::i2p;
#endif

		for (auto& aep : ae.endpoints)
		{
			if (!aep.enabled) continue;

			for (protocol_version const ih : all_versions)
			{
				if (ih == protocol_version::V1 && !m_info_hash.has_v1()) continue;
				if (ih == protocol_version::V2 && !m_info_hash.has_v2()) continue;

				auto& a = aep.info_hashes[ih];

				if (!a.start_sent && e == event_t::stopped) continue;
				if (!a.can_announce(now, is_seed(), ae.fail_limit)) continue;

				send_tracker_request(req, aep, a, ih, e, high_priority, now);
			}
		}

		// if the tracker wasn't immediately eligible to announce (e.g. its
		// min_interval hasn't expired), make sure the timer is (re)armed so
		// the forced announce isn't silently dropped, and instead happens
		// as soon as the tracker becomes eligible.
		update_tracker_timer(now);
	}

	void torrent::scrape_tracker(int idx, bool const user_triggered)
	{
		TORRENT_ASSERT(is_single_thread());

		if (m_trackers.empty()) return;

		aux::announce_entry* ae = nullptr;
		if (idx < 0 || idx >= int(m_trackers.size()))
		{
			ae = m_trackers.last_working();
			if (ae == nullptr) ae = m_trackers.first();
		}
		else
		{
			ae = m_trackers.find(idx);
		}
		if (ae == nullptr) return;

		scrape_tracker_impl(*ae, user_triggered);
	}

	void torrent::scrape_tracker_url(std::string url, bool const user_triggered)
	{
		TORRENT_ASSERT(is_single_thread());
		if (m_trackers.empty()) return;

		aux::announce_entry* ae = m_trackers.find_tracker(url);
		if (ae == nullptr) ae = m_trackers.last_working();
		if (ae == nullptr) ae = m_trackers.first();
		if (ae == nullptr) return;

		scrape_tracker_impl(*ae, user_triggered);
	}

	void torrent::scrape_tracker_impl(aux::announce_entry& ae, bool user_triggered)
	{
		tracker_request req;
		if (settings().get_bool(settings_pack::apply_ip_filter_to_trackers)
			&& m_apply_ip_filter)
			req.filter = m_ip_filter;

		req.kind |= tracker_request::scrape_request;

#if TORRENT_USE_I2P
		if (!i2p_compatible_tracker(ae)) return;
		if (ae.i2p) req.kind |= tracker_request::i2p;
#endif
		refresh_endpoint_list(m_ses, is_ssl_torrent(), bool(m_complete_sent), ae);
		req.url = ae.url;
		req.private_torrent = m_torrent_file && m_torrent_file->priv();
#if TORRENT_ABI_VERSION == 1
		req.auth = tracker_login();
#endif
		req.key = tracker_key();
		req.triggered_manually = user_triggered;
		for (aux::announce_endpoint const& aep : ae.endpoints)
		{
			if (!aep.enabled) continue;
			req.outgoing_socket = aep.socket;
			m_torrent_file->info_hashes().for_each([&](sha1_hash const& ih, protocol_version)
			{
				req.info_hash = ih;
				m_ses.queue_tracker_request(req, shared_from_this());
			});
		}
#if TORRENT_ABI_VERSION == 1
		m_last_scrape = aux::time_now32();
#endif
	}

	void torrent::tracker_warning(tracker_request const& req, std::string const& msg)
	{
		TORRENT_ASSERT(is_single_thread());

		INVARIANT_CHECK;

		protocol_version const hash_version = req.info_hash == m_info_hash.v1
			? protocol_version::V1 : protocol_version::V2;

		aux::announce_entry* ae = m_trackers.find_tracker(req.url);
		tcp::endpoint local_endpoint;
		if (ae)
		{
			for (auto& aep : ae->endpoints)
			{
				if (aep.socket != req.outgoing_socket) continue;
				local_endpoint = aep.local_endpoint;
				aep.info_hashes[hash_version].message = msg;
				break;
			}
		}

		if (m_ses.alerts().should_post<tracker_warning_alert>())
			m_ses.alerts().emplace_alert<tracker_warning_alert>(get_handle()
				, local_endpoint, req.url, hash_version, msg);
	}

	void torrent::tracker_scrape_response(tracker_request const& req
		, int const complete, int const incomplete, int const downloaded, int /* downloaders */)
	{
		TORRENT_ASSERT(is_single_thread());

		INVARIANT_CHECK;
		TORRENT_ASSERT(req.kind & tracker_request::scrape_request);

		protocol_version const hash_version = req.info_hash == m_info_hash.v1
			? protocol_version::V1 : protocol_version::V2;

		aux::announce_entry* ae = m_trackers.find_tracker(req.url);
		tcp::endpoint local_endpoint;
		if (ae)
		{
			auto* aep = ae->find_endpoint(req.outgoing_socket);
			if (aep)
			{
				local_endpoint = aep->local_endpoint;
				if (incomplete >= 0) aep->info_hashes[hash_version].scrape_incomplete = incomplete;
				if (complete >= 0) aep->info_hashes[hash_version].scrape_complete = complete;
				if (downloaded >= 0) aep->info_hashes[hash_version].scrape_downloaded = downloaded;

				update_scrape_state();
			}
		}

		// if this was triggered manually we need to post this unconditionally,
		// since the client expects a response from its action, regardless of
		// whether all tracker events have been enabled by the alert mask
		if (m_ses.alerts().should_post<scrape_reply_alert>()
			|| req.triggered_manually)
		{
			m_ses.alerts().emplace_alert<scrape_reply_alert>(
				get_handle(), local_endpoint, incomplete, complete, req.url, hash_version);
		}
	}

	void torrent::update_scrape_state()
	{
		// loop over all trackers and find the largest numbers for each scrape field
		// then update the torrent-wide understanding of number of downloaders and seeds
		int complete = -1;
		int incomplete = -1;
		int downloaded = -1;
		for (auto const& t : m_trackers)
		{
			for (auto const& aep : t.endpoints)
			{
				for (protocol_version const ih : all_versions)
				{
					auto const& a = aep.info_hashes[ih];
					complete = std::max(a.scrape_complete, complete);
					incomplete = std::max(a.scrape_incomplete, incomplete);
					downloaded = std::max(a.scrape_downloaded, downloaded);
				}
			}
		}

		if ((complete >= 0 && int(m_complete) != complete)
			|| (incomplete >= 0 && int(m_incomplete) != incomplete)
			|| (downloaded >= 0 && int(m_downloaded) != downloaded))
			state_updated();

		if (int(m_complete) != complete
			|| int(m_incomplete) != incomplete
			|| int(m_downloaded) != downloaded)
		{
			m_complete = std::uint32_t(complete);
			m_incomplete = std::uint32_t(incomplete);
			m_downloaded = std::uint32_t(downloaded);

			update_auto_sequential();

			// these numbers are cached in the resume data
			set_need_save_resume(torrent_handle::if_counters_changed);
		}
	}

	void torrent::tracker_response(
		tracker_request const& r
		, address const& tracker_ip // this is the IP we connected to
		, std::list<address> const& tracker_ips // these are all the IPs it resolved to
		, struct tracker_response const& resp)
	{
		TORRENT_ASSERT(is_single_thread());

		INVARIANT_CHECK;
		TORRENT_ASSERT(!(r.kind & tracker_request::scrape_request));

		// if the tracker told us what our external IP address is, record it with
		// out external IP counter (and pass along the IP of the tracker to know
		// who to attribute this vote to)
		if (resp.external_ip != address() && !tracker_ip.is_unspecified() && r.outgoing_socket)
			m_ses.set_external_address(r.outgoing_socket.get_local_endpoint()
				, resp.external_ip
				, aux::session_interface::source_tracker, tracker_ip);

		time_point32 const now = aux::time_now32();

		protocol_version const v = r.info_hash == torrent_file().info_hashes().v1
			? protocol_version::V1 : protocol_version::V2;

		// TODO: move this into a function on tracker_list
		aux::announce_entry* ae = m_trackers.find_tracker(r.url);

		tcp::endpoint local_endpoint;
		if (ae)
		{
			auto* aep = ae->find_endpoint(r.outgoing_socket);
			if (aep)
			{
				auto& a = aep->info_hashes[v];

				local_endpoint = aep->local_endpoint;
				if (resp.incomplete >= 0) a.scrape_incomplete = resp.incomplete;
				if (resp.complete >= 0) a.scrape_complete = resp.complete;
				if (resp.downloaded >= 0) a.scrape_downloaded = resp.downloaded;
				if (!a.start_sent && r.event == event_t::started)
					a.start_sent = true;
				if (!a.complete_sent && r.event == event_t::completed)
				{
					a.complete_sent = true;
					// we successfully reported event=completed to one tracker. Don't
					// send it to any other ones from now on (there may be other
					// announces outstanding right now though)
					m_complete_sent = true;
				}
				ae->verified = true;
				a.next_announce = now + resp.interval;
				a.min_announce = now + resp.min_interval;
				a.updating = false;
				a.fails = 0;
				a.last_error.clear();
				a.message = !resp.warning_message.empty() ? resp.warning_message : std::string();
				m_trackers.record_working(ae);

				if ((!resp.trackerid.empty()) && (ae->trackerid != resp.trackerid))
				{
					ae->trackerid = resp.trackerid;
					if (m_ses.alerts().should_post<trackerid_alert>())
						m_ses.alerts().emplace_alert<trackerid_alert>(get_handle()
							, aep->local_endpoint, v, r.url, resp.trackerid);
				}

				update_scrape_state();
			}
		}
		update_tracker_timer(now);

#if TORRENT_ABI_VERSION == 1
		if (resp.complete >= 0 && resp.incomplete >= 0)
			m_last_scrape = aux::time_now32();
#endif

#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			std::string resolved_to;
			for (auto const& i : tracker_ips)
			{
				resolved_to += i.to_string();
				resolved_to += ", ";
			}
			debug_log("TRACKER RESPONSE [ interval: %d | min-interval: %d | "
				"external ip: %s | resolved to: %s | we connected to: %s ]"
				, resp.interval.count()
				, resp.min_interval.count()
				, print_address(resp.external_ip).c_str()
				, resolved_to.c_str()
				, print_address(tracker_ip).c_str());
		}
#else
		TORRENT_UNUSED(tracker_ips);
#endif

		// for each of the peers we got from the tracker
		for (auto const& i : resp.peers)
		{
			// don't make connections to ourself
			if (i.pid == m_peer_id)
				continue;

#if TORRENT_USE_I2P
			if (r.i2pconn)
			{
				// this is an i2p name, we need to use the SAM connection
				// to do the name lookup
				if (string_ends_with(i.hostname, ".i2p"))
				{
					ADD_OUTSTANDING_ASYNC("torrent::on_i2p_resolve");
					r.i2pconn->async_name_lookup(i.hostname.c_str()
						, [self = shared_from_this()] (error_code const& ec, char const* dest)
						{ self->torrent::on_i2p_resolve(ec, dest, peer_info::tracker); });
				}
				else
				{
					// TODO: the destination string should be base64 decoded and
					// sha256 hashed to form the destination address and store
					// that in the i2p_peer object directly
					torrent_state st = get_peer_list_state();
					need_peer_list();
					if (m_peer_list->add_i2p_peer(i.hostname, peer_info::tracker, {}, &st))
						state_updated();
					peers_erased(st.erased);
				}
			}
			else
#endif
			{
				ADD_OUTSTANDING_ASYNC("torrent::on_peer_name_lookup");
				m_ses.get_resolver().async_resolve(i.hostname, aux::resolver_interface::abort_on_shutdown
					, std::bind(&torrent::on_peer_name_lookup, shared_from_this(), _1, _2, i.port, v));
			}
		}

#if TORRENT_USE_I2P
		if (r.i2pconn)
		{
			for (auto const& i : resp.i2p_peers)
			{
				add_i2p_peer(i.destination, peer_info::tracker);
			}
		}
#endif

		// there are 2 reasons to allow local IPs to be returned from a
		// non-local tracker
		// 1. retrackers are popular in russia, where an ISP runs a tracker within
		//    the AS (but not on the local network) giving out peers only from the
		//    local network
		// 2. it might make sense to have a tracker extension in the future where
		//    trackers records a peer's internal and external IP, and match up
		//    peers on the same local network

		pex_flags_t flags = v == protocol_version::V2 ? pex_lt_v2 : pex_flags_t(0);

		bool need_update = false;
		for (auto const& i : resp.peers4)
		{
			tcp::endpoint const a(address_v4(i.ip), i.port);
			need_update |= bool(add_peer(a, peer_info::tracker, flags) != nullptr);
		}

		for (auto const& i : resp.peers6)
		{
			tcp::endpoint const a(address_v6(i.ip), i.port);
			need_update |= bool(add_peer(a, peer_info::tracker, flags) != nullptr);
		}

#ifndef TORRENT_DISABLE_LOGGING
		if (should_log() && (!resp.peers4.empty() || !resp.peers6.empty()))
		{
			std::string str;
			for (auto const& peer : resp.peers4)
			{
				str += address_v4(peer.ip).to_string();
				str += ' ';
			}
			for (auto const& peer : resp.peers6)
			{
				str += address_v6(peer.ip).to_string();
				str += ' ';
			}
			debug_log("tracker add_peer() [ %s] connect-candidates: %d"
				, str.c_str(), m_peer_list
				? m_peer_list->num_connect_candidates() : -1);
		}
#endif
		if (need_update) state_updated();

		update_want_peers();

		// post unconditionally if the announce was triggered manually
		if (m_ses.alerts().should_post<tracker_reply_alert>()
			|| r.triggered_manually)
		{
			int peer_count = int(resp.peers.size() + resp.peers4.size());
#if TORRENT_USE_I2P
			peer_count += int(resp.i2p_peers.size());
#endif
			m_ses.alerts().emplace_alert<tracker_reply_alert>(
				get_handle(), local_endpoint, peer_count
				+ int(resp.peers6.size()), v, r.url);
		}

		do_connect_boost();

		state_updated();
	}

	void torrent::update_auto_sequential()
	{
		if (!settings().get_bool(settings_pack::auto_sequential))
		{
			m_auto_sequential = false;
			return;
		}

		if (num_peers() - m_num_connecting < 10)
		{
			// there are too few peers. Be conservative and don't assume it's
			// well seeded until we can connect to more peers
			m_auto_sequential = false;
			return;
		}

		// if there are at least 10 seeds, and there are 10 times more
		// seeds than downloaders, enter sequential download mode
		// (for performance)
		int const downloaders = num_downloaders();
		int const seeds = num_seeds();
		m_auto_sequential = downloaders * 10 <= seeds
			&& seeds > 9;
	}

	void torrent::do_connect_boost()
	{
		if (m_connect_boost_counter == 0) return;

		// this is the first tracker response for this torrent
		// instead of waiting one second for session_impl::on_tick()
		// to be called, connect to a few peers immediately
		int conns = std::min(int(m_connect_boost_counter)
			, settings().get_int(settings_pack::connections_limit) - m_ses.num_connections());

		if (conns == 0) return;

		// if we don't know of any peers
		if (!m_peer_list) return;

		while (want_peers() && conns > 0)
		{
			TORRENT_ASSERT(m_connect_boost_counter > 0);
			--conns;
			--m_connect_boost_counter;
			torrent_state st = get_peer_list_state();
			torrent_peer* p = m_peer_list->connect_one_peer(m_ses.session_time(), &st);
			peers_erased(st.erased);
			inc_stats_counter(counters::connection_attempt_loops, st.loop_counter);
			if (p == nullptr)
			{
				update_want_peers();
				continue;
			}

#ifndef TORRENT_DISABLE_LOGGING
			if (should_log())
			{
				external_ip const& external = m_ses.external_address();
				debug_log(" *** FOUND CONNECTION CANDIDATE ["
					" ip: %s rank: %u external: %s t: %d ]"
					, print_endpoint(p->ip()).c_str()
					, p->rank(external, m_ses.listen_port())
					, print_address(external.external_address(p->address())).c_str()
					, int(m_ses.session_time() - p->last_connected));
			}
#endif

			if (!connect_to_peer(p))
			{
				m_peer_list->inc_failcount(p);
				update_want_peers();
			}
			else
			{
				// increase m_ses.m_boost_connections for each connection
				// attempt. This will be deducted from the connect speed
				// the next time session_impl::on_tick() is triggered
				m_ses.inc_boost_connections();
				update_want_peers();
			}
		}

		if (want_peers()) m_ses.prioritize_connections(shared_from_this());
	}

namespace {

	bool trigger_announce(aux::session_interface& ses
		, bool const is_ssl
		, bool const complete_sent
		, reannounce_flags_t const flags
		, time_point const t
		, aux::announce_entry& e)
	{
		bool ret = false;
		// make sure we check for new endpoints from the listen sockets
		refresh_endpoint_list(ses, is_ssl, complete_sent, e);
		for (auto& aep : e.endpoints)
		{
			for (auto& a : aep.info_hashes)
			{
				a.next_announce = (flags & torrent_handle::ignore_min_interval)
					? time_point_cast<seconds32>(t) + seconds32(1)
					: std::max(time_point_cast<seconds32>(t), a.min_announce) + seconds32(1);
				// ignore_min_interval means the caller explicitly wants to
				// bypass the tracker's requested minimum interval, so clear
				// it. Otherwise leave it as-is (e.g. reflecting a real
				// interval requested by the tracker) rather than pushing it
				// a second into the future, which would make can_announce()
				// falsely report this endpoint as not yet eligible.
				if (flags & torrent_handle::ignore_min_interval)
					a.min_announce = (time_point32::min)();
				a.triggered_manually = true;
				ret = true;
			}
		}
		return ret;
	}
}

	// this is the entry point for the client to force a re-announce. It's
	// considered a client-initiated announce (as opposed to the regular ones,
	// issued by libtorrent)
	void torrent::force_tracker_request(time_point const t, int const tracker_idx
		, reannounce_flags_t const flags)
	{
		TORRENT_ASSERT_PRECOND((tracker_idx >= 0
			&& tracker_idx < int(m_trackers.size()))
			|| tracker_idx == -1);

		if (is_paused()) return;

		if (tracker_idx == -1)
		{
			bool found_one = false;
			for (auto& e : m_trackers)
			{
				// make sure we check for new endpoints from the listen sockets
				refresh_endpoint_list(m_ses, is_ssl_torrent(), bool(m_complete_sent), e);
				found_one |= trigger_announce(m_ses, is_ssl_torrent(), bool(m_complete_sent), flags, t, e);
			}

#ifndef TORRENT_DISABLE_LOGGING
			if (!found_one)
			{
				debug_log("*** found no tracker endpoints to announce");
			}
#else
			TORRENT_UNUSED(found_one);
#endif

			if (flags & torrent_handle::high_priority)
			{
				announce_with_tracker(event_t::none, true);
			}
			else
			{
				update_tracker_timer(aux::time_now32());
			}
			return;
		}

		if (tracker_idx < 0 || tracker_idx >= int(m_trackers.size())) return;
		auto* e = m_trackers.find(tracker_idx);
		if (e == nullptr) return;
		bool const found_one =
			trigger_announce(m_ses, is_ssl_torrent(), bool(m_complete_sent), flags, t, *e);

#ifndef TORRENT_DISABLE_LOGGING
		if (!found_one)
		{
			debug_log("*** found no tracker endpoints to announce");
		}
#endif

		// unlike the "reannounce to all trackers" case above, forcing a
		// specific tracker must actually reach that tracker, regardless of
		// whether some other tracker earlier in the list hasn't failed yet.
		// announce_with_tracker() would silently skip this tracker in that
		// case, so use the single-tracker path instead.
		if (found_one)
			announce_tracker_now(*e, event_t::none, bool(flags & torrent_handle::high_priority));
	}

	// this is the entry point for the client to force a re-announce. It's
	// considered a client-initiated announce (as opposed to the regular ones,
	// issued by libtorrent)
	void torrent::force_tracker_request_url(time_point const t, std::string const& url
		, reannounce_flags_t const flags)
	{
		if (is_paused()) return;
		aux::announce_entry* e = m_trackers.find_tracker(url);
		if (e == nullptr)
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("*** tracker URL not found %s", url.c_str());
#endif
			return;
		}

		bool const found = trigger_announce(m_ses, is_ssl_torrent(), bool(m_complete_sent), flags, t, *e);

		// see the comment in force_tracker_request() above: this must reach
		// this specific tracker regardless of the state of any other one,
		// so use the single-tracker path rather than announce_with_tracker().
		if (found)
		{
			announce_tracker_now(*e, event_t::none, bool(flags & torrent_handle::high_priority));
		}
#ifndef TORRENT_DISABLE_LOGGING
		else
		{
			debug_log("*** found no tracker endpoints to announce");
		}
#endif
	}

#if TORRENT_ABI_VERSION == 1
	void torrent::set_tracker_login(std::string const& name
		, std::string const& pw)
	{
		m_username = name;
		m_password = pw;
	}
#endif

#if TORRENT_USE_I2P
	void torrent::add_i2p_peer(sha256_hash const& dest, peer_source_flags_t const source)
	{
		// TODO: store i2p addresses as a 32 byte sha256 hash in the
		// i2p_peer object, rather than resolving it into the full vase64
		// destination. We should keep the full destination as a cache in the
		// i2p_peer object still.
		i2p_connection& i2pconn = session().i2p_conn();
		if (!i2pconn.is_open()) return;

		std::string destination = base32encode_i2p(dest);
		destination += ".b32.i2p";

		ADD_OUTSTANDING_ASYNC("torrent::on_i2p_resolve");
		i2pconn.async_name_lookup(destination.c_str()
			, [self = shared_from_this(), source] (error_code const& ec, char const* d)
			{ self->torrent::on_i2p_resolve(ec, d, source); });
	}

	void torrent::on_i2p_resolve(error_code const& ec, char const* dest, peer_source_flags_t const source) try
	{
		TORRENT_ASSERT(is_single_thread());

		INVARIANT_CHECK;

		COMPLETE_ASYNC("torrent::on_i2p_resolve");
#ifndef TORRENT_DISABLE_LOGGING
		if (ec && should_log())
			debug_log("i2p_resolve error: %s", ec.message().c_str());
#endif
		if (ec || m_abort || m_ses.is_aborted()) return;

		need_peer_list();
		torrent_state st = get_peer_list_state();
		if (m_peer_list->add_i2p_peer(dest, source, {}, &st))
			state_updated();
		peers_erased(st.erased);

		update_want_peers();
	}
	catch (...) { handle_exception(); }
#endif

	void torrent::on_peer_name_lookup(error_code const& e
		, std::vector<address> const& host_list, int const port
		, protocol_version const v) try
	{
		TORRENT_ASSERT(is_single_thread());

		INVARIANT_CHECK;

		COMPLETE_ASYNC("torrent::on_peer_name_lookup");

#ifndef TORRENT_DISABLE_LOGGING
		if (e && should_log())
			debug_log("peer name lookup error: %s", e.message().c_str());
#endif

		if (e || m_abort || host_list.empty() || m_ses.is_aborted()) return;

		// TODO: add one peer per IP the hostname resolves to
		tcp::endpoint host(host_list.front(), std::uint16_t(port));

		if (m_ip_filter && m_ip_filter->access(host.address()) & ip_filter::blocked)
		{
#ifndef TORRENT_DISABLE_LOGGING
			if (should_log())
			{
				debug_log("blocked ip from tracker: %s", host.address().to_string().c_str());
			}
#endif
			if (m_ses.alerts().should_post<peer_blocked_alert>())
				m_ses.alerts().emplace_alert<peer_blocked_alert>(get_handle()
					, host, peer_blocked_alert::ip_filter);
			return;
		}

		if (add_peer(host, peer_info::tracker, v == protocol_version::V2 ? pex_lt_v2 : pex_flags_t(0)))
		{
			state_updated();

#ifndef TORRENT_DISABLE_LOGGING
			if (should_log())
			{
				debug_log("name-lookup add_peer() [ %s ] connect-candidates: %d"
					, host.address().to_string().c_str()
					, m_peer_list ? m_peer_list->num_connect_candidates() : -1);
			}
#endif
		}
		update_want_peers();
	}
	catch (...) { handle_exception(); }

	std::optional<std::int64_t> torrent::bytes_left() const
	{
		// if we don't have the metadata yet, we
		// cannot tell how big the torrent is.
		if (!valid_metadata()) return {};
		TORRENT_ASSERT(m_torrent_file->num_pieces() > 0);
		if (m_seed_mode) return std::int64_t(0);
		if (!has_picker()) return is_seed() ? std::int64_t(0) : m_torrent_file->total_size();

		piece_count const pc = m_picker->have();
		std::int64_t left
			= m_torrent_file->total_size()
			- std::int64_t(pc.num_pieces) * m_torrent_file->piece_length();

		// if we have the last piece, we may have subtracted too much, as it can
		// be smaller than the normal piece size.
		// we have to correct it
		if (pc.last_piece)
		{
			left += m_torrent_file->piece_length() - m_torrent_file->piece_size(m_torrent_file->last_piece());
		}

		return left;
	}

	// we assume the last block is never a pad block. Should be a fairly
	// safe assumption, and you just get a few kiB off if it is
	std::int64_t calc_bytes(file_storage const& fs, piece_count const& pc)
	{
		// it's an impossible combination to have 0 pieces, but still have one of them be the last piece
		TORRENT_ASSERT(!(pc.num_pieces == 0 && pc.last_piece == true));

		// if we have 0 pieces, we can't have any pad blocks either
		TORRENT_ASSERT(!(pc.num_pieces == 0 && pc.pad_bytes > 0));

		// if we have all pieces, we must also have the last one
		TORRENT_ASSERT(!(pc.num_pieces == fs.num_pieces() && pc.last_piece == false));

		// every block should not be a pad block
		TORRENT_ASSERT(pc.pad_bytes <= std::int64_t(pc.num_pieces) * fs.piece_length());

		return std::int64_t(pc.num_pieces) * fs.piece_length()
			- (pc.last_piece ? fs.piece_length() - fs.piece_size(fs.last_piece()) : 0)
			- std::int64_t(pc.pad_bytes);
	}

	// fills in total_wanted, total_wanted_done and total_done
// TODO: 3 this could probably be pulled out into a free function
	void torrent::bytes_done(torrent_status& st, status_flags_t const flags) const
	{
		INVARIANT_CHECK;

		st.total_done = 0;
		st.total_wanted_done = 0;
		st.total_wanted = m_size_on_disk;
		st.total = m_size_on_disk;

		TORRENT_ASSERT(st.total_wanted <= m_torrent_file->total_size());
		TORRENT_ASSERT(st.total_wanted >= 0);

		TORRENT_ASSERT(!valid_metadata() || m_torrent_file->num_pieces() > 0);
		if (!valid_metadata()) return;

		if (m_seed_mode || is_seed())
		{
			// once we're a seed and remove the piece picker, we stop tracking
			// piece- and file priority. We consider everything as being
			// "wanted"
			st.total_done = m_size_on_disk;
			st.total_wanted_done = m_size_on_disk;
			TORRENT_ASSERT(st.total_wanted <= st.total_done);
			TORRENT_ASSERT(st.total_wanted_done <= st.total_wanted);
			TORRENT_ASSERT(st.total_done <= m_torrent_file->total_size());
			return;
		}
		else if (!has_picker())
			return;

		TORRENT_ASSERT(has_picker());

		file_storage const& files = m_torrent_file->layout();

		st.total_wanted = std::min(m_size_on_disk, calc_bytes(files, m_picker->want()));
		st.total_wanted_done = std::min(m_file_progress.total_on_disk()
			, calc_bytes(files, m_picker->have_want()));
		st.total_done = calc_bytes(files, m_picker->have());
		st.total = calc_bytes(files, m_picker->all_pieces());

		TORRENT_ASSERT(st.total_done <= calc_bytes(files, m_picker->all_pieces()));
		TORRENT_ASSERT(st.total_wanted <= calc_bytes(files, m_picker->all_pieces()));

		TORRENT_ASSERT(st.total_wanted_done >= 0);
		TORRENT_ASSERT(st.total_wanted >= 0);
		TORRENT_ASSERT(st.total_wanted >= st.total_wanted_done);
		TORRENT_ASSERT(st.total_done >= 0);
		TORRENT_ASSERT(st.total_done >= st.total_wanted_done);

		// this is expensive, we might not want to do it all the time
		if (!(flags & torrent_handle::query_accurate_download_counters)) return;

		// to get higher accuracy of the download progress, include
		// blocks from currently downloading pieces as well
		std::vector<piece_picker::downloading_piece> const dl_queue
			= m_picker->get_download_queue();

		// look at all unfinished pieces and add the completed
		// blocks to our 'done' counter
		for (auto i = dl_queue.begin(); i != dl_queue.end(); ++i)
		{
			piece_index_t const index = i->index;

			// completed pieces are already accounted for
			if (m_picker->have_piece(index)) continue;

			TORRENT_ASSERT(i->finished + i->writing <= m_picker->blocks_in_piece(index));

			auto const additional_bytes = std::int64_t(i->finished + i->writing
				- m_picker->pad_bytes_in_piece(index) / block_size())
				* block_size();
			TORRENT_ASSERT(additional_bytes >= 0);
			st.total_done += additional_bytes;
			if (m_picker->piece_priority(index) > dont_download)
				st.total_wanted_done += additional_bytes;
		}

		TORRENT_ASSERT(st.total_wanted_done >= 0);
		TORRENT_ASSERT(st.total_done >= st.total_wanted_done);
	}

	void torrent::on_piece_verified(aux::vector<sha256_hash> block_hashes
		, piece_index_t const piece
		, sha1_hash const& piece_hash, storage_error const& error) try
	{
		TORRENT_ASSERT(is_single_thread());

		if (m_abort) return;
		if (m_deleted) return;

		if (!m_picker) return;

		m_picker->completed_hash_job(piece);

		boost::tribool passed = boost::indeterminate;
		boost::tribool v2_passed = boost::indeterminate;

		if (settings().get_bool(settings_pack::disable_hash_checks))
		{
			passed = v2_passed = true;
		}
		else if (error)
		{
			passed = v2_passed = false;
		}
		else
		{
			if (torrent_file().info_hashes().has_v1() && !m_disable_v1_hashes)
			{
				passed = sha1_hash(piece_hash) == m_torrent_file->hash_for_piece(piece);
			}

			if (!block_hashes.empty())
			{
				TORRENT_ASSERT(torrent_file().info_hashes().has_v2());
				v2_passed = on_blocks_hashed(piece, block_hashes);
			}
		}

		if (!error && ((passed && !v2_passed) || (!passed && v2_passed)))
		{
			handle_inconsistent_hashes(piece);
			return;
		}

		bool const disk_error = (!passed || !v2_passed) && error;

		if (disk_error) handle_disk_error("piece_verified", error);

#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			debug_log("*** PIECE_FINISHED [ p: %d | chk: %s | size: %d ]"
				, static_cast<int>(piece)
				, (passed || v2_passed) ? "passed"
				: disk_error ? "disk failed"
				: (!passed || !v2_passed) ? "failed"
				: "-"
				, m_torrent_file->piece_size(piece));
		}
#endif
		TORRENT_ASSERT(valid_metadata());

		// if we're a seed we don't have a picker
		// and we also don't have to do anything because
		// we already have this piece
		if (!has_picker() && m_have_all) return;

		need_picker();

		TORRENT_ASSERT(!m_picker->have_piece(piece));

		state_updated();

		// even though the piece passed the hash-check
		// it might still have failed being written to disk
		// if so, piece_picker::write_failed() has been
		// called, and the piece is no longer finished.
		// in this case, we have to ignore the fact that
		// it passed the check
		if (!m_picker->is_piece_finished(piece)) return;

		if (disk_error)
		{
			update_gauge();
		}
		else if (passed || v2_passed)
		{
			// the following call may cause picker to become invalid
			// in case we just became a seed
			piece_passed(piece);
			// if we're in seed mode, we just acquired this piece
			// mark it as verified
			if (m_seed_mode) verified(piece);
		}
		else if (!passed || !v2_passed)
		{
			// piece_failed() will restore the piece
			piece_failed(piece);
		}
	}
	catch (...) { handle_exception(); }

	void torrent::add_suggest_piece(piece_index_t const index)
	{
		TORRENT_ASSERT(settings().get_int(settings_pack::suggest_mode)
			== settings_pack::suggest_read_cache);

		// when we care about suggest mode, we keep the piece picker
		// around to track piece availability
		need_picker();
		int const peers = std::max(num_peers(), 1);
		int const availability = m_picker->get_availability(index) * 100 / peers;

		m_suggest_pieces.add_piece(index, availability
			, settings().get_int(settings_pack::max_suggest_pieces));
	}

	// this is called when either:
	// * we have completely downloaded piece 'index' and its hash has been verified.
	// * during initial file check when we find a piece whose hash is correct
	// * if there's a pad-file that extends over the entire piece
	// this function does not update the piece picker, telling it we have the
	// piece, it just does all the torrent-level accounting that needs to
	// happen. It may not be called twice for the same piece (if it is,
	// file_progress will assert)
	void torrent::we_have(piece_index_t const index, bool const loading_resume)
	{
		TORRENT_ASSERT(is_single_thread());
		TORRENT_ASSERT(!has_picker() || m_picker->have_piece(index));

		inc_stats_counter(counters::num_have_pieces);

		// at this point, we have the piece for sure. It has been
		// successfully written to disk. We may announce it to peers
		// (unless it has already been announced through predictive_piece_announce
		// feature).
		bool announce_piece = true;
#ifndef TORRENT_DISABLE_PREDICTIVE_PIECES
		auto const it = std::lower_bound(m_predictive_pieces.begin()
			, m_predictive_pieces.end(), index);
		if (it != m_predictive_pieces.end() && *it == index)
		{
			// this means we've already announced the piece
			announce_piece = false;
			m_predictive_pieces.erase(it);
		}
#endif

		// make a copy of the peer list since peers
		// may disconnect while looping
		for (auto* c : m_connections)
		{
			auto p = c->self();

			// received_piece will check to see if we're still interested
			// in this peer, and if neither of us is interested in the other,
			// disconnect it.
			p->received_piece(index);
			if (p->is_disconnecting()) continue;

			// if we're not announcing the piece, it means we
			// already have, and that we might have received
			// a request for it, and not sending it because
			// we were waiting to receive the piece, now that
			// we have received it, try to send stuff (fill_send_buffer)
			if (announce_piece) p->announce_piece(index);
			else p->fill_send_buffer();
		}

#ifndef TORRENT_DISABLE_EXTENSIONS
		for (auto& ext : m_extensions)
		{
			ext->on_piece_pass(index);
		}
#endif

		// since this piece just passed, we might have
		// become uninterested in some peers where this
		// was the last piece we were interested in
		// update_interest may disconnect the peer and
		// invalidate the iterator
		for (auto* p : m_connections)
		{
			TORRENT_INCREMENT(m_iterating_connections);
			// if we're not interested already, no need to check
			if (!p->is_interesting()) continue;
			// if the peer doesn't have the piece we just got, it
			// shouldn't affect our interest
			if (!p->has_piece(index)) continue;
			p->update_interest();
		}

		if (!loading_resume)
		{
			set_need_save_resume(torrent_handle::if_download_progress);
			state_updated();
			update_want_tick();
		}

		if (m_ses.alerts().should_post<piece_finished_alert>())
			m_ses.alerts().emplace_alert<piece_finished_alert>(get_handle(), index);

		// update m_file_progress (if we have one)
		m_file_progress.update(m_torrent_file->layout(), index
			, [this](file_index_t const file_index)
			{
				if (m_ses.alerts().should_post<file_completed_alert>())
				{
					// this file just completed, post alert
					m_ses.alerts().emplace_alert<file_completed_alert>(
						get_handle(), file_index);
				}
			});

#ifndef TORRENT_DISABLE_STREAMING
		remove_time_critical_piece(index, true);
#endif

		if (is_downloading_state(m_state))
		{
			if (m_state != torrent_status::finished
				&& m_state != torrent_status::seeding
				&& is_finished())
			{
				// torrent finished
				// i.e. all the pieces we're interested in have
				// been downloaded. Release the files (they will open
				// in read only mode if needed)
				finished();
				// if we just became a seed, picker is now invalid, since it
				// is deallocated by the torrent once it starts seeding
			}

			m_last_download = aux::time_now32();

#ifndef TORRENT_DISABLE_SHARE_MODE
			if (m_share_mode)
				recalc_share_mode();
#endif
		}
		update_want_tick();
	}

	boost::tribool torrent::on_blocks_hashed(piece_index_t const piece
		, span<sha256_hash const> const block_hashes)
	{
		boost::tribool ret = boost::indeterminate;
		need_hash_picker();

		int const blocks_in_piece = torrent_file().layout().blocks_in_piece2(piece);
		int const blocks_per_piece = torrent_file().blocks_per_piece();

		// the blocks are guaranteed to represent exactly one piece
		TORRENT_ASSERT(blocks_in_piece == int(block_hashes.size()));

		TORRENT_ALLOCA(block_passed, bool, blocks_in_piece);
		std::fill(block_passed.begin(), block_passed.end(), false);

		auto last_result = set_block_hash_result(set_block_hash_result::result::unknown);

		// collect adjacent pieces verified as side-effects of this block's hash;
		// defer piece_flushed()/we_have() until after the loop so we can skip
		// them if the last block causes a piece_hash_failed (which would otherwise
		// leave m_have_pieces set for a piece that gets we_dont_have()'d, causing
		// a double we_have() assert on re-download)
		std::vector<piece_index_t> adjacent_verified;

		for (int i = 0; i < blocks_in_piece; ++i)
		{
			// if there was an enoent or eof error the block hashes array may be incomplete
			// bail if we've hit the end of the valid hashes
			if (block_hashes[i].is_all_zeros()) return false;

			auto const result = get_hash_picker().set_block_hash(piece
				, i * default_block_size, block_hashes[i]);
			last_result = result;

			// all verified ranges should always be full pieces or less
			TORRENT_ASSERT(result.first_verified_block >= 0
				|| (result.first_verified_block % blocks_per_piece) == 0);
			TORRENT_ASSERT(result.num_verified <= blocks_per_piece
				|| (result.num_verified % blocks_per_piece) == 0);

			if (result.status == set_block_hash_result::result::success)
			{
				TORRENT_ASSERT(result.first_verified_block < blocks_in_piece);
				TORRENT_ASSERT(blocks_in_piece <= blocks_per_piece);

				// note that result.num_verified may cover pad blocks too, and
				// so may be > blocks_in_piece

				// sometimes, completing a single block may "unlock" validating
				// multiple pieces. e.g. if we don't have the piece layer yet,
				// but we completed the last block in the whole torrent, now we
				// can validate everything. For this reason,
				// first_verified_block may be negative.

				// In this call, we track the blocks in this piece though, in
				// the block_passed array. For that tracking we need to clamp
				// the start index to 0.
				auto const first_block = std::max(0, result.first_verified_block);
				auto const count = std::min(blocks_in_piece - first_block, result.num_verified);
				std::fill_n(block_passed.begin() + first_block, count, true);

				// the current block (i) should be part of the range that was
				// verified
				TORRENT_ASSERT(first_block <= i);
				TORRENT_ASSERT(i < first_block + count);

				// if the hashes for more than one piece have been verified,
				// check for any pieces which were already checked but couldn't
				// be verified and mark them as verified
				for (piece_index_t verified_piece : result.piece_range(piece, blocks_per_piece))
				{
					if (!has_picker()
						|| verified_piece == piece
						|| !m_picker->is_piece_finished(verified_piece)
						|| m_picker->have_piece(verified_piece))
						continue;

					TORRENT_ASSERT(get_hash_picker().piece_verified(verified_piece));
					adjacent_verified.push_back(verified_piece);
				}
			}
			else if (result.status == set_block_hash_result::result::block_hash_failed)
			{
				ret = false;
			}
		}
		if (last_result.status == set_block_hash_result::result::piece_hash_failed)
		{
			// only if the *last* block causes the piece to fail, do we know
			// it actually failed. Otherwise it might have been failing
			// because of other, previously existing block hashes.
			ret = false;

			// if the hashes for more than one piece have been verified,
			// check for any pieces which were already checked but couldn't
			// be verified and mark them as verified
			for (piece_index_t verified_piece : last_result.piece_range(piece, blocks_per_piece))
			{
				if (!has_picker()
					|| verified_piece == piece)
					continue;

#if TORRENT_USE_INVARIANT_CHECKS
				// if we already called we_have() for this piece (via adjacent_verified)
				// the m_file_progress bit would be orphaned after we_dont_have().
				// The deferred approach prevents this from ever happening.
				TORRENT_ASSERT(!m_file_progress.have_piece(verified_piece));
#endif
				m_picker->we_dont_have(verified_piece);
				update_gauge();
				piece_failed(verified_piece);
			}
		}

		if (last_result.status != set_block_hash_result::result::piece_hash_failed)
		{
			for (piece_index_t verified_piece : adjacent_verified)
			{
				if (m_picker->have_piece(verified_piece)) continue;
				// we_have() has not been called yet for this piece — confirm the bit is clear
#if TORRENT_USE_INVARIANT_CHECKS
				TORRENT_ASSERT(!m_file_progress.have_piece(verified_piece));
#endif
				m_picker->piece_flushed(verified_piece);
				update_gauge();
				we_have(verified_piece);
			}
		}

		if (boost::indeterminate(ret) && std::all_of(block_passed.begin(), block_passed.end()
			, [](bool e) { return e; }))
		{
			ret = true;
		}
		return ret;
	}

	// this is called when the piece hash is checked as correct. Note
	// that the piece picker and the torrent won't necessarily consider
	// us to have this piece yet, since it might not have been flushed
	// to disk yet. Only if we have predictive_piece_announce on will
	// we announce this piece to peers at this point.
	void torrent::piece_passed(piece_index_t const index)
	{
//		INVARIANT_CHECK;
		TORRENT_ASSERT(is_single_thread());
		TORRENT_ASSERT(!m_picker->have_piece(index));

#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
			debug_log("PIECE_PASSED (%d) (num_have: %d)", int(index), num_have());
#endif

//		std::fprintf(stderr, "torrent::piece_passed piece:%d\n", index);

		TORRENT_ASSERT(index >= piece_index_t(0));
		TORRENT_ASSERT(index < m_torrent_file->end_piece());

		set_need_save_resume(torrent_handle::if_download_progress);

		inc_stats_counter(counters::num_piece_passed);

		if (settings().get_int(settings_pack::suggest_mode)
			== settings_pack::suggest_read_cache)
		{
			// we just got a new piece. Chances are that it's actually the
			// rarest piece (since we're likely to download pieces rarest first)
			// if it's rarer than any other piece that we currently suggest, insert
			// it in the suggest set and pop the last one out
			add_suggest_piece(index);
		}

		// increase the trust point of all peers that sent
		// parts of this piece.
		std::set<torrent_peer*> const peers = [&]
		{
			std::vector<torrent_peer*> const downloaders = m_picker->get_downloaders(index);

			std::set<torrent_peer*> ret;
			// these torrent_peer pointers are owned by m_peer_list and they may be
			// invalidated if a peer disconnects. We cannot keep them across any
			// significant operations, but we should use them right away
			// ignore nullptrs
			std::remove_copy(downloaders.begin(), downloaders.end()
				, std::inserter(ret, ret.begin()), static_cast<torrent_peer*>(nullptr));
			return ret;
		}();

		for (auto* p : peers)
		{
			TORRENT_ASSERT(p != nullptr);
			if (p == nullptr) continue;
			TORRENT_ASSERT(p->in_use);
			p->on_parole = false;
			int trust_points = p->trust_points;
			++trust_points;
			if (trust_points > 8) trust_points = 8;
			p->trust_points = trust_points;
			if (p->connection)
			{
				auto* peer = static_cast<peer_connection*>(p->connection);
				TORRENT_ASSERT(peer->m_in_use == 1337);
				peer->received_valid_data(index);
			}
		}

		m_picker->piece_passed(index);
		update_gauge();
		we_have(index);

#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
			debug_log("we_have(%d) (num_have: %d)", int(index), num_have());
#endif
#ifndef TORRENT_DISABLE_STREAMING
		remove_time_critical_piece(index, true);
#endif
	}

#ifndef TORRENT_DISABLE_PREDICTIVE_PIECES
	// we believe we will complete this piece very soon
	// announce it to peers ahead of time to eliminate the
	// round-trip times involved in announcing it, requesting it
	// and sending it
	void torrent::predicted_have_piece(piece_index_t const index, time_duration const duration)
	{
		auto const i = std::lower_bound(m_predictive_pieces.begin()
			, m_predictive_pieces.end(), index);
		if (i != m_predictive_pieces.end() && *i == index) return;

		for (auto* p : m_connections)
		{
			TORRENT_INCREMENT(m_iterating_connections);
#ifndef TORRENT_DISABLE_LOGGING
			p->peer_log(peer_log_alert::outgoing, peer_log_alert::predictive_have, "piece: %d expected in %d ms"
				, static_cast<int>(index), static_cast<int>(total_milliseconds(duration)));
#else
			TORRENT_UNUSED(duration);
#endif
			p->announce_piece(index);
		}

		m_predictive_pieces.insert(i, index);
	}
#endif

	// blocks may contain the block indices of the blocks that failed (if this is
	// a v2 torrent).
	void torrent::piece_failed(piece_index_t const index, std::vector<int> blocks)
	{
		// if the last piece fails the peer connection will still
		// think that it has received all of it until this function
		// resets the download queue. So, we cannot do the
		// invariant check here since it assumes:
		// (total_done == m_torrent_file->total_size()) => is_seed()
		INVARIANT_CHECK;
		TORRENT_ASSERT(is_single_thread());

		TORRENT_ASSERT(has_picker());
		TORRENT_ASSERT(index >= piece_index_t(0));
		TORRENT_ASSERT(index < m_torrent_file->end_piece());
		TORRENT_ASSERT(std::is_sorted(blocks.begin(), blocks.end()));

		inc_stats_counter(counters::num_piece_failed);

#ifndef TORRENT_DISABLE_PREDICTIVE_PIECES
		auto const it = std::lower_bound(m_predictive_pieces.begin()
			, m_predictive_pieces.end(), index);
		if (it != m_predictive_pieces.end() && *it == index)
		{
			for (auto* p : m_connections)
			{
				TORRENT_INCREMENT(m_iterating_connections);
				// send reject messages for
				// potential outstanding requests to this piece
				p->reject_piece(index);
				// let peers that support the dont-have message
				// know that we don't actually have this piece
				p->write_dont_have(index);
			}
			m_predictive_pieces.erase(it);
		}
#endif

		std::vector<torrent_peer*> const downloaders = m_picker->get_downloaders(index);

		// decrease the trust point of all peers that sent
		// parts of this piece.
		// first, build a set of all peers that participated
		// if we know which blocks failed, just include the peer(s) sending those
		// blocks
		std::set<torrent_peer*> const peers = [&]
		{
			std::set<torrent_peer*> ret;
			if (!blocks.empty() && !downloaders.empty())
			{
				for (auto const b : blocks) ret.insert(downloaders[std::size_t(b)]);
			}
			else
			{
				std::copy(downloaders.begin(), downloaders.end(), std::inserter(ret, ret.begin()));
			}
			return ret;
		}();

		// if this piece wasn't downloaded from peers, we just found it on disk.
		// In that case, we should just consider it as "not-have" and there's no
		// need to try to get higher fidelity hashes (yet)
		bool const found_on_disk = peers.size() == 1 && peers.count(nullptr);

		if (torrent_file().info_hashes().has_v2() && blocks.empty() && !found_on_disk)
		{
			// TODO: only do this if the piece size > 1 blocks
			// This is a v2 torrent so we can request get block
			// level hashes.
			verify_block_hashes(index);
		}

		// the below code is penalizing peers that sent use bad data.
		// increase the total amount of failed bytes
		if (!found_on_disk)
		{
			if (blocks.empty())
				add_failed_bytes(m_torrent_file->piece_size(index));
			else
				add_failed_bytes(static_cast<int>(blocks.size()) * default_block_size);

#ifndef TORRENT_DISABLE_EXTENSIONS
			for (auto& ext : m_extensions)
			{
				ext->on_piece_failed(index);
			}
#endif

			// did we receive this piece from a single peer?
			// if we know exactly which blocks failed the hash, we can also be certain
			// that all peers in the list sent us bad data
			bool const known_bad_peer = peers.size() == 1 || !blocks.empty();

			penalize_peers(peers, index, known_bad_peer);
		}

		// If m_storage isn't set here, it means we're shutting down
		if (m_storage)
		{
			// it doesn't make much sense to fail to hash a piece
			// without having a storage associated with the torrent.
			// restoring the piece in the piece picker without calling
			// clear piece on the disk thread will make them out of
			// sync, and if we try to write more blocks to this piece
			// the disk thread will barf, because it hasn't been cleared
			TORRENT_ASSERT(m_storage);

			// don't allow picking any blocks from this piece
			// until we're done synchronizing with the disk threads.
			m_picker->lock_piece(index);

			// don't do this until after the plugins have had a chance
			// to read back the blocks that failed, for blame purposes
			// this way they have a chance to hit the cache
			m_ses.disk_thread().async_clear_piece(m_storage, index
				, [self = shared_from_this(), c = std::move(blocks)](piece_index_t const& p)
				{ self->on_piece_sync(p, c); });
			m_ses.deferred_submit_jobs();
		}
		else
		{
			TORRENT_ASSERT(m_abort);
			// it doesn't really matter what we do
			// here, since we're about to destruct the
			// torrent anyway.
			on_piece_sync(index, std::move(blocks));
		}
	}

	void torrent::penalize_peers(std::set<torrent_peer*> const& peers
		, piece_index_t const index, bool const known_bad_peer)
	{
		for (auto* p : peers)
		{
			if (p == nullptr) continue;
			TORRENT_ASSERT(p->in_use);
			bool allow_disconnect = true;
			if (p->connection)
			{
				auto* peer = static_cast<peer_connection*>(p->connection);
				TORRENT_ASSERT(peer->m_in_use == 1337);

				// the peer implementation can ask not to be disconnected.
				// this is used for web seeds for instance, to instead of
				// disconnecting, mark the file as not being had.
				allow_disconnect = peer->received_invalid_data(index, known_bad_peer);
			}

			if (settings().get_bool(settings_pack::use_parole_mode))
				p->on_parole = true;

			int hashfails = p->hashfails;
			int trust_points = p->trust_points;

			// we decrease more than we increase, to keep the
			// allowed failed/passed ratio low.
			trust_points -= 2;
			++hashfails;
			if (trust_points < -7) trust_points = -7;
			p->trust_points = trust_points;
			if (hashfails > 255) hashfails = 255;
			p->hashfails = std::uint8_t(hashfails);

			// either, we have received too many failed hashes
			// or this was the only peer that sent us this piece.
			// if we have failed more than 3 pieces from this peer,
			// don't trust it regardless.
			if (p->trust_points <= -7
				|| (known_bad_peer && allow_disconnect))
			{
				// we don't trust this peer anymore
				// ban it.
				if (m_ses.alerts().should_post<peer_ban_alert>())
				{
					peer_id const pid = p->connection
						? p->connection->pid() : peer_id();
					m_ses.alerts().emplace_alert<peer_ban_alert>(
						get_handle(), p->ip(), pid);
				}

				// mark the peer as banned
				ban_peer(p);
				update_want_peers();
				inc_stats_counter(counters::banned_for_hash_failure);

				if (p->connection)
				{
					auto* peer = static_cast<peer_connection*>(p->connection);
#ifndef TORRENT_DISABLE_LOGGING
					if (should_log())
					{
						debug_log("*** BANNING PEER: \"%s\" Too many corrupt pieces"
							, print_endpoint(p->ip()).c_str());
					}
					peer->peer_log(peer_log_alert::info, peer_log_alert::banning_peer, "Too many corrupt pieces");
#endif
					peer->disconnect(errors::too_many_corrupt_pieces, operation_t::bittorrent);
				}
			}
		}
	}

	void torrent::peer_is_interesting(peer_connection& c)
	{
		INVARIANT_CHECK;

		// no peer should be interesting if we're finished
		TORRENT_ASSERT(!is_finished());

		if (c.in_handshake()) return;
		c.send_interested();
		if (c.has_peer_choked()
			&& c.allowed_fast().empty())
			return;

		if (request_a_block(*this, c))
			inc_stats_counter(counters::interesting_piece_picks);
		c.send_block_requests();
	}

	void torrent::on_piece_sync(piece_index_t const piece, std::vector<int> const& blocks) try
	{
		// the user may have called force_recheck, which clears
		// the piece picker
		if (!has_picker()) return;

		// unlock the piece and restore it, as if no block was
		// ever downloaded for it.
		m_picker->restore_piece(piece, blocks);

		if (m_ses.alerts().should_post<hash_failed_alert>())
			m_ses.alerts().emplace_alert<hash_failed_alert>(get_handle(), piece);

		// we have to let the piece_picker know that
		// this piece failed the check as it can restore it
		// and mark it as being interesting for download
		TORRENT_ASSERT(!m_picker->have_piece(piece));

		// loop over all peers and re-request potential duplicate
		// blocks to this piece
		for (auto* p : m_connections)
		{
			TORRENT_INCREMENT(m_iterating_connections);
			for (auto const& b : p->download_queue())
			{
				if (b.timed_out || b.not_wanted) continue;
				if (b.block.piece_index != piece) continue;
				if (!blocks.empty()
					&& std::find(blocks.begin(), blocks.end(), b.block.block_index) == blocks.end())
					continue;
				m_picker->mark_as_downloading(b.block, p->peer_info_struct()
					, p->picker_options());
			}
			for (auto const& b : p->request_queue())
			{
				if (b.block.piece_index != piece) continue;
				if (!blocks.empty()
					&& std::find(blocks.begin(), blocks.end(), b.block.block_index) == blocks.end())
					continue;
				m_picker->mark_as_downloading(b.block, p->peer_info_struct()
					, p->picker_options());
			}
		}
	}
	catch (...) { handle_exception(); }

	void torrent::peer_has(piece_index_t const index, peer_connection const* peer)
	{
		if (has_picker())
		{
			torrent_peer* pp = peer->peer_info_struct();
			m_picker->inc_refcount(index, pp);
		}
		else
		{
			TORRENT_ASSERT(is_seed() || !m_have_all);
		}
	}

	// when we get a bitfield message, this is called for that piece
	void torrent::peer_has(typed_bitfield<piece_index_t> const& bits
		, peer_connection const* peer)
	{
		if (has_picker())
		{
			TORRENT_ASSERT(bits.size() == torrent_file().num_pieces());
			torrent_peer* pp = peer->peer_info_struct();
			m_picker->inc_refcount(bits, pp);
		}
		else
		{
			TORRENT_ASSERT(is_seed() || !m_have_all);
		}
	}

	void torrent::peer_has_all(peer_connection const* peer)
	{
		if (has_picker())
		{
			torrent_peer* pp = peer->peer_info_struct();
			m_picker->inc_refcount_all(pp);
		}
		else
		{
			TORRENT_ASSERT(is_seed() || !m_have_all);
		}
	}

	void torrent::peer_lost(typed_bitfield<piece_index_t> const& bits
		, peer_connection const* peer)
	{
		if (has_picker())
		{
			TORRENT_ASSERT(bits.size() == torrent_file().num_pieces());
			torrent_peer* pp = peer->peer_info_struct();
			m_picker->dec_refcount(bits, pp);
		}
		else
		{
			TORRENT_ASSERT(is_seed() || !m_have_all);
		}
	}

	void torrent::peer_lost(piece_index_t const index, peer_connection const* peer)
	{
		if (m_picker)
		{
			torrent_peer* pp = peer->peer_info_struct();
			m_picker->dec_refcount(index, pp);
		}
		else
		{
			TORRENT_ASSERT(is_seed() || !m_have_all);
		}
	}

	void torrent::abort()
	{
		TORRENT_ASSERT(is_single_thread());

		if (m_abort) return;

		m_abort = true;
		m_paused = false;

		// flush the live segment into the accumulators and mark them all
		// not-live, now that m_abort/m_paused have settled into their
		// final values
		update_state_timers();
		// clear graceful pause too. on_remove_peers() below would otherwise
		// hit the "last peer in graceful pause" branch and assert is_paused(),
		// which no longer holds now that m_paused has been cleared.
		m_graceful_pause_mode = false;
		m_auto_managed = false;
		m_state_subscription = false;
		for (torrent_list_index_t i{}; i != m_links.end_index(); ++i)
		{
			if (!m_links[i].in_list()) continue;
			m_links[i].unlink(m_ses.torrent_list(i), i);
		}

		update_want_peers();
		update_want_tick();
		update_want_scrape();
		update_gauge();
		stop_announcing();

		// remove from download queue
		m_ses.set_queue_position(this, queue_position_t{-1});

		if (m_peer_class > peer_class_t{0})
		{
			remove_class(m_ses.peer_classes(), m_peer_class);
			m_ses.peer_classes().decref(m_peer_class);
			m_peer_class = peer_class_t{0};
		}

		m_inactivity_timer.cancel();

#ifndef TORRENT_DISABLE_LOGGING
		log_to_all_peers("aborting");
#endif

		// disconnect all peers and close all
		// files belonging to the torrents
		disconnect_all(errors::torrent_aborted, operation_t::bittorrent);

		// make sure to destruct the peers immediately
		on_remove_peers();
		TORRENT_ASSERT(m_connections.empty());

		// post a message to the main thread to destruct
		// the torrent object from there
		if (m_storage)
		{
			try {
				m_ses.disk_thread().async_stop_torrent(m_storage
					, std::bind(&torrent::on_torrent_aborted, shared_from_this()));
			}
			catch (std::exception const& e)
			{
				TORRENT_UNUSED(e);
				m_storage.reset();
#ifndef TORRENT_DISABLE_LOGGING
				debug_log("Failed to flush disk cache: %s", e.what());
#endif
				// clients may rely on this alert to be posted, so it's probably a
				// good idea to post it here, even though we failed
				// TODO: 3 should this alert have an error code in it?
				if (alerts().should_post<cache_flushed_alert>())
					alerts().emplace_alert<cache_flushed_alert>(get_handle());
			}
			m_ses.deferred_submit_jobs();
		}
		else
		{
			if (alerts().should_post<cache_flushed_alert>())
				alerts().emplace_alert<cache_flushed_alert>(get_handle());
			alerts().emplace_alert<torrent_removed_alert>(get_handle()
				, info_hash(), get_userdata());
		}

		// TODO: 2 abort lookups this torrent has made via the
		// session host resolver interface

		if (!m_apply_ip_filter)
		{
			inc_stats_counter(counters::non_filter_torrents, -1);
			m_apply_ip_filter = true;
		}

#if TORRENT_USE_RTC
		if(m_rtc_signaling)
			m_rtc_signaling->close();
#endif
	}

	// this is called when we're destructing non-gracefully. i.e. we're _just_
	// destructing everything.
	void torrent::panic()
	{
		m_storage.reset();
		// if there are any other peers allocated still, we need to clear them
		// now. They can't be cleared later because the allocator will already
		// have been destructed
		if (m_peer_list) m_peer_list->clear();
		m_connections.clear();
		m_outgoing_pids.clear();
		m_peers_to_disconnect.clear();
		m_num_uploads = 0;
		m_num_connecting = 0;
		m_num_connecting_seeds = 0;
	}

#ifndef TORRENT_DISABLE_SUPERSEEDING
	void torrent::set_super_seeding(bool const on)
	{
		if (on == m_super_seeding) return;

		m_super_seeding = on;
		set_need_save_resume(torrent_handle::if_state_changed);
		state_updated();

		if (m_super_seeding) return;

		// disable super seeding for all peers
		for (auto* pc : *this)
		{
			pc->superseed_piece(piece_index_t(-1), piece_index_t(-1));
		}
	}

	// TODO: 3 this should return optional<>. piece index -1 should not be
	// allowed
	piece_index_t torrent::get_piece_to_super_seed(typed_bitfield<piece_index_t> const& bits)
	{
		// return a piece with low availability that is not in
		// the bitfield and that is not currently being super
		// seeded by any peer
		TORRENT_ASSERT(m_super_seeding);

		// do a linear search from the first piece
		int min_availability = 9999;
		std::vector<piece_index_t> avail_vec;
		for (auto const i : m_torrent_file->piece_range())
		{
			if (bits[i]) continue;

			int availability = 0;
			for (auto* pc : *this)
			{
				if (pc->super_seeded_piece(i))
				{
					// avoid super-seeding the same piece to more than one
					// peer if we can avoid it. Do this by artificially
					// increase the availability
					availability = 999;
					break;
				}
				if (pc->has_piece(i)) ++availability;
			}
			if (availability > min_availability) continue;
			if (availability == min_availability)
			{
				avail_vec.push_back(i);
				continue;
			}
			TORRENT_ASSERT(availability < min_availability);
			min_availability = availability;
			avail_vec.clear();
			avail_vec.push_back(i);
		}

		if (avail_vec.empty()) return piece_index_t{-1};
		return avail_vec[random(std::uint32_t(avail_vec.size() - 1))];
	}
#endif

	void torrent::on_files_deleted(storage_error const& error) try
	{
		TORRENT_ASSERT(is_single_thread());

		if (error)
		{
			if (alerts().should_post<torrent_delete_failed_alert>())
				alerts().emplace_alert<torrent_delete_failed_alert>(get_handle()
					, error.ec, m_torrent_file->info_hashes());
		}
		else
		{
			alerts().emplace_alert<torrent_deleted_alert>(get_handle(), m_torrent_file->info_hashes());
		}
	}
	catch (...) { handle_exception(); }

	void torrent::on_file_renamed(std::string const& filename
		, file_index_t const file_idx
		, storage_error const& error) try
	{
		TORRENT_ASSERT(is_single_thread());

		if (error)
		{
			alerts().emplace_alert<file_rename_failed_alert>(get_handle()
				, file_idx, error.ec);
		}
		else
		{
			file_storage const& fs = m_torrent_file->layout();
			alerts().emplace_alert<file_renamed_alert>(get_handle()
				, filename, m_renamed_files.file_path(fs, file_idx), file_idx);
			m_renamed_files.rename_file(fs, file_idx, filename);

			set_need_save_resume(torrent_handle::if_state_changed);
			state_updated();
		}
	}
	catch (...) { handle_exception(); }

	void torrent::on_torrent_paused() try
	{
		TORRENT_ASSERT(is_single_thread());

		if (alerts().should_post<torrent_paused_alert>())
			alerts().emplace_alert<torrent_paused_alert>(get_handle());
	}
	catch (...) { handle_exception(); }

#if TORRENT_ABI_VERSION == 1
	std::string torrent::tracker_login() const
	{
		if (m_username.empty() && m_password.empty()) return "";
		return m_username + ":" + m_password;
	}
#endif

	std::uint32_t torrent::tracker_key() const
	{
		auto const self = reinterpret_cast<uintptr_t>(this);
		auto const ses = reinterpret_cast<uintptr_t>(&m_ses);
		std::uint32_t const storage = m_storage
			? static_cast<std::uint32_t>(static_cast<storage_index_t>(m_storage))
			: 0;
		sha1_hash const h = hasher(reinterpret_cast<char const*>(&self), sizeof(self))
			.update(reinterpret_cast<char const*>(&storage), sizeof(storage))
			.update(reinterpret_cast<char const*>(&ses), sizeof(ses))
			.final();
		unsigned char const* ptr = &h[0];
		return aux::read_uint32(ptr);
	}

#ifndef TORRENT_DISABLE_STREAMING
	void torrent::cancel_non_critical()
	{
		// if we don't have a piece picker, there's nothing to cancel.
		// e.g. We may have become a seed already.
		if (!has_picker()) return;

		std::set<piece_index_t> time_critical;
		for (auto const& p : m_time_critical_pieces)
			time_critical.insert(p.piece);

		for (auto* p : m_connections)
		{
			TORRENT_INCREMENT(m_iterating_connections);
			// for each peer, go through its download and request queue
			// and cancel everything, except pieces that are time critical

			// make a copy of the download queue since we may be cancelling entries
			// from it from within the loop
			std::vector<pending_block> dq = p->download_queue();
			for (auto const& k : dq)
			{
				if (time_critical.count(k.block.piece_index)) continue;
				if (k.not_wanted || k.timed_out) continue;
				p->cancel_request(k.block, true);
			}

			// make a copy of the download queue since we may be cancelling entries
			// from it from within the loop
			std::vector<pending_block> rq = p->request_queue();
			for (auto const& k : rq)
			{
				if (time_critical.count(k.block.piece_index)) continue;
				p->cancel_request(k.block, true);
			}
		}
	}

	void torrent::set_piece_deadline(piece_index_t const piece, int const t
		, deadline_flags_t const flags)
	{
		INVARIANT_CHECK;

		TORRENT_ASSERT_PRECOND(piece >= piece_index_t(0));
		TORRENT_ASSERT_PRECOND(valid_metadata());
		TORRENT_ASSERT_PRECOND(valid_metadata() && piece < m_torrent_file->end_piece());

		if (m_abort || !valid_metadata()
			|| piece < piece_index_t(0)
			|| piece >= m_torrent_file->end_piece())
		{
			// failed
			if (flags & torrent_handle::alert_when_available)
			{
				m_ses.alerts().emplace_alert<read_piece_alert>(
					get_handle(), piece, error_code(boost::system::errc::operation_canceled, generic_category()));
			}
			return;
		}

		time_point const deadline = aux::time_now() + milliseconds(t);

		// if we already have the piece, no need to set the deadline.
		// however, if the user asked to get the piece data back, we still
		// need to read it and post it back to the user
		if (is_seed() || (has_picker() && m_picker->have_piece(piece)))
		{
			if (flags & torrent_handle::alert_when_available)
				read_piece(piece);
			return;
		}

		// if this is the first time critical piece we add. in order to make it
		// react quickly, cancel all the currently outstanding requests
		if (m_time_critical_pieces.empty())
		{
			// defer this by posting it to the end of the message queue.
			// this gives the client a chance to specify multiple time-critical
			// pieces before libtorrent cancels requests
			auto self = shared_from_this();
			post(m_ses.get_context(), [self] { self->wrap(&torrent::cancel_non_critical); });
		}

		for (auto i = m_time_critical_pieces.begin()
			, end(m_time_critical_pieces.end()); i != end; ++i)
		{
			if (i->piece != piece) continue;
			i->deadline = deadline;
			i->flags = flags;

			// resort i since deadline might have changed
			while (std::next(i) != m_time_critical_pieces.end() && i->deadline > std::next(i)->deadline)
			{
				std::iter_swap(i, std::next(i));
				++i;
			}
			while (i != m_time_critical_pieces.begin() && i->deadline < std::prev(i)->deadline)
			{
				std::iter_swap(i, std::prev(i));
				--i;
			}
			// just in case this piece had priority 0
			download_priority_t const prev_prio = m_picker->piece_priority(piece);
			bool const was_finished = is_finished();
			bool const filter_updated = m_picker->set_piece_priority(piece, top_priority);
			if (prev_prio == dont_download)
			{
				update_gauge();
				if (filter_updated) update_peer_interest(was_finished);
			}
			return;
		}

		need_picker();

		time_critical_piece p;
		p.first_requested = min_time();
		p.last_requested = min_time();
		p.flags = flags;
		p.deadline = deadline;
		p.peers = 0;
		p.piece = piece;
		auto const critical_piece_it = std::upper_bound(m_time_critical_pieces.begin()
			, m_time_critical_pieces.end(), p);
		m_time_critical_pieces.insert(critical_piece_it, p);

		// just in case this piece had priority 0
		download_priority_t const prev_prio = m_picker->piece_priority(piece);
		bool const was_finished = is_finished();
		bool const filter_updated = m_picker->set_piece_priority(piece, top_priority);
		if (prev_prio == dont_download)
		{
			update_gauge();
			if (filter_updated) update_peer_interest(was_finished);
		}

		piece_picker::downloading_piece pi;
		m_picker->piece_info(piece, pi);
		if (pi.requested == 0) return;
		// this means we have outstanding requests (or queued
		// up requests that haven't been sent yet). Promote them
		// to deadline pieces immediately
		std::vector<torrent_peer*> const downloaders
			= m_picker->get_downloaders(piece);

		int block = 0;
		for (auto i = downloaders.begin()
			, end(downloaders.end()); i != end; ++i, ++block)
		{
			torrent_peer* const tp = *i;
			if (tp == nullptr || tp->connection == nullptr) continue;
			auto* peer = static_cast<peer_connection*>(tp->connection);
			peer->make_time_critical(piece_block(piece, block));
		}
	}

	void torrent::reset_piece_deadline(piece_index_t piece)
	{
		remove_time_critical_piece(piece);
	}

	void torrent::remove_time_critical_piece(piece_index_t const piece, bool const finished)
	{
		for (auto i = m_time_critical_pieces.begin(), end(m_time_critical_pieces.end());
			i != end; ++i)
		{
			if (i->piece != piece) continue;
			if (finished)
			{
				if (i->flags & torrent_handle::alert_when_available)
				{
					read_piece(i->piece);
				}

				// if first_requested is min_time(), it wasn't requested as a critical piece
				// and we shouldn't adjust any average download times
				if (i->first_requested != min_time())
				{
					// update the average download time and average
					// download time deviation
					int const dl_time = aux::numeric_cast<int>(total_milliseconds(aux::time_now() - i->first_requested));

					if (m_average_piece_time == 0)
					{
						m_average_piece_time = dl_time;
					}
					else
					{
						int diff = std::abs(dl_time - m_average_piece_time);
						if (m_piece_time_deviation == 0) m_piece_time_deviation = diff;
						else m_piece_time_deviation = (m_piece_time_deviation * 9 + diff) / 10;

						m_average_piece_time = (m_average_piece_time * 9 + dl_time) / 10;
					}
				}
			}
			else if (i->flags & torrent_handle::alert_when_available)
			{
				// post an empty read_piece_alert to indicate it failed
				alerts().emplace_alert<read_piece_alert>(
					get_handle(), piece, error_code(boost::system::errc::operation_canceled, generic_category()));
			}
			if (has_picker()) m_picker->set_piece_priority(piece, low_priority);
			m_time_critical_pieces.erase(i);
			return;
		}
	}

	void torrent::clear_time_critical()
	{
		for (auto i = m_time_critical_pieces.begin(); i != m_time_critical_pieces.end();)
		{
			if (i->flags & torrent_handle::alert_when_available)
			{
				// post an empty read_piece_alert to indicate it failed
				m_ses.alerts().emplace_alert<read_piece_alert>(
					get_handle(), i->piece, error_code(boost::system::errc::operation_canceled, generic_category()));
			}
			if (has_picker()) m_picker->set_piece_priority(i->piece, low_priority);
			i = m_time_critical_pieces.erase(i);
		}
	}

	// remove time critical pieces where priority is 0
	void torrent::remove_time_critical_pieces(aux::vector<download_priority_t, piece_index_t> const& priority)
	{
		for (auto i = m_time_critical_pieces.begin(); i != m_time_critical_pieces.end();)
		{
			if (priority[i->piece] == dont_download)
			{
				if (i->flags & torrent_handle::alert_when_available)
				{
					// post an empty read_piece_alert to indicate it failed
					alerts().emplace_alert<read_piece_alert>(
						get_handle(), i->piece, error_code(boost::system::errc::operation_canceled, generic_category()));
				}
				i = m_time_critical_pieces.erase(i);
				continue;
			}
			++i;
		}
	}
#endif // TORRENT_DISABLE_STREAMING

	void torrent::post_piece_availability()
	{
		aux::vector<int, piece_index_t> avail;
		piece_availability(avail);
		alerts().emplace_alert<piece_availability_alert>(get_handle(), std::move(avail));
	}

	void torrent::piece_availability(aux::vector<int, piece_index_t>& avail) const
	{
		INVARIANT_CHECK;

		TORRENT_ASSERT(valid_metadata());
		if (!has_picker())
		{
			avail.clear();
			return;
		}

		m_picker->get_availability(avail);
	}

	void torrent::set_piece_priority(piece_index_t const index
		, download_priority_t const priority)
	{
//		INVARIANT_CHECK;

#ifndef TORRENT_DISABLE_LOGGING
		if (!valid_metadata())
		{
			debug_log("*** SET_PIECE_PRIORITY [ idx: %d prio: %d ignored. "
				"no metadata yet ]", static_cast<int>(index)
				, static_cast<std::uint8_t>(priority));
		}
#endif
		if (!valid_metadata() || is_seed()) return;

		// this call is only valid on torrents with metadata
		if (index < piece_index_t(0) || index >= m_torrent_file->end_piece())
		{
			return;
		}

		need_picker();

		bool const was_finished = is_finished();
		bool const filter_updated = m_picker->set_piece_priority(index, priority);

		update_gauge();

		if (filter_updated)
		{
			update_peer_interest(was_finished);
#ifndef TORRENT_DISABLE_STREAMING
			if (priority == dont_download) remove_time_critical_piece(index);
#endif // TORRENT_DISABLE_STREAMING
		}

	}

	download_priority_t torrent::piece_priority(piece_index_t const index) const
	{
//		INVARIANT_CHECK;

		if (!has_picker()) return default_priority;

		// this call is only valid on torrents with metadata
		TORRENT_ASSERT(valid_metadata());
		if (index < piece_index_t(0) || index >= m_torrent_file->end_piece())
		{
			TORRENT_ASSERT_FAIL();
			return dont_download;
		}

		return m_picker->piece_priority(index);
	}

	void torrent::prioritize_piece_list(std::vector<std::pair<piece_index_t
		, download_priority_t>> const& pieces)
	{
		INVARIANT_CHECK;

		// this call is only valid on torrents with metadata
		TORRENT_ASSERT(valid_metadata());
		if (is_seed()) return;

		need_picker();

		bool filter_updated = false;
		bool const was_finished = is_finished();
		for (auto const& p : pieces)
		{
			static_assert(std::is_unsigned<decltype(p.second)::underlying_type>::value
				, "we need assert p.second >= dont_download");
			TORRENT_ASSERT(p.second <= top_priority);
			TORRENT_ASSERT(p.first >= piece_index_t(0));
			TORRENT_ASSERT(p.first < m_torrent_file->end_piece());

			if (p.first < piece_index_t(0)
				|| p.first >= m_torrent_file->end_piece()
				|| p.second > top_priority)
			{
				static_assert(std::is_unsigned<decltype(p.second)::underlying_type>::value
					, "we need additional condition: p.second < dont_download");
				continue;
			}

			filter_updated |= m_picker->set_piece_priority(p.first, p.second);
		}
		update_gauge();
		if (filter_updated)
		{
			// we need to save this new state
			set_need_save_resume(torrent_handle::if_config_changed);

			update_peer_interest(was_finished);
		}

		state_updated();
	}

	void torrent::prioritize_pieces(aux::vector<download_priority_t, piece_index_t> const& pieces)
	{
		INVARIANT_CHECK;

		// this call is only valid on torrents with metadata
		TORRENT_ASSERT(valid_metadata());
		if (is_seed()) return;

		if (!valid_metadata())
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("*** PRIORITIZE_PIECES [ ignored. no metadata yet ]");
#endif
			return;
		}

		need_picker();

		piece_index_t index(0);
		bool filter_updated = false;
		bool const was_finished = is_finished();
		for (auto prio : pieces)
		{
			static_assert(std::is_unsigned<decltype(prio)::underlying_type>::value
				, "we need assert prio >= dont_download");
			TORRENT_ASSERT(prio <= top_priority);
			filter_updated |= m_picker->set_piece_priority(index, prio);
			++index;
		}
		update_gauge();
		update_want_tick();

		if (filter_updated)
		{
			// we need to save this new state
			set_need_save_resume(torrent_handle::if_config_changed);

			update_peer_interest(was_finished);
#ifndef TORRENT_DISABLE_STREAMING
			remove_time_critical_pieces(pieces);
#endif
		}

		state_updated();
		update_state_list();
	}

	void torrent::piece_priorities(aux::vector<download_priority_t, piece_index_t>* pieces) const
	{
		INVARIANT_CHECK;

		// this call is only valid on torrents with metadata
		if (!valid_metadata())
		{
			pieces->clear();
			return;
		}

		if (!has_picker())
		{
			pieces->clear();
			pieces->resize(m_torrent_file->num_pieces(), default_priority);
			return;
		}

		TORRENT_ASSERT(m_picker);
		m_picker->piece_priorities(*pieces);
	}

	namespace
	{
		aux::vector<download_priority_t, file_index_t> fix_priorities(
			aux::vector<download_priority_t, file_index_t> input
			, file_storage const* fs)
		{
			if (fs) input.resize(fs->num_files(), default_priority);

			for (file_index_t i : input.range())
			{
				// initialize pad files to priority 0
				if (input[i] > dont_download && fs && fs->pad_file_at(i))
					input[i] = dont_download;
				else if (input[i] > top_priority)
					input[i] = top_priority;
			}

			return input;
		}
	}

	void torrent::on_file_priority(storage_error const& err
		, aux::vector<download_priority_t, file_index_t> prios)
	{
		m_outstanding_file_priority = false;
		COMPLETE_ASYNC("file_priority");

		if (m_file_priority != prios)
		{
			update_piece_priorities(prios);
			m_file_priority = std::move(prios);
			set_need_save_resume(torrent_handle::if_config_changed);
#ifndef TORRENT_DISABLE_SHARE_MODE
			if (m_share_mode)
				recalc_share_mode();
#endif
		}

		if (err)
		{
			// in this case, some file priorities failed to get set
			if (alerts().should_post<file_error_alert>())
				alerts().emplace_alert<file_error_alert>(err.ec
					, resolve_filename(err.file()), err.operation, get_handle());

			set_error(err.ec, err.file());
			pause();
			return;
		}

		if (alerts().should_post<file_prio_alert>())
			alerts().emplace_alert<file_prio_alert>(get_handle());

		if (!m_deferred_file_priorities.empty() && !m_abort)
		{
			auto new_priority = m_file_priority;
			// resize the vector if we have to. The last item in the map has the
			// highest file index.
			auto const max_idx = std::prev(m_deferred_file_priorities.end())->first;
			if (new_priority.end_index() <= max_idx)
			{
				// any unallocated slot is assumed to have the default priority
				new_priority.resize(static_cast<int>(max_idx) + 1, default_priority);
			}
			for (auto const& p : m_deferred_file_priorities)
			{
				file_index_t const index = p.first;
				download_priority_t const prio = p.second;
				new_priority[index] = prio;
			}
			m_deferred_file_priorities.clear();
			prioritize_files(std::move(new_priority));
		}
	}

	void torrent::prioritize_files(aux::vector<download_priority_t, file_index_t> files)
	{
		INVARIANT_CHECK;

		auto new_priority = fix_priorities(std::move(files)
			, valid_metadata() ? &m_torrent_file->layout() : nullptr);

		m_deferred_file_priorities.clear();

		// storage may be NULL during shutdown
		if (m_storage)
		{
			// the update of m_file_priority is deferred until the disk job comes
			// back, but to preserve sanity and consistency, the piece priorities are
			// updated immediately. If, on the off-chance, there's a disk failure, the
			// piece priorities still stay the same, but the file priorities are
			// possibly not fully updated.

			m_outstanding_file_priority = true;
			ADD_OUTSTANDING_ASYNC("file_priority");
			m_ses.disk_thread().async_set_file_priority(m_storage
				, std::move(new_priority)
				, [self = shared_from_this()] (storage_error const& ec, aux::vector<download_priority_t, file_index_t> p)
				{ self->on_file_priority(ec, std::move(p)); });
			m_ses.deferred_submit_jobs();
		}
		else
		{
			m_file_priority = std::move(new_priority);
			set_need_save_resume(torrent_handle::if_config_changed);
		}
	}

	void torrent::set_file_priority(file_index_t const index
		, download_priority_t prio)
	{
		INVARIANT_CHECK;

		// setting file priority on a torrent that doesn't have metadata yet is
		// similar to having passed in file priorities through add_torrent_params.
		// we store the priorities in m_file_priority until we get the metadata
		if (index < file_index_t(0)
			|| (valid_metadata() && index >= m_torrent_file->layout().end_file()))
		{
			return;
		}

		prio = std::clamp(prio, dont_download, top_priority);

		if (m_outstanding_file_priority)
		{
			m_deferred_file_priorities[index] = prio;
			return;
		}

		auto new_priority = m_file_priority;
		if (new_priority.end_index() <= index)
		{
			// any unallocated slot is assumed to have the default priority
			new_priority.resize(static_cast<int>(index) + 1, default_priority);
		}

		new_priority[index] = prio;

		// storage may be nullptr during shutdown
		if (m_storage)
		{
			m_outstanding_file_priority = true;
			ADD_OUTSTANDING_ASYNC("file_priority");
			m_ses.disk_thread().async_set_file_priority(m_storage
				, std::move(new_priority)
				, [self = shared_from_this()] (storage_error const& ec, aux::vector<download_priority_t, file_index_t> p)
				{ self->on_file_priority(ec, std::move(p)); });
			m_ses.deferred_submit_jobs();
		}
		else
		{
			m_file_priority = std::move(new_priority);
			set_need_save_resume(torrent_handle::if_config_changed);
		}
	}

	download_priority_t torrent::file_priority(file_index_t const index) const
	{
		TORRENT_ASSERT_PRECOND(index >= file_index_t(0));
		if (index < file_index_t(0)) return dont_download;

		// if we have metadata, perform additional checks
		if (valid_metadata())
		{
			file_storage const& fs = m_torrent_file->layout();
			TORRENT_ASSERT_PRECOND(index < fs.end_file());
			if (index >= fs.end_file()) return dont_download;

			// pad files always have priority 0
			if (fs.pad_file_at(index)) return dont_download;
		}

		// any unallocated slot is assumed to have the default priority
		if (m_file_priority.end_index() <= index) return default_priority;

		return m_file_priority[index];
	}

	void torrent::post_file_priorities()
	{
		aux::vector<download_priority_t, file_index_t> prio;
		file_priorities(&prio);
		alerts().emplace_alert<file_priorities_alert>(get_handle(), std::move(prio));
	}

	void torrent::file_priorities(aux::vector<download_priority_t, file_index_t>* files) const
	{
		INVARIANT_CHECK;

		files->assign(m_file_priority.begin(), m_file_priority.end());

		if (!valid_metadata())
		{
			return;
		}

		files->resize(m_torrent_file->num_files(), default_priority);
	}

	void torrent::update_piece_priorities(
		aux::vector<download_priority_t, file_index_t> const& file_prios)
	{
		INVARIANT_CHECK;

		if (m_torrent_file->num_pieces() == 0) return;

		auto const pieces = file_to_piece_prio(m_torrent_file->layout(), file_prios);
		prioritize_pieces(pieces);
	}

	// this is called when piece priorities have been updated
	// updates the interested flag in peers
	void torrent::update_peer_interest(bool const was_finished)
	{
		for (auto i = begin(); i != end();)
		{
			peer_connection* p = *i;
			// update_interest may disconnect the peer and
			// invalidate the iterator
			++i;
			p->update_interest();
		}

		if (!is_downloading_state(m_state))
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("*** UPDATE_PEER_INTEREST [ skipping, state: %d ]"
				, int(m_state));
#endif
			return;
		}

#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			debug_log("*** UPDATE_PEER_INTEREST [ finished: %d was_finished %d ]"
				, is_finished(), was_finished);
		}
#endif

		// the torrent just became finished
		if (!was_finished && is_finished())
		{
			finished();
		}
		else if (was_finished && !is_finished())
		{
			// if we used to be finished, but we aren't anymore
			// we may need to connect to peers again
			resume_download();
		}
	}

	void torrent::post_trackers()
	{
		auto t = trackers();
		m_ses.alerts().emplace_alert<tracker_list_alert>(get_handle(), std::move(t));
	}

	std::vector<lt::announce_entry> torrent::trackers() const
	{
		std::vector<lt::announce_entry> ret;
		ret.reserve(m_trackers.size());
		for (auto const& t : m_trackers)
		{
			ret.emplace_back(t.url);
			auto& tr = ret.back();
			tr.source = t.source;
			tr.trackerid = t.trackerid;
			tr.verified = t.verified;
			tr.tier = t.tier;
			tr.fail_limit = t.fail_limit;
			tr.endpoints.reserve(t.endpoints.size());
			for (auto const& ep : t.endpoints)
			{
				tr.endpoints.emplace_back();
				auto& aep = tr.endpoints.back();
				aep.local_endpoint = ep.local_endpoint;
				aep.enabled = ep.enabled;

				for (protocol_version v : {protocol_version::V1, protocol_version::V2})
				{
					aep.info_hashes[v].message = ep.info_hashes[v].message;
					aep.info_hashes[v].last_error = ep.info_hashes[v].last_error;
					aep.info_hashes[v].next_announce = ep.info_hashes[v].next_announce;
					aep.info_hashes[v].min_announce = ep.info_hashes[v].min_announce;
					aep.info_hashes[v].scrape_incomplete = ep.info_hashes[v].scrape_incomplete;
					aep.info_hashes[v].scrape_complete = ep.info_hashes[v].scrape_complete;
					aep.info_hashes[v].scrape_downloaded = ep.info_hashes[v].scrape_downloaded;
					aep.info_hashes[v].fails = ep.info_hashes[v].fails;
					aep.info_hashes[v].updating = ep.info_hashes[v].updating;
					aep.info_hashes[v].start_sent = ep.info_hashes[v].start_sent;
					aep.info_hashes[v].complete_sent = ep.info_hashes[v].complete_sent;
					aep.info_hashes[v].triggered_manually = ep.info_hashes[v].triggered_manually;
#if TORRENT_ABI_VERSION == 1
					tr.complete_sent |= ep.info_hashes[v].complete_sent;
#endif
				}
#if TORRENT_ABI_VERSION <= 2
#include "libtorrent/aux_/disable_warnings_push.hpp"
				aep.message = aep.info_hashes[protocol_version::V1].message;
				aep.scrape_incomplete = ep.info_hashes[protocol_version::V1].scrape_incomplete;
				aep.scrape_complete = ep.info_hashes[protocol_version::V1].scrape_complete;
				aep.scrape_downloaded = ep.info_hashes[protocol_version::V1].scrape_downloaded;
				aep.complete_sent = ep.info_hashes[protocol_version::V1].complete_sent;
				aep.last_error = ep.info_hashes[protocol_version::V1].last_error;
				aep.fails = ep.info_hashes[protocol_version::V1].fails;
				aep.next_announce = ep.info_hashes[protocol_version::V1].next_announce;
				aep.min_announce = ep.info_hashes[protocol_version::V1].min_announce;
				aep.updating = ep.info_hashes[protocol_version::V1].updating;
#include "libtorrent/aux_/disable_warnings_pop.hpp"
#endif
			}
		}
		return ret;
	}

	void torrent::replace_trackers(std::vector<lt::announce_entry> const& urls)
	{
		m_trackers.replace(urls);

		if (settings().get_bool(settings_pack::prefer_udp_trackers))
			m_trackers.prioritize_udp_trackers();

		if (m_announcing && !m_trackers.empty()) announce_with_tracker();

		set_need_save_resume(torrent_handle::if_metadata_changed);
	}

	bool torrent::add_tracker(lt::announce_entry const& url)
	{
		bool const added = m_trackers.add_tracker(aux::announce_entry(url));
		if (added) set_need_save_resume(torrent_handle::if_metadata_changed);
		if (m_announcing && added) announce_with_tracker();
		return added;
	}

	bool torrent::choke_peer(peer_connection& c)
	{
		INVARIANT_CHECK;

		TORRENT_ASSERT(!c.is_choked());
		TORRENT_ASSERT(!c.ignore_unchoke_slots());
		TORRENT_ASSERT(m_num_uploads > 0);
		if (!c.send_choke()) return false;
		--m_num_uploads;
		state_updated();
		return true;
	}

	bool torrent::unchoke_peer(peer_connection& c, bool optimistic)
	{
		INVARIANT_CHECK;

		TORRENT_ASSERT(!m_graceful_pause_mode);
		TORRENT_ASSERT(c.is_choked());
		TORRENT_ASSERT(!c.ignore_unchoke_slots());
		// when we're unchoking the optimistic slots, we might
		// exceed the limit temporarily while we're iterating
		// over the peers
		if (m_num_uploads >= m_max_uploads && !optimistic) return false;
		if (!c.send_unchoke()) return false;
		++m_num_uploads;
		state_updated();
		return true;
	}

	void torrent::trigger_unchoke() noexcept
	{
		m_ses.trigger_unchoke();
	}

	void torrent::trigger_optimistic_unchoke() noexcept
	{
		m_ses.trigger_optimistic_unchoke();
	}

	void torrent::cancel_block(piece_block block)
	{
		INVARIANT_CHECK;

		TORRENT_ASSERT(has_picker());

		for (auto* p : m_connections)
		{
			TORRENT_INCREMENT(m_iterating_connections);
			p->cancel_request(block);
		}
	}

#ifdef TORRENT_SSL_PEERS
	// certificate is a filename to a .pem file which is our
	// certificate. The certificate must be signed by the root
	// cert of the torrent file. any peer we connect to or that
	// connect to use must present a valid certificate signed
	// by the torrent root cert as well
	void torrent::set_ssl_cert(std::string const& certificate
		, std::string const& private_key
		, std::string const& dh_params
		, std::string const& passphrase)
	{
		if (!m_ssl_ctx)
		{
			if (alerts().should_post<torrent_error_alert>())
				alerts().emplace_alert<torrent_error_alert>(get_handle()
					, errors::not_an_ssl_torrent, "");
			return;
		}

		error_code ec;
		m_ssl_ctx->set_password_callback(
				[passphrase](std::size_t, ssl::context::password_purpose purpose)
				{
					return purpose == ssl::context::for_reading ? passphrase : "";
				}
				, ec);
		if (ec)
		{
			if (alerts().should_post<torrent_error_alert>())
				alerts().emplace_alert<torrent_error_alert>(get_handle(), ec, "");
		}
		m_ssl_ctx->use_certificate_file(certificate, ssl::context::pem, ec);
		if (ec)
		{
			if (alerts().should_post<torrent_error_alert>())
				alerts().emplace_alert<torrent_error_alert>(get_handle(), ec, certificate);
		}
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
			debug_log("*** use certificate file: %s", ec.message().c_str());
#endif
		m_ssl_ctx->use_private_key_file(private_key, ssl::context::pem, ec);
		if (ec)
		{
			if (alerts().should_post<torrent_error_alert>())
				alerts().emplace_alert<torrent_error_alert>(get_handle(), ec, private_key);
		}
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
			debug_log("*** use private key file: %s", ec.message().c_str());
#endif
		m_ssl_ctx->use_tmp_dh_file(dh_params, ec);
		if (ec)
		{
			if (alerts().should_post<torrent_error_alert>())
				alerts().emplace_alert<torrent_error_alert>(get_handle(), ec, dh_params);
		}
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
			debug_log("*** use DH file: %s", ec.message().c_str());
#endif
	}

	void torrent::set_ssl_cert_buffer(std::string const& certificate
		, std::string const& private_key
		, std::string const& dh_params)
	{
		if (!m_ssl_ctx) return;

		boost::asio::const_buffer certificate_buf(certificate.c_str(), certificate.size());

		error_code ec;
		m_ssl_ctx->use_certificate(certificate_buf, ssl::context::pem, ec);
		if (ec)
		{
			if (alerts().should_post<torrent_error_alert>())
				alerts().emplace_alert<torrent_error_alert>(get_handle(), ec, "[certificate]");
		}

		boost::asio::const_buffer private_key_buf(private_key.c_str(), private_key.size());
		m_ssl_ctx->use_private_key(private_key_buf, ssl::context::pem, ec);
		if (ec)
		{
			if (alerts().should_post<torrent_error_alert>())
				alerts().emplace_alert<torrent_error_alert>(get_handle(), ec, "[private key]");
		}

		boost::asio::const_buffer dh_params_buf(dh_params.c_str(), dh_params.size());
		m_ssl_ctx->use_tmp_dh(dh_params_buf, ec);
		if (ec)
		{
			if (alerts().should_post<torrent_error_alert>())
				alerts().emplace_alert<torrent_error_alert>(get_handle(), ec, "[dh params]");
		}
	}

#endif

	void torrent::on_exception(std::exception const&)
	{
		set_error(errors::no_memory, torrent_status::error_file_none);
	}

	void torrent::on_error(error_code const& ec)
	{
		set_error(ec, torrent_status::error_file_none);
	}

#if TORRENT_USE_RTC
	void torrent::generate_rtc_offers(int const count
		, std::function<void(error_code const&, std::vector<aux::rtc_offer>)> handler)
	{
		// rtc_signaling is created lazily
		if (!m_rtc_signaling)
		{
			TORRENT_ASSERT(count > 0);
			m_rtc_signaling = std::make_shared<aux::rtc_signaling>(m_ses.get_context()
				, this
				, std::bind(&torrent::on_rtc_stream, this, _1));
		}

		m_rtc_signaling->generate_offers(count, std::move(handler));
	}

	void torrent::on_rtc_offer(aux::rtc_offer const& offer)
	{
		TORRENT_ASSERT(m_rtc_signaling);
		if(m_rtc_signaling) m_rtc_signaling->process_offer(offer);
	}

	void torrent::on_rtc_answer(aux::rtc_answer const& answer)
	{
		TORRENT_ASSERT(m_rtc_signaling);
		if(m_rtc_signaling) m_rtc_signaling->process_answer(answer);
	}

	void torrent::on_rtc_stream(aux::rtc_stream_init stream_init)
	{
		aux::socket_type s(aux::rtc_stream(m_ses.get_context(), stream_init));

		error_code ec;
		auto endpoint = s.remote_endpoint(ec);
		if(ec)
		{
			// This can happen if the peer immediately disconnects
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("failed to get RTC stream remote endpoint: %s", ec.message().c_str());
#endif
			return;
		}

		need_peer_list();
		torrent_state st = get_peer_list_state();
		if(torrent_peer* peerinfo = m_peer_list->add_rtc_peer(endpoint, peer_source_flags_t{}, {}, &st))
			create_peer_connection(peerinfo, std::move(s), std::move(endpoint));
	}
#endif

	void torrent::remove_connection(peer_connection const* p)
	{
		TORRENT_ASSERT(m_iterating_connections == 0);
		auto const i = sorted_find(m_connections, p);
		if (i != m_connections.end())
			m_connections.erase(i);
	}

	void torrent::remove_peer(std::shared_ptr<peer_connection> p) noexcept
	{
		TORRENT_ASSERT(p);
		TORRENT_ASSERT(is_single_thread());
		TORRENT_ASSERT(std::count(m_peers_to_disconnect.begin()
			, m_peers_to_disconnect.end(), p) == 0);

		auto it = m_outgoing_pids.find(p->our_pid());
		if (it != m_outgoing_pids.end())
		{
			m_outgoing_pids.erase(it);
		}

		// only schedule the peer for actual removal if in fact
		// we can be sure peer_connection will be kept alive until
		// the deferred function is called. If a peer_connection
		// has not associated torrent, the session_impl object may
		// remove it at any time, which may be while the non-owning
		// pointer in m_peers_to_disconnect (if added to it) is
		// waiting for the deferred function to be called.
		//
		// one example of this situation is if for example, this
		// function is called from the attach_peer path and fail to
		// do so because of too many connections.
		bool const is_attached = p->associated_torrent().lock().get() == this;
		if (is_attached)
		{
			std::weak_ptr<torrent> weak_t = shared_from_this();
			TORRENT_ASSERT_VAL(m_peers_to_disconnect.capacity() > m_peers_to_disconnect.size()
				, m_peers_to_disconnect.capacity());
			m_peers_to_disconnect.push_back(p);

			using deferred_handler_type = aux::handler<
				torrent
				, &torrent::on_remove_peers
				, &torrent::on_error
				, &torrent::on_exception
				, &torrent::m_deferred_handler_storage
				>;
			static_assert(sizeof(deferred_handler_type) == sizeof(std::shared_ptr<peer_connection>)
				, "deferred handler does not have the expected size");
			m_deferred_disconnect.post_deferred(m_ses.get_context(), deferred_handler_type(shared_from_this()));
		}
		else
		{
			// if the peer was inserted in m_connections but instructed to
			// be removed from this torrent, just remove it from it, see
			// attach_peer logic.
			remove_connection(p.get());
		}

		torrent_peer* pp = p->peer_info_struct();
		if (ready_for_connections())
		{
			TORRENT_ASSERT(p->associated_torrent().lock().get() == nullptr
				|| p->associated_torrent().lock().get() == this);

			if (has_picker())
			{
				if (p->is_seed())
				{
					m_picker->dec_refcount_all(pp);
				}
				else
				{
					auto const& pieces = p->get_bitfield();
					TORRENT_ASSERT(pieces.count() <= pieces.size());
					m_picker->dec_refcount(pieces, pp);
				}
			}
		}

		if (!p->is_choked() && !p->ignore_unchoke_slots())
		{
			--m_num_uploads;
			trigger_unchoke();
		}

		if (pp)
		{
			if (pp->optimistically_unchoked)
			{
				pp->optimistically_unchoked = false;
				m_stats_counters.inc_stats_counter(
					counters::num_peers_up_unchoked_optimistic, -1);
				trigger_optimistic_unchoke();
			}

			TORRENT_ASSERT(pp->prev_amount_upload == 0);
			TORRENT_ASSERT(pp->prev_amount_download == 0);
			pp->prev_amount_download += aux::numeric_cast<std::uint32_t>(p->statistics().total_payload_download() / 1024);
			pp->prev_amount_upload += aux::numeric_cast<std::uint32_t>(p->statistics().total_payload_upload() / 1024);

			// only decrement the seed count if the peer completed attaching to the torrent
			// otherwise the seed count did not get incremented for this peer
			if (is_attached && pp->seed)
			{
				TORRENT_ASSERT(m_num_seeds > 0);
				--m_num_seeds;
			}

			if (pp->connection && m_peer_list)
			{
				torrent_state st = get_peer_list_state();
				m_peer_list->connection_closed(*p, m_ses.session_time(), &st);
				peers_erased(st.erased);
			}
		}

		p->set_peer_info(nullptr);

		update_want_peers();
		update_want_tick();
	}

	void torrent::on_remove_peers() noexcept
	{
		TORRENT_ASSERT(is_single_thread());
		INVARIANT_CHECK;

#if TORRENT_USE_ASSERTS
		auto const num = m_peers_to_disconnect.size();
#endif
		for (auto const& p : m_peers_to_disconnect)
		{
			TORRENT_ASSERT(p);
			TORRENT_ASSERT(p->associated_torrent().lock().get() == this);

			remove_connection(p.get());
			m_ses.close_connection(p.get());
		}
		TORRENT_ASSERT_VAL(m_peers_to_disconnect.size() == num, m_peers_to_disconnect.size() - num);
		m_peers_to_disconnect.clear();

		if (m_graceful_pause_mode && m_connections.empty())
		{
			// we're in graceful pause mode and this was the last peer we
			// disconnected. This will clear the graceful_pause_mode and post the
			// torrent_paused_alert.
			TORRENT_ASSERT(is_paused());

			// this will post torrent_paused alert
			set_paused(true);
		}

		update_want_peers();
		update_want_tick();
	}

	void torrent::remove_web_seed_iter(std::list<web_seed_t>::iterator web)
	{
		if (web->resolving)
		{
			web->removed = true;
		}
		else
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("removing web seed: \"%s\"", web->url.c_str());
#endif

			auto* peer = static_cast<peer_connection*>(web->peer_info.connection);
			if (peer != nullptr)
			{
				// if we have a connection for this web seed, we also need to
				// disconnect it and clear its reference to the peer_info object
				// that's part of the web_seed_t we're about to remove
				TORRENT_ASSERT(peer->m_in_use == 1337);
				peer->disconnect(boost::asio::error::operation_aborted, operation_t::bittorrent);
				peer->set_peer_info(nullptr);
			}
			if (has_picker()) picker().clear_peer(&web->peer_info);

			m_web_seeds.erase(web);
		}

		update_want_tick();
	}

	void torrent::connect_to_url_seed(std::list<web_seed_t>::iterator web)
	{
		TORRENT_ASSERT(is_single_thread());
		INVARIANT_CHECK;

		TORRENT_ASSERT(!web->resolving);
		if (web->resolving) return;

		if (num_peers() >= int(m_max_connections)
			|| m_ses.num_connections() >= settings().get_int(settings_pack::connections_limit))
			return;

		std::string protocol;
		std::string auth;
		std::string hostname;
		int port;
		std::string path;
		error_code ec;
		std::tie(protocol, auth, hostname, port, path)
			= parse_url_components(web->url, ec);

		if (!settings().get_bool(settings_pack::allow_idna) && is_idna(hostname))
		{
#ifndef TORRENT_DISABLE_LOGGING
			if (should_log())
				debug_log("IDNA disallowed in web seeds: %s", web->url.c_str());
#endif
			if (m_ses.alerts().should_post<url_seed_alert>())
			{
				m_ses.alerts().emplace_alert<url_seed_alert>(get_handle()
					, web->url, error_code(errors::blocked_by_idna));
			}
			// never try it again
			web->disabled = true;
			return;
		}

		if (port == -1)
		{
			port = protocol == "http" ? 80 : 443;
		}

		if (ec)
		{
#ifndef TORRENT_DISABLE_LOGGING
			if (should_log())
				debug_log("failed to parse web seed url: %s", ec.message().c_str());
#endif
			if (m_ses.alerts().should_post<url_seed_alert>())
			{
				m_ses.alerts().emplace_alert<url_seed_alert>(get_handle()
					, web->url, ec);
			}
			// never try it again
			remove_web_seed_iter(web);
			return;
		}

		if (web->peer_info.banned)
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("banned web seed: %s", web->url.c_str());
#endif
			if (m_ses.alerts().should_post<url_seed_alert>())
			{
				m_ses.alerts().emplace_alert<url_seed_alert>(get_handle(), web->url
					, libtorrent::errors::peer_banned);
			}
			// never try it again
			web->disabled = true;
			return;
		}

#if TORRENT_USE_SSL
		if (protocol != "http" && protocol != "https")
#else
		if (protocol != "http")
#endif
		{
			if (m_ses.alerts().should_post<url_seed_alert>())
			{
				m_ses.alerts().emplace_alert<url_seed_alert>(get_handle(), web->url, errors::unsupported_url_protocol);
			}
			// never try it again for this session
			web->disabled = true;
			return;
		}

		if (hostname.empty())
		{
			if (m_ses.alerts().should_post<url_seed_alert>())
			{
				m_ses.alerts().emplace_alert<url_seed_alert>(get_handle(), web->url
					, errors::invalid_hostname);
			}
			// never try it again
			remove_web_seed_iter(web);
			return;
		}

		if (port == 0)
		{
			if (m_ses.alerts().should_post<url_seed_alert>())
			{
				m_ses.alerts().emplace_alert<url_seed_alert>(get_handle(), web->url
					, errors::invalid_port);
			}
			// never try it again
			remove_web_seed_iter(web);
			return;
		}

		if (m_ses.get_port_filter().access(std::uint16_t(port)) & port_filter::blocked)
		{
			if (m_ses.alerts().should_post<url_seed_alert>())
			{
				m_ses.alerts().emplace_alert<url_seed_alert>(get_handle()
					, web->url, errors::port_blocked);
			}
			// never try it again
			web->disabled = true;
			return;
		}

		if (!web->endpoints.empty())
		{
			connect_web_seed(web, web->endpoints.front());
			return;
		}

		aux::proxy_settings const& ps = m_ses.proxy();
		if ((ps.type == settings_pack::http
			|| ps.type == settings_pack::http_pw)
			&& ps.proxy_peer_connections)
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("resolving proxy for web seed: %s", web->url.c_str());
#endif

			std::uint16_t const proxy_port = ps.port;

			// use proxy
			web->resolving = true;
			m_ses.get_resolver().async_resolve(ps.hostname, aux::resolver_interface::abort_on_shutdown
				, [self = shared_from_this(), web, proxy_port](error_code const& e, std::vector<address> const& addrs)
				{ self->wrap(&torrent::on_proxy_name_lookup, e, addrs, web, proxy_port); });
		}
		else if (ps.proxy_hostnames
			&& (ps.type == settings_pack::socks5
				|| ps.type == settings_pack::socks5_pw)
			&& ps.proxy_peer_connections)
		{
			connect_web_seed(web, {address(), std::uint16_t(port)});
		}
		else
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("resolving web seed: \"%s\" %s", hostname.c_str(), web->url.c_str());
#endif

			auto self = shared_from_this();
			web->resolving = true;

			m_ses.get_resolver().async_resolve(hostname, aux::resolver_interface::abort_on_shutdown
				, [self, web, port](error_code const& e, std::vector<address> const& addrs)
				{
					self->wrap(&torrent::on_name_lookup, e, addrs, port, web);
				});
		}
	}

	void torrent::on_proxy_name_lookup(error_code const& e
		, std::vector<address> const& addrs
		, std::list<web_seed_t>::iterator web, int port) try
	{
		TORRENT_ASSERT(is_single_thread());

		INVARIANT_CHECK;

		TORRENT_ASSERT(web->resolving);
#ifndef TORRENT_DISABLE_LOGGING
		debug_log("completed resolve proxy hostname for: %s", web->url.c_str());
		if (e && should_log())
			debug_log("proxy name lookup error: %s", e.message().c_str());
#endif
		web->resolving = false;

		if (web->removed)
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("removed web seed");
#endif
			remove_web_seed_iter(web);
			return;
		}

		if (m_abort) return;
		if (m_ses.is_aborted()) return;

		if (e || addrs.empty())
		{
			if (m_ses.alerts().should_post<url_seed_alert>())
			{
				m_ses.alerts().emplace_alert<url_seed_alert>(get_handle()
					, web->url, e);
			}

			// the name lookup failed for the http host. Don't try
			// this host again
			web->disabled = true;
			return;
		}

		if (num_peers() >= int(m_max_connections)
			|| m_ses.num_connections() >= settings().get_int(settings_pack::connections_limit))
			return;

		tcp::endpoint a(addrs[0], std::uint16_t(port));

		std::string hostname;
		error_code ec;
		std::string protocol;
		std::tie(protocol, std::ignore, hostname, port, std::ignore)
			= parse_url_components(web->url, ec);
		if (port == -1) port = protocol == "http" ? 80 : 443;

		if (ec)
		{
			if (m_ses.alerts().should_post<url_seed_alert>())
			{
				m_ses.alerts().emplace_alert<url_seed_alert>(get_handle()
					, web->url, ec);
			}
			remove_web_seed_iter(web);
			return;
		}

		if (m_ip_filter && m_ip_filter->access(a.address()) & ip_filter::blocked)
		{
			if (m_ses.alerts().should_post<peer_blocked_alert>())
				m_ses.alerts().emplace_alert<peer_blocked_alert>(get_handle()
					, a, peer_blocked_alert::ip_filter);
			return;
		}

		auto self = shared_from_this();
		web->resolving = true;
		m_ses.get_resolver().async_resolve(hostname, aux::resolver_interface::abort_on_shutdown
			, [self, web, port](error_code const& err, std::vector<address> const& addr)
			{
				self->wrap(&torrent::on_name_lookup, err, addr, port, web);
			});
	}
	catch (...) { handle_exception(); }

	void torrent::on_name_lookup(error_code const& e
		, std::vector<address> const& addrs
		, int const port
		, std::list<web_seed_t>::iterator web) try
	{
		TORRENT_ASSERT(is_single_thread());

		INVARIANT_CHECK;

		TORRENT_ASSERT(web->resolving);
#ifndef TORRENT_DISABLE_LOGGING
		debug_log("completed resolve: %s", web->url.c_str());
#endif
		web->resolving = false;
		if (web->removed)
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("removed web seed");
#endif
			remove_web_seed_iter(web);
			return;
		}

		if (m_abort) return;

		if (e || addrs.empty())
		{
			if (m_ses.alerts().should_post<url_seed_alert>())
				m_ses.alerts().emplace_alert<url_seed_alert>(get_handle(), web->url, e);
#ifndef TORRENT_DISABLE_LOGGING
			if (should_log())
			{
				debug_log("*** HOSTNAME LOOKUP FAILED: %s: (%d) %s"
					, web->url.c_str(), e.value(), e.message().c_str());
			}
#endif

			// unavailable, retry in `settings_pack::web_seed_name_lookup_retry` seconds
			web->retry = aux::time_now32()
				+ seconds32(settings().get_int(settings_pack::web_seed_name_lookup_retry));
			return;
		}

		for (auto const& addr : addrs)
		{
			// if this is set, we don't allow this web seed to have resolved to a
			// local IP
			if (web->no_local_ips && !aux::is_global(addr)) continue;

			// fill in the peer struct's address field
			web->endpoints.emplace_back(addr, std::uint16_t(port));

#ifndef TORRENT_DISABLE_LOGGING
			if (should_log())
				debug_log("  -> %s", print_endpoint(tcp::endpoint(addr, std::uint16_t(port))).c_str());
#endif
		}

		if (web->endpoints.empty())
		{
			if (m_ses.alerts().should_post<url_seed_alert>())
			{
				m_ses.alerts().emplace_alert<url_seed_alert>(get_handle()
					, web->url, errors::banned_by_ip_filter);
			}

			// the name lookup failed for the http host. Don't try
			// this host again
			web->disabled = true;
			return;
		}

		if (num_peers() >= int(m_max_connections)
			|| m_ses.num_connections() >= settings().get_int(settings_pack::connections_limit))
			return;

		connect_web_seed(web, web->endpoints.front());
	}
	catch (...) { handle_exception(); }

	void torrent::connect_web_seed(std::list<web_seed_t>::iterator web, tcp::endpoint a)
	{
		INVARIANT_CHECK;

		TORRENT_ASSERT(is_single_thread());
		if (m_abort) return;

		if (m_ip_filter && m_ip_filter->access(a.address()) & ip_filter::blocked)
		{
			if (m_ses.alerts().should_post<peer_blocked_alert>())
				m_ses.alerts().emplace_alert<peer_blocked_alert>(get_handle()
					, a, peer_blocked_alert::ip_filter);
			return;
		}

		TORRENT_ASSERT(!web->resolving);
		TORRENT_ASSERT(web->peer_info.connection == nullptr);

		if (aux::is_v4(a))
		{
			web->peer_info.addr = a.address().to_v4();
			web->peer_info.port = a.port();
		}

		if (is_paused()) return;
		if (m_ses.is_aborted()) return;
		if (is_upload_only()) return;

		// this web seed may have redirected all files to other URLs, leaving it
		// having no file left, and there's no longer any point in connecting to
		// it.
		if (!web->have_files.empty()
			&& web->have_files.none_set()) return;

		void* userdata = nullptr;
#if TORRENT_USE_SSL
		const bool ssl = string_begins_no_case("https://", web->url);
		if (ssl)
		{
#ifdef TORRENT_SSL_PEERS
			userdata = m_ssl_ctx.get();
#endif
			if (!userdata) userdata = m_ses.ssl_ctx();
		}
#endif
		aux::socket_type s = instantiate_connection(m_ses.get_context()
			, m_ses.proxy(), userdata, nullptr, true, false);

		if (auto* inner = std::get_if<http_stream>(&s))
		{
			// the web seed connection will talk immediately to
			// the proxy, without requiring CONNECT support
			inner->set_no_connect(true);
		}

		std::string hostname;
		std::string path;
		error_code ec;
		using std::ignore;
		std::tie(ignore, ignore, hostname, ignore, path)
			= parse_url_components(web->url, ec);
		if (ec)
		{
			if (m_ses.alerts().should_post<url_seed_alert>())
				m_ses.alerts().emplace_alert<url_seed_alert>(get_handle(), web->url, ec);
			return;
		}

		if (!settings().get_bool(settings_pack::allow_idna) && is_idna(hostname))
		{
#ifndef TORRENT_DISABLE_LOGGING
			if (should_log())
				debug_log("IDNA disallowed in web seeds: %s", web->url.c_str());
#endif
			if (m_ses.alerts().should_post<url_seed_alert>())
			{
				m_ses.alerts().emplace_alert<url_seed_alert>(get_handle()
					, web->url, error_code(errors::blocked_by_idna));
			}
			// never try it again
			web->disabled = true;
			return;
		}

		// The SSRF mitigation for web seeds is that any HTTP server on the
		// local network may not use any query string parameters
		if (settings().get_bool(settings_pack::ssrf_mitigation)
			&& aux::is_local(web->peer_info.addr)
			&& path.find('?') != std::string::npos)
		{
#ifndef TORRENT_DISABLE_LOGGING
			if (should_log())
			{
				debug_log("*** SSRF MITIGATION BLOCKED WEB SEED: %s"
					, web->url.c_str());
			}
#endif
			if (m_ses.alerts().should_post<url_seed_alert>())
				m_ses.alerts().emplace_alert<url_seed_alert>(get_handle()
					, web->url, errors::ssrf_mitigation);
			if (m_ses.alerts().should_post<peer_blocked_alert>())
				m_ses.alerts().emplace_alert<peer_blocked_alert>(get_handle()
					, a, peer_blocked_alert::ssrf_mitigation);
			// never try it again
			web->disabled = true;
			return;
		}

		bool const is_ip = aux::is_ip_address(hostname);
		if (is_ip) a.address(make_address(hostname, ec));
		bool const proxy_hostnames = settings().get_bool(settings_pack::proxy_hostnames)
			&& !is_ip;

		if (!is_ip
			&& settings().get_bool(settings_pack::proxy_send_host_in_connect))
		{
			if (auto* inner1 = std::get_if<http_stream>(&s))
			{
				inner1->set_host(hostname);
			}
#if TORRENT_USE_SSL
			else if (auto* inner2 = std::get_if<ssl_stream<http_stream>>(&s))
			{
				inner2->next_layer().set_host(hostname);
			}
#endif
		}

		if (proxy_hostnames
			&& (std::get_if<socks5_stream>(&s)
#if TORRENT_USE_SSL
				|| std::get_if<ssl_stream<socks5_stream>>(&s)
#endif
				))
		{
			// we're using a socks proxy and we're resolving
			// hostnames through it
			auto& str =
#if TORRENT_USE_SSL
				ssl ? std::get<ssl_stream<socks5_stream>>(s).next_layer() :
#endif
			std::get<socks5_stream>(s);

			str.set_dst_name(hostname);
		}

		setup_ssl_hostname(s, hostname, ec);
		if (ec)
		{
			if (m_ses.alerts().should_post<url_seed_alert>())
				m_ses.alerts().emplace_alert<url_seed_alert>(get_handle(), web->url, ec);
			return;
		}

		peer_connection_args pack{
			&m_ses
			, &settings()
			, &m_ses.stats_counters()
			, &m_ses.disk_thread()
			, &m_ses.get_context()
			, shared_from_this()
			, std::move(s)
			, a
			, &web->peer_info
			, aux::generate_peer_id(settings())
		};

		auto c = std::make_shared<web_peer_connection>(pack, *web);
		if (!c) return;

#if TORRENT_USE_ASSERTS
		c->m_in_constructor = false;
#endif

#ifndef TORRENT_DISABLE_EXTENSIONS
		for (auto const& ext : m_extensions)
		{
			std::shared_ptr<peer_plugin>
				pp(ext->new_connection(peer_connection_handle(c->self())));
			if (pp) c->add_extension(pp);
		}
#endif

		TORRENT_ASSERT(!c->m_in_constructor);
		// add the newly connected peer to this torrent's peer list
		TORRENT_ASSERT(m_iterating_connections == 0);

		// we don't want to have to allocate memory to disconnect this peer, so
		// make sure there's enough memory allocated in the deferred_disconnect
		// list up-front
		m_peers_to_disconnect.reserve(m_connections.size() + 1);

		sorted_insert(m_connections, c.get());
		update_want_peers();
		update_want_tick();
		m_ses.insert_peer(c);

		if (web->peer_info.seed)
		{
			TORRENT_ASSERT(m_num_seeds < 0xffff);
			++m_num_seeds;
		}

		TORRENT_ASSERT(!web->peer_info.connection);
		web->peer_info.connection = c.get();
#if TORRENT_USE_ASSERTS
		web->peer_info.in_use = true;
#endif

		c->add_stat(std::int64_t(web->peer_info.prev_amount_download) * 1024
			, std::int64_t(web->peer_info.prev_amount_upload) * 1024);
		web->peer_info.prev_amount_download = 0;
		web->peer_info.prev_amount_upload = 0;
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			debug_log("web seed connection started: [%s] %s"
				, print_endpoint(a).c_str(), web->url.c_str());
		}
#endif

		c->start();

		if (c->is_disconnecting()) return;

#ifndef TORRENT_DISABLE_LOGGING
		debug_log("START queue peer [%p] (%d)", static_cast<void*>(c.get())
			, num_peers());
#endif
	}

	hash_request torrent::pick_hashes(peer_connection* peer)
	{
		need_hash_picker();
		if (!m_hash_picker) return {};
		return m_hash_picker->pick_hashes(peer->get_bitfield());
	}

	std::vector<sha256_hash> torrent::get_hashes(hash_request const& req) const
	{
		TORRENT_ASSERT(m_torrent_file->is_valid());
		if (!m_torrent_file->is_valid()) return {};
		TORRENT_ASSERT(validate_hash_request(req, m_torrent_file->layout()));

		// m_merkle_trees is only populated for v2 (or hybrid) torrents with
		// valid metadata (see initialize_merkle_trees()); req.file is
		// validated against the full file layout, not against m_merkle_trees,
		// so a v1-only torrent (or one whose metadata hasn't resolved yet)
		// must be rejected here rather than indexed out of bounds. Callers
		// are expected to have already checked has_v2() and valid_metadata(),
		// so reaching this in a debug build indicates a caller bug.
		TORRENT_ASSERT(req.file >= file_index_t{0} && req.file < m_merkle_trees.end_index());
		if (req.file < file_index_t{0} || req.file >= m_merkle_trees.end_index())
			return {};

		auto const& f = m_merkle_trees[req.file];

		return f.get_hashes(req.base, req.index, req.count, req.proof_layers);
	}

	bool torrent::add_hashes(hash_request const& req, span<sha256_hash> hashes)
	{
		need_hash_picker();
		if (!m_hash_picker) return true;
		add_hashes_result const result = m_hash_picker->add_hashes(req, hashes);
		for (auto& p : result.hash_failed)
		{
			if (torrent_file().info_hashes().has_v1() && have_piece(p.first))
			{
				handle_inconsistent_hashes(p.first);
				return result.valid;
			}

			TORRENT_ASSERT(!have_piece(p.first));

			// the piece may not have been downloaded in this session
			// it should be open for downloading so nothing needs to be done here
			if (!m_picker || !m_picker->is_downloading(p.first)) continue;
			piece_failed(p.first, p.second);
		}
		for (piece_index_t p : result.hash_passed)
		{
			if (torrent_file().info_hashes().has_v1() && !m_disable_v1_hashes && !have_piece(p))
			{
				handle_inconsistent_hashes(p);
				return result.valid;
			}

			// it's possible the piece has already passed because we finished
			// downloading it, and we could compute the full piece hash without
			// the underlying block hashes. It's important to not call
			// piece_passed() twice for the same piece
			if (m_picker && m_picker->is_downloading(p) && m_picker->is_piece_finished(p)
				&& !m_picker->is_hashing(p) && !have_piece(p))
			{
				piece_passed(p);
			}
		}
		return result.valid;
	}

	void torrent::hashes_rejected(hash_request const& req)
	{
		if (!m_hash_picker) return;
		m_hash_picker->hashes_rejected(req);
		// we need to poke all of the v2 peers in case there are no other
		// outstanding hash requests
		for (auto* peer : m_connections)
		{
			if (peer->type() != connection_type::bittorrent) continue;
			auto* const btpeer = static_cast<bt_peer_connection*>(peer);
			btpeer->maybe_send_hash_request();
		}
	}

	void torrent::verify_block_hashes(piece_index_t index)
	{
		need_hash_picker();
		if (!m_hash_picker) return;
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			debug_log("Piece %d hash failure, requesting block hashes", int(index));
		}
#endif
		m_hash_picker->verify_block_hashes(index);
	}

	std::shared_ptr<const torrent_info> torrent::get_torrent_file() const
	{
		if (!m_torrent_file->is_valid()) return {};
		return m_torrent_file;
	}

	std::shared_ptr<torrent_info const> torrent::get_torrent_file_external() const
	{
		std::lock_guard<std::mutex> l(m_torrent_file_mutex);
		return m_torrent_file_external;
	}

	renamed_files torrent::get_renamed_files() const
	{
		return m_renamed_files;
	}

#if TORRENT_ABI_VERSION < 4
	std::shared_ptr<torrent_info> torrent::get_torrent_copy_with_hashes() const
	{
		if (!m_torrent_file->is_valid()) return {};
		auto ret = std::make_shared<torrent_info>(*m_torrent_file);

		if (ret->v2())
		{
			aux::vector<aux::vector<char>, file_index_t> v2_hashes;
			for (auto const& tree : m_merkle_trees)
			{
				auto const& layer = tree.get_piece_layer();
				std::vector<char> out_layer;
				out_layer.reserve(layer.size() * sha256_hash::size());
				for (auto const& h : layer)
				{
					// we're missing a piece layer. We can't return a valid
					// torrent
					if (h.is_all_zeros()) return {};
					out_layer.insert(out_layer.end(), h.data(), h.data() + sha256_hash::size());
				}
				v2_hashes.emplace_back(std::move(out_layer));
			}
			ret->set_piece_layers(std::move(v2_hashes));
		}

		return ret;
	}
#endif

	std::vector<std::vector<sha256_hash>> torrent::get_piece_layers() const
	{
		std::vector<std::vector<sha256_hash>> ret;
		for (auto const& tree : m_merkle_trees)
			ret.emplace_back(tree.get_piece_layer());
		return ret;
	}

	void torrent::enable_all_trackers()
	{
		m_trackers.enable_all();
	}

	void torrent::write_resume_data(resume_data_flags_t const flags, add_torrent_params& ret) const
	{
		ret.version = LIBTORRENT_VERSION_NUM;
		ret.storage_mode = storage_mode();
		ret.total_uploaded = m_total_uploaded;
		ret.total_downloaded = m_total_downloaded;

		time_point32 const now = aux::time_now32();
		ret.active_time = active_time(now).count();
		ret.finished_time = finished_time(now).count();
		ret.seeding_time = seeding_time(now).count();
		ret.last_seen_complete = m_last_seen_complete;
		ret.last_upload = aux::to_time_t(m_last_upload);
		ret.last_download = aux::to_time_t(m_last_download);

		ret.num_complete = m_complete;
		ret.num_incomplete = m_incomplete;
		ret.num_downloaded = m_downloaded;

		ret.flags = this->flags();

		ret.added_time = m_added_time;
		ret.completed_time = m_completed_time;

		ret.save_path = m_save_path;
		ret.part_file_dir = m_part_file_dir;

		ret.comment = m_comment;
		ret.created_by = m_created_by;
		ret.creation_date = m_creation_date;
		ret.info_hashes = m_info_hash;
		if (valid_metadata()) ret.name = m_torrent_file->name();
		else if (m_name) ret.name = *m_name;

#if TORRENT_ABI_VERSION < 3
		ret.info_hash = ret.info_hashes.get_best();
#endif

		if (valid_metadata() && (flags & torrent_handle::save_info_dict))
		{
			ret.ti = m_torrent_file;
		}

		// if this torrent is a seed, we won't have a piece picker
		// if we don't have anything, we may also not have a picker
		// in either case; there will be no half-finished pieces.
		if (has_picker())
		{
			int const num_blocks_per_piece = torrent_file().blocks_per_piece();

			std::vector<piece_picker::downloading_piece> const q
				= m_picker->get_download_queue();

			// info for each unfinished piece
			for (auto const& dp : q)
			{
				if (dp.finished == 0 && dp.writing == 0)
					continue;

				bitfield bitmask;
				bitmask.resize(num_blocks_per_piece, false);

				auto const info = m_picker->blocks_for_piece(dp);
				for (int i = 0; i < int(info.size()); ++i)
				{
					if (info[i].state == piece_picker::block_info::state_finished)
						bitmask.set_bit(i);
				}
				ret.unfinished_pieces.emplace(dp.index, std::move(bitmask));
			}
		}

		// save trackers
		for (auto const& tr : m_trackers)
		{
			ret.trackers.push_back(tr.url);
			ret.tracker_tiers.push_back(tr.tier);
		}

		// save web seeds
		for (auto const& ws : m_web_seeds)
		{
			if (ws.removed || ws.ephemeral) continue;
			ret.url_seeds.push_back(ws.url);
		}

		if (m_add_torrent_params)
			ret.dht_nodes = m_add_torrent_params->dht_nodes;

		// write have bitmask
		// the pieces string has one byte per piece. Each
		// byte is a bitmask representing different properties
		// for the piece
		// bit 0: set if we have the piece
		// bit 1: set if we have verified the piece (in seed mode)
		bool const is_checking = state() == torrent_status::checking_files;

		// if we are checking, only save the have_pieces bitfield up to the piece
		// we have actually checked. This allows us to resume the checking when we
		// load this torrent up again. If we have not completed checking nor is
		// currently checking, don't save any pieces from the have_pieces
		// bitfield.
		piece_index_t const max_piece
			= is_checking ? m_num_checked_pieces
			: m_files_checked ? m_torrent_file->end_piece()
			: piece_index_t(0);

		TORRENT_ASSERT(ret.have_pieces.empty());
		if (max_piece > piece_index_t(0))
		{
			if (is_seed())
			{
				ret.have_pieces.resize(static_cast<int>(max_piece), true);
			}
			else if (has_picker())
			{
				ret.have_pieces.resize(static_cast<int>(max_piece), false);
				for (auto const i : ret.have_pieces.range())
					if (m_picker->is_piece_flushed(i)) ret.have_pieces.set_bit(i);
			}

			if (m_seed_mode)
				ret.verified_pieces = m_verified;
		}

		// write renamed files
		if (valid_metadata())
			ret.renamed_files = m_renamed_files.export_filenames(m_torrent_file->layout());

		// write local peers
		std::vector<torrent_peer const*> deferred_peers;
		if (m_peer_list)
		{
			for (auto* p : *m_peer_list)
			{
#if TORRENT_USE_I2P
				if (p->is_i2p_addr) continue;
#endif
				if (p->banned)
				{
					ret.banned_peers.push_back(p->ip());
					continue;
				}

				// we cannot save remote connection
				// since we don't know their listen port
				// unless they gave us their listen port
				// through the extension handshake
				// so, if the peer is not connectable (i.e. we
				// don't know its listen port) or if it has
				// been banned, don't save it.
				if (!p->connectable) continue;

				// don't save peers that don't work
				if (int(p->failcount) > 0) continue;

				// don't save peers that appear to send corrupt data
				if (int(p->trust_points) < 0) continue;

				if (p->last_connected == 0)
				{
					// we haven't connected to this peer. It might still
					// be useful to save it, but only save it if we
					// don't have enough peers that we actually did connect to
					if (int(deferred_peers.size()) < 100)
						deferred_peers.push_back(p);
					continue;
				}

				ret.peers.push_back(p->ip());
			}
		}

		// if we didn't save 100 peers, fill in with second choice peers
		if (int(ret.peers.size()) < 100)
		{
			aux::random_shuffle(deferred_peers);
			for (auto const* p : deferred_peers)
			{
				ret.peers.push_back(p->ip());
				if (int(ret.peers.size()) >= 100) break;
			}
		}

		ret.upload_limit = upload_limit();
		ret.download_limit = download_limit();
		ret.max_connections = max_connections();
		ret.max_uploads = max_uploads();

		// piece priorities and file priorities are mutually exclusive. If there
		// are file priorities set, don't save piece priorities.
		// when in seed mode (i.e. the client promises that we have all files)
		// it does not make sense to save file priorities.
		if (!m_file_priority.empty() && !m_seed_mode)
		{
			// write file priorities
			ret.file_priorities = m_file_priority;
		}

		if (valid_metadata() && has_picker())
		{
			// write piece priorities
			// but only if they are not set to the default
			bool default_prio = true;
			for (auto const i : m_torrent_file->piece_range())
			{
				if (m_picker->piece_priority(i) == default_priority) continue;
				default_prio = false;
				break;
			}

			if (!default_prio)
			{
				ret.piece_priorities.clear();
				ret.piece_priorities.reserve(static_cast<std::size_t>(m_torrent_file->num_pieces()));

				for (auto const i : m_torrent_file->piece_range())
					ret.piece_priorities.push_back(m_picker->piece_priority(i));
			}
		}

		if (m_torrent_file->info_hashes().has_v2())
		{
			auto const num_files = m_merkle_trees.size();
			ret.merkle_trees.clear();
			ret.merkle_trees.reserve(num_files);
			ret.merkle_tree_mask.clear();
			ret.merkle_tree_mask.reserve(num_files);
			ret.verified_leaf_hashes.clear();
			ret.verified_leaf_hashes.reserve(num_files);
			for (auto i : m_merkle_trees.range())
			{
				if (valid_metadata() && m_torrent_file->layout().pad_file_at(i))
				{
					ret.merkle_trees.emplace_back();
					ret.merkle_tree_mask.emplace_back();
					ret.verified_leaf_hashes.emplace_back();
					continue;
				}

				auto const& t = m_merkle_trees[i];
				auto [sparse_tree, mask] = t.build_sparse_vector();
				ret.merkle_trees.emplace_back(std::move(sparse_tree));
				ret.merkle_tree_mask.emplace_back(std::move(mask));
				ret.verified_leaf_hashes.emplace_back(t.verified_leafs());
			}

			if (!has_hash_picker() && !m_have_all && valid_metadata())
			{
				file_storage const& fs = m_torrent_file->layout();
				ret.verified_leaf_hashes.reserve(fs.num_files());
				for (file_index_t f(0); f != fs.end_file(); ++f)
				{
					if (fs.pad_file_at(f) || fs.file_size(f) == 0)
					{
						ret.verified_leaf_hashes.emplace_back();
						continue;
					}
					ret.verified_leaf_hashes.emplace_back(
						fs.file_num_blocks(f), false);
				}
			}
		}
	}

#if TORRENT_ABI_VERSION == 1
	void torrent::get_full_peer_list(std::vector<peer_list_entry>* v) const
	{
		v->clear();
		if (!m_peer_list) return;

		v->reserve(aux::numeric_cast<std::size_t>(m_peer_list->num_peers()));
		for (auto const* p : *m_peer_list)
		{
			peer_list_entry e;
			e.ip = p->ip();
			e.flags = p->banned ? peer_list_entry::banned : 0;
			e.failcount = p->failcount;
			e.source = p->source;
			v->push_back(e);
		}
	}
#endif

	void torrent::get_peer_info(std::vector<peer_info>* v)
	{
		v->clear();
		v->reserve(m_connections.size());

		for (auto const* peer : *this)
		{
			TORRENT_ASSERT(peer->m_in_use == 1337);

			// incoming peers that haven't finished the handshake should
			// not be included in this list
			if (peer->associated_torrent().expired()) continue;

			v->emplace_back();
			peer_info& p = v->back();

			peer->get_peer_info(p);
		}
	}

	void torrent::post_peer_info()
	{
		std::vector<peer_info> v;
		get_peer_info(&v);
		alerts().emplace_alert<peer_info_alert>(get_handle(), std::move(v));
	}

namespace {
	void initialize_piece_info(piece_picker const& p
		, torrent_info const& ti
		, int const block_size
		, std::vector<block_info>& blk
		, span<piece_picker::downloading_piece const> q
		, std::vector<partial_piece_info>* queue)
	{
		queue->reserve(static_cast<std::size_t>(q.size()));
		const int blocks_per_piece = p.blocks_in_piece(piece_index_t(0));
		int counter = 0;
		for (auto i = q.begin(); i != q.end(); ++i, ++counter)
		{
			partial_piece_info pi;
			pi.blocks_in_piece = p.blocks_in_piece(i->index);
			pi.finished = int(i->finished);
			pi.writing = int(i->writing);
			pi.requested = int(i->requested);
#if TORRENT_ABI_VERSION == 1
			pi.piece_state = partial_piece_info::none;
#endif
			TORRENT_ASSERT(counter * blocks_per_piece + pi.blocks_in_piece <= int(blk.size()));
			block_info* blocks = &blk[std::size_t(counter * blocks_per_piece)];
			pi.blocks = blocks;
			int const piece_size = ti.piece_size_for_req(i->index);
			int idx = -1;
			for (auto const& info : p.blocks_for_piece(*i))
			{
				++idx;
				block_info& bi = blocks[idx];
				bi.state = info.state;
				bi.block_size = idx < pi.blocks_in_piece - 1
					? aux::numeric_cast<std::uint32_t>(block_size)
					: aux::numeric_cast<std::uint32_t>(piece_size - (idx * block_size));
				bool const complete = bi.state == block_info::writing
					|| bi.state == block_info::finished;
				if (info.peer == nullptr)
				{
					bi.set_peer(tcp::endpoint());
					bi.bytes_progress = complete ? bi.block_size : 0;
				}
				else
				{
					torrent_peer* tp = info.peer;
					TORRENT_ASSERT(tp->in_use);
					if (tp->connection)
					{
						auto* peer = static_cast<peer_connection*>(tp->connection);
						TORRENT_ASSERT(peer->m_in_use);
						bi.set_peer(peer->remote());
						if (bi.state == block_info::requested)
						{
							auto pbp = peer->downloading_piece_progress();
							if (pbp.piece_index == i->index && pbp.block_index == idx)
							{
								bi.bytes_progress = aux::numeric_cast<std::uint32_t>(pbp.bytes_downloaded);
								TORRENT_ASSERT(bi.bytes_progress <= bi.block_size);
							}
							else
							{
								bi.bytes_progress = 0;
							}
						}
						else
						{
							bi.bytes_progress = complete ? bi.block_size : 0;
						}
					}
					else
					{
						bi.set_peer(tp->ip());
						bi.bytes_progress = complete ? bi.block_size : 0;
					}
				}

				bi.num_peers = info.num_peers;
			}
			pi.piece_index = i->index;
			queue->push_back(pi);
		}
	}
}

	void torrent::post_file_status()
	{
		std::vector<open_file_state> state;
		if (has_storage())
			state = m_ses.disk_thread().get_status(m_storage);
		alerts().emplace_alert<file_status_alert>(get_handle(), std::move(state));
	}

	void torrent::post_download_queue()
	{
		if (!valid_metadata()) return;
		std::vector<block_info> blk;
		std::vector<partial_piece_info> queue;
		if (has_picker())
		{
			piece_picker const& p = picker();
			std::vector<piece_picker::downloading_piece> const q = p.get_download_queue();
			if (!q.empty())
			{
				const int blocks_per_piece = m_picker->blocks_in_piece(piece_index_t(0));
				blk.resize(q.size() * aux::numeric_cast<std::size_t>(blocks_per_piece));
				initialize_piece_info(p, torrent_file(), block_size(), blk, q, &queue);
			}
		}
		alerts().emplace_alert<piece_info_alert>(get_handle(), std::move(queue), std::move(blk));
	}

	void torrent::get_download_queue(std::vector<partial_piece_info>* queue) const
	{
		TORRENT_ASSERT(is_single_thread());
		queue->clear();
		std::vector<block_info>& blk = m_ses.block_info_storage();
		blk.clear();

		if (!valid_metadata() || !has_picker()) return;
		piece_picker const& p = picker();
		std::vector<piece_picker::downloading_piece> q
			= p.get_download_queue();
		if (q.empty()) return;

		const int blocks_per_piece = m_picker->blocks_in_piece(piece_index_t(0));
		blk.resize(q.size() * aux::numeric_cast<std::size_t>(blocks_per_piece));

		initialize_piece_info(p, torrent_file(), block_size(), blk, q, queue);
	}

#if defined TORRENT_SSL_PEERS
	struct hostname_visitor
	{
		explicit hostname_visitor(std::string const& h) : hostname_(h) {}
		template <typename T>
		void operator()(T&) {}
		template <typename T>
		void operator()(ssl_stream<T>& s) { s.set_host_name(hostname_); }
		std::string const& hostname_;
	};

	struct ssl_handle_visitor
	{
		template <typename T>
		ssl::stream_handle_type operator()(T&)
		{ return nullptr; }

		template <typename T>
		ssl::stream_handle_type operator()(ssl_stream<T>& s)
		{ return s.handle(); }
	};
#endif

	bool torrent::connect_to_peer(torrent_peer* peerinfo, bool const ignore_limit)
	{
		TORRENT_ASSERT(is_single_thread());
		INVARIANT_CHECK;
		TORRENT_UNUSED(ignore_limit);

		TORRENT_ASSERT(peerinfo);
		TORRENT_ASSERT(peerinfo->connection == nullptr);

#if TORRENT_USE_RTC
		if (peerinfo->is_rtc_addr)
		{
			// unsolicited connection is not possible
			TORRENT_ASSERT_FAIL();
			return false;
		}
#endif

		if (m_abort) return false;

		peerinfo->last_connected = m_ses.session_time();
#if TORRENT_USE_ASSERTS
		if (!settings().get_bool(settings_pack::allow_multiple_connections_per_ip)
#if TORRENT_USE_I2P
			&& !peerinfo->is_i2p_addr
#endif
		)
		{
			// this asserts that we don't have duplicates in the peer_list's peer list
			peer_iterator i_ = std::find_if(m_connections.begin(), m_connections.end()
				, [peerinfo] (peer_connection const* p)
				{
					return !p->is_disconnecting() && p->remote() == peerinfo->ip();
				});

			TORRENT_ASSERT(i_ == m_connections.end()
				|| (*i_)->type() != connection_type::bittorrent);
		}
#endif // TORRENT_USE_ASSERTS

		TORRENT_ASSERT(want_peers() || ignore_limit);
		TORRENT_ASSERT(m_ses.num_connections()
			< settings().get_int(settings_pack::connections_limit) || ignore_limit);

		tcp::endpoint a(peerinfo->ip());
		TORRENT_ASSERT(!m_apply_ip_filter
			|| !m_ip_filter
			|| (m_ip_filter->access(peerinfo->address()) & ip_filter::blocked) == 0);

		// this is where we determine if we open a regular TCP connection
		// or a uTP connection. If the utp_socket_manager pointer is not passed in
		// we'll instantiate a TCP connection
		aux::utp_socket_manager* sm = nullptr;

#if TORRENT_USE_I2P
		if (peerinfo->is_i2p_addr)
		{
			if (settings().get_str(settings_pack::i2p_hostname).empty())
			{
				// we have an i2p torrent, but we're not connected to an i2p
				// SAM proxy.
				if (alerts().should_post<i2p_alert>())
					alerts().emplace_alert<i2p_alert>(errors::no_i2p_router);
				return false;
			}
		}
		else
#endif
		{
			if (settings().get_bool(settings_pack::enable_outgoing_utp)
				&& (!settings().get_bool(settings_pack::enable_outgoing_tcp)
					|| peerinfo->supports_utp
					|| peerinfo->confirmed_supports_utp))
			{
				sm = m_ses.utp_socket_manager();
			}

			// don't make a TCP connection if it's disabled
			if (sm == nullptr && !settings().get_bool(settings_pack::enable_outgoing_tcp))
			{
#ifndef TORRENT_DISABLE_LOGGING
				if (should_log())
				{
					debug_log("discarding peer \"%s\": TCP connections disabled "
						"[ supports-utp: %d ]", peerinfo->to_string().c_str()
						, peerinfo->supports_utp);
				}
#endif
				return false;
			}
		}

		// TODO: come up with a better way of doing this, instead of an
		// immediately invoked lambda expression.
		aux::socket_type s = [&] {

#if TORRENT_USE_I2P
		if (peerinfo->is_i2p_addr)
		{
			// It's not entirely obvious why this peer connection is not marked as
			// one. The main feature of a peer connection is that whether or not we
			// proxy it is configurable. When we use i2p, we want to always prox
			// everything via i2p.

			aux::proxy_settings proxy;
			proxy.hostname = settings().get_str(settings_pack::i2p_hostname);
			proxy.port = std::uint16_t(settings().get_int(settings_pack::i2p_port));
			proxy.type = settings_pack::i2p_proxy;

			aux::socket_type ret = instantiate_connection(m_ses.get_context()
				, proxy, nullptr, nullptr, false, false);
			auto& str = std::get<i2p_stream>(ret);
			str.set_local_i2p_endpoint(m_ses.local_i2p_endpoint());
			str.set_destination(static_cast<i2p_peer*>(peerinfo)->dest());
			str.set_command(i2p_stream::cmd_connect);
			str.set_session_id(m_ses.i2p_session());
			return ret;
		}
		else
#endif
		{
			void* userdata = nullptr;
#ifdef TORRENT_SSL_PEERS
			if (is_ssl_torrent())
			{
				userdata = m_ssl_ctx.get();
				// if we're creating a uTP socket, since this is SSL now, make sure
				// to pass in the corresponding utp socket manager
				if (sm) sm = m_ses.ssl_utp_socket_manager();
			}
#endif

			aux::socket_type ret = instantiate_connection(m_ses.get_context()
				, m_ses.proxy(), userdata, sm, true, false);

#if defined TORRENT_SSL_PEERS
			if (is_ssl_torrent())
			{
				// for ssl sockets, set the hostname
				std::string const host_name = aux::to_hex(
					m_torrent_file->info_hashes().get(peerinfo->protocol()));

				std::visit(hostname_visitor{host_name}, ret.var());
			}
#endif
			return ret;
		}
		}();

		if(!create_peer_connection(peerinfo, std::move(s), std::move(a)))
			return false;

		return peerinfo->connection != nullptr;
	}

	bool torrent::create_peer_connection(torrent_peer* peerinfo, aux::socket_type socket, tcp::endpoint endpoint)
	{
		peer_id const our_pid = aux::generate_peer_id(settings());
		peer_connection_args pack{
			&m_ses
			, &settings()
			, &m_ses.stats_counters()
			, &m_ses.disk_thread()
			, &m_ses.get_context()
			, shared_from_this()
			, std::move(socket)
			, std::move(endpoint)
			, peerinfo
			, our_pid
		};

		auto c = std::make_shared<bt_peer_connection>(pack);

#if TORRENT_USE_ASSERTS
		c->m_in_constructor = false;
#endif

		c->add_stat(std::int64_t(peerinfo->prev_amount_download) * 1024
			, std::int64_t(peerinfo->prev_amount_upload) * 1024);
		peerinfo->prev_amount_download = 0;
		peerinfo->prev_amount_upload = 0;

#ifndef TORRENT_DISABLE_EXTENSIONS
		for (auto const& ext : m_extensions)
		{
			std::shared_ptr<peer_plugin> pp(ext->new_connection(
					peer_connection_handle(c->self())));
			if (pp) c->add_extension(pp);
		}
#endif

		// add the newly connected peer to this torrent's peer list
		TORRENT_ASSERT(m_iterating_connections == 0);

		// we don't want to have to allocate memory to disconnect this peer, so
		// make sure there's enough memory allocated in the deferred_disconnect
		// list up-front
		m_peers_to_disconnect.reserve(m_connections.size() + 1);

		sorted_insert(m_connections, c.get());
		try
		{
			m_outgoing_pids.insert(our_pid);
			m_ses.insert_peer(c);
			need_peer_list();
			m_peer_list->set_connection(peerinfo, c.get());
			if (peerinfo->seed)
			{
				TORRENT_ASSERT(m_num_seeds < 0xffff);
				++m_num_seeds;
			}
			update_want_peers();
			update_want_tick();
			c->start();

			if (c->is_disconnecting()) return false;
		}
		catch (std::exception const&)
		{
			TORRENT_ASSERT(m_iterating_connections == 0);
			c->disconnect(errors::no_error, operation_t::bittorrent, peer_connection_interface::failure);
			return false;
		}

#ifndef TORRENT_DISABLE_SHARE_MODE
		if (m_share_mode)
			recalc_share_mode();
#endif
		return true;
	}

	error_code torrent::initialize_merkle_trees()
	{
		if (!info_hash().has_v2()) return {};

#if TORRENT_ABI_VERSION < 4
		bool valid = m_torrent_file->v2_piece_hashes_verified();
#endif

		file_storage const& fs = m_torrent_file->layout();
		m_merkle_trees.reserve(fs.num_files());
		for (file_index_t i : fs.file_range())
		{
			if (fs.pad_file_at(i) || fs.file_size(i) == 0)
			{
				m_merkle_trees.emplace_back();
				continue;
			}
			m_merkle_trees.emplace_back(fs.file_num_blocks(i)
				, fs.blocks_per_piece(), fs.root_ptr(i));
#if TORRENT_ABI_VERSION < 4
			auto const piece_layer = m_torrent_file->piece_layer(i);
			if (piece_layer.empty())
			{
				valid = false;
				continue;
			}

			if (!m_merkle_trees[i].load_piece_layer(piece_layer))
			{
				m_merkle_trees[i] = aux::merkle_tree();
				m_v2_piece_layers_validated = false;
				return errors::torrent_invalid_piece_layer;
			}
#endif
		}

#if TORRENT_ABI_VERSION < 4
		m_v2_piece_layers_validated = valid;

		m_torrent_file->free_piece_layers();
#endif
		return {};
	}

	bool torrent::set_metadata(span<char const> metadata_buf)
	{
		TORRENT_ASSERT(is_single_thread());
		INVARIANT_CHECK;

		if (m_torrent_file->is_valid()) return false;

		if (m_torrent_file->info_hashes().has_v1())
		{
			sha1_hash const info_hash = hasher(metadata_buf).final();
			if (info_hash != m_torrent_file->info_hashes().v1)
			{
				// check if the v1 hash is a truncated v2 hash
				sha256_hash const info_hash2 = hasher256(metadata_buf).final();
				if (sha1_hash(info_hash2.data()) != m_torrent_file->info_hashes().v1)
				{
					if (alerts().should_post<metadata_failed_alert>())
					{
						alerts().emplace_alert<metadata_failed_alert>(get_handle()
							, errors::mismatching_info_hash);
					}
					return false;
				}
			}
		}
		if (m_torrent_file->info_hashes().has_v2())
		{
			// we don't have to worry about computing the v2 hash twice because
			// if the v1 hash was a truncated v2 hash then the torrent_file should
			// not have a v2 hash and we shouldn't get here
			sha256_hash const info_hash = hasher256(metadata_buf).final();
			if (info_hash != m_torrent_file->info_hashes().v2)
			{
				if (alerts().should_post<metadata_failed_alert>())
				{
					alerts().emplace_alert<metadata_failed_alert>(get_handle()
						, errors::mismatching_info_hash);
				}
				return false;
			}
		}

		// the torrent's info hash might change
		// e.g. it could be a hybrid torrent which we only had one of the hashes for
		// so remove the existing entry
		info_hash_t const old_ih = m_torrent_file->info_hashes();

		error_code ec;
		int pos = 0;
		auto const& sett = settings();
		bdecode_node const metadata = bdecode(metadata_buf, ec, &pos, 200
			, sett.get_int(settings_pack::metadata_token_limit));

		std::shared_ptr<torrent_info> info;
		std::map<file_index_t, std::string> renamed_files;
		if (!ec)
		{
			load_torrent_limits cfg;
			cfg.max_pieces = sett.get_int(settings_pack::max_piece_count);
			info = std::make_shared<torrent_info>(metadata, ec, cfg, from_info_section);
			// ec is an output parameter set by torrent_info's constructor on
			// parse failure; cppcheck doesn't see the constructor definition
			// (in torrent_info.cpp) from this translation unit
			// cppcheck-suppress identicalInnerCondition
			if (!ec)
			{
				renamed_files = aux::resolve_duplicate_filenames(
					info->layout(), cfg.max_duplicate_filenames, ec);
#if TORRENT_ABI_VERSION < 4
				// for backwards compatibility, make sure the file_storage
				// returned by the deprecated files() has updated filenames
				// as well
				for (auto const& entry : renamed_files)
					info->rename_file(entry.first, entry.second);
#endif
			}
		}

		if (ec)
		{
			update_gauge();
			// this means the metadata is correct, since we
			// verified it against the info-hash, but we
			// failed to parse it. Pause the torrent
			if (alerts().should_post<metadata_failed_alert>())
			{
				alerts().emplace_alert<metadata_failed_alert>(get_handle(), ec);
			}
			set_error(errors::invalid_swarm_metadata, torrent_status::error_file_none);
			pause();
			return false;
		}

		// we might already have this torrent in the session.
		bool failed = false;
		info->info_hashes().for_each([&](sha1_hash const& ih, protocol_version)
		{
			if (failed) return;

			auto t = m_ses.find_torrent(info_hash_t(ih)).lock();
			if (t && t != shared_from_this())
			{
				TORRENT_ASSERT(!t->valid_metadata());

				// if we get a collision, both torrents fail and have to be
				// removed. This is because updating the info_hash_t for this
				// torrent would conflict with torrent "t". That would violate
				// the invariants:
				//   1. an info-hash can only refer to a single torrent
				//   2. every torrent needs at least one info-hash.
				t->set_error(errors::duplicate_torrent, torrent_status::error_file_metadata);
				t->pause();

				set_error(errors::duplicate_torrent, torrent_status::error_file_metadata);
				pause();
				failed = true;

				if (alerts().should_post<torrent_conflict_alert>())
				{
					alerts().emplace_alert<torrent_conflict_alert>(get_handle()
						, torrent_handle(std::move(t)), std::move(info));
				}
			}
		});

		if (failed) return true;
		if (m_abort) return true;

		m_name_idx.clear();
		m_torrent_file = info;
		m_renamed_files.import_filenames(m_torrent_file->layout(), renamed_files);
		m_info_hash = m_torrent_file->info_hashes();
		{
			std::lock_guard<std::mutex> l(m_torrent_file_mutex);
			m_torrent_file_external = m_torrent_file;
		}

		// a peer's protocol_v2 flag can be set speculatively before metadata
		// is valid, since a magnet link's info-hash doesn't establish
		// whether the torrent has a v2 info hash
		if (!m_info_hash.has_v2() && m_peer_list)
		{
			for (auto* pp : *m_peer_list)
				pp->protocol_v2 = false;
		}

		m_size_on_disk = m_torrent_file->layout().size_on_disk();

		m_ses.update_torrent_info_hash(shared_from_this(), old_ih);

		ec = initialize_merkle_trees();
		if (ec)
		{
			set_error(ec, torrent_status::error_file_metadata);
			pause();
			return false;
		}

		update_gauge();
		update_want_tick();

		if (m_ses.alerts().should_post<metadata_received_alert>())
		{
			m_ses.alerts().emplace_alert<metadata_received_alert>(
				get_handle());
		}

		// we have to initialize the torrent before we start
		// disconnecting redundant peers, otherwise we'll think
		// we're a seed, because we have all 0 pieces
		init();

		inc_stats_counter(counters::num_total_pieces_added
			, m_torrent_file->num_pieces());

		// disconnect redundant peers
		for (auto* p : m_connections)
			p->disconnect_if_redundant();

		set_need_save_resume(torrent_handle::if_metadata_changed);

		return true;
	}

	namespace {

	bool connecting_time_compare(peer_connection const* lhs, peer_connection const* rhs)
	{
		bool const lhs_connecting = lhs->is_connecting() && !lhs->is_disconnecting();
		bool const rhs_connecting = rhs->is_connecting() && !rhs->is_disconnecting();
		if (lhs_connecting != rhs_connecting) return (int(lhs_connecting) < int(rhs_connecting));

		// a lower value of connected_time means it's been waiting
		// longer. This is a less-than comparison, so if lhs has
		// waited longer than rhs, we should return false.
		return lhs->connected_time() > rhs->connected_time();
	}

	} // anonymous namespace

	bool torrent::attach_peer(peer_connection* p) try
	{
//		INVARIANT_CHECK;

#ifdef TORRENT_SSL_PEERS
		if (is_ssl_torrent())
		{
			// if this is an SSL torrent, don't allow non SSL peers on it
			aux::socket_type& s = p->get_socket();

			ssl::stream_handle_type stream_handle = std::visit(ssl_handle_visitor{}, s.var());

			if (!stream_handle)
			{
				// don't allow non SSL peers on SSL torrents
				p->disconnect(errors::requires_ssl_connection, operation_t::bittorrent);
				return false;
			}

			if (!m_ssl_ctx)
			{
				// we don't have a valid cert, don't accept any connection!
				p->disconnect(errors::invalid_ssl_cert, operation_t::ssl_handshake);
				return false;
			}

			if (!ssl::has_context(stream_handle, ssl::get_handle(*m_ssl_ctx)))
			{
				// if the SSL context associated with this connection is
				// not the one belonging to this torrent, the SSL handshake
				// connected to one torrent, and the BitTorrent protocol
				// to a different one. This is probably an attempt to circumvent
				// access control. Don't allow it.
				p->disconnect(errors::invalid_ssl_cert, operation_t::bittorrent);
				return false;
			}
		}
#else // TORRENT_SSL_PEERS
		if (is_ssl_torrent())
		{
			// Don't accidentally allow seeding of SSL torrents, just
			// because libtorrent wasn't built with SSL support
			p->disconnect(errors::requires_ssl_connection, operation_t::ssl_handshake);
			return false;
		}
#endif // TORRENT_SSL_PEERS

		TORRENT_ASSERT(p != nullptr);
		TORRENT_ASSERT(!p->is_outgoing());

		m_has_incoming = true;

		if (m_apply_ip_filter
			&& m_ip_filter
			&& m_ip_filter->access(p->remote().address()) & ip_filter::blocked)
		{
			if (m_ses.alerts().should_post<peer_blocked_alert>())
				m_ses.alerts().emplace_alert<peer_blocked_alert>(get_handle()
					, p->remote(), peer_blocked_alert::ip_filter);
			p->disconnect(errors::banned_by_ip_filter, operation_t::bittorrent);
			return false;
		}

		if (!is_downloading_state(m_state) && valid_metadata())
		{
			p->disconnect(errors::torrent_not_ready, operation_t::bittorrent);
			return false;
		}

		if (!m_ses.has_connection(p))
		{
			p->disconnect(errors::peer_not_constructed, operation_t::bittorrent);
			return false;
		}

		if (m_ses.is_aborted())
		{
			p->disconnect(errors::session_closing, operation_t::bittorrent);
			return false;
		}

		int connection_limit_factor = 0;
		for (int i = 0; i < p->num_classes(); ++i)
		{
			peer_class_t pc = p->class_at(i);
			if (m_ses.peer_classes().at(pc) == nullptr) continue;
			int f = m_ses.peer_classes().at(pc)->connection_limit_factor;
			if (connection_limit_factor < f) connection_limit_factor = f;
		}
		if (connection_limit_factor == 0) connection_limit_factor = 100;

		std::int64_t const limit = std::int64_t(m_max_connections) * 100 / connection_limit_factor;

		bool maybe_replace_peer = false;

		if (m_connections.end_index() >= limit)
		{
			// if more than 10% of the connections are outgoing
			// connection attempts that haven't completed yet,
			// disconnect one of them and let this incoming
			// connection through.
			if (m_num_connecting > m_max_connections / 10)
			{
				// find one of the connecting peers and disconnect it
				// find any peer that's connecting (i.e. a half-open TCP connection)
				// that's also not disconnecting
				// disconnect the peer that's been waiting to establish a connection
				// the longest
				auto i = std::max_element(begin(), end(), &connecting_time_compare);

				if (i == end() || !(*i)->is_connecting() || (*i)->is_disconnecting())
				{
					// this seems odd, but we might as well handle it
					p->disconnect(errors::too_many_connections, operation_t::bittorrent);
					return false;
				}
				(*i)->disconnect(errors::too_many_connections, operation_t::bittorrent);

				// if this peer was let in via connections slack,
				// it has done its duty of causing the disconnection
				// of another peer
				p->peer_disconnected_other();
			}
			else
			{
				maybe_replace_peer = true;
			}
		}

#ifndef TORRENT_DISABLE_EXTENSIONS
		for (auto& ext : m_extensions)
		{
			std::shared_ptr<peer_plugin> pp(ext->new_connection(
					peer_connection_handle(p->self())));
			if (pp) p->add_extension(pp);
		}
#endif
		torrent_state st = get_peer_list_state();
		need_peer_list();
		if (!m_peer_list->new_connection(*p, m_ses.session_time(), &st))
		{
			peers_erased(st.erased);
#ifndef TORRENT_DISABLE_LOGGING
			if (should_log())
			{
				debug_log("CLOSING CONNECTION \"%s\" peer list full "
					"connections: %d limit: %d"
					, print_endpoint(p->remote()).c_str()
					, num_peers()
					, m_max_connections);
			}
#endif
			p->disconnect(errors::too_many_connections, operation_t::bittorrent);
			return false;
		}
		peers_erased(st.erased);

		m_peers_to_disconnect.reserve(m_connections.size() + 1);
		m_connections.reserve(m_connections.size() + 1);

#if TORRENT_USE_ASSERTS
		error_code ec;
		TORRENT_ASSERT(p->remote() == p->get_socket().remote_endpoint(ec) || ec);
#endif

		TORRENT_ASSERT(p->peer_info_struct() != nullptr);

		// we need to do this after we've added the peer to the peer_list
		// since that's when the peer is assigned its peer_info object,
		// which holds the rank
		if (maybe_replace_peer)
		{
			// now, find the lowest rank peer and disconnect that
			// if it's lower rank than the incoming connection
			peer_connection* peer = find_lowest_ranking_peer();

			// TODO: 2 if peer is a really good peer, maybe we shouldn't disconnect it
			// perhaps this logic should be disabled if we have too many idle peers
			// (with some definition of idle)
			if (peer != nullptr && peer->peer_rank() < p->peer_rank())
			{
#ifndef TORRENT_DISABLE_LOGGING
				if (should_log())
				{
					debug_log("CLOSING CONNECTION \"%s\" peer list full (low peer rank) "
						"connections: %d limit: %d"
						, print_endpoint(peer->remote()).c_str()
						, num_peers()
						, m_max_connections);
				}
#endif
				peer->disconnect(errors::too_many_connections, operation_t::bittorrent);
				p->peer_disconnected_other();
			}
			else
			{
#ifndef TORRENT_DISABLE_LOGGING
				if (should_log())
				{
					debug_log("CLOSING CONNECTION \"%s\" peer list full (low peer rank) "
						"connections: %d limit: %d"
						, print_endpoint(p->remote()).c_str()
						, num_peers()
						, m_max_connections);
				}
#endif
				p->disconnect(errors::too_many_connections, operation_t::bittorrent);
				// we have to do this here because from the peer's point of view
				// it wasn't really attached to the torrent, but we do need
				// to let peer_list know we're removing it
				remove_peer(p->self());
				return false;
			}
		}

#if TORRENT_USE_INVARIANT_CHECKS
		if (m_peer_list) m_peer_list->check_invariant();
#endif

#ifndef TORRENT_DISABLE_SHARE_MODE
		if (m_share_mode)
			recalc_share_mode();
#endif

		// once we add the peer to our m_connections list, we can't throw an
		// exception. That will end up violating an invariant between the session,
		// torrent and peers
		TORRENT_ASSERT(sorted_find(m_connections, p) == m_connections.end());
		TORRENT_ASSERT(m_iterating_connections == 0);
		sorted_insert(m_connections, p);
		update_want_peers();
		update_want_tick();

		if (p->peer_info_struct() && p->peer_info_struct()->seed)
		{
			TORRENT_ASSERT(m_num_seeds < 0xffff);
			++m_num_seeds;
		}

#ifndef TORRENT_DISABLE_LOGGING
		if (should_log()) try
		{
			debug_log("ATTACHED CONNECTION \"%s\" connections: %d limit: %d num-peers: %d"
				, print_endpoint(p->remote()).c_str(), num_peers()
				, m_max_connections
				, num_peers());
		}
		catch (std::exception const&) {}
#endif

		return true;
	}
	catch (...)
	{
		p->disconnect(errors::torrent_not_ready, operation_t::bittorrent);
		// from the peer's point of view it was never really added to the torrent.
		// So we need to clean it up here before propagating the error
		remove_peer(p->self());
		return false;
	}

	bool torrent::want_tick() const
	{
		if (m_abort) return false;

		if (!m_connections.empty()) return true;

		// we might want to connect web seeds
		if (!is_finished() && !m_web_seeds.empty() && m_files_checked)
			return true;

		if (m_stat.low_pass_upload_rate() > 0 || m_stat.low_pass_download_rate() > 0)
			return true;

		// if we don't get ticks we won't become inactive
		if (!m_paused && !m_inactive) return true;

		return false;
	}

	void torrent::update_want_tick()
	{
		update_list(aux::session_interface::torrent_want_tick, want_tick());
	}

	// this function adjusts which lists this torrent is part of (checking,
	// seeding or downloading)
	void torrent::update_state_list()
	{
		bool is_checking = false;
		bool is_downloading = false;
		bool is_seeding = false;

		if (is_auto_managed() && !has_error())
		{
			if (m_state == torrent_status::checking_files)
			{
				is_checking = true;
			}
			else if (m_state == torrent_status::downloading_metadata
				|| m_state == torrent_status::downloading
				|| m_state == torrent_status::finished
				|| m_state == torrent_status::seeding)
			{
				// torrents that are started (not paused) and
				// inactive are not part of any list. They will not be touched because
				// they are inactive
				if (is_finished())
					is_seeding = true;
				else
					is_downloading = true;
			}
		}

		update_list(aux::session_interface::torrent_downloading_auto_managed
			, is_downloading);
		update_list(aux::session_interface::torrent_seeding_auto_managed
			, is_seeding);
		update_list(aux::session_interface::torrent_checking_auto_managed
			, is_checking);
	}

	// returns true if this torrent is interested in connecting to more peers
	bool torrent::want_peers() const
	{
		// if all our connection slots are taken, we can't connect to more
		if (num_peers() >= int(m_max_connections)) return false;

		// if we're paused, obviously we're not connecting to peers
		if (is_paused() || m_abort || m_graceful_pause_mode) return false;

		// if metadata are valid and we are either checking files or checking resume data without no_verify_files flag,
		// we don't want peers
		if ((m_state == torrent_status::checking_files
			|| (m_state == torrent_status::checking_resume_data
				&& !(m_add_torrent_params && m_add_torrent_params->flags & torrent_flags::no_verify_files)))
			&& valid_metadata())
			return false;

		// if we don't know of any more potential peers to connect to, there's
		// no point in trying
		if (!m_peer_list || m_peer_list->num_connect_candidates() == 0)
			return false;

		// if the user disabled outgoing connections for seeding torrents,
		// don't make any
		if (!settings().get_bool(settings_pack::seeding_outgoing_connections)
			&& (m_state == torrent_status::seeding
				|| m_state == torrent_status::finished))
			return false;

		if (!settings().get_bool(settings_pack::enable_outgoing_tcp)
			&& !settings().get_bool(settings_pack::enable_outgoing_utp))
			return false;

		return true;
	}

	bool torrent::want_peers_download() const
	{
		return (m_state == torrent_status::downloading
			|| m_state == torrent_status::downloading_metadata)
			&& want_peers();
	}

	bool torrent::want_peers_finished() const
	{
		return (m_state == torrent_status::finished
			|| m_state == torrent_status::seeding)
			&& want_peers();
	}

	void torrent::update_want_peers()
	{
		update_list(aux::session_interface::torrent_want_peers_download, want_peers_download());
		update_list(aux::session_interface::torrent_want_peers_finished, want_peers_finished());
	}

	void torrent::update_want_scrape()
	{
		update_list(aux::session_interface::torrent_want_scrape
			, m_paused && m_auto_managed && !m_abort);
	}

	namespace {

#ifndef TORRENT_DISABLE_LOGGING
	char const* list_name(torrent_list_index_t const idx)
	{
#define TORRENT_LIST_NAME(n) case static_cast<int>(aux::session_interface:: n): return #n
		switch (static_cast<int>(idx))
		{
			TORRENT_LIST_NAME(torrent_state_updates);
			TORRENT_LIST_NAME(torrent_want_tick);
			TORRENT_LIST_NAME(torrent_want_peers_download);
			TORRENT_LIST_NAME(torrent_want_peers_finished);
			TORRENT_LIST_NAME(torrent_want_scrape);
			TORRENT_LIST_NAME(torrent_downloading_auto_managed);
			TORRENT_LIST_NAME(torrent_seeding_auto_managed);
			TORRENT_LIST_NAME(torrent_checking_auto_managed);
			default: TORRENT_ASSERT_FAIL_VAL(idx);
		}
#undef TORRENT_LIST_NAME
		return "";
	}
#endif // TORRENT_DISABLE_LOGGING

	} // anonymous namespace

	void torrent::update_list(torrent_list_index_t const list, bool in)
	{
		// once we've started aborting, we must never re-insert this torrent
		// into any of session_impl::m_torrent_lists. The torrent is on its
		// way out and any lingering pointer would dangle once we destruct.
		// unlinking is still allowed (and is what abort() relies on).
		if (m_abort) in = false;

		link& l = m_links[list];
		aux::vector<torrent*>& v = m_ses.torrent_list(list);

		if (in)
		{
			if (l.in_list()) return;
			l.insert(v, this);
		}
		else
		{
			if (!l.in_list()) return;
			l.unlink(v, list);
		}

#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
			debug_log("*** UPDATE LIST [ %s : %d ]", list_name(list), int(in));
#endif
	}

	void torrent::disconnect_all(error_code const& ec, operation_t op)
	{
		TORRENT_ASSERT(m_iterating_connections == 0);
		for (auto const& p : m_connections)
		{
			TORRENT_INCREMENT(m_iterating_connections);
			TORRENT_ASSERT(p->associated_torrent().lock().get() == this);
			p->disconnect(ec, op);
		}

		update_want_peers();
		update_want_tick();
	}

	namespace {

	// this returns true if lhs is a better disconnect candidate than rhs
	bool compare_disconnect_peer(peer_connection const* lhs, peer_connection const* rhs)
	{
		// prefer to disconnect peers that are already disconnecting
		if (lhs->is_disconnecting() != rhs->is_disconnecting())
			return lhs->is_disconnecting();

		// prefer to disconnect peers we're not interested in
		if (lhs->is_interesting() != rhs->is_interesting())
			return rhs->is_interesting();

		// prefer to disconnect peers that are not seeds
		if (lhs->is_seed() != rhs->is_seed())
			return rhs->is_seed();

		// prefer to disconnect peers that are on parole
		if (lhs->on_parole() != rhs->on_parole())
			return lhs->on_parole();

		// prefer to disconnect peers that send data at a lower rate
		std::int64_t lhs_transferred = lhs->statistics().total_payload_download();
		std::int64_t rhs_transferred = rhs->statistics().total_payload_download();

		time_point const now = aux::time_now();
		std::int64_t const lhs_time_connected = total_seconds(now - lhs->connected_time());
		std::int64_t const rhs_time_connected = total_seconds(now - rhs->connected_time());

		lhs_transferred /= lhs_time_connected + 1;
		rhs_transferred /= (rhs_time_connected + 1);
		if (lhs_transferred != rhs_transferred)
			return lhs_transferred < rhs_transferred;

		// prefer to disconnect peers that chokes us
		if (lhs->is_choked() != rhs->is_choked())
			return lhs->is_choked();

		return lhs->last_received() < rhs->last_received();
	}

	} // anonymous namespace

	int torrent::disconnect_peers(int const num, error_code const& ec)
	{
		INVARIANT_CHECK;

#if TORRENT_USE_ASSERTS
		// make sure we don't have any dangling pointers
		for (auto p : m_connections)
		{
			TORRENT_INCREMENT(m_iterating_connections);
			TORRENT_ASSERT(m_ses.has_peer(p));
		}
#endif
		aux::vector<peer_connection*> to_disconnect;
		to_disconnect.resize(num);
		auto end = std::partial_sort_copy(m_connections.begin(), m_connections.end()
			, to_disconnect.begin(), to_disconnect.end(), compare_disconnect_peer);
		for (auto* p : aux::range(to_disconnect.begin(), end))
		{
			TORRENT_ASSERT(p->associated_torrent().lock().get() == this);
			p->disconnect(ec, operation_t::bittorrent);
		}
		return static_cast<int>(end - to_disconnect.begin());
	}

	// called when torrent is finished (all interesting
	// pieces have been downloaded)
	void torrent::finished()
	{
		update_want_tick();
		update_state_list();

		INVARIANT_CHECK;

		TORRENT_ASSERT(is_finished());

		set_state(torrent_status::finished);
		set_queue_position(no_pos);

		update_state_timers();

		// we have to call completed() before we start
		// disconnecting peers, since there's an assert
		// to make sure we're cleared the piece picker
		if (is_seed()) completed();

		send_upload_only();
		state_updated();

		if (m_completed_time == 0)
			m_completed_time = time(nullptr);

		// disconnect all seeds
		if (settings().get_bool(settings_pack::close_redundant_connections))
		{
			// TODO: 1 should disconnect all peers that have the pieces we have
			// not just seeds. It would be pretty expensive to check all pieces
			// for all peers though
			std::vector<peer_connection*> seeds;
			for (auto* p : m_connections)
			{
				TORRENT_INCREMENT(m_iterating_connections);
				TORRENT_ASSERT(p->associated_torrent().lock().get() == this);
				if (p->upload_only() && p->can_disconnect(errors::torrent_finished))
				{
#ifndef TORRENT_DISABLE_LOGGING
					p->peer_log(peer_log_alert::info, peer_log_alert::seed, "CLOSING CONNECTION");
#endif
					seeds.push_back(p);
				}
			}
			for (auto& p : seeds)
				p->disconnect(errors::torrent_finished, operation_t::bittorrent
					, peer_connection_interface::normal);
		}

		if (m_abort) return;

		update_want_peers();

		if (m_storage)
		{
			// we need to keep the object alive during this operation
			m_ses.disk_thread().async_release_files(m_storage
				, std::bind(&torrent::on_cache_flushed, shared_from_this(), false));
			m_ses.deferred_submit_jobs();
		}

		// this torrent just completed downloads, which means it will fall
		// under a different limit with the auto-manager. Make sure we
		// update auto-manage torrents in that case
		if (m_auto_managed)
			m_ses.trigger_auto_manage();
	}

	// this is called when we were finished, but some files were
	// marked for downloading, and we are no longer finished
	void torrent::resume_download()
	{
		// the invariant doesn't hold here, because it expects the torrent
		// to be in downloading state (which it will be set to shortly)
//		INVARIANT_CHECK;

		TORRENT_ASSERT(m_state != torrent_status::checking_resume_data
			&& m_state != torrent_status::checking_files);

		// we're downloading now, which means we're no longer in seed mode
		if (m_seed_mode)
			leave_seed_mode(seed_mode_t::check_files);

		TORRENT_ASSERT(!is_finished());
		set_state(torrent_status::downloading);
		set_queue_position(last_pos);

		update_state_timers();

		m_completed_time = 0;

#ifndef TORRENT_DISABLE_LOGGING
		debug_log("*** RESUME_DOWNLOAD");
#endif
		send_upload_only();
		update_want_tick();
		update_state_list();
	}

	void torrent::set_have_all(bool const b)
	{
		if (bool(m_have_all) == b)
			return;
		m_have_all = b;
		update_state_timers();
	}

	void torrent::update_state_timers()
	{
		time_point32 const now = aux::time_now32();
		bool const live = !is_paused() && !m_abort;
		m_active_timer.set_live(live, now);
		m_seeding_timer.set_live(live && is_seed(), now);
		m_finished_timer.set_live(live && is_finished(), now);
	}

	void torrent::maybe_done_flushing()
	{
		if (!has_picker()) return;

		if (m_picker->is_seeding())
		{
			// no need for the piece picker anymore
			// when we're suggesting read cache pieces, we
			// still need the piece picker, to keep track
			// of availability counts for pieces
			if (settings().get_int(settings_pack::suggest_mode)
				!= settings_pack::suggest_read_cache)
			{
				m_picker.reset();
				m_hash_picker.reset();
				m_file_progress.clear();
			}
			set_have_all(true);
		}
		update_gauge();
	}

	// called when torrent is complete. i.e. all pieces downloaded
	// not necessarily flushed to disk
	void torrent::completed()
	{
		maybe_done_flushing();

		set_state(torrent_status::seeding);
		update_state_timers();

		if (!m_announcing) return;

		m_trackers.completed(aux::time_now32());
		announce_with_tracker();
	}

	void torrent::files_checked()
	{
		TORRENT_ASSERT(is_single_thread());
		TORRENT_ASSERT(m_torrent_file->is_valid());

		if (m_abort)
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("files_checked(), paused");
#endif
			return;
		}

		// calling pause will also trigger the auto managed
		// recalculation
		// if we just got here by downloading the metadata,
		// just keep going, no need to disconnect all peers just
		// to restart the torrent in a second
		if (m_auto_managed)
		{
			// if this is an auto managed torrent, force a recalculation
			// of which torrents to have active
			m_ses.trigger_auto_manage();
		}

		if (!is_seed())
		{
#ifndef TORRENT_DISABLE_SUPERSEEDING
			// turn off super seeding if we're not a seed
			if (m_super_seeding)
			{
				m_super_seeding = false;
				set_need_save_resume(torrent_handle::if_state_changed);
				state_updated();
			}
#endif

			if (m_state != torrent_status::finished && is_finished())
				finished();
		}
		else
		{
			// we just added this torrent as a seed, or force-rechecked it, and we
			// have all of it. Assume that we sent the event=completed when we
			// finished downloading it, and don't send any more.
			m_complete_sent = true;
			m_trackers.set_complete_sent();

			if (m_state != torrent_status::finished
				&& m_state != torrent_status::seeding)
				finished();
		}

		// we might be finished already, in which case we should
		// not switch to downloading mode. If all files are
		// filtered, we're finished when we start.
		if (m_state != torrent_status::finished
			&& m_state != torrent_status::seeding
			&& !m_seed_mode)
		{
			set_state(torrent_status::downloading);
		}

		INVARIANT_CHECK;

		if (m_ses.alerts().should_post<torrent_checked_alert>())
		{
			m_ses.alerts().emplace_alert<torrent_checked_alert>(
				get_handle());
		}

#ifndef TORRENT_DISABLE_EXTENSIONS
		for (auto& ext : m_extensions)
		{
			ext->on_files_checked();
		}
#endif

		bool const notify_initialized = !m_connections_initialized;
		m_connections_initialized = true;
		m_files_checked = true;

		update_want_tick();

		for (auto* pc : m_connections)
		{
			TORRENT_INCREMENT(m_iterating_connections);
			// all peer connections have to initialize themselves now that the metadata
			// is available
			if (notify_initialized)
			{
				if (pc->is_disconnecting()) continue;
				pc->on_metadata_impl();
				if (pc->is_disconnecting()) continue;
				pc->init();
			}

#ifndef TORRENT_DISABLE_LOGGING
			pc->peer_log(peer_log_alert::info, peer_log_alert::on_files_checked);
#endif
			if (pc->is_interesting() && !pc->has_peer_choked())
			{
				if (request_a_block(*this, *pc))
				{
					inc_stats_counter(counters::unchoke_piece_picks);
					pc->send_block_requests();
				}
			}
		}

		start_announcing();

		maybe_connect_web_seeds();
	}

	aux::alert_manager& torrent::alerts() const
	{
		TORRENT_ASSERT(is_single_thread());
		return m_ses.alerts();
	}

	bool torrent::is_seed() const
	{
		if (!valid_metadata()) return false;
		if (m_seed_mode) return true;
		if (m_have_all) return true;
		if (m_picker && m_picker->is_seeding()) return true;
		return m_state == torrent_status::seeding;
	}

	bool torrent::is_finished() const
	{
		if (is_seed()) return true;
		return valid_metadata() && has_picker() && m_picker->is_finished();
	}

	bool torrent::is_inactive() const
	{
		if (!settings().get_bool(settings_pack::dont_count_slow_torrents))
			return false;
		return m_inactive;
	}

	std::string torrent::save_path() const
	{
		return m_save_path;
	}

	void torrent::rename_file(file_index_t const index, std::string name)
	{
		INVARIANT_CHECK;

		file_storage const& fs = m_torrent_file->layout();
		TORRENT_ASSERT(index >= file_index_t(0));
		TORRENT_ASSERT(index < fs.end_file());
		TORRENT_UNUSED(fs);

		// storage may be nullptr during shutdown
		if (!m_storage)
		{
			if (alerts().should_post<file_rename_failed_alert>())
				alerts().emplace_alert<file_rename_failed_alert>(get_handle()
					, index, errors::session_is_closing);
			return;
		}

		m_ses.disk_thread().async_rename_file(m_storage, index, std::move(name)
			, std::bind(&torrent::on_file_renamed, shared_from_this(), _1, _2, _3));
		m_ses.deferred_submit_jobs();
	}

	void torrent::move_storage(std::string const& save_path, move_flags_t const flags)
	{
		TORRENT_ASSERT(is_single_thread());
		INVARIANT_CHECK;

		if (m_abort)
		{
			if (alerts().should_post<storage_moved_failed_alert>())
				alerts().emplace_alert<storage_moved_failed_alert>(get_handle()
					, boost::asio::error::operation_aborted
					, "", operation_t::unknown);
			return;
		}

		// if we don't have metadata yet, we don't know anything about the file
		// structure and we have to assume we don't have any file.
		if (!valid_metadata())
		{
			if (alerts().should_post<storage_moved_alert>())
				alerts().emplace_alert<storage_moved_alert>(get_handle(), save_path, m_save_path);
#if TORRENT_USE_UNC_PATHS
			std::string path = canonicalize_path(save_path);
#else
			std::string const& path = save_path;
#endif
			m_save_path = complete(path);
			return;
		}

		// storage may be nullptr during shutdown
		if (m_storage)
		{
#if TORRENT_USE_UNC_PATHS
			std::string path = canonicalize_path(save_path);
#else
			std::string path = save_path;
#endif
			m_ses.disk_thread().async_move_storage(m_storage, std::move(path), flags
				, std::bind(&torrent::on_storage_moved, shared_from_this(), _1, _2, _3));
			m_moving_storage = true;
			m_ses.deferred_submit_jobs();
		}
		else
		{
			if (alerts().should_post<storage_moved_alert>())
				alerts().emplace_alert<storage_moved_alert>(get_handle(), save_path, m_save_path);

#if TORRENT_USE_UNC_PATHS
			m_save_path = canonicalize_path(save_path);
#else

			m_save_path = save_path;
#endif
			set_need_save_resume(torrent_handle::if_config_changed);
		}
	}

	void torrent::on_storage_moved(status_t const status, std::string const& path
		, storage_error const& error) try
	{
		TORRENT_ASSERT(is_single_thread());

		m_moving_storage = false;
		bool const has_error_status
			= (status & disk_status::fatal_disk_error)
			|| (status & disk_status::file_exist);

		if (!has_error_status)
		{
			if (alerts().should_post<storage_moved_alert>())
				alerts().emplace_alert<storage_moved_alert>(get_handle(), path, m_save_path);
			m_save_path = path;
			set_need_save_resume(torrent_handle::if_config_changed);
			if (status & disk_status::need_full_check)
				force_recheck();
		}
		else
		{
			if (alerts().should_post<storage_moved_failed_alert>())
				alerts().emplace_alert<storage_moved_failed_alert>(get_handle(), error.ec
					, resolve_filename(error.file()), error.operation);
		}
	}
	catch (...) { handle_exception(); }

	torrent_handle torrent::get_handle()
	{
		TORRENT_ASSERT(is_single_thread());
		return torrent_handle(shared_from_this());
	}

	aux::session_settings const& torrent::settings() const
	{
		TORRENT_ASSERT(is_single_thread());
		return m_ses.settings();
	}

#if TORRENT_USE_INVARIANT_CHECKS
	void torrent::check_invariant() const
	{
		TORRENT_ASSERT(m_connections.size() >= m_outgoing_pids.size());

		// the piece picker and the file progress states are supposed to be
		// created in sync
		TORRENT_ASSERT(has_picker() == !m_file_progress.empty());
		TORRENT_ASSERT(current_stats_state() == int(m_current_gauge_state + counters::num_checking_torrents)
			|| m_current_gauge_state == no_gauge_state);

		TORRENT_ASSERT(m_sequence_number == no_pos
			|| m_ses.verify_queue_position(this, m_sequence_number));

#ifndef TORRENT_DISABLE_STREAMING
		for (auto const& i : m_time_critical_pieces)
		{
			TORRENT_ASSERT(!is_seed());
			TORRENT_ASSERT(!has_picker() || !m_picker->have_piece(i.piece));
		}
#endif

		switch (current_stats_state())
		{
			case counters::num_error_torrents: TORRENT_ASSERT(has_error()); break;
			case counters::num_checking_torrents:
#if TORRENT_ABI_VERSION == 1
				TORRENT_ASSERT(state() == torrent_status::checking_files
					|| state() == torrent_status::queued_for_checking);
#else
				TORRENT_ASSERT(state() == torrent_status::checking_files);
#endif
				break;
			case counters::num_seeding_torrents: TORRENT_ASSERT(is_seed()); break;
			case counters::num_upload_only_torrents: TORRENT_ASSERT(is_upload_only()); break;
			case counters::num_stopped_torrents: TORRENT_ASSERT(!is_auto_managed()
				&& (m_paused || m_graceful_pause_mode));
				break;
			case counters::num_queued_seeding_torrents:
				TORRENT_ASSERT((m_paused || m_graceful_pause_mode) && is_seed()); break;
		}

		if (m_torrent_file)
		{
			TORRENT_ASSERT(m_info_hash.v1 == m_torrent_file->info_hashes().v1);
			TORRENT_ASSERT(m_info_hash.v2 == m_torrent_file->info_hashes().v2);
		}

		for (torrent_list_index_t i{}; i != m_links.end_index(); ++i)
		{
			if (!m_links[i].in_list()) continue;

			// an aborted torrent must not be a member of any of
			// session_impl::m_torrent_lists, otherwise we risk
			// leaving a dangling pointer behind when we destruct.
			TORRENT_ASSERT(!m_abort);

			int const index = m_links[i].index;

			TORRENT_ASSERT(index >= 0);
			TORRENT_ASSERT(index < int(m_ses.torrent_list(i).size()));
		}

		TORRENT_ASSERT(want_peers_download() == m_links[aux::session_interface::torrent_want_peers_download].in_list());
		TORRENT_ASSERT(want_peers_finished() == m_links[aux::session_interface::torrent_want_peers_finished].in_list());
		TORRENT_ASSERT(want_tick() == m_links[aux::session_interface::torrent_want_tick].in_list());
		TORRENT_ASSERT((m_paused && m_auto_managed && !m_abort) == m_links[aux::session_interface::torrent_want_scrape].in_list());

		bool is_checking = false;
		bool is_downloading = false;
		bool is_seeding = false;

		if (is_auto_managed() && !has_error())
		{
			if (m_state == torrent_status::checking_files)
			{
				is_checking = true;
			}
			else if (m_state == torrent_status::downloading_metadata
				|| m_state == torrent_status::downloading
				|| m_state == torrent_status::finished
				|| m_state == torrent_status::seeding)
			{
				if (is_finished())
					is_seeding = true;
				else
					is_downloading = true;
			}
		}

		TORRENT_ASSERT(m_links[aux::session_interface::torrent_checking_auto_managed].in_list()
			== is_checking);
		TORRENT_ASSERT(m_links[aux::session_interface::torrent_downloading_auto_managed].in_list()
			== is_downloading);
		TORRENT_ASSERT(m_links[aux::session_interface::torrent_seeding_auto_managed].in_list()
			== is_seeding);

		if (m_seed_mode)
		{
			TORRENT_ASSERT(is_seed());
		}

		TORRENT_ASSERT(is_single_thread());
		// this fires during disconnecting peers
		if (is_paused()) TORRENT_ASSERT(num_peers() == 0 || m_graceful_pause_mode);

		int seeds = 0;
		int num_uploads = 0;
		int num_connecting = 0;
		int num_connecting_seeds = 0;
		std::map<piece_block, int> num_requests;
		for (peer_connection const* peer : *this)
		{
			peer_connection const& p = *peer;

			if (p.is_connecting()) ++num_connecting;

			if (p.is_connecting() && p.peer_info_struct()->seed)
				++num_connecting_seeds;

			if (p.peer_info_struct())
			{
				if (p.peer_info_struct()->seed)
				{
					++seeds;
				}
				else
				{
					// During init(), before on_metadata_impl()/pc->init() have
					// run, valid_metadata() just became true so is_seed() can
					// spuriously return true for peers whose have-bitmask hasn't
					// been reconciled with the actual piece count yet.
					if (p.m_initialized) TORRENT_ASSERT(!p.is_seed());
				}
			}

			for (auto const& j : p.request_queue())
			{
				if (!j.not_wanted && !j.timed_out) ++num_requests[j.block];
			}

			for (auto const& j : p.download_queue())
			{
				if (!j.not_wanted && !j.timed_out) ++num_requests[j.block];
			}

			if (!p.is_choked() && !p.ignore_unchoke_slots()) ++num_uploads;
			torrent* associated_torrent = p.associated_torrent().lock().get();
			if (associated_torrent != this && associated_torrent != nullptr)
				TORRENT_ASSERT_FAIL();
		}
		TORRENT_ASSERT_VAL(num_uploads == int(m_num_uploads), int(m_num_uploads) - num_uploads);
		TORRENT_ASSERT_VAL(seeds == int(m_num_seeds), int(m_num_seeds) - seeds);
		TORRENT_ASSERT_VAL(num_connecting == int(m_num_connecting), int(m_num_connecting) - num_connecting);
		TORRENT_ASSERT_VAL(num_connecting_seeds == int(m_num_connecting_seeds)
			, int(m_num_connecting_seeds) - num_connecting_seeds);
		TORRENT_ASSERT_VAL(int(m_num_uploads) <= num_peers(), m_num_uploads - num_peers());
		TORRENT_ASSERT_VAL(int(m_num_seeds) <= num_peers(), m_num_seeds - num_peers());
		TORRENT_ASSERT_VAL(int(m_num_connecting) <= num_peers(), int(m_num_connecting) - num_peers());
		TORRENT_ASSERT_VAL(int(m_num_connecting_seeds) <= num_peers(), int(m_num_connecting_seeds) - num_peers());
		TORRENT_ASSERT_VAL(int(m_num_connecting) + int(m_num_seeds) >= int(m_num_connecting_seeds)
			, int(m_num_connecting_seeds) - (int(m_num_connecting) + int(m_num_seeds)));
		TORRENT_ASSERT_VAL(int(m_num_connecting) + int(m_num_seeds) - int(m_num_connecting_seeds) <= num_peers()
			, num_peers() - (int(m_num_connecting) + int(m_num_seeds) - int(m_num_connecting_seeds)));

		if (has_picker())
		{
			for (std::map<piece_block, int>::iterator i = num_requests.begin()
				, end(num_requests.end()); i != end; ++i)
			{
				piece_block const b = i->first;
				int const count = i->second;
				int const picker_count = m_picker->num_peers(b);
				// if we're no longer downloading the piece
				// (for instance, it may be fully downloaded and waiting
				// for the hash check to return), the piece picker always
				// returns 0 requests, regardless of how many peers may still
				// have the block in their queue
				if (!m_picker->is_downloaded(b) && m_picker->is_downloading(b.piece_index))
				{
					if (picker_count != count)
					{
						std::fprintf(stderr, "picker count discrepancy: "
							"picker peer-count: %d != "
							"peers download queue count: %d "
							"block: (%d, %d)\n", picker_count, count, int(b.piece_index), int(b.block_index));

						for (const_peer_iterator j = this->begin(); j != this->end(); ++j)
						{
							peer_connection const& p = *(*j);
							std::fprintf(stderr, "peer: %s\n", print_endpoint(p.remote()).c_str());
							std::fprintf(stderr, "  rq: ");
							for (auto const& k : p.request_queue())
							{
								std::fprintf(stderr, "(%d, %d)%s%s%s | "
									, static_cast<int>(k.block.piece_index)
									, k.block.block_index, k.not_wanted ? " not-wanted" : ""
									, k.timed_out ? " timed-out" : "", k.busy ? " busy": "");
							}
							std::fprintf(stderr, "\n  dq: ");
							for (auto const& k : p.download_queue())
							{
								std::fprintf(stderr, "(%d, %d)%s%s%s | "
									, static_cast<int>(k.block.piece_index)
									, k.block.block_index, k.not_wanted ? " not-wanted" : ""
									, k.timed_out ? " timed-out" : "", k.busy ? " busy": "");
							}
							std::fprintf(stderr, "\n");
						}

						piece_picker::downloading_piece dp;
						m_picker->piece_info(b.piece_index, dp);
						std::fprintf(stderr, "picker piece: %d "
							"finished: %d writing: %d "
							"requested: %d hashing: %d "
							"locked: %d passed-hash-check: %d (info-idx: %d)\n"
							, int(b.piece_index)
							, int(dp.finished), int(dp.writing)
							, int(dp.requested), int(dp.hashing)
							, int(dp.locked), int(dp.passed_hash_check)
							, int(dp.info_idx));

						if (dp.writing + dp.finished + dp.requested > 0)
						{
							span<piece_picker::block_info const> blocks = m_picker->blocks_for_piece(dp);
							std::fprintf(stderr, "blocks:\n");
							int marker = b.block_index;
							int counter = 0;
							for (auto const& bi : blocks)
							{
								if (marker == 0)
									std::fprintf(stderr, "\x1b[7m");
								static const char state_chars[4] = {'.', 'r', 'w', 'f'};
								std::fprintf(stderr, "%c", state_chars[bi.state]);
								if (marker == 0)
									std::fprintf(stderr, "\x1b[0m");
								if (++counter == 64)
								{
									std::fprintf(stderr, "\n");
									counter = 0;
								}
								--marker;
							}
							if (counter != 0)
								std::fprintf(stderr, "\n");
						}
						TORRENT_ASSERT_FAIL();
					}
				}
			}
		}

		if (valid_metadata())
		{
			TORRENT_ASSERT(m_abort || m_error || !m_picker || m_picker->num_pieces() == m_torrent_file->num_pieces());
		}
		else
		{
			TORRENT_ASSERT(m_abort || m_error || !m_picker || m_picker->num_pieces() == 0);
		}

#ifdef TORRENT_EXPENSIVE_INVARIANT_CHECKS
		// make sure we haven't modified the peer object
		// in a way that breaks the sort order
		if (m_peer_list && m_peer_list->begin() != m_peer_list->end())
		{
			auto i = m_peer_list->begin();
			auto p = i++;
			auto end(m_peer_list->end());
			peer_address_compare cmp;
			for (; i != end; ++i, ++p)
			{
				TORRENT_ASSERT(!cmp(*i, *p));
			}
		}
#endif

/*
		if (m_picker && !m_abort)
		{
			// make sure that pieces that have completed the download
			// of all their blocks are in the disk io thread's queue
			// to be checked.
			std::vector<piece_picker::downloading_piece> dl_queue
				= m_picker->get_download_queue();
			for (std::vector<piece_picker::downloading_piece>::const_iterator i =
				dl_queue.begin(); i != dl_queue.end(); ++i)
			{
				const int blocks_per_piece = m_picker->blocks_in_piece(i->index);

				bool complete = true;
				for (int j = 0; j < blocks_per_piece; ++j)
				{
					if (i->info[j].state == piece_picker::block_info::state_finished)
						continue;
					complete = false;
					break;
				}
				TORRENT_ASSERT(complete);
			}
		}
*/
		if (m_files_checked && valid_metadata())
		{
			TORRENT_ASSERT(block_size() > 0);
		}
	}
#endif

	void torrent::set_sequential_download(bool const sd)
	{
		TORRENT_ASSERT(is_single_thread());
		if (m_sequential_download == sd) return;
		if (!sd && m_picker) m_picker->set_sequential_range(piece_index_t(0), m_torrent_file->last_piece());
		m_sequential_download = sd;
#ifndef TORRENT_DISABLE_LOGGING
		debug_log("*** set-sequential-download: %d", sd);
#endif

		set_need_save_resume(torrent_handle::if_config_changed);

		state_updated();
	}

	void torrent::queue_up()
	{
		// finished torrents may not change their queue positions, as it's set to
		// -1
		if (m_abort || is_finished()) return;

		set_queue_position(queue_position() == queue_position_t{0}
			? queue_position() : prev(queue_position()));
	}

	void torrent::queue_down()
	{
		set_queue_position(next(queue_position()));
	}

	void torrent::set_queue_position(queue_position_t const p)
	{
		TORRENT_ASSERT(is_single_thread());

		// finished torrents may not change their queue positions, as it's set to
		// -1
		if ((m_abort || is_finished()) && p != no_pos) return;

		TORRENT_ASSERT((p == no_pos) == is_finished()
			|| (!m_auto_managed && p == no_pos)
			|| (m_abort && p == no_pos)
			|| (!m_added && p == no_pos));
		if (p == m_sequence_number) return;

		TORRENT_ASSERT(p >= no_pos);

		state_updated();

		m_ses.set_queue_position(this, p);
	}

	void torrent::set_max_uploads(int limit, bool const state_update)
	{
		TORRENT_ASSERT(is_single_thread());
		// TODO: perhaps 0 should actually mean 0
		if (limit <= 0) limit = (1 << 24) - 1;
		if (int(m_max_uploads) == limit) return;
		if (state_update) state_updated();
		m_max_uploads = aux::numeric_cast<std::uint32_t>(limit);
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log() && state_update)
			debug_log("*** set-max-uploads: %d", m_max_uploads);
#endif

		if (state_update)
			set_need_save_resume(torrent_handle::if_config_changed);
	}

	void torrent::set_max_connections(int limit, bool const state_update)
	{
		TORRENT_ASSERT(is_single_thread());
		// TODO: perhaps 0 should actually mean 0
		if (limit <= 0) limit = (1 << 24) - 1;
		if (int(m_max_connections) == limit) return;
		if (state_update) state_updated();
		m_max_connections = aux::numeric_cast<std::uint32_t>(limit);
		update_want_peers();

#ifndef TORRENT_DISABLE_LOGGING
		if (should_log() && state_update)
			debug_log("*** set-max-connections: %d", m_max_connections);
#endif

		if (num_peers() > int(m_max_connections))
		{
			disconnect_peers(num_peers() - m_max_connections
				, errors::too_many_connections);
		}

		if (state_update)
			set_need_save_resume(torrent_handle::if_config_changed);
	}

	void torrent::set_upload_limit(int const limit)
	{
		set_limit_impl(limit, peer_connection::upload_channel);
#ifndef TORRENT_DISABLE_LOGGING
		debug_log("*** set-upload-limit: %d", limit);
#endif
	}

	void torrent::set_download_limit(int const limit)
	{
		set_limit_impl(limit, peer_connection::download_channel);
#ifndef TORRENT_DISABLE_LOGGING
		debug_log("*** set-download-limit: %d", limit);
#endif
	}

	void torrent::set_limit_impl(int limit, int const channel, bool const state_update)
	{
		TORRENT_ASSERT(is_single_thread());
		if (limit <= 0 || limit == aux::bandwidth_channel::inf) limit = 0;

		if (m_peer_class == peer_class_t{0})
		{
			if (limit == 0) return;
			setup_peer_class();
		}

		struct peer_class* tpc = m_ses.peer_classes().at(m_peer_class);
		TORRENT_ASSERT(tpc);
		if (tpc->channel[channel].throttle() == limit) return;
		if (state_update)
		{
			state_updated();
			set_need_save_resume(torrent_handle::if_config_changed);
		}
		tpc->channel[channel].throttle(limit);
	}

	void torrent::setup_peer_class()
	{
		TORRENT_ASSERT(m_peer_class == peer_class_t{0});
		m_peer_class = m_ses.peer_classes().new_peer_class(name());
		add_class(m_ses.peer_classes(), m_peer_class);
	}

	int torrent::limit_impl(int const channel) const
	{
		TORRENT_ASSERT(is_single_thread());

		if (m_peer_class == peer_class_t{0}) return -1;
		int limit = m_ses.peer_classes().at(m_peer_class)->channel[channel].throttle();
		if (limit == std::numeric_limits<int>::max()) limit = -1;
		return limit;
	}

	int torrent::upload_limit() const
	{
		return limit_impl(peer_connection::upload_channel);
	}

	int torrent::download_limit() const
	{
		return limit_impl(peer_connection::download_channel);
	}

	bool torrent::delete_files(remove_flags_t const options)
	{
		TORRENT_ASSERT(is_single_thread());

#ifndef TORRENT_DISABLE_LOGGING
		log_to_all_peers("deleting files");
#endif

		disconnect_all(errors::torrent_removed, operation_t::bittorrent);
		stop_announcing();

		// storage may be nullptr during shutdown
		if (m_storage)
		{
			TORRENT_ASSERT(m_storage);
			m_ses.disk_thread().async_delete_files(m_storage, options
				, std::bind(&torrent::on_files_deleted, shared_from_this(), _1));
			m_deleted = true;
			m_ses.deferred_submit_jobs();
			return true;
		}
		return false;
	}

	void torrent::clear_error()
	{
		TORRENT_ASSERT(is_single_thread());
		if (!m_error) return;
		bool const checking_files = should_check_files();
		m_ses.trigger_auto_manage();
		m_error.clear();
		m_error_file = torrent_status::error_file_none;

		update_gauge();
		state_updated();
		update_want_peers();
		update_state_list();

		// if the error happened during initialization, try again now
		if (!m_torrent_initialized && valid_metadata()) init();
		if (!checking_files && should_check_files())
			start_checking();
	}
	std::string torrent::resolve_filename(file_index_t const file) const
	{
		if (file == torrent_status::error_file_none) return "";
		if (file == torrent_status::error_file_ssl_ctx) return "SSL Context";
		if (file == torrent_status::error_file_exception) return "exception";
		if (file == torrent_status::error_file_partfile) return "partfile";
		if (file == torrent_status::error_file_metadata) return "metadata";

		if (m_storage && file >= file_index_t(0))
		{
			file_storage const& fs = m_torrent_file->layout();
			return m_renamed_files.file_path(fs, file, m_save_path);
		}
		else
		{
			return m_save_path;
		}
	}

	void torrent::set_error(error_code const& ec, file_index_t const error_file)
	{
		TORRENT_ASSERT(is_single_thread());
		m_error = ec;
		m_error_file = error_file;

		update_gauge();

		if (alerts().should_post<torrent_error_alert>())
			alerts().emplace_alert<torrent_error_alert>(get_handle(), ec
				, resolve_filename(error_file));

#ifndef TORRENT_DISABLE_LOGGING
		if (ec)
		{
			char buf[1024];
			std::snprintf(buf, sizeof(buf), "error %s: %s", ec.message().c_str()
				, resolve_filename(error_file).c_str());
			log_to_all_peers(buf);
		}
#endif

		state_updated();
		update_state_list();
	}

	void torrent::auto_managed(bool const a)
	{
		TORRENT_ASSERT(is_single_thread());
		INVARIANT_CHECK;

		if (bool(m_auto_managed) == a) return;
		bool const checking_files = should_check_files();
		m_auto_managed = a;
		update_gauge();
		update_want_scrape();
		update_state_list();

		state_updated();

		// we need to save this new state as well
		set_need_save_resume(torrent_handle::if_config_changed);

		// recalculate which torrents should be
		// paused
		m_ses.trigger_auto_manage();

		if (!checking_files && should_check_files())
		{
			start_checking();
		}
	}

	namespace {

	std::uint16_t clamped_subtract_u16(int const a, int const b)
	{
		if (a < b) return 0;
		return std::uint16_t(a - b);
	}

	} // anonymous namespace

	// this is called every time the session timer takes a step back. Since the
	// session time is meant to fit in 16 bits, it only covers a range of
	// about 18 hours. This means every few hours the whole epoch of this
	// clock is shifted forward. All timestamp in this clock must then be
	// shifted backwards to remain the same. Anything that's shifted back
	// beyond the new epoch is clamped to 0 (to represent the oldest timestamp
	// currently representable by the session_time)
	void torrent::step_session_time(int const seconds)
	{
		if (!m_peer_list) return;
		for (auto* pe : *m_peer_list)
		{
			pe->last_optimistically_unchoked
				= clamped_subtract_u16(pe->last_optimistically_unchoked, seconds);
			pe->last_connected = clamped_subtract_u16(pe->last_connected, seconds);
		}
	}

	// the higher seed rank, the more important to seed
	int torrent::seed_rank(aux::session_settings const& s) const
	{
		TORRENT_ASSERT(is_single_thread());
		enum flags
		{
			seed_ratio_not_met = 0x40000000,
			no_seeds           = 0x20000000,
			recently_started   = 0x10000000,
			prio_mask          = 0x0fffffff
		};

		if (!is_finished()) return 0;

		int scale = 1000;
		if (!is_seed()) scale = 500;

		int ret = 0;

		time_point32 const now = aux::time_now32();
		seconds32 const act_time = active_time(now);
		seconds32 const fin_time = finished_time(now);
		seconds32 const download_time = act_time - fin_time;

		// if we haven't yet met the seed limits, set the seed_ratio_not_met
		// flag. That will make this seed prioritized
		// downloaded may be 0 if the torrent is 0-sized
		std::int64_t const downloaded = std::max(m_total_downloaded, m_torrent_file->total_size());
		if (fin_time < seconds(s.get_int(settings_pack::seed_time_limit))
			&& (download_time.count() > 1
				&& fin_time * 100 / download_time < s.get_int(settings_pack::seed_time_ratio_limit))
			&& downloaded > 0
			&& m_total_uploaded * 100 / downloaded < s.get_int(settings_pack::share_ratio_limit))
			ret |= seed_ratio_not_met;

		// if this torrent is running, and it was started less
		// than 30 minutes ago, give it priority, to avoid oscillation
		if (!is_paused() && act_time < minutes(30))
			ret |= recently_started;

		// if we have any scrape data, use it to calculate
		// seed rank
		int seeds = 0;
		int downloaders = 0;

		// If we're currently seeding and using tracker supplied scrape
		// data, we should remove ourselves from the seed count
		int const self_seed = is_seed() && !is_paused() ? 1 : 0;

		if (m_complete != 0xffffff) seeds = std::max(0, int(m_complete) - self_seed);
		else seeds = m_peer_list ? m_peer_list->num_seeds() : 0;

		if (m_incomplete != 0xffffff) downloaders = m_incomplete;
		else downloaders = m_peer_list ? m_peer_list->num_peers() - m_peer_list->num_seeds() : 0;

		if (seeds == 0)
		{
			ret |= no_seeds;
			ret |= downloaders & prio_mask;
		}
		else
		{
			ret |= ((1 + downloaders) * scale / seeds) & prio_mask;
		}

		return ret;
	}

	// this is an async operation triggered by the client
	void torrent::save_resume_data(resume_data_flags_t const flags)
	{
		TORRENT_ASSERT(is_single_thread());
		INVARIANT_CHECK;

		if (m_abort)
		{
			alerts().emplace_alert<save_resume_data_failed_alert>(get_handle()
				, errors::torrent_removed);
			return;
		}

		auto conditions = flags & (
			torrent_handle::if_counters_changed
			| torrent_handle::if_download_progress
			| torrent_handle::if_config_changed
			| torrent_handle::if_state_changed
			| torrent_handle::if_metadata_changed
			);

		if (conditions && !(m_need_save_resume_data & conditions))
		{
			// if conditions were specified, but none of those conditions are
			// met (i.e. none of them have been updated since last
			// save_resume_data()), we don't save it.
			alerts().emplace_alert<save_resume_data_failed_alert>(get_handle()
				, errors::resume_data_not_modified);
			return;
		}

		m_need_save_resume_data = resume_data_flags_t{};
		state_updated();

		if ((flags & torrent_handle::flush_disk_cache) && m_storage)
		{
			m_ses.disk_thread().async_release_files(m_storage);
			m_ses.deferred_submit_jobs();
		}

		state_updated();

		add_torrent_params atp;
		write_resume_data(flags, atp);
		alerts().emplace_alert<save_resume_data_alert>(std::move(atp), get_handle());
	}

	add_torrent_params torrent::get_resume_data(resume_data_flags_t const flags) const
	{
		TORRENT_ASSERT(is_single_thread());
		INVARIANT_CHECK;

		if (m_abort)
			return {};

		add_torrent_params atp;
		write_resume_data(flags, atp);

		return atp;
	}

	bool torrent::should_check_files() const
	{
		TORRENT_ASSERT(is_single_thread());
		return m_state == torrent_status::checking_files
			&& !m_paused
			&& !has_error()
			&& !m_abort
			&& !m_session_paused;
	}

	void torrent::flush_cache()
	{
		TORRENT_ASSERT(is_single_thread());

		// storage may be nullptr during shutdown
		if (!m_storage)
		{
			TORRENT_ASSERT(m_abort);
			return;
		}
		m_ses.disk_thread().async_release_files(m_storage
			, std::bind(&torrent::on_cache_flushed, shared_from_this(), true));
		m_ses.deferred_submit_jobs();
	}

	void torrent::on_cache_flushed(bool const manually_triggered) try
	{
		TORRENT_ASSERT(is_single_thread());

		if (m_ses.is_aborted()) return;

		if (manually_triggered || alerts().should_post<cache_flushed_alert>())
			alerts().emplace_alert<cache_flushed_alert>(get_handle());
	}
	catch (...) { handle_exception(); }

	void torrent::on_torrent_aborted()
	{
		TORRENT_ASSERT(is_single_thread());

		// there should be no more disk activity for this torrent now, we can
		// release the disk io handle
		m_storage.reset();

		alerts().emplace_alert<torrent_removed_alert>(get_handle()
			, info_hash(), get_userdata());
	}

	bool torrent::is_paused() const
	{
		return m_paused || m_session_paused;
	}

	void torrent::pause(pause_flags_t const flags)
	{
		TORRENT_ASSERT(is_single_thread());
		INVARIANT_CHECK;

		if (!m_paused)
		{
			// we need to save this new state
			set_need_save_resume(torrent_handle::if_state_changed);
		}

		set_paused(true, flags);
	}

	void torrent::do_pause()
	{
		TORRENT_ASSERT(is_single_thread());
		if (!is_paused()) return;

		// is_paused() already reflects the new state (the caller set
		// m_paused before calling us), so flush/re-anchor the timers now,
		// regardless of whether an extension below defers the rest of the
		// pause handling
		update_state_timers();

		// this torrent may be about to consider itself inactive. If so, we want
		// to prevent it from doing so, since it's being paused unconditionally
		// now. An illustrative example of this is a torrent that completes
		// downloading when active_seeds = 0. It completes, it gets paused and it
		// should not come back to life again.
		if (m_pending_active_change)
		{
			m_inactivity_timer.cancel();
		}

#ifndef TORRENT_DISABLE_EXTENSIONS
		for (auto& ext : m_extensions)
		{
			if (ext->on_pause()) return;
		}
#endif

		m_connect_boost_counter
			= static_cast<std::uint8_t>(settings().get_int(settings_pack::torrent_connect_boost));
		m_inactive = false;

		update_state_list();
		update_want_tick();

		m_announce_to_dht = false;
		m_announce_to_trackers = false;
		m_announce_to_lsd = false;

		state_updated();
		update_want_peers();
		update_want_scrape();
		update_gauge();
		update_state_list();

#ifndef TORRENT_DISABLE_LOGGING
		log_to_all_peers("pausing");
#endif

		// when checking and being paused in graceful pause mode, we
		// post the paused alert when the last outstanding disk job completes
		if (m_state == torrent_status::checking_files)
		{
			if (m_checking_piece == m_num_checked_pieces)
			{
				if (alerts().should_post<torrent_paused_alert>())
					alerts().emplace_alert<torrent_paused_alert>(get_handle());
			}
			disconnect_all(errors::torrent_paused, operation_t::bittorrent);
			return;
		}

		if (!m_graceful_pause_mode)
		{
			// this will make the storage close all
			// files and flush all cached data
			if (m_storage)
			{
				// the torrent_paused alert will be posted from on_torrent_paused
				m_ses.disk_thread().async_stop_torrent(m_storage
					, [self = shared_from_this()] { self->on_torrent_paused(); });
				m_ses.deferred_submit_jobs();
			}
			else
			{
				if (alerts().should_post<torrent_paused_alert>())
					alerts().emplace_alert<torrent_paused_alert>(get_handle());
			}

			disconnect_all(errors::torrent_paused, operation_t::bittorrent);
		}
		else
		{
			// disconnect all peers with no outstanding data to receive
			// and choke all remaining peers to prevent responding to new
			// requests
			for (auto* p : m_connections)
			{
				TORRENT_INCREMENT(m_iterating_connections);
				TORRENT_ASSERT(p->associated_torrent().lock().get() == this);

				if (p->is_disconnecting()) continue;

				if (p->outstanding_bytes() > 0)
				{
#ifndef TORRENT_DISABLE_LOGGING
					p->peer_log(peer_log_alert::info, peer_log_alert::choking_peer, "torrent graceful paused");
#endif
					// remove any un-sent requests from the queue
					p->clear_request_queue();
					// don't accept new requests from the peer
					p->choke_this_peer();
					continue;
				}

				// since we're currently in graceful pause mode, the last peer to
				// disconnect (assuming all peers end up begin disconnected here)
				// will post the torrent_paused_alert
#ifndef TORRENT_DISABLE_LOGGING
				p->peer_log(peer_log_alert::info, peer_log_alert::closing_connection, "torrent_paused");
#endif
				p->disconnect(errors::torrent_paused, operation_t::bittorrent);
			}
		}

		stop_announcing();
	}

#ifndef TORRENT_DISABLE_LOGGING
	void torrent::log_to_all_peers(char const* message)
	{
		TORRENT_ASSERT(is_single_thread());

		bool const log_peers = !m_connections.empty()
			&& m_connections.front()->should_log(peer_log_alert::info);

		if (log_peers)
		{
			for (auto const* p : m_connections)
			{
				TORRENT_INCREMENT(m_iterating_connections);
				p->peer_log(peer_log_alert::info, peer_log_alert::torrent, "%s", message);
			}
		}

		debug_log("%s", message);
	}
#endif

	// add or remove a url that will be attempted for
	// finding the file(s) in this torrent.
	web_seed_t* torrent::add_web_seed(std::string const& url
		, std::string const& auth
		, web_seed_entry::headers_t const& extra_headers
		, web_seed_flag_t const flags)
	{
		web_seed_t ent(url, auth, extra_headers);
		ent.ephemeral = bool(flags & ephemeral);
		ent.no_local_ips = bool(flags & no_local_ips);

		// don't add duplicates
		auto const it = std::find(m_web_seeds.begin(), m_web_seeds.end(), ent);
		if (it != m_web_seeds.end())
		{
			// if we're adding a web seed (as non-ephemeral) and we have an
			// ephemeral web seed already, promote it to non-ephemeral
			if (it->ephemeral && !ent.ephemeral)
			{
				set_need_save_resume(torrent_handle::if_metadata_changed);
				it->ephemeral = false;
			}
			return &*it;
		}
		m_web_seeds.emplace_back(std::move(ent));

		// ephemeral web seeds are not saved in the resume data
		if (!ent.ephemeral)
			set_need_save_resume(torrent_handle::if_metadata_changed);

		update_want_tick();
		return &m_web_seeds.back();
	}

	void torrent::set_session_paused(bool const b)
	{
		if (bool(m_session_paused) == b) return;
		bool const paused_before = is_paused();
		m_session_paused = b;

		if (paused_before == is_paused()) return;

		if (b) do_pause();
		else do_resume();
	}

	void torrent::set_paused(bool const b, pause_flags_t flags)
	{
		TORRENT_ASSERT(is_single_thread());

		// if there are no peers, there is no point in a graceful pause mode. In
		// fact, the promise to post the torrent_paused_alert exactly once is
		// maintained by the last peer to be disconnected in graceful pause mode,
		// if there are no peers, we must not enter graceful pause mode, and post
		// the torrent_paused_alert immediately instead.
		if (num_peers() == 0)
			flags &= ~torrent_handle::graceful_pause;

		if (bool(m_paused) == b)
		{
			// there is one special case here. If we are
			// currently in graceful pause mode, and we just turned into regular
			// paused mode, we need to actually pause the torrent properly
			if (m_paused == true
				&& m_graceful_pause_mode == true
				&& !(flags & torrent_handle::graceful_pause))
			{
				m_graceful_pause_mode = false;
				update_gauge();
				do_pause();
			}
			return;
		}

		bool const paused_before = is_paused();

		m_paused = b;

		// the session may still be paused, in which case
		// the effective state of the torrent did not change
		if (paused_before == is_paused()) return;

		m_graceful_pause_mode = bool(flags & torrent_handle::graceful_pause);

		if (b) do_pause();
		else do_resume();
	}

	void torrent::resume()
	{
		TORRENT_ASSERT(is_single_thread());
		INVARIANT_CHECK;

		if (!m_paused
			&& m_announce_to_dht
			&& m_announce_to_trackers
			&& m_announce_to_lsd) return;

		m_paused = false;
		if (!m_session_paused) m_graceful_pause_mode = false;

		update_gauge();

		// we need to save this new state
		set_need_save_resume(torrent_handle::if_state_changed);

		do_resume();
	}

	void torrent::do_resume()
	{
		TORRENT_ASSERT(is_single_thread());
		if (is_paused())
		{
			update_want_tick();
			return;
		}

		// is_paused() already reflects the new state (the caller cleared
		// m_paused before calling us), so flush/re-anchor the timers now,
		// regardless of whether an extension below defers the rest of the
		// resume handling
		update_state_timers();

#ifndef TORRENT_DISABLE_EXTENSIONS
		for (auto& ext : m_extensions)
		{
			if (ext->on_resume()) return;
		}
#endif

		if (alerts().should_post<torrent_resumed_alert>())
			alerts().emplace_alert<torrent_resumed_alert>(get_handle());

		m_announce_to_dht = true;
		m_announce_to_trackers = true;
		m_announce_to_lsd = true;

		clear_error();

		if (m_state == torrent_status::checking_files
			&& m_auto_managed)
		{
			m_ses.trigger_auto_manage();
		}

		if (should_check_files()) start_checking();

		state_updated();
		update_want_peers();
		update_want_tick();
		update_want_scrape();
		update_gauge();

		if (m_state == torrent_status::checking_files) return;

		start_announcing();

		do_connect_boost();
	}

	namespace
	{
		struct timer_state
		{
			explicit timer_state(aux::listen_socket_handle s)
				: socket(std::move(s)) {}

			aux::listen_socket_handle socket;

			struct state_t
			{
				int tier = INT_MAX;
				bool found_working = false;
				bool done = false;
			};
			aux::array<state_t, num_protocols, protocol_version> state;
		};
	}

	void torrent::update_tracker_timer(time_point32 const now)
	{
		TORRENT_ASSERT(is_single_thread());
		if (!m_announcing)
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("*** update tracker timer: not announcing");
#endif
			return;
		}

#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wmissing-braces"
#endif
		aux::array<bool const, num_protocols, protocol_version> const supports_protocol{
		{
			m_info_hash.has_v1(),
			m_info_hash.has_v2()
		}};
#ifdef __clang__
#pragma clang diagnostic pop
#endif

		time_point32 next_announce = time_point32::max();

		// one entry per listen socket, mirroring announce_with_tracker()'s
		// announce_state vector. The set of listen sockets is small (a
		// handful at most), so linear search is cheaper than a map.
		std::vector<timer_state> listen_socket_states;

		bool const announce_to_all_tiers = settings().get_bool(settings_pack::announce_to_all_tiers);
		bool const announce_to_all_trackers = settings().get_bool(settings_pack::announce_to_all_trackers);
#ifndef TORRENT_DISABLE_LOGGING
		int idx = -1;
		if (should_log())
		{
			debug_log("*** update_tracker_timer: "
				"[ announce_to_all_tiers: %d announce_to_all_trackers: %d num_trackers: %d ]"
				, announce_to_all_tiers
				, announce_to_all_trackers
				, int(m_trackers.size()));
		}
#endif
		for (auto const& t : m_trackers)
		{
#ifndef TORRENT_DISABLE_LOGGING
			++idx;
#endif
#if TORRENT_USE_I2P
			// Skip trackers that announce_with_tracker() will skip too.
			// Without this, those trackers' default next_announce
			// (time_point32::min()) drives the timer to fire immediately,
			// calling announce_with_tracker() which skips again, looping
			// forever and spinning the CPU.
			if (!i2p_compatible_tracker(t)) continue;
#endif
			for (auto const& aep : t.endpoints)
			{
				auto ep_state_iter = std::find_if(listen_socket_states.begin(),
					listen_socket_states.end(),
					[&](timer_state const& s) { return s.socket == aep.socket; });
				if (ep_state_iter == listen_socket_states.end())
				{
					listen_socket_states.emplace_back(aep.socket);
					ep_state_iter = listen_socket_states.end() - 1;
				}
				timer_state& ep_state = *ep_state_iter;

				if (!aep.enabled) continue;
				for (protocol_version const ih : all_versions)
				{
					if (!supports_protocol[ih]) continue;

					auto& state = ep_state.state[ih];
					auto const& a = aep.info_hashes[ih];

					if (state.done) continue;

#ifndef TORRENT_DISABLE_LOGGING
					if (should_log())
					{
						debug_log("*** tracker: (%d) [ep: %s ] \"%s\" ["
							" found: %d i->tier: %d tier: %d"
							" working: %d fails: %d limit: %d upd: %d ]"
							, idx, print_endpoint(aep.local_endpoint).c_str(), t.url.c_str()
							, state.found_working, t.tier, state.tier, a.is_working()
							, a.fails, t.fail_limit, a.updating);
					}
#endif

					if (announce_to_all_tiers
						&& !announce_to_all_trackers
						&& state.found_working
						&& t.tier <= state.tier
						&& state.tier != INT_MAX)
						continue;

					if (t.tier > state.tier)
					{
						if (!announce_to_all_tiers) break;
						state.found_working = false;
					}
					state.tier = t.tier;
					if (a.fails >= t.fail_limit && t.fail_limit != 0) continue;
					if (a.updating)
					{
						state.found_working = true;
					}
					else if (a.next_announce != time_point32::min())
					{
						// defensive: announce_with_tracker() always
						// advances next_announce for trackers it would
						// actually contact, so a leftover min() means
						// something filtered this tracker upstream. Don't let
						// it snap the timer to "now" and re-fire endlessly.
						time_point32 const next_tracker_announce = std::max(a.next_announce, a.min_announce);
						if (next_tracker_announce < next_announce)
							next_announce = next_tracker_announce;
					}
					if (a.is_working()) state.found_working = true;
					if (state.found_working
						&& !announce_to_all_trackers
						&& !announce_to_all_tiers)
						state.done = true;
				}
			}

			if (std::all_of(listen_socket_states.begin(),
					listen_socket_states.end(),
					[supports_protocol](timer_state const& s) {
						for (protocol_version const ih : all_versions)
						{
							if (supports_protocol[ih] && !s.state[ih].done) return false;
						}
						return true;
					}))
				break;
		}

#ifndef TORRENT_DISABLE_LOGGING
		bool before_now = false;
		bool none_eligible = false;
#endif
		if (next_announce <= now)
		{
			next_announce = now;
#ifndef TORRENT_DISABLE_LOGGING
			before_now = true;
#endif
		}
		else if (next_announce == time_point32::max())
		{
			// if no tracker can be announced to, check again in a minute
			next_announce = now + minutes32(1);
#ifndef TORRENT_DISABLE_LOGGING
			none_eligible = true;
#endif
		}

#ifndef TORRENT_DISABLE_LOGGING
		debug_log("*** update tracker timer: "
			"before_now: %d "
			"none_eligible: %d "
			"m_waiting_tracker: %d "
			"next_announce_in: %d"
			, before_now
			, none_eligible
			, m_waiting_tracker
			, int(total_seconds(next_announce - now)));
#endif

		// don't re-issue the timer if it's the same expiration time as last time
		// if m_waiting_tracker is 0, expires_at() is undefined
		if (m_waiting_tracker && m_tracker_timer.expiry() == next_announce) return;

		m_tracker_timer.expires_at(next_announce);
		ADD_OUTSTANDING_ASYNC("tracker::on_tracker_announce");
		++m_waiting_tracker;
		m_tracker_timer.async_wait([self = shared_from_this()](error_code const& e)
			{ self->wrap(&torrent::on_tracker_announce, e); });
	}

	void torrent::start_announcing()
	{
		TORRENT_ASSERT(is_single_thread());
		TORRENT_ASSERT(state() != torrent_status::checking_files);
		if (is_paused())
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("start_announcing(), paused");
#endif
			return;
		}
		// if we don't have metadata, we need to announce
		// before checking files, to get peers to
		// request the metadata from
		if (!m_files_checked && valid_metadata())
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("start_announcing(), files not checked (with valid metadata)");
#endif
			return;
		}
		if (m_announcing) return;

		m_announcing = true;

#ifndef TORRENT_DISABLE_DHT
		if ((!m_peer_list || m_peer_list->num_peers() < 50) && m_ses.dht())
		{
			// we don't have any peers, prioritize
			// announcing this torrent with the DHT
			m_ses.prioritize_dht(shared_from_this());
		}
#endif

		// tell the tracker that we're back
		m_trackers.reset();

		// reset the stats, since from the tracker's
		// point of view, this is a new session
		m_total_failed_bytes = 0;
		m_total_redundant_bytes = 0;
		m_stat.clear();

		update_want_tick();

		bool const high_priority = m_connect_boost_counter > 0;
		announce_with_tracker(event_t::none, high_priority);

		if (m_ses.has_lsd())
			m_ses.prioritize_lsd(shared_from_this());
	}

	void torrent::stop_announcing()
	{
		TORRENT_ASSERT(is_single_thread());
		if (!m_announcing) return;

		m_tracker_timer.cancel();

		m_announcing = false;

		m_trackers.stop_announcing(aux::time_now32());

		announce_with_tracker(event_t::stopped);
	}

	seconds32 torrent::finished_time() const { return finished_time(aux::time_now32()); }

	seconds32 torrent::active_time() const { return active_time(aux::time_now32()); }

	seconds32 torrent::seeding_time() const { return seeding_time(aux::time_now32()); }

	seconds32 torrent::upload_mode_time() const
	{
		if(!m_upload_mode)
			return seconds32(0);

		return aux::time_now32() - m_upload_mode_time;
	}

	void torrent::second_tick(int const tick_interval_ms)
	{
		TORRENT_ASSERT(want_tick());
		TORRENT_ASSERT(is_single_thread());
		INVARIANT_CHECK;

		auto self = shared_from_this();

#ifndef TORRENT_DISABLE_EXTENSIONS
		for (auto const& ext : m_extensions)
		{
			ext->tick();
		}

		if (m_abort) return;
#endif

		// if we're in upload only mode and we're auto-managed
		// leave upload mode every 10 minutes hoping that the error
		// condition has been fixed
		if (m_upload_mode && m_auto_managed && upload_mode_time() >=
			seconds(settings().get_int(settings_pack::optimistic_disk_retry)))
		{
			set_upload_mode(false);
		}

		if (is_paused() && !m_graceful_pause_mode)
		{
			// let the stats fade out to 0
			// check the rate before ticking the stats so that the last update is sent
			// with the rate equal to zero
			if (m_stat.low_pass_upload_rate() > 0 || m_stat.low_pass_download_rate() > 0)
				state_updated();
			m_stat.second_tick(tick_interval_ms);

			// the low pass transfer rate may just have dropped to 0
			update_want_tick();

			return;
		}

		if (settings().get_bool(settings_pack::rate_limit_ip_overhead))
		{
			int const up_limit = upload_limit();
			int const down_limit = download_limit();

			if (down_limit > 0
				&& m_stat.download_ip_overhead() >= down_limit
				&& alerts().should_post<performance_alert>())
			{
				alerts().emplace_alert<performance_alert>(get_handle()
					, performance_alert::download_limit_too_low);
			}

			if (up_limit > 0
				&& m_stat.upload_ip_overhead() >= up_limit
				&& alerts().should_post<performance_alert>())
			{
				alerts().emplace_alert<performance_alert>(get_handle()
					, performance_alert::upload_limit_too_low);
			}
		}

#ifndef TORRENT_DISABLE_STREAMING
		// ---- TIME CRITICAL PIECES ----

#if TORRENT_DEBUG_STREAMING > 0
		std::vector<partial_piece_info> queue;
		get_download_queue(&queue);

		std::vector<peer_info> peer_list;
		get_peer_info(peer_list);

		std::sort(queue.begin(), queue.end(), [](partial_piece_info const& lhs, partial_piece_info const& rhs)
			{ return lhs.piece_index < rhs.piece_index;; });

		std::printf("average piece download time: %.2f s (+/- %.2f s)\n"
			, m_average_piece_time / 1000.f
			, m_piece_time_deviation / 1000.f);
		for (auto& i : queue)
		{
			extern void print_piece(libtorrent::partial_piece_info* pp
				, std::vector<libtorrent::peer_info> const& peers
				, std::vector<time_critical_piece> const& time_critical);

			print_piece(&i, peer_list, m_time_critical_pieces);
		}
#endif // TORRENT_DEBUG_STREAMING

		if (!m_time_critical_pieces.empty() && !upload_mode())
		{
			request_time_critical_pieces();
		}
#endif // TORRENT_DISABLE_STREAMING

		// ---- WEB SEEDS ----

		maybe_connect_web_seeds();

		m_swarm_last_seen_complete = m_last_seen_complete;
		for (auto* p : m_connections)
		{
			TORRENT_INCREMENT(m_iterating_connections);

			// look for the peer that saw a seed most recently
			m_swarm_last_seen_complete = std::max(p->last_seen_complete(), m_swarm_last_seen_complete);

			// updates the peer connection's ul/dl bandwidth
			// resource requests
			p->second_tick(tick_interval_ms);
		}
#if TORRENT_ABI_VERSION <= 2
		if (m_ses.alerts().should_post<stats_alert>())
			m_ses.alerts().emplace_alert<stats_alert>(get_handle(), tick_interval_ms, m_stat);
#endif

		m_total_uploaded += m_stat.last_payload_uploaded();
		m_total_downloaded += m_stat.last_payload_downloaded();

		// if the rate is 0, there's no update because of network transfers.
		// check the rate before ticking the stats so that the last update is
		// sent with the rate equal to zero
		if (m_stat.low_pass_upload_rate() > 0 || m_stat.low_pass_download_rate() > 0)
			state_updated();

		m_stat.second_tick(tick_interval_ms);

		// these counters are saved in the resume data, since they updated
		// we need to save the resume data too
		set_need_save_resume(torrent_handle::if_counters_changed);

		// this section determines whether the torrent is active or not. When it
		// changes state, it may also trigger the auto-manage logic to reconsider
		// which torrents should be queued and started.
		bool const is_inactive = is_inactive_internal();

		if (settings().get_bool(settings_pack::dont_count_slow_torrents))
		{
			if (is_inactive == bool(m_inactive))
			{
				if (m_pending_active_change) m_inactivity_timer.cancel();
			}
			else if (!m_pending_active_change)
			{
				// becoming inactive is debounced, to avoid flapping.
				// recovering from inactive is posted immediately (0 delay)
				int const delay =
					is_inactive ? settings().get_int(settings_pack::auto_manage_startup) : 0;
				m_inactivity_timer.expires_after(seconds(delay));
				ADD_OUTSTANDING_ASYNC("torrent::on_inactivity_tick");
				m_inactivity_timer.async_wait(
					[self](error_code const& ec) { self->wrap(&torrent::on_inactivity_tick, ec); });
				m_pending_active_change = true;
			}
		}

		// want_tick depends on whether the low pass transfer rates are non-zero
		// or not. They may just have turned zero in this last tick.
		update_want_tick();
	}

	bool torrent::is_inactive_internal() const
	{
		if (is_finished())
			return m_stat.upload_payload_rate()
				< settings().get_int(settings_pack::inactive_up_rate);
		else
			return m_stat.download_payload_rate()
				< settings().get_int(settings_pack::inactive_down_rate);
	}

	void torrent::on_inactivity_tick(error_code const& ec) try
	{
		COMPLETE_ASYNC("torrent::on_inactivity_tick");

		m_pending_active_change = false;

		if (ec) return;

		bool const is_inactive = is_inactive_internal();
		if (is_inactive == bool(m_inactive)) return;

		m_inactive = is_inactive;

		update_state_list();
		update_want_tick();

		if (settings().get_bool(settings_pack::dont_count_slow_torrents))
			m_ses.trigger_auto_manage();
	}
	catch (...) { handle_exception(); }

	namespace {
		int zero_or(int const val, int const def_val)
		{ return (val <= 0) ? def_val : val; }
	}

	void torrent::maybe_connect_web_seeds()
	{
		if (m_abort) return;

		// if we have everything we want we don't need to connect to any web-seed
		if (m_web_seeds.empty()
			|| is_finished()
			|| !m_files_checked
			|| num_peers() >= int(m_max_connections)
			|| m_ses.num_connections() >= settings().get_int(settings_pack::connections_limit))
		{
			return;
		}

		// when set to unlimited, use 100 as the limit
		int limit = zero_or(settings().get_int(settings_pack::max_web_seed_connections)
			, 100);

		auto const now = aux::time_now32();

		// keep trying web-seeds if there are any
		// first find out which web seeds we are connected to
		for (auto i = m_web_seeds.begin(); i != m_web_seeds.end() && limit > 0;)
		{
			auto const w = i++;
			if (w->disabled || w->removed || w->retry > now || !w->interesting)
				continue;

			--limit;
			if (w->peer_info.connection || w->resolving)
				continue;

			connect_to_url_seed(w);
		}
	}

#ifndef TORRENT_DISABLE_SHARE_MODE
	void torrent::recalc_share_mode()
	{
		TORRENT_ASSERT(share_mode());
		if (is_seed()) return;

		int const pieces_in_torrent = m_torrent_file->num_pieces();
		int num_seeds = 0;
		int num_peers = 0;
		int num_downloaders = 0;
		int missing_pieces = 0;
		for (auto const* const p : m_connections)
		{
			TORRENT_INCREMENT(m_iterating_connections);
			if (p->is_connecting()) continue;
			if (p->is_disconnecting()) continue;
			++num_peers;
			if (p->is_seed())
			{
				++num_seeds;
				continue;
			}

			if (p->share_mode()) continue;
			if (p->upload_only()) continue;

			++num_downloaders;
			missing_pieces += pieces_in_torrent - p->num_have_pieces();
		}

		if (num_peers == 0) return;

		if (num_seeds * 100 / num_peers > 50
			&& (num_peers * 100 / m_max_connections > 90
				|| num_peers > 20))
		{
			// we are connected to more than 50% seeds (and we're beyond
			// 90% of the max number of connections). That will
			// limit our ability to upload. We need more downloaders.
			// disconnect some seeds so that we don't have more than 50%
			int const to_disconnect = num_seeds - num_peers / 2;
			aux::vector<peer_connection*> seeds;
			seeds.reserve(num_seeds);
			std::copy_if(m_connections.begin(), m_connections.end(), std::back_inserter(seeds)
				, [](peer_connection const* p) { return p->is_seed(); });

			aux::random_shuffle(seeds);
			TORRENT_ASSERT(to_disconnect <= seeds.end_index());
			for (auto const& p : span<peer_connection*>(seeds).first(to_disconnect))
				p->disconnect(errors::upload_upload_connection, operation_t::bittorrent);
		}

		if (num_downloaders == 0) return;

		// assume that the seeds are about as fast as us. During the time
		// we can download one piece, and upload one piece, each seed
		// can upload two pieces.
		missing_pieces -= 2 * num_seeds;

		if (missing_pieces <= 0) return;

		// missing_pieces represents our opportunity to download pieces
		// and share them more than once each

		// now, download at least one piece, otherwise download one more
		// piece if our downloaded (and downloading) pieces is less than 50%
		// of the uploaded bytes
		int const num_downloaded_pieces = std::max(m_picker->have().num_pieces
			, m_picker->want().num_pieces);

		if (std::int64_t(num_downloaded_pieces) * m_torrent_file->piece_length()
			* settings().get_int(settings_pack::share_mode_target) > m_total_uploaded
			&& num_downloaded_pieces > 0)
			return;

		// don't have more pieces downloading in parallel than 5% of the total
		// number of pieces we have downloaded
		if (m_picker->get_download_queue_size() > num_downloaded_pieces / 20)
			return;

		// one more important property is that there are enough pieces
		// that more than one peer wants to download
		// make sure that there are enough downloaders for the rarest
		// piece. Go through all pieces, figure out which one is the rarest
		// and how many peers that has that piece

		aux::vector<piece_index_t> rarest_pieces;

		int const num_pieces = m_torrent_file->num_pieces();
		int rarest_rarity = INT_MAX;
		for (piece_index_t i(0); i < piece_index_t(num_pieces); ++i)
		{
			piece_picker::piece_stats_t ps = m_picker->piece_stats(i);
			if (ps.peer_count == 0) continue;
			if (ps.priority == 0 && (ps.flushed || ps.downloading))
			{
				m_picker->set_piece_priority(i, default_priority);
				continue;
			}
			// don't count pieces we already have or are trying to download
			if (ps.priority > 0 || ps.flushed) continue;
			if (ps.peer_count > rarest_rarity) continue;
			if (ps.peer_count == rarest_rarity)
			{
				rarest_pieces.push_back(i);
				continue;
			}

			rarest_pieces.clear();
			rarest_rarity = ps.peer_count;
			rarest_pieces.push_back(i);
		}

		update_gauge();
		update_want_peers();

		// now, rarest_pieces is a list of all pieces that are the rarest ones.
		// and rarest_rarity is the number of peers that have the rarest pieces

		// if there's only a single peer that doesn't have the rarest piece
		// it's impossible for us to download one piece and upload it
		// twice. i.e. we cannot get a positive share ratio
		if (num_peers - rarest_rarity
			< settings().get_int(settings_pack::share_mode_target))
			return;

		// now, pick one of the rarest pieces to download
		int const pick = int(random(aux::numeric_cast<std::uint32_t>(rarest_pieces.end_index() - 1)));
		bool const was_finished = is_finished();
		m_picker->set_piece_priority(rarest_pieces[pick], default_priority);
		update_gauge();
		update_peer_interest(was_finished);
		update_want_peers();
	}
#endif // TORRENT_DISABLE_SHARE_MODE

	void torrent::sent_bytes(int const bytes_payload, int const bytes_protocol)
	{
		m_stat.sent_bytes(bytes_payload, bytes_protocol);
		m_ses.sent_bytes(bytes_payload, bytes_protocol);
	}

	void torrent::received_bytes(int const bytes_payload, int const bytes_protocol)
	{
		m_stat.received_bytes(bytes_payload, bytes_protocol);
		m_ses.received_bytes(bytes_payload, bytes_protocol);
	}

	void torrent::trancieve_ip_packet(int const bytes, bool const ipv6)
	{
		m_stat.trancieve_ip_packet(bytes, ipv6);
		m_ses.trancieve_ip_packet(bytes, ipv6);
	}

	void torrent::sent_syn(bool const ipv6)
	{
		m_stat.sent_syn(ipv6);
		m_ses.sent_syn(ipv6);
	}

	void torrent::received_synack(bool const ipv6)
	{
		m_stat.received_synack(ipv6);
		m_ses.received_synack(ipv6);
	}

#ifndef TORRENT_DISABLE_STREAMING

#if TORRENT_DEBUG_STREAMING > 0
	char const* esc(char const* code)
	{
		// this is a silly optimization
		// to avoid copying of strings
		int const num_strings = 200;
		static char buf[num_strings][20];
		static int round_robin = 0;
		char* ret = buf[round_robin];
		++round_robin;
		if (round_robin >= num_strings) round_robin = 0;
		ret[0] = '\033';
		ret[1] = '[';
		int i = 2;
		int j = 0;
		while (code[j]) ret[i++] = code[j++];
		ret[i++] = 'm';
		ret[i++] = 0;
		return ret;
	}

	int peer_index(libtorrent::tcp::endpoint addr
		, std::vector<libtorrent::peer_info> const& peers)
	{
		std::vector<peer_info>::const_iterator i = std::find_if(peers.begin()
			, peers.end(), std::bind(&peer_info::ip, _1) == addr);
		if (i == peers.end()) return -1;

		return i - peers.begin();
	}

	void print_piece(libtorrent::partial_piece_info* pp
		, std::vector<libtorrent::peer_info> const& peers
		, std::vector<time_critical_piece> const& time_critical)
	{
		time_point const now = clock_type::now();

		float deadline = 0.f;
		float last_request = 0.f;
		int timed_out = -1;

		int piece = pp->piece_index;
		std::vector<time_critical_piece>::const_iterator i
			= std::find_if(time_critical.begin(), time_critical.end()
				, std::bind(&time_critical_piece::piece, _1) == piece);
		if (i != time_critical.end())
		{
			deadline = total_milliseconds(i->deadline - now) / 1000.f;
			if (i->last_requested == min_time())
				last_request = -1;
			else
				last_request = total_milliseconds(now - i->last_requested) / 1000.f;
			timed_out = i->timed_out;
		}

		int num_blocks = pp->blocks_in_piece;

		std::printf("%5d: [", piece);
		for (int j = 0; j < num_blocks; ++j)
		{
			int index = pp ? peer_index(pp->blocks[j].peer(), peers) % 36 : -1;
			char chr = '+';
			if (index >= 0)
				chr = (index < 10)?'0' + index:'A' + index - 10;

			char const* color = "";
			char const* multi_req = "";

			if (pp->blocks[j].num_peers > 1)
				multi_req = esc("1");

			if (pp->blocks[j].bytes_progress > 0
				&& pp->blocks[j].state == block_info::requested)
			{
				color = esc("33;7");
				chr = '0' + (pp->blocks[j].bytes_progress * 10 / pp->blocks[j].block_size);
			}
			else if (pp->blocks[j].state == block_info::finished) color = esc("32;7");
			else if (pp->blocks[j].state == block_info::writing) color = esc("36;7");
			else if (pp->blocks[j].state == block_info::requested) color = esc("0");
			else { color = esc("0"); chr = ' '; }

			std::printf("%s%s%c%s", color, multi_req, chr, esc("0"));
		}
		std::printf("%s]", esc("0"));
		if (deadline != 0.f)
			std::printf(" deadline: %f last-req: %f timed_out: %d\n"
				, deadline, last_request, timed_out);
		else
			std::printf("\n");
	}
#endif // TORRENT_DEBUG_STREAMING

	namespace {

	struct busy_block_t
	{
		int peers;
		int index;
		bool operator<(busy_block_t const& rhs) const { return peers < rhs.peers; }
	};

	void pick_busy_blocks(piece_picker const* picker
		, piece_index_t const piece
		, int const blocks_in_piece
		, int const timed_out
		, std::vector<piece_block>& interesting_blocks
		, piece_picker::downloading_piece const& pi)
	{
		// if there aren't any free blocks in the piece, and the piece is
		// old enough, we may switch into busy mode for this piece. In this
		// case busy_blocks and busy_count are set to contain the eligible
		// busy blocks we may pick
		// first, figure out which blocks are eligible for picking
		// in "busy-mode"
		TORRENT_ALLOCA(busy_blocks, busy_block_t, blocks_in_piece);
		int busy_count = 0;

		// pick busy blocks from the piece
		int idx = -1;
		for (auto const& info : picker->blocks_for_piece(pi))
		{
			++idx;
			// only consider blocks that have been requested
			// and we're still waiting for them
			if (info.state != piece_picker::block_info::state_requested)
				continue;

			piece_block b(piece, idx);

			// only allow a single additional request per block, in order
			// to spread it out evenly across all stalled blocks
			if (int(info.num_peers) > timed_out)
				continue;

			busy_blocks[busy_count].peers = info.num_peers;
			busy_blocks[busy_count].index = idx;
			++busy_count;

#if TORRENT_DEBUG_STREAMING > 1
			std::printf(" [%d (%d)]", b.block_index, info.num_peers);
#endif
		}
#if TORRENT_DEBUG_STREAMING > 1
		std::printf("\n");
#endif

		busy_blocks = busy_blocks.first(busy_count);

		// then sort blocks by the number of peers with requests
		// to the blocks (request the blocks with the fewest peers
		// first)
		std::sort(busy_blocks.begin(), busy_blocks.end());

		// then insert them into the interesting_blocks vector
		for (auto const& block : busy_blocks)
			interesting_blocks.emplace_back(piece, block.index);
	}

	void pick_time_critical_block(std::vector<peer_connection*>& peers
		, std::vector<peer_connection*>& ignore_peers
		, std::set<peer_connection*>& peers_with_requests
		, piece_picker::downloading_piece const& pi
		, time_critical_piece* i
		, piece_picker const* picker
		, int const blocks_in_piece
		, int const timed_out)
	{
		std::vector<piece_block> interesting_blocks;
		std::vector<piece_block> backup;
		std::vector<piece_index_t> ignore;

		time_point const now = aux::time_now();

		// loop until every block has been requested from this piece (i->piece)
		do
		{
			// if this peer's download time exceeds 2 seconds, we're done.
			// We don't want to build unreasonably long request queues
			if (!peers.empty() && peers[0]->download_queue_time() > milliseconds(2000))
			{
#if TORRENT_DEBUG_STREAMING > 1
				std::printf("queue time: %d ms, done\n"
					, int(total_milliseconds(peers[0]->download_queue_time())));
#endif
				break;
			}

			// pick the peer with the lowest download_queue_time that has i->piece
			auto p = std::find_if(peers.begin(), peers.end()
				, std::bind(&peer_connection::has_piece, _1, i->piece));

			// obviously we'll have to skip it if we don't have a peer that has
			// this piece
			if (p == peers.end())
			{
#if TORRENT_DEBUG_STREAMING > 1
				std::printf("out of peers, done\n");
#endif
				break;
			}
			peer_connection& c = **p;

			interesting_blocks.clear();
			backup.clear();

			// specifically request blocks with no affinity towards fast or slow
			// pieces. If we would, the picked block might end up in one of
			// the backup lists
			picker->add_blocks(i->piece, c.get_bitfield(), interesting_blocks
				, backup, blocks_in_piece, 0, c.peer_info_struct()
				, ignore, {});

			interesting_blocks.insert(interesting_blocks.end()
				, backup.begin(), backup.end());

			bool busy_mode = false;

			if (interesting_blocks.empty())
			{
				busy_mode = true;

#if TORRENT_DEBUG_STREAMING > 1
				std::printf("interesting_blocks.empty()\n");
#endif

				// there aren't any free blocks to pick, and the piece isn't
				// old enough to pick busy blocks yet. break to continue to
				// the next piece.
				if (timed_out == 0)
				{
#if TORRENT_DEBUG_STREAMING > 1
					std::printf("not timed out, moving on to next piece\n");
#endif
					break;
				}

#if TORRENT_DEBUG_STREAMING > 1
				std::printf("pick busy blocks\n");
#endif

				pick_busy_blocks(picker, i->piece, blocks_in_piece, timed_out
					, interesting_blocks, pi);
			}

			// we can't pick anything from this piece, we're done with it.
			// move on to the next one
			if (interesting_blocks.empty()) break;

			piece_block const b = interesting_blocks.front();

			// in busy mode we need to make sure we don't do silly
			// things like requesting the same block twice from the
			// same peer
			std::vector<pending_block> const& dq = c.download_queue();

			bool const already_requested = std::find_if(dq.begin(), dq.end()
				, aux::has_block(b)) != dq.end();

			if (already_requested)
			{
				// if the piece is stalled, we may end up picking a block
				// that we've already requested from this peer. If so, we should
				// simply disregard this peer from this piece, since this peer
				// is likely to be causing the stall. We should request it
				// from the next peer in the list
				// the peer will be put back in the set for the next piece
				ignore_peers.push_back(*p);
				peers.erase(p);
#if TORRENT_DEBUG_STREAMING > 1
				std::printf("piece already requested by peer, try next peer\n");
#endif
				// try next peer
				continue;
			}

			std::vector<pending_block> const& rq = c.request_queue();

			bool const already_in_queue = std::find_if(rq.begin(), rq.end()
				, aux::has_block(b)) != rq.end();

			if (already_in_queue)
			{
				if (!c.make_time_critical(b))
				{
#if TORRENT_DEBUG_STREAMING > 1
					std::printf("piece already time-critical and in queue for peer, trying next peer\n");
#endif
					ignore_peers.push_back(*p);
					peers.erase(p);
					continue;
				}
				i->last_requested = now;

#if TORRENT_DEBUG_STREAMING > 1
				std::printf("piece already in queue for peer, making time-critical\n");
#endif

				// we inserted a new block in the request queue, this
				// makes us actually send it later
				peers_with_requests.insert(peers_with_requests.begin(), &c);
			}
			else
			{
				if (!c.add_request(b, peer_connection::time_critical
					| (busy_mode ? peer_connection::busy : request_flags_t{})))
				{
#if TORRENT_DEBUG_STREAMING > 1
					std::printf("failed to request block [%d, %d]\n"
						, b.piece_index, b.block_index);
#endif
					ignore_peers.push_back(*p);
					peers.erase(p);
					continue;
				}

#if TORRENT_DEBUG_STREAMING > 1
				std::printf("requested block [%d, %d]\n"
					, b.piece_index, b.block_index);
#endif
				peers_with_requests.insert(peers_with_requests.begin(), &c);
			}

			if (!busy_mode) i->last_requested = now;

			if (i->first_requested == min_time()) i->first_requested = now;

			if (!c.can_request_time_critical())
			{
#if TORRENT_DEBUG_STREAMING > 1
				std::printf("peer cannot pick time critical pieces\n");
#endif
				peers.erase(p);
				// try next peer
				continue;
			}

			// resort p, since it will have a higher download_queue_time now
			while (p != peers.end()-1 && (*p)->download_queue_time()
				> (*(p+1))->download_queue_time())
			{
				std::iter_swap(p, p+1);
				++p;
			}
		} while (!interesting_blocks.empty());
	}

	} // anonymous namespace

	void torrent::request_time_critical_pieces()
	{
		TORRENT_ASSERT(is_single_thread());
		TORRENT_ASSERT(!upload_mode());

		// build a list of peers and sort it by download_queue_time
		// we use this sorted list to determine which peer we should
		// request a block from. The earlier a peer is in the list,
		// the sooner we will fully download the block we request.
		aux::vector<peer_connection*> peers;
		peers.reserve(num_peers());

		// some peers are marked as not being able to request time critical
		// blocks from. For instance, peers that have choked us, peers that are
		// on parole (i.e. they are believed to have sent us bad data), peers
		// that are being disconnected, in upload mode etc.
		std::remove_copy_if(m_connections.begin(), m_connections.end()
			, std::back_inserter(peers), [] (peer_connection* p)
			{ return !p->can_request_time_critical(); });

		// sort by the time we believe it will take this peer to send us all
		// blocks we've requested from it. The shorter time, the better candidate
		// it is to request a time critical block from.
		std::sort(peers.begin(), peers.end()
			, [] (peer_connection const* lhs, peer_connection const* rhs)
			{ return lhs->download_queue_time(16*1024) < rhs->download_queue_time(16*1024); });

		// remove the bottom 10% of peers from the candidate set.
		// this is just to remove outliers that might stall downloads
		int const new_size = (peers.end_index() * 9 + 9) / 10;
		TORRENT_ASSERT(new_size <= peers.end_index());
		peers.resize(new_size);

		// remember all the peers we issued requests to, so we can commit them
		// at the end of this function. Instead of sending the requests right
		// away, we batch them up and send them in a single write to the TCP
		// socket, increasing the chance that they will all be sent in the same
		// packet.
		std::set<peer_connection*> peers_with_requests;

		// peers that should be temporarily ignored for a specific piece
		// in order to give priority to other peers. They should be used for
		// subsequent pieces, so they are stored in this vector until the
		// piece is done
		std::vector<peer_connection*> ignore_peers;

		time_point const now = clock_type::now();

		// now, iterate over all time critical pieces, in order of importance, and
		// request them from the peers, in order of responsiveness. i.e. request
		// the most time critical pieces from the fastest peers.
		bool first_piece{true};
		for (auto& i : m_time_critical_pieces)
		{
#if TORRENT_DEBUG_STREAMING > 1
			std::printf("considering %d\n", i->piece);
#endif

			if (peers.empty())
			{
#if TORRENT_DEBUG_STREAMING > 1
				std::printf("out of peers, done\n");
#endif
				break;
			}

			// the +1000 is to compensate for the fact that we only call this
			// function once per second, so if we need to request it 500 ms from
			// now, we should request it right away
			if (!first_piece && i.deadline > now
				+ milliseconds(m_average_piece_time + m_piece_time_deviation * 4 + 1000))
			{
				// don't request pieces whose deadline is too far in the future
				// this is one of the termination conditions. We don't want to
				// send requests for all pieces in the torrent right away
#if TORRENT_DEBUG_STREAMING > 0
				std::printf("reached deadline horizon [%f + %f * 4 + 1]\n"
					, m_average_piece_time / 1000.f
					, m_piece_time_deviation / 1000.f);
#endif
				break;
			}
			first_piece = false;

			piece_picker::downloading_piece pi;
			m_picker->piece_info(i.piece, pi);

			// the number of "times" this piece has timed out.
			int timed_out = 0;

			int const blocks_in_piece = m_picker->blocks_in_piece(i.piece);

#if TORRENT_DEBUG_STREAMING > 0
			i.timed_out = timed_out;
#endif
			int const free_to_request = blocks_in_piece
				- pi.finished - pi.writing - pi.requested;

			if (free_to_request == 0)
			{
				if (i.last_requested == min_time())
					i.last_requested = now;

				// if it's been more than half of the typical download time
				// of a piece since we requested the last block, allow
				// one more request per block
				if (m_average_piece_time > 0)
					timed_out = int(total_milliseconds(now - i.last_requested)
						/ std::max(int(m_average_piece_time + m_piece_time_deviation / 2), 1));

#if TORRENT_DEBUG_STREAMING > 0
				i.timed_out = timed_out;
#endif
				// every block in this piece is already requested
				// there's no need to consider this piece, unless it
				// appears to be stalled.
				if (pi.requested == 0 || timed_out == 0)
				{
#if TORRENT_DEBUG_STREAMING > 1
					std::printf("skipping %d (full) [req: %d timed_out: %d ]\n"
						, i.piece, pi.requested
						, timed_out);
#endif

					// if requested is 0, it means all blocks have been received, and
					// we're just waiting for it to flush them to disk.
					// if last_requested is recent enough, we should give it some
					// more time
					// skip to the next piece
					continue;
				}

				// it's been too long since we requested the last block from
				// this piece. Allow re-requesting blocks from this piece
#if TORRENT_DEBUG_STREAMING > 1
				std::printf("timed out [average-piece-time: %d ms ]\n"
					, m_average_piece_time);
#endif
			}

			// pick all blocks for this piece. the peers list is kept up to date
			// and sorted. when we issue a request to a peer, its download queue
			// time will increase and it may need to be bumped in the peers list,
			// since it's ordered by download queue time
			pick_time_critical_block(peers, ignore_peers
				, peers_with_requests
				, pi, &i, m_picker.get()
				, blocks_in_piece, timed_out);

			// put back the peers we ignored into the peer list for the next piece
			if (!ignore_peers.empty())
			{
				peers.insert(peers.begin(), ignore_peers.begin(), ignore_peers.end());
				ignore_peers.clear();

				// TODO: instead of resorting the whole list, insert the peers
				// directly into the right place
				std::sort(peers.begin(), peers.end()
					, [] (peer_connection const* lhs, peer_connection const* rhs)
					{ return lhs->download_queue_time(16*1024) < rhs->download_queue_time(16*1024); });
			}

			// if this peer's download time exceeds 2 seconds, we're done.
			// We don't want to build unreasonably long request queues
			if (!peers.empty() && peers[0]->download_queue_time() > milliseconds(2000))
				break;
		}

		// commit all the time critical requests
		for (auto* p : peers_with_requests)
		{
			p->send_block_requests();
		}
	}
#endif // TORRENT_DISABLE_STREAMING

	std::set<std::string> torrent::web_seeds() const
	{
		TORRENT_ASSERT(is_single_thread());
		std::set<std::string> ret;
		for (auto const& s : m_web_seeds)
		{
			if (s.peer_info.banned) continue;
			if (s.removed) continue;
			ret.insert(s.url);
		}
		return ret;
	}

	void torrent::remove_web_seed(std::string const& url)
	{
		auto const i = std::find_if(m_web_seeds.begin(), m_web_seeds.end()
			, [&] (web_seed_t const& w) { return w.url == url; });

		if (i != m_web_seeds.end())
		{
			if (!i->ephemeral)
				set_need_save_resume(torrent_handle::if_metadata_changed);
			remove_web_seed_iter(i);
		}
	}

	void torrent::remove_web_seed_conn(web_seed_t* w)
	{
		auto const i =
			std::find_if(m_web_seeds.begin(), m_web_seeds.end(), [w](web_seed_t const& ws) {
				return &ws == w;
			});

		TORRENT_ASSERT(i != m_web_seeds.end());
		if (i == m_web_seeds.end()) return;
		remove_web_seed_iter(i);
	}

	void torrent::retry_web_seed(peer_connection* p, std::optional<seconds32> const retry)
	{
		TORRENT_ASSERT(is_single_thread());
		auto const i = std::find_if(m_web_seeds.begin(), m_web_seeds.end()
			, [p] (web_seed_t const& ws) { return ws.peer_info.connection == p; });

		TORRENT_ASSERT(i != m_web_seeds.end());
		if (i == m_web_seeds.end()) return;
		if (i->removed || i->disabled) return;
		i->retry = aux::time_now32() + retry.value_or(seconds32(
			settings().get_int(settings_pack::urlseed_wait_retry)));
	}

	torrent_state torrent::get_peer_list_state()
	{
		torrent_state ret;
		ret.is_finished = is_finished();
		ret.allow_multiple_connections_per_ip = settings().get_bool(settings_pack::allow_multiple_connections_per_ip);
		ret.max_peerlist_size = is_paused()
			? settings().get_int(settings_pack::max_paused_peerlist_size)
			: settings().get_int(settings_pack::max_peerlist_size);
		ret.min_reconnect_time = settings().get_int(settings_pack::min_reconnect_time);

		ret.ip = m_ses.external_address();
		ret.port = m_ses.listen_port();
		ret.max_failcount = settings().get_int(settings_pack::max_failcount);
		return ret;
	}

	bool torrent::try_connect_peer()
	{
		TORRENT_ASSERT(is_single_thread());
		TORRENT_ASSERT(want_peers());

		torrent_state st = get_peer_list_state();
		need_peer_list();
		torrent_peer* p = m_peer_list->connect_one_peer(m_ses.session_time(), &st);
		peers_erased(st.erased);
		inc_stats_counter(counters::connection_attempt_loops, st.loop_counter);

		if (p == nullptr)
		{
			m_stats_counters.inc_stats_counter(counters::no_peer_connection_attempts);
			update_want_peers();
			return false;
		}

		if (!connect_to_peer(p))
		{
			m_stats_counters.inc_stats_counter(counters::missed_connection_attempts);
			m_peer_list->inc_failcount(p);
			update_want_peers();
			return false;
		}
		update_want_peers();

		return true;
	}

	torrent_peer* torrent::add_peer(tcp::endpoint const& adr
		, peer_source_flags_t const source, pex_flags_t flags)
	{
		TORRENT_ASSERT(is_single_thread());

		TORRENT_ASSERT(info_hash().has_v2() || !(flags & pex_lt_v2));

		if (m_apply_ip_filter
			&& m_ip_filter
			&& m_ip_filter->access(adr.address()) & ip_filter::blocked)
		{
			if (alerts().should_post<peer_blocked_alert>())
				alerts().emplace_alert<peer_blocked_alert>(get_handle()
					, adr, peer_blocked_alert::ip_filter);

#ifndef TORRENT_DISABLE_EXTENSIONS
			notify_extension_add_peer(adr, source, torrent_plugin::filtered);
#endif
			return nullptr;
		}

		if (m_ses.get_port_filter().access(adr.port()) & port_filter::blocked)
		{
			if (alerts().should_post<peer_blocked_alert>())
				alerts().emplace_alert<peer_blocked_alert>(get_handle()
					, adr, peer_blocked_alert::port_filter);
#ifndef TORRENT_DISABLE_EXTENSIONS
			notify_extension_add_peer(adr, source, torrent_plugin::filtered);
#endif
			return nullptr;
		}

#if TORRENT_USE_I2P
		// if this is an i2p torrent, and we don't allow mixed mode
		// no regular peers should ever be added!
		if (!settings().get_bool(settings_pack::allow_i2p_mixed) && is_i2p())
		{
			if (alerts().should_post<peer_blocked_alert>())
				alerts().emplace_alert<peer_blocked_alert>(get_handle()
					, adr, peer_blocked_alert::i2p_mixed);
			return nullptr;
		}
#endif

		if (settings().get_bool(settings_pack::no_connect_privileged_ports) && adr.port() < 1024)
		{
			if (alerts().should_post<peer_blocked_alert>())
				alerts().emplace_alert<peer_blocked_alert>(get_handle()
					, adr, peer_blocked_alert::privileged_ports);
#ifndef TORRENT_DISABLE_EXTENSIONS
			notify_extension_add_peer(adr, source, torrent_plugin::filtered);
#endif
			return nullptr;
		}

#ifndef TORRENT_DISABLE_DHT
		if (source != peer_info::resume_data)
		{
			// try to send a DHT ping to this peer
			// as well, to figure out if it supports
			// DHT (uTorrent and BitComet don't
			// advertise support)
			session().add_dht_node({adr.address(), adr.port()});
		}
#endif

		if (!torrent_file().info_hashes().has_v1())
			flags |= pex_lt_v2;

		need_peer_list();
		torrent_state st = get_peer_list_state();
		torrent_peer* p = m_peer_list->add_peer(adr, source, flags, &st);
		peers_erased(st.erased);

		if (p)
		{
			state_updated();
#ifndef TORRENT_DISABLE_EXTENSIONS
			notify_extension_add_peer(adr, source
				, st.first_time_seen
					? torrent_plugin::first_time
					: add_peer_flags_t{});
#endif
		}
		else
		{
#ifndef TORRENT_DISABLE_EXTENSIONS
			notify_extension_add_peer(adr, source, torrent_plugin::filtered);
#endif
		}
		update_want_peers();
		state_updated();
		return p;
	}

	bool torrent::ban_peer(torrent_peer* tp)
	{
		if (!settings().get_bool(settings_pack::ban_web_seeds) && tp->web_seed)
			return false;

		need_peer_list();
		if (!m_peer_list->ban_peer(tp)) return false;
		update_want_peers();

		inc_stats_counter(counters::num_banned_peers);
		return true;
	}

	void torrent::set_seed(torrent_peer* p, bool const s)
	{
		if (bool(p->seed) == s) return;
		if (s)
		{
			TORRENT_ASSERT(m_num_seeds < 0xffff);
			++m_num_seeds;
		}
		else
		{
			TORRENT_ASSERT(m_num_seeds > 0);
			--m_num_seeds;
		}

		need_peer_list();
		m_peer_list->set_seed(p, s);
		update_auto_sequential();
		update_want_peers();
	}

	void torrent::set_upload_only(torrent_peer* p, bool const s)
	{
		need_peer_list();
		m_peer_list->set_upload_only(p, s);
		update_want_peers();
	}

	void torrent::clear_failcount(torrent_peer* p)
	{
		need_peer_list();
		m_peer_list->set_failcount(p, 0);
		update_want_peers();
	}

	std::pair<peer_list::iterator, peer_list::iterator> torrent::find_peers(address const& a)
	{
		need_peer_list();
		return m_peer_list->find_peers(a);
	}

	void torrent::update_peer_port(int const port, torrent_peer* p
		, peer_source_flags_t const src)
	{
		need_peer_list();
		torrent_state st = get_peer_list_state();
		m_peer_list->update_peer_port(port, p, src, &st);
		peers_erased(st.erased);
		update_want_peers();
	}

	// verify piece is used when checking resume data or when the user
	// adds a piece
	void torrent::verify_piece(piece_index_t const piece)
	{
		TORRENT_ASSERT(m_storage);
		TORRENT_ASSERT(!m_picker->is_hashing(piece));

		// we just completed the piece, it should be flushed to disk
		disk_job_flags_t flags{};

		auto const write_mode = settings().get_int(settings_pack::disk_io_write_mode);
		if (write_mode == settings_pack::write_through)
			flags |= disk_interface::flush_piece;
		else if (write_mode == settings_pack::disable_os_cache)
			flags |= disk_interface::flush_piece | disk_interface::volatile_read;
		if (torrent_file().info_hashes().has_v1() && !m_disable_v1_hashes)
			flags |= disk_interface::v1_hash;

		aux::vector<sha256_hash> hashes;
		if (torrent_file().info_hashes().has_v2())
		{
			hashes.resize(torrent_file().layout().blocks_in_piece2(piece));
		}

		if (settings().get_bool(settings_pack::disable_hash_checks))
		{
			// short-circuit the hash check if it's disabled
			m_picker->started_hash_job(piece);
			on_piece_verified(std::move(hashes), piece, sha1_hash(), storage_error{});
			return;
		}

		span<sha256_hash> v2_span(hashes);
		m_ses.disk_thread().async_hash(m_storage, piece, v2_span, flags
			, [self = shared_from_this(), hashes1 = std::move(hashes)]
			(piece_index_t p, sha1_hash const& h, storage_error const& error) mutable
			{ self->on_piece_verified(std::move(hashes1), p, h, error); });
		m_picker->started_hash_job(piece);
		m_ses.deferred_submit_jobs();
	}

	void torrent::ip_filter_updated()
	{
		if (!m_apply_ip_filter) return;
		if (!m_peer_list) return;
		if (!m_ip_filter) return;

		torrent_state st = get_peer_list_state();
		std::vector<address> banned;
		m_peer_list->apply_ip_filter(*m_ip_filter, &st, banned);

		if (alerts().should_post<peer_blocked_alert>())
		{
			for (auto const& addr : banned)
				alerts().emplace_alert<peer_blocked_alert>(get_handle()
					, tcp::endpoint(addr, 0)
					, peer_blocked_alert::ip_filter);
		}

		peers_erased(st.erased);
		update_want_peers();
	}

	// this call will disconnect any peers whose remote port is < 1024
	void torrent::privileged_port_updated()
	{
		if (!m_peer_list) return;

		port_filter ports;
		ports.add_rule(0, 1023, port_filter::blocked);

		torrent_state st = get_peer_list_state();
		std::vector<tcp::endpoint> banned;
		m_peer_list->apply_port_filter(ports, &st, banned);

		if (alerts().should_post<peer_blocked_alert>())
		{
			for (auto const& ep : banned)
				alerts().emplace_alert<peer_blocked_alert>(get_handle()
					, ep, peer_blocked_alert::privileged_ports);
		}

		peers_erased(st.erased);
		update_want_peers();
	}

	void torrent::port_filter_updated()
	{
		if (!m_peer_list) return;

		torrent_state st = get_peer_list_state();
		std::vector<tcp::endpoint> banned;
		m_peer_list->apply_port_filter(m_ses.get_port_filter(), &st, banned);

		if (alerts().should_post<peer_blocked_alert>())
		{
			for (auto const& ep : banned)
				alerts().emplace_alert<peer_blocked_alert>(get_handle()
					, ep, peer_blocked_alert::port_filter);
		}

		peers_erased(st.erased);
		update_want_peers();
	}

#ifndef TORRENT_DISABLE_EXTENSIONS
	// Defined in src/smart_ban.cpp. This is a hack to give the smart-ban
	// plugin an erasure notification without adding a new virtual to the
	// public torrent_plugin interface (which would break ABI).
	void smart_ban_notify_erase_peers(torrent_plugin* ext, span<aux::torrent_peer* const> peers);
#endif

	// this is called when torrent_peers are removed from the peer_list
	// (peer-list). It removes any references we may have to those torrent_peers,
	// so we don't leave then dangling
	void torrent::peers_erased(std::vector<torrent_peer*> const& peers)
	{
#ifndef TORRENT_DISABLE_EXTENSIONS
		for (auto& ext : m_extensions)
			smart_ban_notify_erase_peers(ext.get(), peers);
#endif

		if (!has_picker()) return;

		for (auto* const p : peers)
		{
			m_picker->clear_peer(p);
		}
#if TORRENT_USE_INVARIANT_CHECKS
		m_picker->check_peers();
#endif
	}

#if TORRENT_ABI_VERSION == 1
#if !TORRENT_NO_FPU
	void torrent::file_progress_float(aux::vector<float, file_index_t>& fp)
	{
		TORRENT_ASSERT(is_single_thread());
		if (!valid_metadata())
		{
			fp.clear();
			return;
		}

		fp.resize(m_torrent_file->num_files(), 1.f);
		if (is_seed()) return;

		aux::vector<std::int64_t, file_index_t> progress;
		file_progress(progress, {});
		file_storage const& fs = m_torrent_file->layout();
		for (auto const i : fs.file_range())
		{
			std::int64_t file_size = fs.file_size(i);
			if (file_size == 0) fp[i] = 1.f;
			else fp[i] = float(progress[i]) / float(file_size);
		}
	}
#endif
#endif // TORRENT_ABI_VERSION

	void torrent::post_file_progress(file_progress_flags_t const flags)
	{
		aux::vector<std::int64_t, file_index_t> fp;
		file_progress(fp, flags);
		alerts().emplace_alert<file_progress_alert>(get_handle(), std::move(fp));
	}

	void torrent::file_progress(aux::vector<std::int64_t, file_index_t>& fp
		, file_progress_flags_t const flags)
	{
		TORRENT_ASSERT(is_single_thread());
		if (!valid_metadata())
		{
			fp.clear();
			return;
		}

		// if we're a seed, we don't have an m_file_progress anyway
		// since we don't need one. We know we have all files
		// just fill in the full file sizes as a shortcut
		if (is_seed())
		{
			fp.resize(m_torrent_file->num_files());
			file_storage const& fs = m_torrent_file->layout();
			for (auto const i : fs.file_range())
				fp[i] = fs.file_size(i);
			return;
		}

		if (num_have() == 0 || m_file_progress.empty())
		{
			// if we don't have any pieces, just return zeroes
			fp.clear();
			fp.resize(m_torrent_file->num_files(), 0);
		}
		else
		{
			m_file_progress.export_progress(fp);
		}

		if (flags & torrent_handle::piece_granularity)
			return;

		if (!has_picker())
			return;

		std::vector<piece_picker::downloading_piece> q = m_picker->get_download_queue();

		file_storage const& fs = m_torrent_file->layout();
		for (auto const& dp : q)
		{
			if (have_piece(dp.index))
			{
				// in this case this piece has already been accounted for in fp
				continue;
			}

			std::int64_t offset = std::int64_t(static_cast<int>(dp.index))
				* m_torrent_file->piece_length();
			file_index_t file = fs.file_index_at_offset(offset);
			int idx = -1;
			for (auto const& info : m_picker->blocks_for_piece(dp))
			{
				++idx;
				TORRENT_ASSERT(file < fs.end_file());
				TORRENT_ASSERT(offset == std::int64_t(static_cast<int>(dp.index))
					* m_torrent_file->piece_length()
					+ idx * block_size());
				TORRENT_ASSERT(offset < m_torrent_file->total_size());
				while (offset >= fs.file_offset(file) + fs.file_size(file))
				{
					++file;
				}
				TORRENT_ASSERT(file < fs.end_file());

				std::int64_t block = block_size();

				if (info.state == piece_picker::block_info::state_none)
				{
					offset += block;
					continue;
				}

				if (info.state == piece_picker::block_info::state_requested)
				{
					block = 0;
					torrent_peer* p = info.peer;
					if (p != nullptr && p->connection)
					{
						auto* peer = static_cast<peer_connection*>(p->connection);
						auto pbp = peer->downloading_piece_progress();
						if (pbp.piece_index == dp.index && pbp.block_index == idx)
							block = pbp.bytes_downloaded;
						TORRENT_ASSERT(block <= block_size());
					}

					if (block == 0)
					{
						offset += block_size();
						continue;
					}
				}

				if (offset + block > fs.file_offset(file) + fs.file_size(file))
				{
					std::int64_t left_over = block_size() - block;
					// split the block on multiple files
					while (block > 0)
					{
						TORRENT_ASSERT(offset <= fs.file_offset(file) + fs.file_size(file));
						std::int64_t const slice = std::min(fs.file_offset(file) + fs.file_size(file) - offset
							, block);
						TORRENT_ASSERT(fp[file] <= fs.file_size(file) - slice);
						fp[file] += slice;
						offset += slice;
						block -= slice;
						TORRENT_ASSERT(offset <= fs.file_offset(file) + fs.file_size(file));
						if (offset == fs.file_offset(file) + fs.file_size(file))
						{
							++file;
							if (file == fs.end_file())
							{
								offset += block;
								break;
							}
						}
					}
					offset += left_over;
					TORRENT_ASSERT(offset == std::int64_t(static_cast<int>(dp.index))
						* m_torrent_file->piece_length()
						+ (idx + 1) * block_size());
				}
				else
				{
					TORRENT_ASSERT(fp[file] <= fs.file_size(file) - block);
					fp[file] += block;
					offset += block_size();
				}
				TORRENT_ASSERT(file <= fs.end_file());
			}
		}
	}

	void torrent::new_external_ip()
	{
		if (m_peer_list) m_peer_list->clear_peer_prio();
	}

	void torrent::stop_when_ready(bool const b)
	{
		m_stop_when_ready = b;

		// to avoid race condition, if we're already in a downloading state,
		// trigger the stop-when-ready logic immediately.
		if (m_stop_when_ready && is_downloading_state(m_state))
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("stop_when_ready triggered");
#endif
			auto_managed(false);
			pause();
			m_stop_when_ready = false;
		}
	}

	void torrent::set_state(torrent_status::state_t const s)
	{
		TORRENT_ASSERT(is_single_thread());
		TORRENT_ASSERT(s != 0); // this state isn't used anymore

#if TORRENT_USE_ASSERTS

		if (s == torrent_status::seeding)
		{
			TORRENT_ASSERT(is_seed());
			TORRENT_ASSERT(is_finished());
		}
		if (s == torrent_status::finished)
			TORRENT_ASSERT(is_finished());
		if (s == torrent_status::downloading && m_state == torrent_status::finished)
			TORRENT_ASSERT(!is_finished());
#endif

		if (int(m_state) == s) return;

		if (m_ses.alerts().should_post<state_changed_alert>())
		{
			m_ses.alerts().emplace_alert<state_changed_alert>(get_handle()
				, s, static_cast<torrent_status::state_t>(m_state));
		}

		if (s == torrent_status::finished
			&& alerts().should_post<torrent_finished_alert>())
		{
			alerts().emplace_alert<torrent_finished_alert>(
				get_handle());
		}

		bool const trigger_stop = m_stop_when_ready
			&& !is_downloading_state(m_state)
			&& is_downloading_state(s);

		// we need to update the state before calling pause(), because otherwise
		// it may think we're still checking files, even though we're just
		// leaving that state
		m_state = s;

		update_gauge();
		update_want_peers();
		update_want_tick();
		update_state_list();

		state_updated();

#ifndef TORRENT_DISABLE_LOGGING
		debug_log("set_state() %d", m_state);
#endif

		if (trigger_stop)
		{
#ifndef TORRENT_DISABLE_LOGGING
			debug_log("stop_when_ready triggered");
#endif
			// stop_when_ready is set, and we're transitioning from a downloading
			// state to a non-downloading state. pause the torrent. Note that
			// "downloading" is defined broadly to include any state where we
			// either upload or download (for the purpose of this flag).
			auto_managed(false);
			pause();
			m_stop_when_ready = false;
		}

#ifndef TORRENT_DISABLE_EXTENSIONS
		for (auto& ext : m_extensions)
		{
			ext->on_state(state());
		}
#endif
	}

#ifndef TORRENT_DISABLE_EXTENSIONS
	void torrent::notify_extension_add_peer(tcp::endpoint const& ip
		, peer_source_flags_t const src, add_peer_flags_t const flags)
	{
		for (auto& ext : m_extensions)
		{
			ext->on_add_peer(ip, src, flags);
		}
	}
#endif

	void torrent::state_updated()
	{
		// if this fails, this function is probably called
		// from within the torrent constructor, which it
		// shouldn't be. Whichever function ends up calling
		// this should probably be moved to torrent::start()
		TORRENT_ASSERT(shared_from_this());

		// we can't call state_updated() while the session
		// is building the status update alert
		TORRENT_ASSERT(!m_ses.is_posting_torrent_updates());

		// we're not subscribing to this torrent, don't add it
		if (!m_state_subscription) return;

		aux::vector<torrent*>& list = m_ses.torrent_list(aux::session_interface::torrent_state_updates);

		// if it has already been updated this round, no need to
		// add it to the list twice
		if (m_links[aux::session_interface::torrent_state_updates].in_list())
		{
#ifdef TORRENT_EXPENSIVE_INVARIANT_CHECKS
			TORRENT_ASSERT(find(list.begin(), list.end(), this) != list.end());
#endif
			return;
		}

#ifdef TORRENT_EXPENSIVE_INVARIANT_CHECKS
		TORRENT_ASSERT(find(list.begin(), list.end(), this) == list.end());
#endif

		m_links[aux::session_interface::torrent_state_updates].insert(list, this);
	}

	void torrent::post_status(status_flags_t const flags)
	{
		std::vector<torrent_status> s;
		s.resize(1);
		status(&s.front(), flags);
		m_ses.alerts().emplace_alert<state_update_alert>(std::move(s));
	}

	void torrent::status(torrent_status* st, status_flags_t const flags)
	{
		INVARIANT_CHECK;

		time_point32 const now = aux::time_now32();

		st->handle = get_handle();
		st->info_hashes = info_hash();
#if TORRENT_ABI_VERSION < 3
		st->info_hash = info_hash().get_best();
#endif
#if TORRENT_ABI_VERSION == 1
		st->is_loaded = true;
#endif

		if (flags & torrent_handle::query_name)
			st->name = name();

		if (flags & torrent_handle::query_save_path)
			st->save_path = save_path();

		if (flags & torrent_handle::query_torrent_file)
			st->torrent_file = m_torrent_file;

		if (flags & torrent_handle::query_renamed_files)
			st->renamed_files = m_renamed_files;

		st->has_incoming = m_has_incoming;
		st->errc = m_error;
		st->error_file = m_error_file;

#if TORRENT_ABI_VERSION == 1
		if (m_error) st->error = m_error.message()
			+ ": " + resolve_filename(m_error_file);
		st->seed_mode = m_seed_mode;
#endif
		st->moving_storage = m_moving_storage;

		st->announcing_to_trackers = m_announce_to_trackers;
		st->announcing_to_lsd = m_announce_to_lsd;
		st->announcing_to_dht = m_announce_to_dht;
#if TORRENT_ABI_VERSION == 1
		st->stop_when_ready = m_stop_when_ready;
#endif

		st->added_time = m_added_time;
		st->completed_time = m_completed_time;

#if TORRENT_ABI_VERSION == 1
		st->last_scrape = static_cast<int>(total_seconds(now - m_last_scrape));
#endif

#if TORRENT_ABI_VERSION == 1
#ifndef TORRENT_DISABLE_SHARE_MODE
		st->share_mode = m_share_mode;
#else
		st->share_mode = false;
#endif
		st->upload_mode = m_upload_mode;
#endif
		st->up_bandwidth_queue = 0;
		st->down_bandwidth_queue = 0;
#if TORRENT_ABI_VERSION == 1
		st->priority = priority();
#endif

		st->num_peers = num_peers() - m_num_connecting;

		st->list_peers = m_peer_list ? m_peer_list->num_peers() : 0;
		st->list_seeds = m_peer_list ? m_peer_list->num_seeds() : 0;
		st->connect_candidates = m_peer_list ? m_peer_list->num_connect_candidates() : 0;
		TORRENT_ASSERT(st->connect_candidates >= 0);
		st->seed_rank = seed_rank(settings());

		st->all_time_upload = m_total_uploaded;
		st->all_time_download = m_total_downloaded;

		// activity time
#if TORRENT_ABI_VERSION == 1
		st->finished_time = finished_time(now).count();
		st->active_time = active_time(now).count();
		st->seeding_time = seeding_time(now).count();

		time_point32 const unset{seconds32(0)};

		st->time_since_upload = m_last_upload == unset ? -1
			: static_cast<int>(total_seconds(now - m_last_upload));
		st->time_since_download = m_last_download == unset ? -1
			: static_cast<int>(total_seconds(now - m_last_download));
#endif

		st->finished_duration = finished_time(now);
		st->active_duration = active_time(now);
		st->seeding_duration = seeding_time(now);

		st->last_upload = m_last_upload;
		st->last_download = m_last_download;

		st->storage_mode = static_cast<storage_mode_t>(m_storage_mode);

		st->num_complete = (m_complete == 0xffffff) ? -1 : m_complete;
		st->num_incomplete = (m_incomplete == 0xffffff) ? -1 : m_incomplete;
#if TORRENT_ABI_VERSION == 1
		st->paused = is_torrent_paused();
		st->auto_managed = m_auto_managed;
		st->sequential_download = m_sequential_download;
#endif
		st->is_seeding = is_seed();
		st->is_finished = is_finished();
#if TORRENT_ABI_VERSION == 1
#ifndef TORRENT_DISABLE_SUPERSEEDING
		st->super_seeding = m_super_seeding;
#endif
#endif
		st->has_metadata = valid_metadata();
		bytes_done(*st, flags);
		TORRENT_ASSERT(st->total_wanted_done >= 0);
		TORRENT_ASSERT(st->total_done >= st->total_wanted_done);

		// payload transfer
		st->total_payload_download = m_stat.total_payload_download();
		st->total_payload_upload = m_stat.total_payload_upload();

		// total transfer
		st->total_download = m_stat.total_payload_download()
			+ m_stat.total_protocol_download();
		st->total_upload = m_stat.total_payload_upload()
			+ m_stat.total_protocol_upload();

		// failed bytes
		st->total_failed_bytes = m_total_failed_bytes;
		st->total_redundant_bytes = m_total_redundant_bytes;

		// transfer rate
		st->download_rate = m_stat.download_rate();
		st->upload_rate = m_stat.upload_rate();
		st->download_payload_rate = m_stat.download_payload_rate();
		st->upload_payload_rate = m_stat.upload_payload_rate();

		if (is_paused() || m_tracker_timer.expiry() < now)
			st->next_announce = seconds(0);
		else
			st->next_announce = m_tracker_timer.expiry() - now;

		if (st->next_announce.count() < 0)
			st->next_announce = seconds(0);

#if TORRENT_ABI_VERSION == 1
		st->announce_interval = seconds(0);
#endif

		st->current_tracker = m_trackers.last_working_url();
		if (st->current_tracker.empty())
		{
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wmissing-braces"
#endif
			aux::array<bool const, num_protocols, protocol_version> const supports_protocol{
			{
				m_info_hash.has_v1(),
				m_info_hash.has_v2()
			}};
#ifdef __clang__
#pragma clang diagnostic pop
#endif

			for (auto const& t : m_trackers)
			{
				if (!t.verified) continue;
				if (std::any_of(t.endpoints.begin(), t.endpoints.end()
					, [supports_protocol](aux::announce_endpoint const& aep) {
						for (protocol_version const ih : all_versions)
						{
							if (supports_protocol[ih] && aep.info_hashes[ih].updating)
								return false;
						}
						return true;
					})) continue;
				st->current_tracker = t.url;
				break;
			}
		}

		if ((flags & torrent_handle::query_verified_pieces))
		{
			st->verified_pieces = m_verified;
		}

		st->num_uploads = m_num_uploads;
		st->uploads_limit = m_max_uploads == (1 << 24) - 1 ? -1 : m_max_uploads;
		st->num_connections = num_peers();
		st->connections_limit = m_max_connections == (1 << 24) - 1 ? -1 : m_max_connections;
		st->upload_limit = upload_limit();
		st->download_limit = download_limit();
		// if we don't have any metadata, stop here

		st->queue_position = queue_position();
#if TORRENT_ABI_VERSION < 4
		st->need_save_resume = bool(m_need_save_resume_data);
#endif
		st->need_save_resume_data = m_need_save_resume_data;
#if TORRENT_ABI_VERSION == 1
		st->ip_filter_applies = m_apply_ip_filter;
#endif

		st->state = static_cast<torrent_status::state_t>(m_state);
		st->flags = this->flags();

#if TORRENT_USE_ASSERTS
		if (st->state == torrent_status::finished
			|| st->state == torrent_status::seeding)
		{
			// it may be tempting to assume that st->is_finished == true here, but
			// this assumption does not always hold. We transition to "finished"
			// when we receive the last block of the last piece, which is before
			// the hash check comes back. "is_finished" is set to true once all the
			// pieces have been hash checked. So, there's a short window where it
			// doesn't hold.
		}
#endif

		if (!valid_metadata())
		{
			st->state = torrent_status::downloading_metadata;
			st->progress_ppm = m_progress_ppm;
#if !TORRENT_NO_FPU
			st->progress = m_progress_ppm / 1000000.f;
#endif
			st->block_size = 0;
			return;
		}

		st->block_size = block_size();

		if (m_state == torrent_status::checking_files)
		{
			st->progress_ppm = m_progress_ppm;
#if !TORRENT_NO_FPU
			st->progress = m_progress_ppm / 1000000.f;
#endif
		}
		else if (st->total_wanted == 0)
		{
			st->progress_ppm = 1000000;
			st->progress = 1.f;
		}
		else
		{
			st->progress_ppm = int(st->total_wanted_done * 1000000
				/ st->total_wanted);
#if !TORRENT_NO_FPU
			st->progress = float(st->progress_ppm) / 1000000.f;
#endif
		}

		if (flags & torrent_handle::query_pieces)
		{
			int const num_pieces = m_torrent_file->num_pieces();
			if (has_picker())
			{
				st->pieces.resize(num_pieces, false);
				for (auto const i : st->pieces.range())
					if (m_picker->have_piece(i)) st->pieces.set_bit(i);
			}
			else if (m_have_all)
			{
				st->pieces.resize(num_pieces, true);
			}
			else
			{
				st->pieces.resize(num_pieces, false);
			}
		}
		st->num_pieces = num_have();
#if TORRENT_USE_INVARIANT_CHECKS
		{
			// The documentation states that `num_pieces` is the count of number
			// of bits set in `pieces`. Ensure that invariant holds.
			int num_have_pieces = 0;
			if (m_seed_mode)
			{
				num_have_pieces = m_torrent_file->num_pieces();
			}
			else if (has_picker())
			{
				for (auto const i : m_torrent_file->piece_range())
					if (m_picker->have_piece(i)) ++num_have_pieces;
			}
			else if (m_have_all)
			{
				num_have_pieces = m_torrent_file->num_pieces();
			}
			TORRENT_ASSERT(num_have_pieces == st->num_pieces);
		}
#endif
		st->num_seeds = num_seeds();
		if ((flags & torrent_handle::query_distributed_copies) && m_picker.get())
		{
			std::tie(st->distributed_full_copies, st->distributed_fraction) =
				m_picker->distributed_copies();
#if TORRENT_NO_FPU
			st->distributed_copies = -1.f;
#else
			st->distributed_copies = float(st->distributed_full_copies)
				+ float(st->distributed_fraction) / 1000;
#endif
		}
		else
		{
			st->distributed_full_copies = -1;
			st->distributed_fraction = -1;
			st->distributed_copies = -1.f;
		}

		st->last_seen_complete = m_swarm_last_seen_complete;
	}

	int torrent::priority() const
	{
		int priority = 0;
		for (int i = 0; i < num_classes(); ++i)
		{
			span<int const> prio = m_ses.peer_classes().at(class_at(i))->priority;
			priority = std::max(priority, prio[peer_connection::upload_channel]);
			priority = std::max(priority, prio[peer_connection::download_channel]);
		}
		return priority;
	}

#if TORRENT_ABI_VERSION == 1
	void torrent::set_priority(int const prio)
	{
		// priority 1 is default
		if (prio == 1 && m_peer_class == peer_class_t{}) return;

		if (m_peer_class == peer_class_t{})
			setup_peer_class();

		struct peer_class* tpc = m_ses.peer_classes().at(m_peer_class);
		TORRENT_ASSERT(tpc);
		tpc->priority[peer_connection::download_channel] = prio;
		tpc->priority[peer_connection::upload_channel] = prio;

		state_updated();
	}
#endif

	void torrent::add_redundant_bytes(int const b, waste_reason const reason)
	{
		TORRENT_ASSERT(is_single_thread());
		TORRENT_ASSERT(b > 0);
		TORRENT_ASSERT(static_cast<int>(reason) >= 0);
		TORRENT_ASSERT(static_cast<int>(reason) < static_cast<int>(waste_reason::max));

		if (m_total_redundant_bytes <= std::numeric_limits<std::int64_t>::max() - b)
			m_total_redundant_bytes += b;
		else
			m_total_redundant_bytes = std::numeric_limits<std::int64_t>::max();

		// the stats counters are 64 bits, so we don't check for overflow there
		m_stats_counters.inc_stats_counter(counters::recv_redundant_bytes, b);
		m_stats_counters.inc_stats_counter(counters::waste_piece_timed_out + static_cast<int>(reason), b);
	}

	void torrent::add_failed_bytes(int const b)
	{
		TORRENT_ASSERT(is_single_thread());
		TORRENT_ASSERT(b > 0);
		if (m_total_failed_bytes <= std::numeric_limits<std::int64_t>::max() - b)
			m_total_failed_bytes += b;
		else
			m_total_failed_bytes = std::numeric_limits<std::int64_t>::max();

		// the stats counters are 64 bits, so we don't check for overflow there
		m_stats_counters.inc_stats_counter(counters::recv_failed_bytes, b);
	}

	// the number of connected peers that are seeds
	int torrent::num_seeds() const
	{
		TORRENT_ASSERT(is_single_thread());
		INVARIANT_CHECK;

		return int(m_num_seeds) - int(m_num_connecting_seeds);
	}

	// the number of connected peers that are not seeds
	int torrent::num_downloaders() const
	{
		TORRENT_ASSERT(is_single_thread());
		INVARIANT_CHECK;

		int const ret = num_peers()
			- m_num_seeds
			- m_num_connecting
			+ m_num_connecting_seeds;
		TORRENT_ASSERT(ret >= 0);
		return ret;
	}

	void torrent::tracker_request_error(tracker_request const& r
		, error_code const& ec, operation_t const op, std::string const& msg
		, seconds32 const retry_interval)
	{
		TORRENT_ASSERT(is_single_thread());

		INVARIANT_CHECK;

// some older versions of clang had a bug where it would fire this warning here
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wmissing-braces"
#endif
		aux::array<bool const, num_protocols, protocol_version> const supports_protocol
		{ {
			m_info_hash.has_v1(),
			m_info_hash.has_v2()
		} };
#ifdef __clang__
#pragma clang diagnostic pop
#endif

#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			debug_log("*** tracker error: (%d) %s [%s] %s", ec.value()
				, ec.message().c_str(), operation_name(op), msg.c_str());
		}
#endif
		if (!(r.kind & tracker_request::scrape_request))
		{
			// announce request
			aux::announce_entry* ae = m_trackers.find_tracker(r.url);
			int fails = 0;
			tcp::endpoint local_endpoint;
			protocol_version hash_version = protocol_version::V1;
			if (ae)
			{
				auto* aep = ae->find_endpoint(r.outgoing_socket);
				if (aep)
				{
					hash_version = r.info_hash == m_info_hash.v1
						? protocol_version::V1 : protocol_version::V2;
					auto& a = aep->info_hashes[hash_version];
					local_endpoint = aep->local_endpoint;
					a.failed(settings().get_int(settings_pack::tracker_backoff)
						, retry_interval);
					a.last_error = ec;
					a.message = msg;
					fails = a.fails;

#ifndef TORRENT_DISABLE_LOGGING
					debug_log("*** increment tracker fail count [ep: %s url: %s %d]"
						, print_endpoint(aep->local_endpoint).c_str(), r.url.c_str(), a.fails);
#endif
					if (ec == boost::system::errc::address_family_not_supported)
					{
						// don't try to announce from this endpoint again
						aep->enabled = false;
#ifndef TORRENT_DISABLE_LOGGING
						debug_log("*** disabling endpoint [ep: %s url: %s ]"
							, print_endpoint(aep->local_endpoint).c_str(), r.url.c_str());
#endif
					}
				}
				else if (r.outgoing_socket)
				{
#ifndef TORRENT_DISABLE_LOGGING
					debug_log("*** no matching endpoint for request [%s, %s]"
						, r.url.c_str(), print_endpoint(r.outgoing_socket.get_local_endpoint()).c_str());
#endif
				}

				// never talk to this tracker again
				if (ec == error_code(410, http_category())) m_trackers.dont_try_again(ae);

				// if all endpoints fail, then we de-prioritize the tracker and try
				// the next one in the tier
				if (std::all_of(ae->endpoints.begin(), ae->endpoints.end()
					, [&](aux::announce_endpoint const& ep)
					{
						for (protocol_version const ih : all_versions)
							if (supports_protocol[ih] && ep.info_hashes[ih].is_working())
								return false;
						return true;
					}))
				{
					m_trackers.deprioritize_tracker(ae);
				}
			}
			if (m_ses.alerts().should_post<tracker_error_alert>()
				|| r.triggered_manually)
			{
				m_ses.alerts().emplace_alert<tracker_error_alert>(get_handle()
					, local_endpoint, fails, hash_version, r.url, op, ec, msg);
			}
		}
		else
		{
			aux::announce_entry* ae = m_trackers.find_tracker(r.url);

			// scrape request
			if (ec == error_code(410, http_category()) && ae)
				m_trackers.dont_try_again(ae);

			// if this was triggered manually we need to post this unconditionally,
			// since the client expects a response from its action, regardless of
			// whether all tracker events have been enabled by the alert mask
			if (m_ses.alerts().should_post<scrape_failed_alert>()
				|| r.triggered_manually)
			{
				tcp::endpoint local_endpoint;
				if (ae)
				{
					auto const* aep = ae->find_endpoint(r.outgoing_socket);
					if (aep != nullptr) local_endpoint = aep->local_endpoint;
				}

				protocol_version hash_version = r.info_hash == m_info_hash.v1
					? protocol_version::V1 : protocol_version::V2;

				m_ses.alerts().emplace_alert<scrape_failed_alert>(get_handle()
					, local_endpoint, hash_version, r.url, ec);
			}
		}
		// announce to the next working tracker
		// We may have changed state into checking by now, in which case we
		// shouldn't keep trying to announce
		if ((!m_abort && !is_paused() && state() != torrent_status::checking_files)
			|| r.event == event_t::stopped)
		{
			// if the tracker that failed was high-priority, make the next attempt high priority as well
			announce_with_tracker(r.event, bool(r.kind & tracker_request::high_priority));
		}
		else
		{
			update_tracker_timer(aux::time_now32());
		}
	}

#ifndef TORRENT_DISABLE_LOGGING
	bool torrent::should_log() const
	{
		return alerts().should_post<torrent_log_alert>();
	}

	TORRENT_FORMAT(2,3)
	void torrent::debug_log(char const* fmt, ...) const noexcept try
	{
		if (!alerts().should_post<torrent_log_alert>()) return;

		va_list v;
		va_start(v, fmt);
		alerts().emplace_alert<torrent_log_alert>(
			const_cast<torrent*>(this)->get_handle(), fmt, v);
		va_end(v);
	}
	catch (std::exception const&) {}
#endif

}
