#pragma semicolon 1
#pragma newdecls required
#pragma dynamic 65535

#include <sourcemod>
#include <AsyncSocket>
#include <smjansson>

#define MAX_CLIENTS 16

#include "API.inc"
#include "Subscribe.inc"

static AsyncSocket g_ServerSocket;

static AsyncSocket g_Client_Socket[MAX_CLIENTS] = { null, ... };
static int g_Client_Subscriber[MAX_CLIENTS] = { -1, ... };

ConVar g_ListenAddr;
ConVar g_ListenPort;

public Plugin myinfo =
{
	name = "SM JSON API",
	author = "SourceMod TCP JSON API",
	description = "",
	version = "1.0",
	url = ""
}

public void OnPluginStart()
{
	Subscribe_OnPluginStart();

	g_ListenAddr = CreateConVar("sm_jsonapi_addr", "127.0.0.1", "SM JSON API listen ip address", FCVAR_PROTECTED);
	g_ListenPort = CreateConVar("sm_jsonapi_port", "27021", "SM JSON API listen ip address", FCVAR_PROTECTED, true, 1025.0, true, 65535.0);

	AutoExecConfig(true, "plugin.SMJSONAPI");
}

public void OnConfigsExecuted()
{
	if(g_ServerSocket)
		return;

	g_ServerSocket = new AsyncSocket();

	g_ServerSocket.SetConnectCallback(OnAsyncConnect);
	g_ServerSocket.SetErrorCallback(OnAsyncServerError);

	char sAddr[32];
	g_ListenAddr.GetString(sAddr, sizeof(sAddr));
	int Port = g_ListenPort.IntValue;

	g_ServerSocket.Listen(sAddr, Port);
	LogMessage("Listening on %s:%d", sAddr, Port);
}

static void OnAsyncConnect(AsyncSocket socket)
{
	int Client = GetFreeClientIndex();
	LogMessage("OnAsyncConnect(Client=%d)", Client);

	if(Client == -1)
	{
		delete socket;
		return;
	}

	g_Client_Socket[Client] = socket;
	socket.SetErrorCallback(OnAsyncClientError);
	socket.SetDataCallback(OnAsyncData);
}

static void OnAsyncServerError(AsyncSocket socket, int error, const char[] errorName)
{
	SetFailState("OnAsyncServerError(): %d, \"%s\"", error, errorName);
}

static void OnAsyncClientError(AsyncSocket socket, int error, const char[] errorName)
{
	int Client = ClientFromSocket(socket);
	LogMessage("OnAsyncClientError(Client=%d): %d, \"%s\"", Client, error, errorName);

	if(Client == -1)
	{
		delete socket;
		return;
	}

	if(g_Client_Subscriber[Client] != -1)
	{
		Subscriber_Destroy(g_Client_Subscriber[Client]);
		g_Client_Subscriber[Client] = -1;
	}

	g_Client_Socket[Client] = null;
	delete socket;
}

static void OnAsyncData(AsyncSocket socket, const char[] data, const int size)
{
	int Client = ClientFromSocket(socket);

	int iLine;
	int iColumn;
	static char sError[256];
	JSONValue jRequest = json_load_ex(data, sError, sizeof(sError), iLine, iColumn);

	if(jRequest == null)
	{
		JSONObject jResponse = new JSONObject();

		JSONObject jError = new JSONObject();
		jError.SetString("type", "json");
		jError.SetString("error", sError);
		jError.SetInt("line", iLine);
		jError.SetInt("column", iColumn);

		jResponse.Set("error", jError);

		jResponse.ToString(sError, sizeof(sError));
		socket.WriteNull(sError);

		delete jResponse;
		return;
	}

	if(jRequest.IsObject)
	{
		//view_as<JSONObject>(jRequest).DumpToServer();
		JSONObject jResponse = new JSONObject();
		// Negative values and objects indicate errors
		// 0 and positive integers indicate success
		jResponse.SetInt("error", 0);

		HandleRequest(Client, view_as<JSONObject>(jRequest), jResponse);

		static char sResponse[4096];
		jResponse.ToString(sResponse, sizeof(sResponse));
		socket.WriteNull(sResponse);
		delete jResponse;
	}

	delete jRequest;
}

