#include <iostream>
#include <boost/asio.hpp>
#include <boost/lexical_cast.hpp>
#include <thread>
#include <array>
#include <iomanip>


#include <boost/bind.hpp>

#define SSL_R_SHORT_READ 219
#include "ssl/ssl_locl.h"
#include <boost/asio/ssl.hpp>

#if defined(close)
#undef close
#endif

enum AddressType
{
	AT_IPV4 = 0,
	AT_HOST = 3,
	AT_IPV6 = 4
};

class ConnectResponseRecord
{
public:
	std::vector<unsigned char> rawData;

	enum ConnectResponseType
	{
		CRT_SUCCESS = 0,
		CRT_GENERAL_SOCKS_SERVER_FAILURE = 1,
		CRT_CONNECTION_NOT_ALLOWED_BY_RULESET = 2,
		CRT_NETWORK_UNREACHABLE = 3,
		CRT_HOST_UNREACHABLE = 4,
		CRT_CONNECTION_REFUSED = 5,
		CRT_TTL_EXPIRED = 6,
		CRT_COMMAND_NOT_SUPPORTED = 7,
		CRT_ADDRESS_TYPE_NOT_SUPPORTED = 8,
		CRT_TO_FF_UNASSIGNED = 9
	};

	ConnectResponseType connectResponseType;

	AddressType addressType;

	std::string address;

	uint16_t port;
};

class ConnectRequestRecord
{
public:
	std::string address;
	uint16_t port;

	std::string getRequestData();
};

std::string ConnectRequestRecord::getRequestData()
{
	std::string data = "";
	data += 0x05;
	data += 0x01;
	data += char(0x00);
	data += 0x03;
	data += static_cast<unsigned char>(address.size());
	data += address;
	data += port / 256;
	data += port % 256;
	return data;
}

class proxyClient
{
public:
	proxyClient(boost::asio::io_service& ioServiceIn, boost::asio::ssl::context& context, boost::asio::ip::tcp::resolver::iterator endpointIn)
		: ioService(ioServiceIn)
		, socket(ioServiceIn, context)
	{
		socket.set_verify_mode(boost::asio::ssl::verify_peer);
		socket.set_verify_callback(
			std::bind(&proxyClient::verify_certificate, this, std::placeholders::_1, std::placeholders::_2));


		doConnect(endpointIn);
	}

	void close()
	{
		ioService.post([this]() { lowerSocket().close(); });
	}

private:

	boost::asio::ssl::stream<boost::asio::ip::tcp::socket>::lowest_layer_type& lowerSocket()
	{
		return socket.lowest_layer();
	}

	void doConnect(boost::asio::ip::tcp::resolver::iterator endpointIterator)
	{
		boost::asio::async_connect(lowerSocket(), endpointIterator,
			[this](boost::system::error_code ec, boost::asio::ip::tcp::resolver::iterator)
		{
			if (!ec)
			{
				//sendVersion();

				doHandshake();
			}
		});
	}


	void doHandshake()
	{
		socket.async_handshake(boost::asio::ssl::stream_base::client,
			[this](const boost::system::error_code& error) {
		

			if (!error)
			{
				sendVersion();
			}
			else
			{
				std::cout << "Handshake failed: " << error.message() << "\n";
				lowerSocket().close();
			}

		});
	}



	bool verify_certificate(bool preverified,
		boost::asio::ssl::verify_context& ctx)
	{
		// The verify callback can be used to check whether the certificate that is
		// being presented is valid for the peer. For example, RFC 2818 describes
		// the steps involved in doing this for HTTPS. Consult the OpenSSL
		// documentation for more details. Note that the callback is called once
		// for each certificate in the certificate chain, starting from the root
		// certificate authority.

		// In this example we will simply print the certificate's subject name.
		char subject_name[256];
		X509* cert = X509_STORE_CTX_get_current_cert(ctx.native_handle());
		X509_NAME_oneline(X509_get_subject_name(cert), subject_name, 256);
		std::cout << "Verifying " << subject_name << "\n";

		return preverified;
	}


	void sendVersion()
	{
		std::array<char, 3> version = {0x05, 0x01, 0x02};

		boost::asio::async_write(socket,
			boost::asio::buffer(version.data(),
				version.size()),
			[this](boost::system::error_code ec, std::size_t length)
		{
			if (!ec)
			{
				doReadServerVersion();
			}
			else
			{
				lowerSocket().close();
			}
		});
	}

	std::array<unsigned char, 2> serverVersionMethod;
	std::array<unsigned char, 2> serverAuthStatus;

