VUHDO_COMMS_VERSION = 1;

local sReceiveMode = 1;
local sLastCmd = nil;

local sMaxPacketSize = 254;
local sPacketSendInterval = 0.1;
local sTrivialTimeout = 3;
local sUserDialogTimeout = 15;
local sMaxReceiveSize = 100000;

local sFieldSeparator = "!";
local sCommandSeparator = "§";

local sPrefixCommand = "cmd";
local sPrefixRequest = "rqu";
local sPrefixReply   = "rpy";

local sCmdAbortComms = sPrefixCommand .. "ABORT";

local sCmdVersionRequest = sPrefixRequest .. "VERSION";
local sCmdVersionReply   = sPrefixReply   .. "VERSION"; -- CommsVersion, VuhDo version, locale

local sCmdUserYesNoRequest = sPrefixRequest .. "YESNO";
local sCmdUserYesNoReply   = sPrefixReply   .. "YESNO";

sCmdProfileDataChunk = sPrefixCommand .. "P_DAT";
sCmdProfileDataEnd   = sPrefixCommand .. "P_END";

sCmdKeyLayoutDataChunk = sPrefixCommand .. "K_DAT";
sCmdKeyLayoutDataEnd   = sPrefixCommand .. "K_END";

local sRequestsInProgress = { --[[ [unitName][replyType] = { endTime, aResumable } ]] };
local sRepliesReceived = { --[[ [unitName][replyType] = replyString ]] };
local sUserData = { --[[ [unitName][requestType] = userData ]] };
local sBlockedSenders = { --[[ [unitName] = true ]] };

local sBusyUnitName = nil;
local sNumChunks = 0;

local sCurrentChunkTag = nil;
local sCurrentEndTag = nil;
local sCurrentQuestionText = nil;

--
function VUHDO_commsSetReceiveModeEnabled(aFlag)
	sReceiveMode = aFlag and -1 or 1;
end



--
local function VUHDO_setUserData(aUnitName, aTag, someData)
	if (sUserData[aUnitName] == nil) then
		sUserData[aUnitName] = {};
	end

	sUserData[aUnitName][aTag] = someData;
end



--
local function VUHDO_getUserData(aUnitName, aTag)
	return (sUserData[aUnitName] or {})[aTag];
end



--
local function VUHDO_setRequestInfo(aUnitName, aReplyType, aTimeout, aResumable)
	if (sRequestsInProgress[aUnitName] == nil) then
		sRequestsInProgress[aUnitName] = {};
	end

	if ((aTimeout or 0) > 0) then
		sRequestsInProgress[aUnitName][aReplyType] = { GetTime() + aTimeout, aResumable };
	else
		sRequestsInProgress[aUnitName][aReplyType] = nil;
	end
end



--
local function VUHDO_setReplyData(aUnitName, aReplyType, someData)
	if (sRepliesReceived[aUnitName] == nil) then
		sRepliesReceived[aUnitName] = { };
	end

	sRepliesReceived[aUnitName][aReplyType] = someData;
end



--
local function VUHDO_getReplyData(aUnitName, aReplyType)
	if (sRepliesReceived[aUnitName] == nil) then
		return nil;
	end

	return sRepliesReceived[aUnitName][aReplyType];
end



--
function VUHDO_removeCommsData(aUnitName)
	if (aUnitName ~= nil) then
		sUserData[aUnitName] = nil;
		sRequestsInProgress[aUnitName] = nil;
		sRepliesReceived[aUnitName] = nil;
		if (sBusyUnitName == aUnitName) then
			sBusyUnitName = nil;
			sNumChunks = 0;
		end
	else
		table.wipe(sUserData);
		table.wipe(sRequestsInProgress);
		table.wipe(sRepliesReceived);
		sBusyUnitName = nil;
		sNumChunks = 0;
	end
end



