/**********************************************************************
 * LeechCraft - modular cross-platform feature rich internet client.
 * Copyright (C) 2006-2014  Georg Rudoy
 *
 * Boost Software License - Version 1.0 - August 17th, 2003
 *
 * Permission is hereby granted, free of charge, to any person or organization
 * obtaining a copy of the software and accompanying documentation covered by
 * this license (the "Software") to use, reproduce, display, distribute,
 * execute, and transmit the Software, and to prepare derivative works of the
 * Software, and to permit third-parties to whom the Software is furnished to
 * do so, all subject to the following:
 *
 * The copyright notices in the Software and this entire statement, including
 * the above license grant, this restriction and the following disclaimer,
 * must be included in all copies of the Software, in whole or in part, and
 * all derivative works of the Software, unless such copies or derivative
 * works are solely in the form of machine-executable object code generated by
 * a source language processor.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
 * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 **********************************************************************/

#include "toxthread.h"
#include <cstring>
#include <QElapsedTimer>
#include <QMutexLocker>
#include <QtEndian>
#include <QtDebug>
#include <QEventLoop>
#include <tox/tox.h>
#include "util.h"
#include "callmanager.h"

namespace LeechCraft
{
namespace Azoth
{
namespace Sarin
{
	ToxThread::ToxThread (const QString& name,
			const QByteArray& state, const ToxAccountConfiguration& config)
	: Name_ { name }
	, ToxState_ { state }
	, Config_ (config)
	{
	}

	ToxThread::~ToxThread ()
	{
		if (!isRunning ())
			return;

		ShouldStop_ = true;

		wait (2000);
		if (isRunning ())
			terminate ();
	}

	namespace
	{
		template<typename T>
		typename std::result_of<T (const uint8_t*, size_t)>::type DoTox (const QString& str, T f)
		{
			const auto& strUtf8 = str.toUtf8 ();
			return f (reinterpret_cast<const uint8_t*> (strUtf8.constData ()), strUtf8.size ());
		}

		State ToxStatus2State (int toxStatus)
		{
			switch (toxStatus)
			{
			case TOX_USERSTATUS_AWAY:
				return State::SAway;
			case TOX_USERSTATUS_BUSY:
				return State::SDND;
			case TOX_USERSTATUS_INVALID:
				return State::SInvalid;
			case TOX_USERSTATUS_NONE:
			default:
				return State::SOnline;
			}
		}

		bool SetToxStatus (Tox *tox, const EntryStatus& status)
		{
			const auto res = DoTox (status.StatusString_,
					[tox] (auto bytes, auto size)
					{
						return tox_set_status_message (tox, bytes, size);
					});

			if (res < 0)
			{
				qWarning () << Q_FUNC_INFO
						<< "unable to set status message";
				return false;
			}

			int toxStatus = TOX_USERSTATUS_NONE;
			switch (status.State_)
			{
			case SAway:
			case SXA:
				toxStatus = TOX_USERSTATUS_AWAY;
				break;
			case SDND:
				toxStatus = TOX_USERSTATUS_BUSY;
				break;
			default:
				break;
			}

			if (tox_set_user_status (tox, toxStatus) < 0)
			{
				qWarning () << Q_FUNC_INFO
						<< "unable to set state";
				return false;
			}

			return true;
		}
	}

	CallManager* ToxThread::GetCallManager() const
	{
		return CallManager_.get ();
	}

	EntryStatus ToxThread::GetStatus () const
	{
		return Status_;
	}

	void ToxThread::SetStatus (const EntryStatus& status)
	{
		Status_ = status;

		if (IsStoppable ())
			ScheduleFunction ([status, this] (Tox *tox)
					{
						if (SetToxStatus (tox, status))
							emit statusChanged (status);
					});
	}

	void ToxThread::Stop ()
	{
		ShouldStop_ = true;
	}

	bool ToxThread::IsStoppable () const
	{
		return isRunning () && !ShouldStop_;
	}

	namespace
	{
		template<size_t Size>
		QByteArray ToxId2HR (const uint8_t *address)
		{
			QByteArray result;
			auto toHexChar = [] (uint8_t num) -> char
			{
				return num >= 10 ? (num - 10 + 'A') : (num + '0');
			};

			for (size_t i = 0; i < Size; ++i)
			{
				const auto num = address [i];
				result += toHexChar ((num & 0xf0) >> 4);
				result += toHexChar (num & 0xf);
			}

			return result;
		}

