--------------------
-- NoobHub
-- opensource multiplayer and network messaging for CoronaSDK, Moai, Gideros & LÖVE
--
-- Authors:
-- Igor Korsakov
-- Sergii Tsegelnyk
--
-- License: WTFPL
-- https://github.com/Overtorment/NoobHub
--------------------

socket = require("socket")

-- platform dependent code
if (MOAIJsonParser ~= nil) then -- looks like MoaiSDK
	json = MOAIJsonParser
else
	json = require("json")
end -- /platform dependent code


noobhub = {

	new = function (params) -- constructor method
		params = params or {}
		if (not params.server  or not  params.port) then
			print("Noobhub requires server and port to be specified");
			return false;
		end;
		local self = {}
		self.buffer = ''

		self.server =  params.server
		self.port = params.port

		function self:subscribe(params)
				self.channel = params.channel or 'test-channel'
				self.callback = params.callback or   function() end
				self.sock, error_message = socket.connect(self.server,  self.port)
				if (self.sock == nil) then
					print("Noobhub connection error: "..error_message)
					print "Problems with server..?"
					return false;
				end
				self.sock:setoption( 'tcp-nodelay', true ) -- disable Nagle's algorithm for the connection
				self.sock:settimeout(0)
				local input,output = socket.select(nil,{ self.sock }, 3)
				for i,v in ipairs(output) do  v:send("__SUBSCRIBE__"..self.channel.."__ENDSUBSCRIBE__"); end
				return true
		end

		function self:unsubscribe()
			if self.sock then
				self.sock:close()
				self.sock = nil
			end
				self.buffer = ''
		end

		function self:reconnect()
				if (not self.channel or not self.callback) then return false; end;
				print("Noobhub: attempt to reconnect...");
				return self:subscribe({ channel = self.channel; callback = self.callback})
		end

		function self:publish(message)
				-- TODO: add retries
				if (self.sock == nil) then
					print "NoobHub: Attempt to publish without valid subscription (bad socket)"
					self:reconnect()
					return false;
				end
				local send_result, message, num_bytes = self.sock:send("__JSON__START__"..json.encode(message.message).."__JSON__END__")
				if (send_result == nil) then
					print("Noobhub publish error: "..message..'  sent '..num_bytes..' bytes');
					if (message == 'closed') then  self:reconnect() end
					return false;
				end
				return true
		end

		function self:enterFrame()
				local input,output = socket.select({ self.sock },nil, 0) -- this is a way not to block runtime while reading socket. zero timeout does the trick

				for i,v in ipairs(input) do  -------------

					local got_something_new = false
					while  true  do
						local skt, e, p = v:receive()
						if (skt) then  self.buffer = self.buffer .. skt;  got_something_new=true;  end
						if (p) then  self.buffer = self.buffer .. p;  got_something_new=true;  end
						if (not skt) then break; end
						if (e) then break; end
					end -- /while-do


					-- now, checking if a message is present in buffer...
					while got_something_new do  --  this is for a case of several messages stocker in the buffer
							local start = string.find(self.buffer,'__JSON__START__')
							local finish = string.find(self.buffer,'__JSON__END__')
							if (start and finish) then -- found a message!
								local message = string.sub(self.buffer, start+15, finish-1)
								self.buffer = string.sub(self.buffer, 1, start-1)  ..   string.sub(self.buffer, finish + 13 ) -- cutting our message from buffer
								local data = json.decode(message)
								self.callback(  data  )
							else
								break
							end
					end -- /while-do

				end -- / for-do
				
		end; -- /enterFrame

		-- platform dependent code
		if (system == nil) then
				if (application ~= nil) then -- most likely Gideros
					local timer = Timer.new(33, 0)
					timer:addEventListener(Event.TIMER,  function() self:enterFrame(); end );
					timer:start()
				end
				if (MOAIJsonParser ~= nil) then -- most likely MoaiSDK
					local timer = MOAITimer.new ()
					timer:setSpan( 0.033 )
					timer:setMode(MOAITimer.LOOP)
					timer:setListener( MOAITimer.EVENT_TIMER_END_SPAN, function() self:enterFrame(); end, true )
					timer:start()
				end
				if (love ~= nil) then -- most likely LÖVE
					-- do nothing, as Noobhub's enterFrame() method should be manually
					-- called on every love.update() callback to give it some CPU time
				end
			else  -- most likely CoronaSDK
				Runtime:addEventListener('enterFrame', self)
		end -- /platform dependent code

		return self
	end -- /new
}
