// Copyright SpartanTools (CodeSpartan), 2019. All rights reserved.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "WebSocketConn.generated.h"

// OnDisconnect
DECLARE_DYNAMIC_DELEGATE_FourParams(FWebSocketDisconnectDelegate, int32, ConnectionId, int32, StatusCode, const FString&, Reason, bool, bWasClean);
// OnError
DECLARE_DYNAMIC_DELEGATE_TwoParams(FWebSocketErrorDelegate, int32, ConnectionId, const FString&, ErrorTxt);
// OnConnect
DECLARE_DYNAMIC_DELEGATE_OneParam(FWebSocketConnectDelegate, int32, ConnectionId);
// OnRawMessageReceived
DECLARE_DYNAMIC_DELEGATE_TwoParams(FWebSocketReceivedRawMessageDelegate, int32, ConnectionId, UPARAM(ref) TArray<uint8>&, Message);
// OnTextMessageReceived
DECLARE_DYNAMIC_DELEGATE_TwoParams(FWebSocketReceivedTextMessageDelegate, int32, ConnectionId, const FString&, Message);

UCLASS(Blueprintable, BlueprintType)
class MMOKITCODE_API AWebSocketConn : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AWebSocketConn();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;
	virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	/* Returns the ID of the new connection.
	 * An example of connection params would be ws://localhost:3000/mmo
	 * In this case, you'd set Path to "mmo"
	 * Supported protocols are: 'ws', 'wss', or 'wss+insecure'
	 */
	UFUNCTION(BlueprintCallable, Category = "Socket")
	void Connect(const FString& Protocol,
		const FString& ServerAddress,
		int32 Port,
		const FString& Path,
		const FWebSocketDisconnectDelegate& OnDisconnected, 
		const FWebSocketErrorDelegate& OnError,
		const FWebSocketConnectDelegate& OnConnected,
		const FWebSocketReceivedRawMessageDelegate& OnRawMessageReceived,
		const FWebSocketReceivedTextMessageDelegate& OnTextMessageReceived,
		int32& ConnectionId);

	/* Disconnect from connection ID. */
	UFUNCTION(BlueprintCallable, Category = "Socket")
	void Disconnect(int32 ConnectionId);

	/* Returning False means we're not connected to socket and the data wasn't sent. "True" doesn't guarantee that it was successfully sent, 
	only that we were still connected when we initiating the sending process. */
	UFUNCTION(BlueprintCallable, Category = "Socket")
	bool SendData(int32 ConnectionId, TArray<uint8> DataToSend);

	/* Returning False means we're not connected to socket and the data wasn't sent. "True" doesn't guarantee that it was successfully sent,
	only that we were still connected when we initiating the sending process. */
	UFUNCTION(BlueprintCallable, Category = "Socket")
	bool SendText(int32 ConnectionId, const FString& TextMessage);

	UFUNCTION(BlueprintPure, meta = (DisplayName = "Append Bytes", CommutativeAssociativeBinaryOperator = "true"), Category = "Socket")
	static TArray<uint8> Concat_BytesBytes(TArray<uint8> A, TArray<uint8> B);

	/** Converts an integer to an array of bytes */
	UFUNCTION(BlueprintPure, meta = (DisplayName = "Int To Bytes", CompactNodeTitle = "->", Keywords = "cast convert", BlueprintAutocast), Category = "Socket")
	static TArray<uint8> Conv_IntToBytes(int32 InInt);

	/** Converts a string to an array of bytes */
	UFUNCTION(BlueprintPure, meta = (DisplayName = "String To Bytes", CompactNodeTitle = "->", Keywords = "cast convert", BlueprintAutocast), Category = "Socket")
	static TArray<uint8> Conv_StringToBytes(const FString& InStr);

	/** Converts a float to an array of bytes */
	UFUNCTION(BlueprintPure, meta = (DisplayName = "Float To Bytes", CompactNodeTitle = "->", Keywords = "cast convert", BlueprintAutocast), Category = "Socket")
	static TArray<uint8> Conv_FloatToBytes(float InFloat);

	/** Converts a byte to an array of bytes */
	UFUNCTION(BlueprintPure, meta = (DisplayName = "Byte To Bytes", CompactNodeTitle = "->", Keywords = "cast convert", BlueprintAutocast), Category = "Socket")
	static TArray<uint8> Conv_ByteToBytes(uint8 InByte);

	UFUNCTION(BlueprintCallable, meta = (DisplayName = "Read Int", Keywords = "read int"), Category = "Socket")
	static int32 Message_ReadInt(UPARAM(ref) TArray<uint8>& Message);

	UFUNCTION(BlueprintCallable, meta = (DisplayName = "Read Byte", Keywords = "read byte int8 uint8"), Category = "Socket")
	static uint8 Message_ReadByte(UPARAM(ref) TArray<uint8>& Message);

	UFUNCTION(BlueprintCallable, meta = (DisplayName = "Read Bytes", Keywords = "read bytes"), Category = "Socket")
	static bool Message_ReadBytes(int32 NumBytes, UPARAM(ref) TArray<uint8>& Message, TArray<uint8>& ReturnArray);

	UFUNCTION(BlueprintCallable, meta = (DisplayName = "Read Float", Keywords = "read float"), Category = "Socket")
	static float Message_ReadFloat(UPARAM(ref) TArray<uint8>& Message);

	UFUNCTION(BlueprintCallable, meta = (DisplayName = "Read String", Keywords = "read string"), Category = "Socket")
	static FString Message_ReadString(UPARAM(ref) TArray<uint8>& Message, int32 StringLength);

	UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Socket")
	bool isConnected(int32 ConnectionId);

	/* Used by the separate threads to print to console on the main thread. */
	static void PrintToConsole(FString Str, bool Error);

	/* Buffer size in bytes. Currently not used. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Socket")
	int32 SendBufferSize = 16384;

	/* Buffer size in bytes. It's set only when creating a socket, never afterwards. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Socket")
	int32 ReceiveBufferSize = 16384;

private:
	TMap<int32, TSharedRef<class IWebSocket>> WebSockets;
	TMap<int32, TArray<uint8>> ReceiveBuffers;
	
	int32 NextConnectionId = 0;
	static bool bPostErrorsToMessageLog;
};