		template<size_t Size>
		QByteArray ToxId2HR (const std::array<uint8_t, Size>& address)
		{
			return ToxId2HR<Size> (address.data ());
		}

		QByteArray GetToxAddress (Tox *tox)
		{
			std::array<uint8_t, TOX_FRIEND_ADDRESS_SIZE> address;
			tox_get_address (tox, address.data ());
			return ToxId2HR (address);
		}

		QByteArray GetFriendId (Tox *tox, int32_t friendId)
		{
			std::array<uint8_t, TOX_CLIENT_ID_SIZE> clientId;
			if (tox_get_client_id (tox, friendId, clientId.data ()) == -1)
				throw std::runtime_error ("Cannot get friend's pubkey.");
			return ToxId2HR (clientId);
		}
	}

	QFuture<QByteArray> ToxThread::GetToxId ()
	{
		return ScheduleFunction (&GetToxAddress);
	}

	namespace
	{
		ToxThread::AddFriendResult ToxFAError2ThreadError (int32_t addResult)
		{
			switch (addResult)
			{
			case TOX_FAERR_TOOLONG:
				return ToxThread::AddFriendResult::TooLong;
			case TOX_FAERR_NOMESSAGE:
				return ToxThread::AddFriendResult::NoMessage;
			case TOX_FAERR_OWNKEY:
				return ToxThread::AddFriendResult::OwnKey;
			case TOX_FAERR_ALREADYSENT:
				return ToxThread::AddFriendResult::AlreadySent;
			case TOX_FAERR_BADCHECKSUM:
				return ToxThread::AddFriendResult::BadChecksum;
			case TOX_FAERR_SETNEWNOSPAM:
				return ToxThread::AddFriendResult::NoSpam;
			case TOX_FAERR_NOMEM:
				return ToxThread::AddFriendResult::NoMem;
			case TOX_FAERR_UNKNOWN:
			default:
				return ToxThread::AddFriendResult::Unknown;
			}
		}
	}

	namespace
	{
		QByteArray Hex2Bin (const QByteArray& key)
		{
			return QByteArray::fromHex (key.toLower ());
		}
	}

	QFuture<ToxThread::AddFriendResult> ToxThread::AddFriend (QByteArray toxId, QString msg)
	{
		toxId = Hex2Bin (toxId);
		if (msg.isEmpty ())
			msg = " ";

		return ScheduleFunction ([toxId, msg, this] (Tox *tox)
				{
					if (toxId.size () != TOX_FRIEND_ADDRESS_SIZE)
					{
						qWarning () << Q_FUNC_INFO
								<< "invalid Tox ID";
						return ToxThread::AddFriendResult::InvalidId;
					}

					const auto& msgUtf8 = msg.toUtf8 ();
					const auto addResult = tox_add_friend (tox,
							reinterpret_cast<const uint8_t*> (toxId.constData ()),
							reinterpret_cast<const uint8_t*> (msgUtf8.constData ()),
							msgUtf8.size ());

					if (addResult < 0)
					{
						qWarning () << Q_FUNC_INFO
								<< "unable to add friend:"
								<< addResult;
						return ToxFAError2ThreadError (addResult);
					}

					SaveState ();

					emit gotFriend (addResult);
					return AddFriendResult::Added;
				});
	}

	void ToxThread::AddFriend (QByteArray toxId)
	{
		toxId = Hex2Bin (toxId);
		ScheduleFunction ([toxId, this] (Tox *tox)
				{
					if (toxId.size () != TOX_FRIEND_ADDRESS_SIZE)
						return;

					const auto addResult = tox_add_friend_norequest (tox,
							reinterpret_cast<const uint8_t*> (toxId.constData ()));

					if (addResult < 0)
					{
						qDebug () << Q_FUNC_INFO
								<< "unable to add friend";
						return;
					}

					SaveState ();
					emit gotFriend (addResult);
				});
	}

	void ToxThread::RemoveFriend (const QByteArray& origId)
	{
		ScheduleFunction ([origId, this] (Tox *tox)
				{
					const auto friendNum = GetFriendId (tox, origId);
					if (friendNum < 0)
					{
						qWarning () << Q_FUNC_INFO
								<< "unknown friend"
								<< origId;
						return;
					}

					if (tox_del_friend (tox, friendNum) < 0)
					{
						qWarning () << Q_FUNC_INFO
								<< "unable to delete friend"
								<< origId;
						return;
					}

					emit removedFriend (origId);
					SaveState ();
				});
	}

