/*

Copyright (c) 2009, 2013-2021, Arvid Norberg
Copyright (c) 2016-2017, 2020-2021, Alden Torres
All rights reserved.

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

#ifndef TORRENT_I2P_STREAM_HPP_INCLUDED
#define TORRENT_I2P_STREAM_HPP_INCLUDED

#include "libtorrent/config.hpp"

#if TORRENT_USE_I2P

#include <list>
#include <string>
#include <vector>
#include <functional>
#include <cstdarg>
#include <cstdio>

#include "libtorrent/aux_/proxy_base.hpp"
#include "libtorrent/aux_/string_util.hpp"
#include "libtorrent/aux_/vector.hpp"
#include "libtorrent/aux_/proxy_settings.hpp"
#include "libtorrent/assert.hpp"
#include "libtorrent/error_code.hpp"
#include "libtorrent/settings_pack.hpp"
#include "libtorrent/aux_/random.hpp"
#include "libtorrent/hex.hpp" // for to_hex
#include "libtorrent/aux_/debug.hpp"

namespace libtorrent {

namespace i2p_error {

	// error values for the i2p_category error_category.
	enum i2p_error_code
	{
		no_error = 0,
		parse_failed,
		cant_reach_peer,
		i2p_error,
		invalid_key,
		invalid_id,
		timeout,
		key_not_found,
		duplicated_id,
		num_errors
	};

	// hidden
	TORRENT_EXPORT boost::system::error_code make_error_code(i2p_error_code e);
}
}

namespace boost {
namespace system {

template<>
struct is_error_code_enum<libtorrent::i2p_error::i2p_error_code>
{ static const bool value = true; };

}
}


namespace libtorrent {

	// returns the error category for I2P errors
	TORRENT_EXPORT boost::system::error_category& i2p_category();

#if TORRENT_ABI_VERSION == 1
	TORRENT_DEPRECATED
	inline boost::system::error_category& get_i2p_category()
	{ return i2p_category(); }
#endif

struct i2p_session_options
{
	int m_inbound_quantity = 3;
	int m_outbound_quantity = 3;
	int m_inbound_length = 3;
	int m_outbound_length = 3;
	int m_inbound_length_variance = 0;
	int m_outbound_length_variance = 0;
};

struct i2p_stream : aux::proxy_base
{
	explicit i2p_stream(io_context& io_context);
	i2p_stream(i2p_stream&&) noexcept = default;
	// explicitly disallow assignment, to silence msvc warning
	i2p_stream& operator=(i2p_stream const&) = delete;

	enum command_t : std::uint8_t
	{
		cmd_none,
		cmd_create_session,
		cmd_connect,
		cmd_accept,
		cmd_name_lookup,
		cmd_incoming
	};

	void set_command(command_t c) { m_command = c; }

	void set_session_options(i2p_session_options const& session_options)
	{
		m_session_options = session_options;
	}

	void set_session_id(char const* id) { m_id = id; }

	void set_local_i2p_endpoint(string_view d) { m_local = d; }
	std::string const& local_i2p_endpoint() const { return m_local; }
	void set_destination(string_view d) { m_dest = d; }
	std::string const& destination() const { return m_dest; }

	template <class Handler>
	void async_connect(endpoint_type const&, Handler h)
	{
		// since we don't support regular endpoints, just ignore the one
		// provided and use m_dest.

		// the connect is split up in the following steps:
		// 1. resolve name of proxy server
		// 2. connect to SAM bridge
		// 4 send command message (CONNECT/ACCEPT)

		m_resolver.async_resolve(m_hostname, aux::to_string(m_port).data(), aux::wrap_allocator(
			[this](error_code const& ec, tcp::resolver::results_type ips, Handler hn) {
				do_connect(ec, std::move(ips), std::move(hn));
			}, std::move(h)));
	}

	std::string name_lookup() const { return m_name_lookup; }
	void set_name_lookup(char const* name) { m_name_lookup = name; }

	template <typename Handler>
	void send_name_lookup(Handler h)
	{
		TORRENT_ASSERT(m_magic == 0x1337);
		m_state = read_name_lookup_response;
		if (!format_command("NAMING LOOKUP NAME=%s\n", m_name_lookup.c_str()))
		{
			handle_error(boost::asio::error::message_size, h);
			return;
		}
		ADD_OUTSTANDING_ASYNC("i2p_stream::start_read_line");
		async_write(m_sock,
			boost::asio::buffer(m_buffer),
			aux::wrap_allocator(
				[this](error_code const& ec, std::size_t, Handler hn) {
					start_read_line(ec, std::move(hn));
				},
				std::move(h)));
	}

private:
	// formats a SAM command into m_buffer, ready to be handed to async_write().
	// The command has to live in a member, and not in a local buffer, since it
	// must stay valid until the asynchronous write completes, which is after
	// the function starting it has returned. Returns false if the command
	// doesn't fit, in which case it would have been truncated into something
	// the SAM bridge can't parse.
	bool format_command(char const* fmt, ...) TORRENT_FORMAT(2, 3)
	{
		int const capacity = 1024;
		m_buffer.resize(capacity);
		std::va_list args;
		va_start(args, fmt);
		int const size = std::vsnprintf(m_buffer.data(), std::size_t(capacity), fmt, args);
		va_end(args);
		if (size < 0 || size >= capacity)
		{
			m_buffer.clear();
			return false;
		}
		m_buffer.resize(size);
		return true;
	}

	template <typename Handler>
	void do_connect(error_code const& e, tcp::resolver::results_type ips, Handler h)
	{
		TORRENT_ASSERT(m_magic == 0x1337);
		if (e || ips.empty())
		{
			h(e);
			error_code ec;
			close(ec);
			return;
		}

		auto i = ips.begin();
		ADD_OUTSTANDING_ASYNC("i2p_stream::connected");
		m_sock.async_connect(i->endpoint(), aux::wrap_allocator(
			[this](error_code const& ec, Handler hn) {
				connected(ec, std::move(hn));
			}, std::move(h)));
	}

	template <typename Handler>
	void connected(error_code const& e, Handler h)
	{
		TORRENT_ASSERT(m_magic == 0x1337);
		COMPLETE_ASYNC("i2p_stream::connected");
		if (handle_error(e, h)) return;

		// send hello command
		m_state = read_hello_response;
		static const char cmd[] = "HELLO VERSION MIN=3.1 MAX=3.1\n";

		ADD_OUTSTANDING_ASYNC("i2p_stream::start_read_line");
		async_write(m_sock, boost::asio::buffer(cmd, sizeof(cmd) - 1), aux::wrap_allocator(
			[this](error_code const& ec, std::size_t, Handler hn) {
				start_read_line(ec, std::move(hn));
			}, std::move(h)));
	}

	template <typename Handler>
	void start_read_line(error_code const& e, Handler h)
	{
		TORRENT_ASSERT(m_magic == 0x1337);
		COMPLETE_ASYNC("i2p_stream::start_read_line");
		if (handle_error(e, h)) return;

		ADD_OUTSTANDING_ASYNC("i2p_stream::read_line");
		m_buffer.resize(1);
		async_read(m_sock, boost::asio::buffer(m_buffer), aux::wrap_allocator(
			[this](error_code const& ec, std::size_t, Handler hn) {
				read_line(ec, std::move(hn));
			}, std::move(h)));
	}

	template <typename Handler>
	void read_line(error_code const& e, Handler h)
	{
		TORRENT_ASSERT(m_magic == 0x1337);
		COMPLETE_ASYNC("i2p_stream::read_line");
		if (handle_error(e, h)) return;

		auto const read_pos = int(m_buffer.size());

		// look for \n which means end of the response
		if (m_buffer[read_pos - 1] != '\n')
		{
			ADD_OUTSTANDING_ASYNC("i2p_stream::read_line");
			// read another byte from the socket
			m_buffer.resize(read_pos + 1);
			async_read(m_sock, boost::asio::buffer(&m_buffer[read_pos], 1), aux::wrap_allocator(
			[this](error_code const& ec, std::size_t, Handler hn) {
				read_line(ec, std::move(hn));
			}, std::move(h)));
			return;
		}
		m_buffer[read_pos - 1] = 0;

		if (m_command == cmd_incoming)
		{
			// this is the line containing the destination
			// of the incoming connection in an accept call
			m_dest = &m_buffer[0];
			h(e);
			std::vector<char>().swap(m_buffer);
			return;
		}

		error_code invalid_response(i2p_error::parse_failed
			, i2p_category());

		string_view expect1;
		string_view expect2;

		switch (m_state)
		{
			case read_hello_response:
				expect1 = "HELLO"_sv;
				expect2 = "REPLY"_sv;
				break;
			case read_connect_response:
			case read_accept_response:
				expect1 = "STREAM"_sv;
				expect2 = "STATUS"_sv;
				break;
			case read_session_create_response:
				expect1 = "SESSION"_sv;
				expect2 = "STATUS"_sv;
				break;
			case read_name_lookup_response:
				expect1 = "NAMING"_sv;
				expect2 = "REPLY"_sv;
				break;
		}

		TORRENT_ASSERT(m_buffer[int(m_buffer.size()) - 1] == '\0');
		string_view remaining(m_buffer.data(), m_buffer.size() - 1);
		string_view token;

		std::tie(token, remaining) = aux::split_string(remaining, ' ');
		if (expect1.empty() || expect1 != token)
		{ handle_error(invalid_response, h); return; }

		std::tie(token, remaining) = aux::split_string(remaining, ' ');
		if (expect2.empty() || expect2 != token)
		{ handle_error(invalid_response, h); return; }

		int result = 0;

		for(;;)
		{
			string_view name;
			std::tie(name, remaining) = aux::split_string(remaining, '=');
			if (name.empty()) break;
			string_view value;
			if (!remaining.empty() && remaining[0] == '"')
			{
				std::tie(value, remaining) = aux::split_string(remaining.substr(1), '"');
			}
			else
			{
				std::tie(value, remaining) = aux::split_string(remaining, ' ');
			}

			if ("RESULT"_sv == name)
			{
				if ("OK"_sv == value)
					result = i2p_error::no_error;
				else if ("CANT_REACH_PEER"_sv == value)
					result = i2p_error::cant_reach_peer;
				else if ("I2P_ERROR"_sv == value)
					result = i2p_error::i2p_error;
				else if ("INVALID_KEY"_sv == value)
					result = i2p_error::invalid_key;
				else if ("INVALID_ID"_sv == value)
					result = i2p_error::invalid_id;
				else if ("TIMEOUT"_sv == value)
					result = i2p_error::timeout;
				else if ("KEY_NOT_FOUND"_sv == value)
					result = i2p_error::key_not_found;
				else if ("DUPLICATED_ID"_sv == value)
					result = i2p_error::duplicated_id;
				else
					result = i2p_error::num_errors; // unknown error
			}
			/*else if ("MESSAGE" == name)
			{
			}
			else if ("VERSION"_sv == name)
			{
			}*/
			else if ("VALUE"_sv == name)
			{
				m_name_lookup = value;
			}
			else if ("DESTINATION"_sv == name)
			{
				m_dest = value;
			}
		}

		error_code ec(result, i2p_category());
		if (ec)
		{
			std::forward<Handler>(h)(ec);
			return;
		}

		switch (m_state)
		{
		case read_hello_response:
			switch (m_command)
			{
				case cmd_create_session:
					send_session_create(std::move(h));
					break;
				case cmd_accept:
					send_accept(std::move(h));
					break;
				case cmd_connect:
					send_connect(std::move(h));
					break;
				case cmd_none:
				case cmd_name_lookup:
				case cmd_incoming:
					std::forward<Handler>(h)(ec);
					std::vector<char>().swap(m_buffer);
			}
			break;
		case read_connect_response:
		case read_session_create_response:
		case read_name_lookup_response:
			std::forward<Handler>(h)(ec);
			std::vector<char>().swap(m_buffer);
			break;
		case read_accept_response:
			// the SAM bridge is waiting for an incoming
			// connection.
			// wait for one more line containing
			// the destination of the remote peer
			m_command = cmd_incoming;
			m_buffer.resize(1);
			ADD_OUTSTANDING_ASYNC("i2p_stream::read_line");
			async_read(m_sock, boost::asio::buffer(m_buffer), aux::wrap_allocator(
				[this](error_code const& err, std::size_t, Handler hn) {
					read_line(err, std::move(hn));
				}, std::move(h)));
			break;
		}
	}

	template <typename Handler>
	void send_connect(Handler h)
	{
		TORRENT_ASSERT(m_magic == 0x1337);
		m_state = read_connect_response;
		if (!format_command("STREAM CONNECT ID=%s DESTINATION=%s\n", m_id, m_dest.c_str()))
		{
			handle_error(boost::asio::error::message_size, h);
			return;
		}
		ADD_OUTSTANDING_ASYNC("i2p_stream::start_read_line");
		async_write(m_sock,
			boost::asio::buffer(m_buffer),
			aux::wrap_allocator(
				[this](error_code const& ec, std::size_t, Handler hn) {
					start_read_line(ec, std::move(hn));
				},
				std::move(h)));
	}

	template <typename Handler>
	void send_accept(Handler h)
	{
		TORRENT_ASSERT(m_magic == 0x1337);
		m_state = read_accept_response;
		if (!format_command("STREAM ACCEPT ID=%s\n", m_id))
		{
			handle_error(boost::asio::error::message_size, h);
			return;
		}
		ADD_OUTSTANDING_ASYNC("i2p_stream::start_read_line");
		async_write(m_sock,
			boost::asio::buffer(m_buffer),
			aux::wrap_allocator(
				[this](error_code const& ec, std::size_t, Handler hn) {
					start_read_line(ec, std::move(hn));
				},
				std::move(h)));
	}

	template <typename Handler>
	void send_session_create(Handler h)
	{
		TORRENT_ASSERT(m_magic == 0x1337);
		m_state = read_session_create_response;
		if (!format_command(
				"SESSION CREATE STYLE=STREAM ID=%s "
				"DESTINATION=TRANSIENT SIGNATURE_TYPE=7 i2cp.leaseSetEncType=4,0 "
				"inbound.quantity=%d outbound.quantity=%d inbound.length=%d outbound.length=%d "
				"inbound.lengthVariance=%d outbound.lengthVariance=%d\n",
				m_id,
				m_session_options.m_inbound_quantity,
				m_session_options.m_outbound_quantity,
				m_session_options.m_inbound_length,
				m_session_options.m_outbound_length,
				m_session_options.m_inbound_length_variance,
				m_session_options.m_outbound_length_variance))
		{
			handle_error(boost::asio::error::message_size, h);
			return;
		}
		ADD_OUTSTANDING_ASYNC("i2p_stream::start_read_line");
		async_write(m_sock,
			boost::asio::buffer(m_buffer),
			aux::wrap_allocator(
				[this](error_code const& ec, std::size_t, Handler hn) {
					start_read_line(ec, std::move(hn));
				},
				std::move(h)));
	}

	// send and receive buffer
	aux::vector<char> m_buffer;
	char const* m_id = nullptr;
	std::string m_dest;
	std::string m_local;
	std::string m_name_lookup;

	i2p_session_options m_session_options;

	enum state_t : std::uint8_t
	{
		read_hello_response,
		read_connect_response,
		read_accept_response,
		read_session_create_response,
		read_name_lookup_response
	};

	command_t m_command;
	state_t m_state;
};

