// Copyright (c) 2018 - present, Twitch Interactive, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so.
//
// 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 AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.

#pragma once

#ifdef CREATING_TWITCH_SDK_DLL
# define TWITCH_SDK_DLL_API __declspec(dllexport)
#elif defined(USING_TWITCH_SDK_DLL)
# define TWITCH_SDK_DLL_API __declspec(dllimport)
#else
# define TWITCH_SDK_DLL_API
#endif

#include <ctime>
#include <condition_variable>
#include <exception>
#include <functional>
#include <future>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <utility>
#include <vector>

namespace TwitchInGames {
#ifdef _WIN32
	using string_t= wchar_t const*;
	using tstring= std::wstring;
#else
	using string_t= char const*;
	using tstring= std::string;
#endif

	class TWITCH_SDK_DLL_API TwitchException;
	template<typename T> class QueryableFuture;
	template<typename T> class Callback;
	class TWITCH_SDK_DLL_API BrowserAuth;
	class TWITCH_SDK_DLL_API ServerAuth;
	class TWITCH_SDK_DLL_API ClientAuth;
	class TWITCH_SDK_DLL_API User;
	class TWITCH_SDK_DLL_API FollowersRequest;
	class TWITCH_SDK_DLL_API FollowersResponse;
	class TWITCH_SDK_DLL_API Follower;
	class TWITCH_SDK_DLL_API Chat;
	class TWITCH_SDK_DLL_API Clip;
	class TWITCH_SDK_DLL_API StreamsRequest;
	class TWITCH_SDK_DLL_API StreamsResponse;
	class TWITCH_SDK_DLL_API Stream;
}

#pragma warning(disable: 4275) // non dll-interface class 'std::exception' used as base for dll-interface class 'TwitchException'
class TWITCH_SDK_DLL_API TwitchInGames::TwitchException : public std::exception {
public:
	explicit TwitchException(int errorCode) noexcept;

	char const* what() const noexcept override;
	int GetErrorCode() const noexcept { return errorCode; }

	__declspec(property(get=GetErrorCode)) int const ErrorCode;

private:
	char description[128];
	int errorCode;
};
#pragma warning(default: 4275)

#ifdef _WIN32
# pragma warning(disable: 4251) // class '*' needs to have dll-interface to be used by clients of class 'TwitchInGames::*'
#endif

template<typename T>
class TwitchInGames::QueryableFuture {
public:
	enum { NotReady, ValueReady, ExceptionReady };

	explicit QueryableFuture(std::future<T>&& deferredValue) : value(), readyState(NotReady) {
		auto fn= [this](std::future<T>&& deferredValue) {
			try {
				value= deferredValue.get();
				readyState= ValueReady;
			} catch(...) {
				this->ex= std::current_exception();
				readyState= ExceptionReady;
			}
		};
		task= std::async(std::launch::async, fn, std::move(deferredValue));
	}

	int GetReadyState() const noexcept { return readyState; }
	bool GetIsReady() const noexcept { return readyState != NotReady; }
	bool GetHasValue() const noexcept { return readyState == ValueReady; }
	bool GetHasException() const noexcept { return readyState == ExceptionReady; }
	T const& GetValue() const {
		if(GetHasException()) {
			std::rethrow_exception(ex);
		} else {
			return value;
		}
	}
	std::exception_ptr GetException() const noexcept { return ex; }

	__declspec(property(get=GetReadyState)) int const ReadyState;
	__declspec(property(get=GetIsReady)) bool const IsReady;
	__declspec(property(get=GetHasValue)) bool const HasValue;
	__declspec(property(get=GetHasException)) bool const HasException;
	__declspec(property(get=GetValue)) T const& Value;
	__declspec(property(get=GetException)) std::exception_ptr const Exception;

private:
	std::future<void> task;
	T value;
	std::exception_ptr ex;
	int readyState;
};