	QFuture<QByteArray> ToxThread::GetFriendPubkey (qint32 id)
	{
		return ScheduleFunction ([id] (Tox *tox)
				{
					return GetFriendId (tox, id);
				});
	}

	namespace
	{
		EntryStatus GetFriendStatus (Tox *tox, qint32 id)
		{
			if (!tox_get_friend_connection_status (tox, id))
				return { SOffline, {} };

			QString statusStr;
			const auto statusMsgSize = tox_get_status_message_size (tox, id);
			if (statusMsgSize > 0)
			{
				std::unique_ptr<uint8_t []> statusMsg { new uint8_t [statusMsgSize] };
				tox_get_status_message (tox, id, statusMsg.get (), statusMsgSize);
				statusStr = QString::fromUtf8 (reinterpret_cast<char*> (statusMsg.get ()));
			}
			return
				{
					ToxStatus2State (tox_get_user_status (tox, id)),
					statusStr
				};
		}
	}

	QFuture<ToxThread::FriendInfo> ToxThread::ResolveFriend (qint32 id)
	{
		return ScheduleFunction ([id, this] (Tox *tox)
				{
					if (!tox_friend_exists (tox, id))
						throw std::runtime_error ("Friend not found.");

					FriendInfo result;
					result.Pubkey_ = GetFriendId (tox, id);

					char name [TOX_MAX_NAME_LENGTH] = { 0 };
					tox_get_name (tox, id, reinterpret_cast<uint8_t*> (name));
					result.Name_ = QString::fromUtf8 (name);
					result.Status_ = GetFriendStatus (tox, id);

					return result;
				});
	}

	void ToxThread::ScheduleFunctionImpl (const std::function<void (Tox*)>& function)
	{
		QMutexLocker locker { &FQueueMutex_ };
		FQueue_ << function;
	}

	void ToxThread::SaveState ()
	{
		const auto size = tox_size (Tox_.get ());
		if (!size)
			return;

		QByteArray newState { static_cast<int> (size), 0 };
		tox_save (Tox_.get (), reinterpret_cast<uint8_t*> (newState.data ()));

		if (newState == ToxState_)
			return;

		ToxState_ = newState;
		emit toxStateChanged (ToxState_);
	}

	void ToxThread::LoadFriends ()
	{
		auto count = tox_count_friendlist (Tox_.get ());
		qDebug () << Q_FUNC_INFO << count;
		if (count <= 0)
			return;

		std::unique_ptr<int32_t []> friendList { new int32_t [count] };
		count = tox_get_friendlist (Tox_.get (), friendList.get (), count);

		for (uint32_t i = 0; i < count; ++i)
			emit gotFriend (friendList [i]);
	}

	void ToxThread::HandleFriendRequest (const uint8_t *pkey, const uint8_t *data, uint16_t size)
	{
		const auto& toxId = ToxId2HR<TOX_FRIEND_ADDRESS_SIZE> (pkey);
		const auto& pubkey = ToxId2HR<TOX_CLIENT_ID_SIZE> (pkey);
		const auto& msg = QString::fromUtf8 (reinterpret_cast<const char*> (data), size);
		qDebug () << Q_FUNC_INFO << toxId << pubkey << msg;

		emit gotFriendRequest (toxId, pubkey, msg);
	}

	void ToxThread::HandleNameChange (int32_t id, const uint8_t *data, uint16_t)
	{
		const auto& toxId = GetFriendId (Tox_.get (), id);
		const auto& name = QString::fromUtf8 (reinterpret_cast<const char*> (data));
		qDebug () << Q_FUNC_INFO << toxId << name;
		emit friendNameChanged (toxId, name);

		SaveState ();
	}

	void ToxThread::UpdateFriendStatus (int32_t friendId)
	{
		const auto& id = GetFriendId (Tox_.get (), friendId);
		const auto& status = GetFriendStatus (Tox_.get (), friendId);
		emit friendStatusChanged (id, status);
	}

	void ToxThread::HandleTypingChange (int32_t friendId, uint8_t isTyping)
	{
		const auto& id = GetFriendId (Tox_.get (), friendId);
		emit friendTypingChanged (id, isTyping);
	}