class TORRENT_EXTRA_EXPORT i2p_connection
{
public:
	explicit i2p_connection(io_context& ios);
	~i2p_connection();
	// explicitly disallow assignment, to silence msvc warning
	i2p_connection& operator=(i2p_connection const&) = delete;

	aux::proxy_settings proxy() const;

	bool is_open() const
	{
		return m_sam_socket
			&& m_sam_socket->is_open()
			&& m_state != sam_connecting;
	}
	template <typename Handler>
	void open(std::string const& hostname, int port,
		i2p_session_options const& session_options, Handler handler)
	{
		// we already seem to have a session to this SAM router
		if (m_hostname == hostname
			&& m_port == port
			&& m_sam_socket
			&& (is_open() || m_state == sam_connecting)) return;

		m_hostname = hostname;
		m_port = port;

		if (m_hostname.empty()) return;

		m_state = sam_connecting;

		char tmp[20];
		aux::random_bytes(tmp);
		m_session_id.resize(sizeof(tmp)*2);
		aux::to_hex(tmp, &m_session_id[0]);

		m_sam_socket = std::make_shared<i2p_stream>(m_io_service);
		m_sam_socket->set_proxy(m_hostname, m_port);
		m_sam_socket->set_command(i2p_stream::cmd_create_session);
		m_sam_socket->set_session_id(m_session_id.c_str());
		m_sam_socket->set_session_options(session_options);

		ADD_OUTSTANDING_ASYNC("i2p_stream::on_sam_connect");
		m_sam_socket->async_connect(tcp::endpoint(), aux::wrap_allocator(
			[this,s=m_sam_socket](error_code const& ec, Handler hn) {
				on_sam_connect(ec, s, std::move(hn));
			}, std::move(handler)));
	}
	void close(error_code&);