--
function VUHDO_getCommsData(aUnitName)

	if (aUnitName ~= nil) then
		VUHDO_xMsg("Dumping comms data for", aUnitName);
		VUHDO_xMsg("sUserData", VUHDO_tableToString(sUserData[aUnitName]));
		VUHDO_xMsg("sRequestsInProgress", VUHDO_tableToString(sRequestsInProgress[aUnitName]));
		VUHDO_xMsg("sRepliesReceived", VUHDO_tableToString(sRepliesReceived[aUnitName]));
		if (sBusyUnitName == aUnitName) then
			VUHDO_xMsg("sBusyUnitName", sBusyUnitName);
			VUHDO_xMsg("sNumChunks", sNumChunks);
		end
	else
		VUHDO_xMsg("Dumping comms data");
		VUHDO_xMsg("sUserData", (sUserData));
		VUHDO_xMsg("sRequestsInProgress", VUHDO_tableToString(sRequestsInProgress));
		VUHDO_xMsg("sRepliesReceived", VUHDO_tableToString(sRepliesReceived));
		if (sBusyUnitName == aUnitName) then
			VUHDO_xMsg("sBusyUnitName", sBusyUnitName);
			VUHDO_xMsg("sNumChunks", sNumChunks);
		end
	end

end



--
local function VUHDO_getFieldsFromReply(aUnitName, aReplyType)
	local tUnitReplies = sRepliesReceived[aUnitName];

	if (tUnitReplies == nil) then
		return nil;
	end

	local tReply = tUnitReplies[aReplyType];
	if (tReply == nil) then
		return nil;
	end

	return strsplit(sFieldSeparator, tReply);
end



--
local function VUHDO_sendMessage(aUnitName, aMessage, aCallbackFn, aCallbackArg)
	ChatThrottleLib:SendAddonMessage("BULK", VUHDO_COMMS_PREFIX, aMessage, "WHISPER", aUnitName, nil, aCallbackFn, aCallbackArg);
end



--
local function VUHDO_sendDirectMessage(aUnitName, aMessage)
	SendAddonMessage(VUHDO_COMMS_PREFIX, aMessage, "WHISPER", aUnitName);
end



--
function VUHDO_sendAbortMessage(aUnitName)
	VUHDO_sendDirectMessage(aUnitName, sCmdAbortComms);
end



--
local function VUHDO_trivialRequest(aUnitName, aRequest, aReplyType, aTimeout, aResumable)
	VUHDO_sendMessage(aUnitName, aRequest, nil);
	VUHDO_setRequestInfo(aUnitName, aReplyType, aTimeout, aResumable);
end



--
local function VUHDO_buildMessage(aCommand, ...)
	local tData = aCommand .. sCommandSeparator;

	for tCnt = 1, select('#', ...) do
		tData = tData .. select(tCnt, ...) .. (tCnt < select('#', ...) and sFieldSeparator or "");
	end

	return tData;
end



--
local function VUHDO_allDataSentCallback()
	VUHDO_Msg("Done. You may now send again to this player.");
	VuhDoLnfShareDialog:Hide();
end



--
local function VUHDO_dataChunkCallback(aProgress)
	VuhDoLnfShareDialogTransmitPaneProgressBar:SetProgress(aProgress[1], aProgress[2]);
end