template<typename T>
class TwitchInGames::Callback {
public:
	Callback(std::future<T>&& deferredValue, std::function<void(T const&, std::exception_ptr)> callback) {
		auto fn= [callback, this](std::future<T>&& deferredValue) {
			T value;
			try {
				value= deferredValue.get();
			} catch(...) {
				callback(T(), std::current_exception());
				return;
			}
			callback(value, nullptr);
		};
		task= std::async(std::launch::async, fn, std::move(deferredValue));
	}

	void CheckForException() {
		if(task.valid()) {
			task.get();
		}
	}

private:
	std::future<void> task;
};

class TWITCH_SDK_DLL_API TwitchInGames::BrowserAuth {
public:
	BrowserAuth();
	~BrowserAuth();

	std::future<tstring> Launch(string_t clientId, string_t redirectUri, string_t responseType, std::vector<string_t> const& scopes, tstring& state, string_t scheme= nullptr);
	void Stop(string_t accessToken);
	tstring GetToken() const;

private:
	std::mutex tokenAccessor;
	using TokenLock= std::unique_lock<decltype(tokenAccessor)>;
#ifdef __ORBIS__
	static void rand_s(unsigned* p);
#else
	std::condition_variable cv;
	bool isReady;
#endif
	tstring token;

	std::future<tstring> Launch(string_t url, tstring const& state, string_t scheme);
};

class TWITCH_SDK_DLL_API TwitchInGames::ServerAuth : TwitchInGames::BrowserAuth {
public:
	ServerAuth();
	~ServerAuth();

	std::future<tstring> StartAuth(string_t clientId, string_t redirectUri, std::vector<string_t> const& scopes, tstring& state);
	void Finish(string_t accessToken, string_t refreshToken);
	void Cancel();
	tstring GetAccessToken() const { return GetToken(); }
	tstring GetRefreshToken() const;

	__declspec(property(get=GetAccessToken)) tstring const AccessToken;
	__declspec(property(get=GetRefreshToken)) tstring const RefreshToken;

private:
	tstring refreshToken;
};

class TWITCH_SDK_DLL_API TwitchInGames::ClientAuth : TwitchInGames::BrowserAuth {
public:
	ClientAuth();
	~ClientAuth();

	std::future<tstring> StartAuth(string_t clientId, string_t redirectUri, std::vector<string_t> const& scopes);
#ifdef __cplusplus_winrt
	void Finish(tstring activationUri);
#endif
	void Cancel();
	tstring GetAccessToken() const { return GetToken(); }

	__declspec(property(get=GetAccessToken)) tstring const AccessToken;

#ifdef _WIN32
private:
# ifdef __cplusplus_winrt
	tstring state;
# else
	std::shared_ptr<struct DdeServer> ddeServer;
# endif
#endif
};

class TWITCH_SDK_DLL_API TwitchInGames::User {
public:
	User();
	~User();

	static std::future<User> FetchCurrent(tstring const& clientId, tstring const& token);
	static std::future<User> FetchOtherById(tstring const& clientId, tstring const& id);
	static std::future<User> FetchOtherByLogin(tstring const& clientId, tstring const& login);
	std::future<std::vector<char>> FetchOfflineImage();
	std::future<std::vector<char>> FetchProfileImage();
	std::future<void> UpdateDescription(tstring const& clientId, tstring const& token, tstring const& description);
	tstring GetBroadcasterType() const { return broadcasterType; }
	tstring GetDescription() const { return description; }
	tstring GetDisplayName() const { return displayName; }
	tstring GetEmail() const { return email; }
	tstring GetId() const { return id; }
	tstring GetLogin() const { return login; }
	tstring GetOfflineImageUrl() const { return offlineImageUrl; }
	tstring GetProfileImageUrl() const { return profileImageUrl; }
	tstring GetType() const { return type; }
	unsigned GetViewCount() const { return viewCount; }