	// TODO: make this a string_view
	char const* session_id() const { return m_session_id.c_str(); }
	std::string const& local_endpoint() const { return m_i2p_local_endpoint; }

	template <typename Handler>
	void async_name_lookup(char const* name, Handler handler)
	{
		if (m_state == sam_idle && m_name_lookup.empty() && is_open())
			do_name_lookup(name, std::move(handler));
		else
			m_name_lookup.emplace_back(std::string(name)
				, std::move(handler));
	}

private:

	template <typename Handler>
	void on_sam_connect(error_code const& ec, std::shared_ptr<i2p_stream>, Handler h)
	{
		COMPLETE_ASYNC("i2p_stream::on_sam_connect");
		m_state = sam_idle;

		if (ec)
		{
			h(ec);
			return;
		}

		do_name_lookup("ME", aux::wrap_allocator(
			[this](error_code const& e, char const* dst, Handler hn) {
				set_local_endpoint(e, dst, std::move(hn));
			}, std::move(h)));
	}

	using name_lookup_handler = std::function<void(error_code const&, char const*)>;

	template <typename Handler>
	void do_name_lookup(std::string const& name, Handler handler)
	{
		TORRENT_ASSERT(m_state == sam_idle);
		m_state = sam_name_lookup;
		m_sam_socket->set_name_lookup(name.c_str());
		m_sam_socket->send_name_lookup(aux::wrap_allocator(
			[this,s=m_sam_socket](error_code const& ec, Handler hn) {
				on_name_lookup(ec, s, std::move(hn));
			}, std::move(handler)));
	}