	std::vector<ConnectRequestRecord> connectRequestArr;
	
	void doReadServerVersion()
	{
		boost::asio::async_read(socket,
			boost::asio::buffer(serverVersionMethod.data(), serverVersionMethod.size()),
			[this](boost::system::error_code ec, std::size_t length)
		{
			if (!ec)
			{
				if (serverVersionMethod[0] == 5 && serverVersionMethod[1] == 2)
				{
					doWriteLoginPassword();
				}
				else
				{
					//Others not supported
					lowerSocket().close();
				}
			}
			else
			{
				lowerSocket().close();
			}
		});
	}

	void doWriteLoginPassword()
	{
		std::string login = "telegram-proxy-user";
		std::string password = "telegram-telegram-999112";
		std::string data = "";
		data += 0x01;
		data += static_cast<unsigned char>(login.size());
		data += login;
		data += static_cast<unsigned char>(password.size());
		data += password;

		boost::asio::async_write(socket,
			boost::asio::buffer(data.data(),
				data.size()),
			[this](boost::system::error_code ec, std::size_t length)
		{
			if (!ec)
			{
				doReadAuthStatus();
			}
			else
			{
				lowerSocket().close();
			}
		});
	}


	void connectToWebAddress(std::string address)
	{
		ConnectRequestRecord record = { address, 80 };

		connectRequestArr.push_back(record);

		auto data = record.getRequestData();

		boost::asio::async_write(socket,
			boost::asio::buffer(data.data(),
				data.size()),
			[this](boost::system::error_code ec, std::size_t length)
		{
			if (!ec)
			{
				doReadConnectToWebAddressResponse();
			}
			else
			{
				lowerSocket().close();
			}
		});

	}




	void doWriteSomething()
	{
		//std::array<char, 100> something = { 0x05, 0x01, 0x00, 0x03 };


		std::string data = "GET / HTTP/1.1\r\nHost: www.google.com\r\nConnection: close\r\n\r\n";

		boost::asio::async_write(socket,
			boost::asio::buffer(data.data(),
				data.size()),
			[this](boost::system::error_code ec, std::size_t length)
		{
			if (!ec)
			{
				doReadSomethingText();
			}
			else
			{
				lowerSocket().close();
			}
		});
	}

	void doReadAuthStatus()
	{
		boost::asio::async_read(socket,
			boost::asio::buffer(serverAuthStatus.data(), serverAuthStatus.size()),
			[this](boost::system::error_code ec, std::size_t length)
		{
			if (!ec)
			{
				if (serverAuthStatus[0] == 1 && serverAuthStatus[1] == 0)
				{
					//doWriteSomething();

					connectToWebAddress("www.google.com");
				}
				else
				{
					//Authorization is not succeed
					lowerSocket().close();
				}
			}
			else
			{
				lowerSocket().close();
			}
		});
	}


	void doReadConnectToWebAddressResponse()
	{
		std::shared_ptr<std::array<unsigned char, 5>> firstPartPtr = std::make_shared<std::array<unsigned char, 5>>();

		boost::asio::async_read(socket,
			boost::asio::buffer(firstPartPtr->data(), firstPartPtr->size()),
			[this, firstPartPtr](boost::system::error_code ec, std::size_t length)
		{
			if (!ec)
			{
				doReadConnectToWebAddressResponsePart2(firstPartPtr);
				
			}
			else
			{
				lowerSocket().close();
			}
		});
	}