static int HandleRequest(int Client, JSONObject jRequest, JSONObject jResponse)
{
	static char sMethod[32];
	if(jRequest.GetString("method", sMethod, sizeof(sMethod)) < 0)
	{
		JSONObject jError = new JSONObject();
		jError.SetString("error", "Request has no 'method' string-value.");
		jResponse.Set("error", jError);
		return -1;
	}
	jResponse.SetString("method", sMethod);

	if(StrEqual(sMethod, "subscribe") || StrEqual(sMethod, "unsubscribe") || StrEqual(sMethod, "replay"))
	{
		int Subscriber = g_Client_Subscriber[Client];
		if(Subscriber == -1)
		{
			Subscriber = Subscriber_Create(Client);
			if(Subscriber < 0)
			{
				JSONObject jError = new JSONObject();
				jError.SetString("type", "subscribe");
				jError.SetString("error", "Could not allocate a subscriber.");
				jError.SetInt("code", Subscriber);
				jResponse.Set("error", jError);
				return -1;
			}
			g_Client_Subscriber[Client] = Subscriber;
		}

		return Subscriber_HandleRequest(Subscriber, jRequest, jResponse);
	}
	else if(StrEqual(sMethod, "function"))
	{
		static char sFunction[64];
		if(jRequest.GetString("function", sFunction, sizeof(sFunction)) < 0)
		{
			JSONObject jError = new JSONObject();
			jError.SetString("error", "Request has no 'function' string-value.");
			jResponse.Set("error", jError);
			return -1;
		}
		jResponse.SetString("function", sFunction);

		static char sAPIFunction[64];
		Format(sAPIFunction, sizeof(sAPIFunction), "API_%s", sFunction);

		Function Fun = GetFunctionByName(INVALID_HANDLE, sAPIFunction);
		if(Fun == INVALID_FUNCTION)
		{
			int Res = Call_StartNative(sFunction);
			if(!Res)
			{
				JSONObject jError = new JSONObject();
				jError.SetString("error", "Invalid function specified.");
				jError.SetInt("code", Res);
				jResponse.Set("error", jError);
				return -1;
			}
		}
		else
			Call_StartFunction(INVALID_HANDLE, Fun);

		JSONArray jArgsArray = view_as<JSONArray>(jRequest.Get("args"));
		if(jArgsArray == null || !jArgsArray.IsArray)
		{
			delete jArgsArray;
			Call_Cancel();
			JSONObject jError = new JSONObject();
			jError.SetString("error", "Request has no 'args' array-value.");
			jResponse.Set("error", jError);
			return -1;
		}

		int aiValues[32];
		int iValues = 0;

		float afValues[32];
		int fValues = 0;

		static char asValues[16][1024];
		int sValues = 0;

		for(int i = 0; i < jArgsArray.Length; i++)
		{
			bool Fail = false;
			JSONValue jValue;
			jValue = jArgsArray.Get(i);

			char sType1[32];
			jValue.TypeToString(sType1, sizeof(sType1));

			if(jValue.IsArray)
			{
				JSONArray jValueArray = view_as<JSONArray>(jValue);
				JSONValue jArrayValue = jValueArray.Get(0);

				char sType2[32];
				jArrayValue.TypeToString(sType2, sizeof(sType2));

				if(jArrayValue.IsInteger || jArrayValue.IsBoolean)
				{
					int iValues_ = iValues;
					for(int j = 0; j < jValueArray.Length; j++)
					{
						JSONInteger jInteger = view_as<JSONInteger>(jValueArray.Get(j));
						aiValues[iValues_++] = jInteger.Value;
						delete jInteger;
					}

					Call_PushArrayEx(aiValues[iValues], jValueArray.Length, SM_PARAM_COPYBACK);
					iValues += jValueArray.Length;
				}
				else if(jArrayValue.IsFloat)
				{
					int fValues_ = fValues;
					for(int j = 0; j < jValueArray.Length; j++)
					{
						JSONFloat jFloat = view_as<JSONFloat>(jValueArray.Get(j));
						afValues[fValues_++] = jFloat.Value;
						delete jFloat;
					}

					Call_PushArrayEx(afValues[fValues], jValueArray.Length, SM_PARAM_COPYBACK);
					fValues += jValueArray.Length;
				}
				/*else if(jArrayValue.IsString)
				{
					int sValues_ = sValues;
					for(int j = 0; j < jValueArray.Length; j++)
					{
						JSONString jString = view_as<JSONString>(jValueArray.Get(j));
						jString.GetString(asValues[sValues_++], sizeof(asValues[]));
						delete jString;
					}

					Call_PushArrayEx(view_as<int>(asValues[sValues]), jValueArray.Length, SM_PARAM_COPYBACK);
					sValues += jValueArray.Length;
				}*/
				else if(jArrayValue.IsArray) // Special
				{
					static char sSpecial[32];
					view_as<JSONArray>(jArrayValue).GetString(0, sSpecial, sizeof(sSpecial));

					if(StrEqual(sSpecial, "NULL_VECTOR"))
						Call_PushArrayEx(NULL_VECTOR, 3, 0);
					else if(StrEqual(sSpecial, "NULL_STRING"))
						Call_PushString(NULL_STRING);
					else
						Fail = true;
				}
				else
					Fail = true;

				if(Fail)
				{
					Call_Cancel();
					delete jArrayValue;
					delete jValue;
					delete jArgsArray;

					static char sType[16];
					jValue.TypeToString(sType, sizeof(sType));

					char sError[128];
					FormatEx(sError, sizeof(sError), "Unsupported parameter in list %d of type '%s'", i, sType);

					JSONObject jError = new JSONObject();
					jError.SetString("error", sError);
					jResponse.Set("error", jError);
					return -1;
				}
				delete jArrayValue;
			}
			else if(jValue.IsInteger || jValue.IsBoolean)
			{
				aiValues[iValues] = view_as<JSONInteger>(jValue).Value;
				Call_PushCell(aiValues[iValues++]);
			}
			else if(jValue.IsFloat)
			{
				afValues[fValues] = view_as<JSONFloat>(jValue).Value;
				Call_PushFloat(afValues[fValues++]);
			}
			else if(jValue.IsString)
			{
				// Broken: view_as<JSONString>(jValue).GetString(asValues[sValues], sizeof(asValues[]));
				JSONString jString = view_as<JSONString>(jValue);
				jString.GetString(asValues[sValues], sizeof(asValues[]));
				Call_PushStringEx(asValues[sValues++], sizeof(asValues[]), SM_PARAM_STRING_COPY, SM_PARAM_COPYBACK);
			}
			else
			{
				Call_Cancel();
				delete jValue;
				delete jArgsArray;

				static char sType[16];
				jValue.TypeToString(sType, sizeof(sType));

				char sError[128];
				FormatEx(sError, sizeof(sError), "Unsupported parameter %d of type '%s'", i, sType);

				JSONObject jError = new JSONObject();
				jError.SetString("error", sError);
				jResponse.Set("error", jError);
				return -1;
			}
			delete jValue;
		}

		int Result;
		static char sException[1024];
		int Error = Call_FinishEx(Result, sException, sizeof(sException));

		if(Error != SP_ERROR_NONE)
		{
			delete jArgsArray;
			JSONObject jError = new JSONObject();
			jError.SetInt("error", Error);
			jError.SetString("exception", sException);
			jResponse.Set("error", jError);
			return -1;
		}

		jResponse.SetInt("result", Result);

		JSONArray jArgsResponse = new JSONArray();
		iValues = 0;
		fValues = 0;
		sValues = 0;

		for(int i = 0; i < jArgsArray.Length; i++)
		{
			JSONValue jValue;
			jValue = jArgsArray.Get(i);

			if(jValue.IsArray)
			{
				JSONArray jArrayResponse = new JSONArray();
				JSONArray jValueArray = view_as<JSONArray>(jValue);
				JSONValue jArrayValue = jValueArray.Get(0);

				if(jArrayValue.IsInteger || jArrayValue.IsBoolean)
				{
					for(int j = 0; j < jValueArray.Length; j++)
						jArrayResponse.AppendInt(aiValues[iValues++]);
				}
				else if(jArrayValue.IsFloat)
				{
					for(int j = 0; j < jValueArray.Length; j++)
						jArrayResponse.AppendFloat(afValues[fValues++]);
				}
				else if(jArrayValue.IsString)
				{
					for(int j = 0; j < jValueArray.Length; j++)
						jArrayResponse.AppendString(asValues[sValues++]);
				}
				else if(jArrayValue.IsArray) // Special
				{
					static char sSpecial[32];
					view_as<JSONArray>(jArrayValue).GetString(0, sSpecial, sizeof(sSpecial));
					jArrayResponse.AppendString(sSpecial);
				}
				delete jArrayValue;
				jArgsResponse.Append(jArrayResponse);
			}
			else if(jValue.IsInteger || jValue.IsBoolean)
			{
				jArgsResponse.AppendInt(aiValues[iValues++]);
			}
			else if(jValue.IsFloat)
			{
				jArgsResponse.AppendFloat(afValues[fValues++]);
			}
			else if(jValue.IsString)
			{
				jArgsResponse.AppendString(asValues[sValues++]);
			}
			delete jValue;
		}

		jResponse.Set("args", jArgsResponse);

		delete jArgsArray;
		return 0;
	}

	JSONObject jError = new JSONObject();
	jError.SetString("error", "No handler found for requested method.");
	jResponse.Set("error", jError);
	return -1;
}

int PublishEvent(int Client, JSONObject Object)
{
	if(Client < 0 || Client > MAX_CLIENTS || g_Client_Socket[Client] == null)
		return -1;

	static char sEvent[4096];
	Object.ToString(sEvent, sizeof(sEvent));
	g_Client_Socket[Client].WriteNull(sEvent);

	return 0;
}

static int GetFreeClientIndex()
{
	for(int i = 0; i < MAX_CLIENTS; i++)
	{
		if(!g_Client_Socket[i])
			return i;
	}
	return -1;
}

static int ClientFromSocket(AsyncSocket socket)
{
	for(int i = 0; i < MAX_CLIENTS; i++)
	{
		if(g_Client_Socket[i] == socket)
			return i;
	}
	return -1;
}
