#include "blitz/network/TCPSocket.h"

#include "blitz/network/IPAddress.h"
#include "blitz/network/Network.h"

#include "blitz/misc/Compression.h"
#include "blitz/misc/Log.h"

#ifdef _WIN32
#define ioctl ioctlsocket
#define WOULDBLOCK WSAEWOULDBLOCK
#define MSG_DONTWAIT 0
#else
#define WOULDBLOCK EWOULDBLOCK
#endif


namespace blitz {
namespace network {

TCPSocket::TCPSocket() :
	m_Blocking(false), m_Status(Status::Disconnected), m_Port(0), m_Handle(static_cast<SocketHandle>(INVALID_SOCKET)) {}


TCPSocket::TCPSocket(TCPSocket&& other) {
	m_Handle = other.m_Handle;
	m_Port = other.m_Port;
	m_RemoteAddr = other.m_RemoteAddr;
	m_RemoteIP = other.m_RemoteIP;
	SetStatus(other.GetStatus());
	SetBlocking(other.IsBlocking());
	other.m_Handle = static_cast<SocketHandle>(INVALID_SOCKET);
}

TCPSocket::~TCPSocket() {
	Disconnect();
}

bool TCPSocket::SetBlocking(bool block) {
	unsigned long mode = !block;

	if (ioctl(m_Handle, FIONBIO, &mode) < 0) {
		return false;
	}

	m_Blocking = block;

	return true;
}

bool TCPSocket::IsBlocking() const noexcept {
	return m_Blocking;
}


SocketHandle TCPSocket::GetHandle() const noexcept {
	return m_Handle;
}

void TCPSocket::SetStatus(TCPSocket::Status status) {
	m_Status = status;
}

TCPSocket::Status TCPSocket::GetStatus() const noexcept {
	return m_Status;
}

bool TCPSocket::Connect(const std::string& ip, uint16_t port) {
	IPAddress addr(ip);
	return Connect(addr, port);
}

std::size_t TCPSocket::Send(const std::string& data) {
	return this->Send(reinterpret_cast<const unsigned char*>(data.c_str()), data.length());
}

std::size_t TCPSocket::Send(DataBuffer& buffer) {
	std::string data = buffer.ToString();
	return this->Send(reinterpret_cast<const unsigned char*>(data.c_str()), data.length());
}

void TCPSocket::Disconnect() {
	if (m_Handle < 0)
		closesocket(m_Handle);
	m_Status = Status::Disconnected;
}

bool TCPSocket::Connect(const IPAddress& address, unsigned short port) {
	if (this->GetStatus() == Status::Connected)
		return true;

	struct addrinfo hints {};
	memset(&hints, 0, sizeof(addrinfo));

	struct addrinfo* result = nullptr;

	hints.ai_family = AF_INET;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = IPPROTO_TCP;

	if (::getaddrinfo(address.ToString().c_str(), std::to_string(static_cast<int>(port)).c_str(), &hints, &result) != 0) {
		utils::LOGE("Failed to get address info !");
		return false;
	}

	if ((m_Handle = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
		utils::LOGE("Failed to create socket !");
		return false;
	}

	struct addrinfo* ptr = nullptr;
	for (ptr = result; ptr != nullptr; ptr = ptr->ai_next) {
		struct sockaddr* sockaddr = ptr->ai_addr;
		if (::connect(m_Handle, sockaddr, sizeof(sockaddr_in)) != 0) {
			utils::LOGE("Failed to connect with this address !");
			continue;
		}
		m_RemoteAddr = *sockaddr;
		break;
	}

	freeaddrinfo(result);

	if (!ptr) {
		utils::LOGE("Could not find a suitable interface for connecting !");
		return false;
	}

	this->SetStatus(Status::Connected);
	m_RemoteIP = address;
	m_Port = port;
	return true;
}

size_t TCPSocket::Send(const unsigned char* data, size_t size) {
	if (this->GetStatus() != Status::Connected)
		return 0;

	std::size_t sent = 0;

	while (sent < size) {
		int cur = ::send(m_Handle, reinterpret_cast<const char*>(data + sent), size - sent, 0);
		if (cur <= 0) {
			Disconnect();
			return 0;
		}
		sent += static_cast<std::size_t>(cur);
	}

	return sent;
}

std::size_t TCPSocket::Receive(DataBuffer& buffer, std::size_t amount) {
	buffer.Resize(amount);
	buffer.SetReadOffset(0);

	int recvAmount = ::recv(m_Handle, reinterpret_cast<char*>(buffer.data()), amount, 0);
	if (recvAmount <= 0) {
#if defined(_WIN32) || defined(WIN32)
		int err = WSAGetLastError();
#else
		int err = errno;
#endif
		if (err == WOULDBLOCK) {
			buffer.Clear();
			return 0;
		}

		Disconnect();
		buffer.Clear();
		return 0;
	}
	buffer.Resize(static_cast<std::size_t>(recvAmount));
	return static_cast<std::size_t>(recvAmount);
}

DataBuffer TCPSocket::Receive(std::size_t amount) {
	std::unique_ptr<char[]> buf(new char[amount]);

	int received = ::recv(m_Handle, buf.get(), amount, 0);

	if (received <= 0) {
#if defined(_WIN32) || defined(WIN32)
		int err = WSAGetLastError();
#else
		int err = errno;
#endif
		if (err == WOULDBLOCK)
			return DataBuffer();

		Disconnect();
		return DataBuffer();
	}

	return DataBuffer(std::string(buf.get(), static_cast<std::size_t>(received)));
}

void SendPacket(const DataBuffer& data, network::TCPSocket& socket) {
	DataBuffer compressed = utils::Compress(data);
	socket.Send(reinterpret_cast<const std::uint8_t*>(compressed.ToString().data()), compressed.GetSize());
}

}  // namespace network
}  // namespace blitz