--
local function VUHDO_sendPacketedMessage(aUnitName, someData, aDataCommand, anEndCommand)
	local tIndex = 0;
	local tChunk;
	local tLength;
	local tPackets;
	local tCurPacket = 0;
	local tMaxLength = sMaxPacketSize - #VUHDO_COMMS_PREFIX - #aDataCommand - #sCommandSeparator;

	tPackets = floor(#someData / (tMaxLength + 1));
	VUHDO_Msg("Sending " .. tPackets .. " packets to " .. aUnitName .. ".");

	while (tIndex < #someData) do
		if (tIndex + tMaxLength < #someData) then
			tLength = tMaxLength;
		else
			tLength = #someData - tIndex;
		end

		tChunk = strsub(someData, tIndex + 1, tIndex + tLength);
		VUHDO_sendMessage(aUnitName, VUHDO_buildMessage(aDataCommand, tChunk), VUHDO_dataChunkCallback, { tCurPacket, tPackets });
		tIndex = tIndex + tLength;
		tCurPacket = tCurPacket + 1;
	end

	VUHDO_sendMessage(aUnitName, anEndCommand, VUHDO_allDataSentCallback);
end



--
local function VUHDO_requestVuhdoVersion(aUnitName, aResumable)
	VUHDO_trivialRequest(aUnitName, sCmdVersionRequest, sCmdVersionReply, sTrivialTimeout, aResumable);
end



--
local function VUHDO_isVersionCompatible(aUnitName)
	local tIsCompatible = true;
	local tCommsVersion, tVuhDoVersion, tLocale = VUHDO_getFieldsFromReply(aUnitName, sCmdVersionReply);

	if (tCommsVersion == nil or tVuhDoVersion == nil or tLocale == nil) then
		VUHDO_Msg("Aborting: Version check failed.", 1, 0.4, 0.4);
		return false;
	end

	if (VUHDO_COMMS_VERSION ~= tonumber(tCommsVersion)) then
		VUHDO_Msg("Aborting: VuhDo comms version mismatch! Please align VuhDo versions.", 1, 0.4, 0.4);
		tIsCompatible = false;
	end

	local tMyVersion, tOtherVersion;
	if (tonumber(VUHDO_VERSION) ~= nil and tonumber(tVuhDoVersion) ~= nil) then
		tMyVersion, tOtherVersion = tonumber(VUHDO_VERSION), tonumber(tVuhDoVersion);
	else
		tMyVersion, tOtherVersion = tostring(VUHDO_VERSION), tostring(tVuhDoVersion);
	end

	if (tMyVersion ~= tOtherVersion) then
		VUHDO_Msg("Aborting: VuhDo version mismatch. Please align VuhDo versions! (my: " .. tMyVersion .. " / receiver:" .. tOtherVersion .. ")", 1, 0.4, 0.4);
		tIsCompatible = false;
	end

	if (GetLocale() ~= tLocale) then
		VUHDO_Msg("Aborting: Language mismatch. Sender has locale " .. tLocale, 1, 0.4, 0.4);
		tIsCompatible = false;
	end

	if (tIsCompatible) then
		VUHDO_Msg("-- VuhDo Version of " .. aUnitName .. " is compatible.");
	else
		VuhDoLnfShareDialog:Hide();
	end

	return tIsCompatible;
end



--
local function VUHDO_requestUserYesNoQuestion(aUnitName, aQuestion, aResumable)
	local tRequest = VUHDO_buildMessage(sCmdUserYesNoRequest, aQuestion);
	VUHDO_trivialRequest(aUnitName, tRequest, sCmdUserYesNoReply, sUserDialogTimeout, aResumable);
end



--
local function VUHDO_confirmedUserYesNoQuestion(aUnitName)
	local tAnswer = VUHDO_getFieldsFromReply(aUnitName, sCmdUserYesNoReply);
	if (tostring(VUHDO_YES) == tAnswer) then
		VUHDO_Msg("-- User " .. aUnitName .. " confirmed transaction.");
		return true;
	else
		VUHDO_Msg("Aborting: User " .. aUnitName .. " declined transaction.", 1, 0.4, 0.4);
		VuhDoLnfShareDialog:Hide();
		return false;
	end
end



--
function VUHDO_compressForSending(aTable)
	local compressedString = VUHDO_compressAndPackTable(aTable);
	local encodedString = VUHDO_LibCompressEncode:Encode(compressedString);

	return encodedString;
end



--
function VUHDO_decompressFromSending(aString)
	local decodedString = VUHDO_LibCompressEncode:Decode(aString);
	local decompressedTable = VUHDO_decompressIfCompressed(decodedString);

	return decompressedTable;
end



--
local function VUHDO_doShareResumeAcceptQuestion(aUnitName)
	if (not VUHDO_confirmedUserYesNoQuestion(aUnitName)) then
		VUHDO_removeCommsData(aUnitName);
		return;
	end

	local tData = VUHDO_getUserData(aUnitName, sCurrentChunkTag);
	VUHDO_sendPacketedMessage(aUnitName, VUHDO_compressForSending(tData), sCurrentChunkTag, sCurrentEndTag);
end



--
local function VUHDO_doShareResumeVersion(aUnitName)
	if (not VUHDO_isVersionCompatible(aUnitName)) then
		VUHDO_removeCommsData(aUnitName);
		return;
	end

	VUHDO_requestUserYesNoQuestion(aUnitName, sCurrentQuestionText, VUHDO_doShareResumeAcceptQuestion);
end




--
function VUHDO_startShare(aUnitName, aTable, aChunkTag, anEndTag, aQuestionText)
	sCurrentChunkTag = aChunkTag;
	sCurrentEndTag = anEndTag;
	sCurrentQuestionText = aQuestionText;

	VUHDO_setUserData(aUnitName, aChunkTag, aTable);
	VUHDO_requestVuhdoVersion(aUnitName, VUHDO_doShareResumeVersion);
end



--
local function VUHDO_addReplyData(aUnitName, aType, someNewData)
	local tReplyData = VUHDO_getReplyData(aUnitName, aType) or "";

	tReplyData = tReplyData .. someNewData;

	if (#tReplyData > sMaxReceiveSize) then
		VUHDO_Msg("Aborting: Received amount of data from " .. aUnitName .. " exceeds max allowed!", 1, 0.4, 0.4);
		sBlockedSenders[aUnitName] = true;
		VUHDO_sendMessage(aUnitName, sCmdAbortComms, nil);
		VUHDO_removeCommsData(aUnitName);
		return;
	end

	VUHDO_setReplyData(aUnitName, aType, tReplyData);
end


--
local function VUHDO_handleCommandReceived(aSenderName, aCommand)
	local tCommandType, tData = strsplit(sCommandSeparator, aCommand, 2);

	if (sCmdAbortComms == tCommandType) then
		VUHDO_removeCommsData(aSenderName);
		VuhDoYesNoFrame:Hide();
		VUHDO_Msg("Transaction aborted by " .. aSenderName);
	else
		if(sCmdProfileDataChunk == tCommandType) then
			VUHDO_addReplyData(aSenderName, sCmdProfileDataChunk, tData);
			if (sNumChunks % 20 == 0) then
				VUHDO_Msg("Receiving profile data: " .. strrep(".", sNumChunks / 20 + 1));
			end
			sNumChunks = sNumChunks + 1;

		elseif(sCmdProfileDataEnd == tCommandType) then
			local tProfile = VUHDO_decompressFromSending(VUHDO_getReplyData(aSenderName, sCmdProfileDataChunk));

			local tName = tProfile["NAME"];
			if (VUHDO_getProfileNamedCompressed(tName) ~= nil) then
				local tPos = strfind(tName, ": ", 1, true);
				if (tPos ~= nil) then
					tName = strsub(tName, tPos + 2);
				end

				tProfile["NAME"] = VUHDO_createNewProfileName(tName, aSenderName);
			end
			tinsert(VUHDO_PROFILES, tProfile);

			VUHDO_Msg("Transmission complete. Profile \"" .. tProfile["NAME"] .. "\" has been added.");
			VUHDO_removeCommsData(aSenderName);

		elseif(sCmdKeyLayoutDataChunk == tCommandType) then
			VUHDO_addReplyData(aSenderName, sCmdKeyLayoutDataChunk, tData);
		elseif(sCmdKeyLayoutDataEnd == tCommandType) then
			local tKeyLayout = VUHDO_decompressFromSending(VUHDO_getReplyData(aSenderName, sCmdKeyLayoutDataChunk));
			while (VUHDO_SPELL_LAYOUTS[tKeyLayout[1]] ~= nil) do
				tKeyLayout[1] = aSenderName .. ": " .. tKeyLayout[1];
			end
			VUHDO_Msg("Transmission complete. Key Layout \"" .. tKeyLayout[1] .. "\" has been added.");
			VUHDO_SPELL_LAYOUTS[tKeyLayout[1]] = tKeyLayout[2];
			VUHDO_removeCommsData(aSenderName);
		else
			VUHDO_Msg("Invalid VuhDo command received from " .. aSenderName .. ". Blocking.");
			sBlockedSenders[aSenderName] = true;
		end
	end
end



--
local function VUHDO_yesNoCommsCallback(aDecision)
	local tMessage = VUHDO_buildMessage(sCmdUserYesNoReply, aDecision);
	VUHDO_sendMessage(VuhDoYesNoFrame:GetAttribute("senderName"), tMessage, nil);
end



--
local function VUHDO_handleRequestReceived(aSenderName, aRequest)
	local tRequestType, tData = strsplit(sCommandSeparator, aRequest, 2);

	if (sCmdVersionRequest == tRequestType) then
		local tMessage = VUHDO_buildMessage(sCmdVersionReply, VUHDO_COMMS_VERSION, VUHDO_VERSION, GetLocale());
		VUHDO_sendMessage(aSenderName, tMessage, nil);
	elseif(sCmdUserYesNoRequest == tRequestType) then
		VuhDoYesNoFrameText:SetText(tData);
		VuhDoYesNoFrame:SetAttribute("callback", VUHDO_yesNoCommsCallback);
		VuhDoYesNoFrame:SetAttribute("senderName", aSenderName);
		VuhDoYesNoFrame:Show();
	else
		VUHDO_xMsg("Unknown request ", aRequest, "from", aSenderName);
	end
end



--
local function VUHDO_handleReplyReceived(aSenderName, aMessage)
	local tReplyType, tData = strsplit(sCommandSeparator, aMessage, 2);

	if (tReplyType == nil or tData == nil) then
		VUHDO_xMsg("3. Invalid VuhDo message received from", aSenderName, aMessage);
		return;
	end

	if (sRequestsInProgress[aSenderName] == nil or sRequestsInProgress[aSenderName][tReplyType] == nil) then
		VUHDO_xMsg("No such reply expected from ", aSenderName, aMessage);
		return;
	end

	VUHDO_setReplyData(aSenderName, tReplyType, tData);
	local tResumable = sRequestsInProgress[aSenderName][tReplyType][2];
	sRequestsInProgress[aSenderName][tReplyType] = nil;
	if (tResumable ~= nil) then
		tResumable(aSenderName);
	end
end



--
function VUHDO_parseVuhDoMessage(aSenderName, aMessage)

	if (sBlockedSenders[aSenderName]) then
		if (sReceiveMode < 4) then
			VUHDO_Msg("Blocking comms from " .. aSenderName .. ". /reload to reset.", 1, 0.4, 0.4);
			sReceiveMode = 4;
		end

		return;
	end

	if (not VUHDO_CONFIG["IS_SHARE"]) then
		if (sReceiveMode > 0 and sReceiveMode < 4) then
			VUHDO_Msg("Blocked comms from " .. aSenderName .. ". VuhDo options => Tools => Share is disabled.");
			sReceiveMode = sReceiveMode + 1;
		end

		return;
	end

	if (sReceiveMode > 0 and sReceiveMode < 4) then
		VUHDO_Msg("Blocked comms from " .. aSenderName .. ". VuhDo options screen must be opened.");
		sReceiveMode = sReceiveMode + 1;
		return;
	elseif(sReceiveMode > 0) then
	  return; -- Silently fail, no spamming
	end

	if (strlen(aMessage or "") < 4) then
		VUHDO_xMsg("1. Invalid VuhDo message received from", aSenderName, aMessage);
		return;
	end
	local tPrefix = strsub(aMessage, 1, 3);

	if ((sBusyUnitName or aSenderName) ~= aSenderName) then
		return;
	end

	sBusyUnitName = aSenderName;

	if (sPrefixCommand == tPrefix) then
		VUHDO_handleCommandReceived(aSenderName, aMessage);
	elseif(sPrefixRequest == tPrefix) then
		VUHDO_handleRequestReceived(aSenderName, aMessage);
	elseif(sPrefixReply == tPrefix) then
		VUHDO_handleReplyReceived(aSenderName, aMessage);
	else
		VUHDO_xMsg("2. Invalid VuhDo message received from", aSenderName, aMessage);
	end
end



--
function VUHDO_updateRequestsInProgress()
	for tReceiverName, tSomeUnitRequests in pairs(sRequestsInProgress) do
		for tReplyType, tSomeReplyInfos in pairs(tSomeUnitRequests) do
			if (GetTime() > tSomeReplyInfos[1]) then
				VUHDO_sendMessage(tReceiverName, sCmdAbortComms, nil);
				VUHDO_removeCommsData(tReceiverName);
				VUHDO_Msg("Aborting: Request to " .. tReceiverName .. " timed out.", 1, 0.4, 0.4);
				VuhDoLnfShareDialog:Hide();
			end
		end
	end
end
