local LIB_IDENTIFIER = "LibGroupSocket"
local lib = LibStub:NewLibrary(LIB_IDENTIFIER, 1)

if not lib then
	return	-- already loaded and no upgrade necessary
end

local LMP = LibStub("LibMapPing", true)
if(not LMP) then
	error(string.format("[%s] Cannot load without LibMapPing", LIB_IDENTIFIER))
end

local LGPS = LibStub("LibGPS2", true)
if(not LGPS) then
	error(string.format("[%s] Cannot load without LibGPS2", LIB_IDENTIFIER))
end

local function Log(message, ...)
	df("[%s] %s", LIB_IDENTIFIER, message:format(...))
end
lib.Log = Log

--lib.debug = true -- TODO
--/script PingMap(88, 1, 1 / 2^16, 1 / 2^16) StartChatInput(table.concat({GetMapPlayerWaypoint()}, ","))
-- smallest step is around 1.428571431461e-005 for Wrothgar, so there should be 70000 steps
-- meaning we can send 4 bytes of data per ping
local stepSize = 1.428571431461e-005
local stepCount = 1/stepSize
local WROTHGAR_MAP_INDEX = 27

function lib:ReadBit(data, index, bitIndex)
	local p = 2 ^ (bitIndex - 1)
	local isSet = (data[index] % (p + p) >= p)
	local nextIndex = (bitIndex == 8 and index + 1 or index)
	local nextBitIndex = (bitIndex == 8 and 1 or bitIndex + 1)
	return isSet, nextIndex, nextBitIndex
end

function lib:WriteBit(data, index, bitIndex, value)
	local p = 2 ^ (bitIndex - 1)
	local oldValue = data[index] or 0
	local isSet = (oldValue % (p + p) >= p)
	if(isSet and not value) then
		oldValue = oldValue - p
	elseif(not isSet and value) then
		oldValue = oldValue + p
	end
	data[index] = oldValue
	local nextIndex = (bitIndex == 8 and index + 1 or index)
	local nextBitIndex = (bitIndex == 8 and 1 or bitIndex + 1)
	return nextIndex, nextBitIndex
end

function lib:ReadChar(data, index)
	return string.char(data[index]), index + 1
end

function lib:WriteChar(data, index, value, charIndex)
	data[index] = value:byte(charIndex)
	return index + 1
end

function lib:ReadUint8(data, index)
	return data[index], index + 1
end

function lib:WriteUint8(data, index, value)
	data[index] = value
	return index + 1
end

function lib:ReadUint16(data, index)
	return (data[index] * 0x100 + data[index + 1]), index + 2
end

function lib:WriteUint16(data, index, value)
	data[index] = math.floor(value / 0x100)
	data[index + 1] = math.floor(value % 0x100)
	return index + 2
end

function lib:EncodeData(b0, b1, b2, b3)
	b0 = b0 or 0
	b1 = b1 or 0
	b2 = b2 or 0
	b3 = b3 or 0
	return (b0 * 0x100 + b1) * stepSize, (b2 * 0x100 + b3) * stepSize
end

function lib:DecodeData(x, y)
	x = math.floor(x * stepCount + 0.5) -- round to next integer
	y = math.floor(y * stepCount + 0.5)
	local b0 = math.floor(x / 0x100)
	local b1 = x % 0x100
	local b2 = math.floor(y / 0x100)
	local b3 = y % 0x100
	return b0, b1, b2, b3
end

function lib:EncodeHeader(addonId, length)
	return addonId * 0x08 + length
end

function lib:DecodeHeader(value)
	local addonId = math.floor(value / 0x08)
	local length = value % 0x08
	return addonId, length
end