	void ToxThread::run ()
	{
		qDebug () << Q_FUNC_INFO;

		Tox_Options opts
		{
			Config_.AllowIPv6_,
			!Config_.AllowUDP_,
			!Config_.ProxyHost_.isEmpty (),
			{ 0 },
			static_cast<uint16_t> (Config_.ProxyPort_)
		};
		if (!Config_.ProxyHost_.isEmpty ())
		{
			const auto size = sizeof (opts.proxy_address) / sizeof (opts.proxy_address [0]);
			std::strncpy (opts.proxy_address, Config_.ProxyHost_.toUtf8 ().constData (), size);
			opts.proxy_address [size - 1] = 0;
		}
		Tox_ = std::shared_ptr<Tox> { tox_new (&opts), &tox_kill };
		CallManager_ = std::make_shared<CallManager> (this, Tox_.get ());

		DoTox (Name_,
				[this] (const uint8_t *bytes, uint16_t size) { tox_set_name (Tox_.get (), bytes, size); });

		SetToxStatus (Tox_.get (), Status_);

		tox_callback_friend_request (Tox_.get (),
				[] (Tox*, const uint8_t *pkey, const uint8_t *data, uint16_t size, void *udata)
				{
					static_cast<ToxThread*> (udata)->HandleFriendRequest (pkey, data, size);
				},
				this);
		tox_callback_name_change (Tox_.get (),
				[] (Tox*, int32_t id, const uint8_t *name, uint16_t len, void *udata)
				{
					static_cast<ToxThread*> (udata)->HandleNameChange (id, name, len + 1);
				},
				this);
		tox_callback_user_status (Tox_.get (),
				[] (Tox*, int32_t friendId, uint8_t, void *udata)
				{
					static_cast<ToxThread*> (udata)->UpdateFriendStatus (friendId);
				},
				this);
		tox_callback_status_message (Tox_.get (),
				[] (Tox*, int32_t friendId, const uint8_t*, uint16_t, void *udata)
				{
					static_cast<ToxThread*> (udata)->UpdateFriendStatus (friendId);
				},
				this);
		tox_callback_connection_status (Tox_.get (),
				[] (Tox*, int32_t friendId, uint8_t, void *udata)
				{
					static_cast<ToxThread*> (udata)->UpdateFriendStatus (friendId);
				},
				this);
		tox_callback_typing_change (Tox_.get (),
				[] (Tox*, int32_t friendId, uint8_t isTyping, void *udata)
				{
					static_cast<ToxThread*> (udata)->HandleTypingChange (friendId, isTyping);
				},
				this);

		emit toxCreated (Tox_.get ());

		if (!ToxState_.isEmpty ())
		{
			const auto res = tox_load (Tox_.get (),
					reinterpret_cast<const uint8_t*> (ToxState_.constData ()),
					ToxState_.size ());
			if (!res)
			{
				qDebug () << "successfully loaded Tox state";
				LoadFriends ();
			}
			else
				qWarning () << "failed to load Tox state";
		}

		qDebug () << "gonna bootstrap..." << Tox_.get ();
		const auto pubkey = Hex2Bin ("F404ABAA1C99A9D37D61AB54898F56793E1DEF8BD46B1038B9D822E8460FAB67");
		tox_bootstrap_from_address (Tox_.get (),
				"192.210.149.121",
				static_cast<uint16_t> (33445),
				reinterpret_cast<const uint8_t*> (pubkey.constData ()));

		bool wasConnected = false;

		QEventLoop evLoop;
		while (!ShouldStop_)
		{
			tox_do (Tox_.get ());
			auto next = tox_do_interval (Tox_.get ());

			if (!wasConnected && tox_isconnected (Tox_.get ()))
			{
				wasConnected = true;
				qDebug () << "connected! tox id is" << GetToxAddress (Tox_.get ());

				emit statusChanged (Status_);

				SaveState ();
			}

			QElapsedTimer timer;
			timer.start ();

			evLoop.processEvents ();

			decltype (FQueue_) queue;
			{
				QMutexLocker locker { &FQueueMutex_ };
				std::swap (queue, FQueue_);
			}
			for (const auto item : queue)
				item (Tox_.get ());

			next -= timer.elapsed ();

			if (next > 0)
			{
				if (next > 100)
					next = 50;

				msleep (next);
			}
		}

		SaveState ();
	}
}
}
}