	__declspec(property(get=GetBroadcasterType)) tstring const BroadcasterType;
	__declspec(property(get=GetDescription)) tstring const Description;
	__declspec(property(get=GetDisplayName)) tstring const DisplayName;
	__declspec(property(get=GetEmail)) tstring const Email;
	__declspec(property(get=GetId)) tstring const Id;
	__declspec(property(get=GetLogin)) tstring const Login;
	__declspec(property(get=GetOfflineImageUrl)) tstring const OfflineImageUrl;
	__declspec(property(get=GetProfileImageUrl)) tstring const ProfileImageUrl;
	__declspec(property(get=GetType)) tstring const Type;
	__declspec(property(get=GetViewCount)) unsigned const ViewCount;

private:
	tstring broadcasterType;	// User's broadcaster type: "partner", "affiliate", or "".
	tstring description;		// User's channel description.
	tstring displayName;		// User's display name.
	tstring email;				// User's email address. Returned if the request includes the user:read:edit scope.
	tstring id;					// User's ID.
	tstring login;				// User's login name.
	tstring offlineImageUrl;	// URL of the user's offline image.
	tstring profileImageUrl;	// URL of the user's profile image.
	tstring type;				// User's type: "staff", "admin", "global_mod", or "".
	unsigned viewCount;			// Number of users following this user.

	static std::future<User> FetchOther(tstring const& clientId, string_t query, tstring const& value);
	static User ParseResponseForUser(std::vector<char> const& response);
};

class TWITCH_SDK_DLL_API TwitchInGames::FollowersRequest {
public:
	enum class PaginationDirection { None, Forward, Backward };

	tstring Cursor;
	PaginationDirection Direction;
	unsigned Count;

	FollowersRequest() : Direction(PaginationDirection::None), Count(0) {}
	FollowersRequest(FollowersRequest const&)= default;
	FollowersRequest(FollowersRequest&&)= default;
	FollowersRequest& operator=(FollowersRequest const&)= default;
	FollowersRequest& operator=(FollowersRequest&&)= default;
	~FollowersRequest()= default;
	std::future<FollowersResponse> Fetch(tstring const& clientId, tstring const& token, tstring const& userId);
};

class TWITCH_SDK_DLL_API TwitchInGames::FollowersResponse {
public:
	std::vector<Follower> Followers;
	tstring Cursor;
	unsigned TotalCount;
};

class TWITCH_SDK_DLL_API TwitchInGames::Follower {
public:
	tstring UserId;
	std::time_t FollowedAt;
};

class TWITCH_SDK_DLL_API TwitchInGames::Chat {
public:
	class TWITCH_SDK_DLL_API Channel {
	public:
		using ReceiveCallback= std::function<void(Channel& channel, tstring const& userName, tstring const& message)>;

		Channel(Channel const&)= delete;
		~Channel();

		std::future<void> Depart();
		tstring GetName() const { return name; }
		std::future<void> SendLine(string_t message);

		__declspec(property(get=GetName)) tstring const Name;

	private:
		friend class Chat;

		Chat& chat;
		tstring name;
		ReceiveCallback receiveCallback;
		std::shared_ptr<struct ChannelImpl> channelImpl;
		enum { Opened, Closing, Closed } state;

		Channel(Chat& chat, tstring const& name, ReceiveCallback receiveCallback);
	};

	Chat();
	Chat(tstring const& clientId, tstring const& token);
	Chat(Chat const&)= delete;
	Chat(Chat&&)= default;
	~Chat();

	std::future<void> SignIn(tstring const& clientId, tstring const& token);
	std::future<void> SignOut();
	std::future<std::shared_ptr<Channel>> JoinChannel(tstring const& channelName, Channel::ReceiveCallback receiveCallback);
	std::vector<std::shared_ptr<Channel>> GetChannels() const;
	tstring GetLogin() const { return login; }

	__declspec(property(get=GetChannels)) std::vector<std::shared_ptr<Channel>> const Channels;
	__declspec(property(get=GetLogin)) tstring const Login;

private:
	std::shared_ptr<struct ChatImpl> chatImpl;
	std::map<tstring, std::weak_ptr<Channel>> channels;
	tstring clientId;
	tstring login;
	tstring token;
	std::mutex channelsAccess;