	template <typename Handler>
	void on_name_lookup(error_code const& ec, std::shared_ptr<i2p_stream>, Handler handler)
	{
		m_state = sam_idle;

		std::string name = m_sam_socket->name_lookup();
		if (!m_name_lookup.empty())
		{
			std::pair<std::string, name_lookup_handler>& nl = m_name_lookup.front();
			do_name_lookup(nl.first, std::move(nl.second));
			m_name_lookup.pop_front();
		}

		if (ec)
		{
			handler(ec, nullptr);
			return;
		}

		handler(ec, name.c_str());
	}


	template <typename Handler>
	void set_local_endpoint(error_code const& ec, char const* dest, Handler h)
	{
		if (!ec && dest != nullptr)
			m_i2p_local_endpoint = dest;
		else
			m_i2p_local_endpoint.clear();

		h(ec);
	}

	// to talk to i2p SAM bridge
	std::shared_ptr<i2p_stream> m_sam_socket;
	std::string m_hostname;
	int m_port;

	// our i2p endpoint key
	std::string m_i2p_local_endpoint;
	std::string m_session_id;

	std::list<std::pair<std::string, name_lookup_handler>> m_name_lookup;

	enum state_t
	{
		sam_connecting,
		sam_name_lookup,
		sam_idle
	};

	state_t m_state;

	io_context& m_io_service;
};

}

#endif // TORRENT_USE_I2P

#endif