local function IsValidData(data)
	if(#data > 7) then
		Log("Tried to send %d of 7 allowed bytes", #data)
		return false
	end
	for i = 1, #data do
		local value = data[i]
		if(type(value) ~= "number" or value < 0 or value > 255) then
			Log("Invalid value '%s' at position %d in byte data", tostring(value), i)
			return false
		end
	end
	return true
end

local function SetMapPingOnCommonMap(x, y)
	local pingType = MAP_PIN_TYPE_PING
	if(lib.debug and not IsUnitGrouped("player")) then
		pingType = MAP_PIN_TYPE_PLAYER_WAYPOINT
	end
	LGPS:PushCurrentMap()
	SetMapToMapListIndex(WROTHGAR_MAP_INDEX) -- TODO: check if we want to use 2 different maps to avoid setting stuff on the local zone map for easier user input detection
	LMP:SetMapPing(pingType, MAP_TYPE_LOCATION_CENTERED, x, y)
	LGPS:PopCurrentMap()
end

local function GetMapPingOnCommonMap(pingType, pingTag)
	LGPS:PushCurrentMap()
	SetMapToMapListIndex(WROTHGAR_MAP_INDEX) -- TODO: check if we want to use 2 different maps to avoid setting stuff on the local zone map for easier user input detection
	local x, y = LMP:GetMapPing(pingType, pingTag)
	LGPS:PopCurrentMap()
	return x, y
end

local function DoSend(isFirst)
	local packet = lib.outgoing[1]
	if(not packet) then Log("Tried to send when no data in queue") return end
	lib.isSending = true

	local x, y = packet:GetNextCoordinates()
	SetMapPingOnCommonMap(x, y)

	lib.hasMore = packet:HasMore()
	if(not lib.hasMore) then
		table.remove(lib.outgoing, 1)
		lib.hasMore = (#lib.outgoing > 0)
	end
end

local OutgoingPacket = ZO_Object:Subclass()

function OutgoingPacket:New(messageType, data)
	local object = ZO_Object.New(self)
	object.messageType = messageType
	object.header = lib:EncodeHeader(messageType, #data)
	object.data = data
	object.index = 0
	return object
end

function OutgoingPacket:GetNext()
	local next
	if(self.index < 1) then
		next = self.header
	else
		next = self.data[self.index]
	end
	self.index = self.index + 1
	return next
end

function OutgoingPacket:GetNextCoordinates()
	local x = lib:EncodeData(self:GetNext(), self:GetNext())
	local y = lib:EncodeData(self:GetNext(), self:GetNext())
	return x, y
end

function OutgoingPacket:HasMore()
	return self.index < #self.data
end

local IncomingPacket = ZO_Object:Subclass()

function IncomingPacket:New()
	local object = ZO_Object.New(self)
	object.messageType = -1
	object.data = {}
	object.length = 0
	return object
end

function IncomingPacket:AddCoordinates(x, y)
	local b0, b1, b2, b3 = lib:DecodeData(x, y)
	local data = self.data
	if(self.messageType < 0) then
		self.messageType, self.length = lib:DecodeHeader(b0)
	else
		data[#data + 1] = b0
	end
	if(#data < self.length) then data[#data + 1] = b1 end
	if(#data < self.length) then data[#data + 1] = b2 end
	if(#data < self.length) then data[#data + 1] = b3 end
end

function IncomingPacket:IsComplete()
	return self.length > 0 and #self.data >= self.length
end

local function IsValidMessageType(messageType)
	return not (messageType < 0 or messageType > 31)
end

function IncomingPacket:HasValidHeader()
	return IsValidMessageType(self.messageType) and self.length > 0 and self.length < 8
end

function IncomingPacket:IsValid()
	return self:HasValidHeader() and #self.data == self.length
end

lib.cm = ZO_CallbackObject:New()
function lib:RegisterCallback(messageType, callback)
	self.cm:RegisterCallback(messageType, callback)
end

function lib:UnregisterCallback(messageType, callback)
	self.cm:UnregisterCallback(messageType, callback)
end

lib.outgoing = {}
lib.incoming = {}
function lib:Send(messageType, data)
	if(not IsValidMessageType(messageType)) then Log("tried to send invalid messageType %s", tostring(messageType)) return end
	if(not IsValidData(data)) then return end
	--  TODO if(lastSendTime[messageType] < now + timeout)...
	lib.outgoing[#lib.outgoing + 1] = OutgoingPacket:New(messageType, data)
	if(not lib.isSending) then
		DoSend()
	else
		lib.hasMore = true
	end
end

function lib:PrepareNext()
	self.current = table.remove(self.sendQueue, 1)
end

local function HandleDataPing(pingType, pingTag, x, y, isPingOwner)
	x, y = GetMapPingOnCommonMap(pingType, pingTag)
	if(not LMP:IsPositionOnMap(x, y)) then return false end
	if(not lib.incoming[pingTag]) then
		lib.incoming[pingTag] = IncomingPacket:New(pingTag)
	end
	local packet = lib.incoming[pingTag]
	packet:AddCoordinates(x, y)
	if(not packet:HasValidHeader()) then -- it might be a user set ping
		lib.incoming[pingTag] = nil
		return false
	end
	if(packet:IsComplete()) then
		lib.incoming[pingTag] = nil
		if(packet:IsValid()) then
			lib.cm:FireCallbacks(packet.messageType, pingTag, packet.data, isPingOwner)
		else
			lib.incoming[pingTag] = nil
			Log("received invalid packet from %s", GetUnitName(pingTag))
			return false
		end
	end
	if(isPingOwner) then
		if(lib.hasMore) then
			DoSend()
		else
			lib.isSending = false
		end
	end
	return true
end

local suppressedList = {}
local function GetKey(pingType, pingTag)
	return string.format("%d_%s", pingType, pingTag)
end

local function SuppressPing(pingType, pingTag)
	local key = GetKey(pingType, pingTag)
	if(not suppressedList[key]) then
		LMP:SuppressPing(pingType, pingTag)
		suppressedList[key] = true
	end
end

local function UnsuppressPing(pingType, pingTag)
	local key = GetKey(pingType, pingTag)
	if(suppressedList[key]) then
		LMP:UnsuppressPing(pingType, pingTag)
		suppressedList[key] = false
	end
end

LMP:RegisterCallback("BeforePingAdded", function(pingType, pingTag, x, y, isPingOwner)
	if(pingType == MAP_PIN_TYPE_PING or (lib.debug and not IsUnitGrouped("player") and pingType == MAP_PIN_TYPE_PLAYER_WAYPOINT)) then
		if(HandleDataPing(pingType, pingTag, x, y, isPingOwner)) then -- it is a valid data ping
			SuppressPing(pingType, pingTag)
		else -- ping is set by player
			UnsuppressPing(pingType, pingTag)
		end
	end
end)

LMP:RegisterCallback("AfterPingRemoved", function(pingType, pingTag, x, y, isPingOwner)
	UnsuppressPing(pingType, pingTag)
end)

lib.handlers = lib.handlers or {}
local handlers = lib.handlers
function lib:RegisterHandler(handlerType, handlerVersion)
	if handlers[handlerType] and handlers[handlerType].version >= handlerVersion then
		return false
	else
		handlers[handlerType] = handlers[handlerType] or {}
		handlers[handlerType].version = handlerVersion
		return handlers[handlerType]
	end
end

function lib:GetHandler(handlerType)
	return handlers[handlerType]
end

lib.MESSAGE_TYPE_RESOURCES = 1
lib.MESSAGE_TYPE_FTC_DPS = 2
lib.MESSAGE_TYPE_SYNC_TIMESTAMP = 3