	void doReadConnectToWebAddressResponsePart2(std::shared_ptr<std::array<unsigned char, 5>> firstPartPtr)
	{
		unsigned int size = 3;
		if ((*firstPartPtr)[3] == AddressType::AT_IPV4) {
			size = 3;
		}
		if ((*firstPartPtr)[3] == AddressType::AT_HOST) {
			size = (*firstPartPtr)[4];
		}
		if ((*firstPartPtr)[3] == AddressType::AT_IPV6) {
			size = 15;
		}

		size += 2; //add port
		
		std::shared_ptr<std::vector<char>> secondPartPtr = std::make_shared<std::vector<char>>();

		secondPartPtr->resize(size);

		boost::asio::async_read(socket,
			boost::asio::buffer(secondPartPtr->data(), secondPartPtr->size()),
			[this, firstPartPtr, secondPartPtr](boost::system::error_code ec, std::size_t length)
		{
			if (!ec)
			{
				ConnectResponseRecord connectResponseRecord;

				connectResponseRecord.rawData.clear();

				for (unsigned char c : *firstPartPtr)
				{
					connectResponseRecord.rawData.push_back(c);
				}

				for (unsigned char c : *secondPartPtr)
				{
					connectResponseRecord.rawData.push_back(c);
				}

				connectResponseRecord.connectResponseType = static_cast<ConnectResponseRecord::ConnectResponseType>(connectResponseRecord.rawData[1]);
				connectResponseRecord.addressType = static_cast<AddressType>(connectResponseRecord.rawData[3]);

				size_t portOffset = 8;

				if (connectResponseRecord.addressType == AddressType::AT_IPV4)
				{
					connectResponseRecord.address = boost::lexical_cast<std::string>(static_cast<unsigned int>(connectResponseRecord.rawData[4]))
						+ "." + boost::lexical_cast<std::string>(static_cast<unsigned int>(connectResponseRecord.rawData[5]))
						+ "." + boost::lexical_cast<std::string>(static_cast<unsigned int>(connectResponseRecord.rawData[6]))
						+ "." + boost::lexical_cast<std::string>(static_cast<unsigned int>(connectResponseRecord.rawData[7]));

					portOffset = 8;
				}

				if (connectResponseRecord.addressType == AddressType::AT_HOST)
				{
					size_t count = connectResponseRecord.rawData[4];

					for (size_t i = 0; i < count; i++)
					{
						connectResponseRecord.address.push_back(connectResponseRecord.rawData[5 + i]);
					}

					portOffset = 5 + count;
				}

				if (connectResponseRecord.addressType == AddressType::AT_IPV6)
				{

					std::stringstream stream;
					stream << std::hex << connectResponseRecord.rawData[4];
					stream << std::hex << connectResponseRecord.rawData[5];
					stream << ":";
					stream << std::hex << connectResponseRecord.rawData[6];
					stream << std::hex << connectResponseRecord.rawData[7];
					stream << ":";
					stream << std::hex << connectResponseRecord.rawData[8];
					stream << std::hex << connectResponseRecord.rawData[9];
					stream << ":";
					stream << std::hex << connectResponseRecord.rawData[10];
					stream << std::hex << connectResponseRecord.rawData[11];
					stream << ":";
					stream << std::hex << connectResponseRecord.rawData[12];
					stream << std::hex << connectResponseRecord.rawData[13];
					stream << ":";
					stream << std::hex << connectResponseRecord.rawData[14];
					stream << std::hex << connectResponseRecord.rawData[15];
					stream << ":";
					stream << std::hex << connectResponseRecord.rawData[16];
					stream << std::hex << connectResponseRecord.rawData[17];
					stream << ":";
					stream << std::hex << connectResponseRecord.rawData[18];
					stream << std::hex << connectResponseRecord.rawData[19];

					connectResponseRecord.address = stream.str();

					portOffset = 20;
				}

				connectResponseRecord.port = connectResponseRecord.rawData[portOffset] * 256 + connectResponseRecord.rawData[portOffset + 1];

				doWriteSomething();
			}
			else
			{
				lowerSocket().close();
			}
		});
	}

	unsigned char item;
	std::string result;

	void doReadSomething()
	{
		boost::asio::async_read(socket,
			boost::asio::buffer(&item, 1),
			[this](boost::system::error_code ec, std::size_t length)
		{
			if (!ec)
			{
				result += boost::lexical_cast<std::string>(static_cast<unsigned int>(item)) + " ";
				std::cout << boost::lexical_cast<std::string>(static_cast<unsigned int>(item)) + " ";
				doReadSomething();
			}
			else
			{
				lowerSocket().close();
			}
		});
	}

	void doReadSomethingText()
	{
		boost::asio::async_read(socket,
			boost::asio::buffer(&item, 1),
			[this](boost::system::error_code ec, std::size_t length)
		{
			if (!ec)
			{
				result += (item);
				std::cout << item;
				doReadSomethingText();
			}
			else
			{
				lowerSocket().close();
			}
		});
	}

private:
	boost::asio::io_service& ioService;

	boost::asio::ssl::stream<boost::asio::ip::tcp::socket> socket;
};



int main()
{
	std::cout << "Hello" << std::endl;
	
	boost::asio::io_service ioService;
	//boost::asio::io_service::work work(ioService);

	boost::asio::ip::tcp::resolver resolver(ioService);
	//auto endpointIterator = resolver.resolve({ "telegram-proxy.fishrungames.com", "8043" });
	auto endpointIterator = resolver.resolve({ "127.0.0.1", "8043" });


	boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
	ctx.load_verify_file("rootca.crt");
	
	
	proxyClient c(ioService, ctx, endpointIterator);

	std::thread t([&ioService]() { ioService.run(); });


	t.join();

	return 0;
}