	std::shared_ptr<Channel> CreateChannel(tstring const& channelName, Channel::ReceiveCallback receiveCallback);
};

class TWITCH_SDK_DLL_API TwitchInGames::Clip {
public:
	static std::future<tstring> Create(tstring const& clientId, tstring const& token, tstring const& broadcasterId);
	static std::future<Clip> Fetch(tstring const& clientId, tstring const& id);

	tstring GetId() const { return id; }
	tstring GetUrl() const { return url; }
	tstring GetEmbedUrl() const { return embedUrl; }
	tstring GetBroadcasterId() const { return broadcasterId; }
	tstring GetCreatorId() const { return creatorId; }
	tstring GetVideoId() const { return videoId; }
	tstring GetGameId() const { return gameId; }
	tstring GetLanguage() const { return language; }
	tstring GetTitle() const { return title; }
	unsigned GetViewCount() const { return viewCount; }
	tstring GetCreatedAt() const { return createdAt; }
	tstring GetThumbnailUrl() const { return thumbnailUrl; }

	__declspec(property(get=GetId)) tstring const Id;
	__declspec(property(get=GetUrl)) tstring const Url;
	__declspec(property(get=GetEmbedUrl)) tstring const EmbedUrl;
	__declspec(property(get=GetBroadcasterId)) tstring const BroadcasterId;
	__declspec(property(get=GetCreatorId)) tstring const CreatorId;
	__declspec(property(get=GetVideoId)) tstring const VideoId;
	__declspec(property(get=GetGameId)) tstring const GameId;
	__declspec(property(get=GetLanguage)) tstring const Language;
	__declspec(property(get=GetTitle)) tstring const Title;
	__declspec(property(get=GetViewCount)) unsigned const ViewCount;
	__declspec(property(get=GetCreatedAt)) tstring const CreatedAt;
	__declspec(property(get=GetThumbnailUrl)) tstring const ThumbnailUrl;

private:
	tstring broadcasterId;
	tstring createdAt;
	tstring creatorId;
	tstring embedUrl;
	tstring gameId;
	tstring id;
	tstring language;
	tstring thumbnailUrl;
	tstring title;
	tstring url;
	tstring videoId;
	unsigned viewCount;
};

class TWITCH_SDK_DLL_API TwitchInGames::StreamsRequest {
public:
	enum class PaginationDirection { None, Forward, Backward };

	tstring Cursor;
	PaginationDirection Direction;
	unsigned Count;
	tstring CommunityId;
	tstring GameId;
	tstring Language;
	tstring UserId;
	tstring UserLogin;

	StreamsRequest() : Direction(PaginationDirection::None), Count(0) {}
	StreamsRequest(StreamsRequest const&) = default;
	StreamsRequest(StreamsRequest&&) = default;
	StreamsRequest& operator=(StreamsRequest const&) = default;
	StreamsRequest& operator=(StreamsRequest&&) = default;
	~StreamsRequest() = default;
	std::future<StreamsResponse> Fetch(tstring const& clientId, tstring const& token);
};

class TWITCH_SDK_DLL_API TwitchInGames::StreamsResponse {
public:
	std::vector<Stream> Streams;
	tstring Cursor;
};

class TWITCH_SDK_DLL_API TwitchInGames::Stream {
public:
	enum class StreamType { None, Live, Vodcast };

	std::vector<tstring> CommunityIds;	// Array of community IDs.
	tstring GameId;						// ID of the game being played on the stream.
	tstring Id;							// Stream ID.
	tstring Language;					// Stream language.
	std::time_t StartedAt;				// UTC timestamp.
	tstring ThumbnailUrl;				// Thumbnail URL of the stream. All image URLs have variable width and height. You can replace {width} and {height} with any values to get that size image
	tstring Title;						// Stream title.
	StreamType Type;					// Stream type: "live", "vodcast", or "".
	tstring UserId;						// ID of the user who is streaming.
	unsigned ViewerCount;				// Number of viewers watching the stream at the time of the query.
};

#ifdef _WIN32
# pragma warning(default: 4251)
#endif
