local verse = require "verse";
local base64 = require "prosody.util.encodings".base64;
local new_id = require "prosody.util.id".short;

local xmlns_jingle_ibb = "urn:xmpp:jingle:transports:ibb:1";
local xmlns_ibb = "http://jabber.org/protocol/ibb";
assert(base64.encode("This is a test.") == "VGhpcyBpcyBhIHRlc3Qu", "Base64 encoding failed");
assert(base64.decode("VGhpcyBpcyBhIHRlc3Qu") == "This is a test.", "Base64 decoding failed");

local ibb_conn = {};
local ibb_conn_mt = { __index = ibb_conn };

local function new_ibb(stream)
	local conn = setmetatable({ stream = stream }, ibb_conn_mt)
	conn = verse.eventable(conn);
	return conn;
end

function ibb_conn:initiate(peer, sid, stanza)
	self.block = 2048; -- ignored for now
	self.stanza = stanza or 'iq';
	self.peer = peer;
	self.sid = sid or tostring(self):match("%x+$");
	self.iseq = 0;
	self.oseq = 0;
	local feeder = function(stanza)
		return self:feed(stanza)
	end
	self.feeder = feeder;
	print("Hooking incoming IQs");
	local stream = self.stream;
		stream:hook("iq/".. xmlns_ibb, feeder)
	if stanza == "message" then
		stream:hook("message", feeder)
	end
end

function ibb_conn:open(callback)
	self.stream:send_iq(verse.iq{ to = self.peer, type = "set" }
		:tag("open", {
			xmlns = xmlns_ibb,
			["block-size"] = self.block,
			sid = self.sid,
			stanza = self.stanza
		})
	, function(reply)
		if callback then
			if reply.attr.type ~= "error" then
				callback(true)
			else
				callback(false, reply:get_error())
			end
		end
	end);
end

function ibb_conn:send(data)
	local stanza = self.stanza;
	local st;
	if stanza == "iq" then
		st = verse.iq{ type = "set", to = self.peer }
	elseif stanza == "message" then
		st = verse.message{ to = self.peer }
	end

	local seq = self.oseq;
	self.oseq = seq + 1;

	st:tag("data", { xmlns = xmlns_ibb, sid = self.sid, seq = seq })
		:text(base64.encode(data));

	if stanza == "iq" then
		self.stream:send_iq(st, function(reply)
			self:event(reply.attr.type == "result" and "drained" or "error");
		end)
	else
		stream:send(st)
		self:event("drained");
	end
end

function ibb_conn:feed(stanza)
	if stanza.attr.from ~= self.peer then return end
	local child = stanza[1];
	if child.attr.sid ~= self.sid then return end
	local ok;
	if child.name == "open" then
		self:event("connected");
		self.stream:send(verse.reply(stanza))
		return true
	elseif child.name == "data" then
		local bdata = stanza:get_child_text("data", xmlns_ibb);
		local seq = tonumber(child.attr.seq);
		local expected_seq = self.iseq;
		if bdata and seq then
			if seq ~= expected_seq then
				self.stream:send(verse.error_reply(stanza, "cancel", "not-acceptable", "Wrong sequence. Packet lost?"))
				self:close();
				self:event("error");
				return true;
			end
			self.iseq = seq + 1;
			local data = base64.decode(bdata);
			if self.stanza == "iq" then
				self.stream:send(verse.reply(stanza))
			end
			self:event("incoming-raw", data);
			return true;
		end
	elseif child.name == "close" then
		self.stream:send(verse.reply(stanza))
		self:close();
		return true
	end
end

--[[ FIXME some day
function ibb_conn:receive(patt)
	-- is this even used?
	print("ibb_conn:receive("..tostring(patt)..")");
	assert(patt == "*a" or tonumber(patt));
	local data = t_concat(self.ibuffer):sub(self.pos, tonumber(patt) or nil);
	self.pos = self.pos + #data;
	return data
end

function ibb_conn:dirty()
	print("ibb_conn:dirty()");
	return false -- ????
end
function ibb_conn:getfd()
	return 0
end
function ibb_conn:settimeout(n)
	-- ignore?
end
-]]

function ibb_conn:close()
	self.stream:unhook("iq/".. xmlns_ibb, self.feeder)
	self:event("disconnected");
end

function verse.plugins.jingle_ibb(stream)
	stream:hook("ready", function ()
		stream:add_disco_feature(xmlns_jingle_ibb);
	end, 10);

	local ibb = {};

	function ibb:_setup()
		local conn = new_ibb(self.stream);
		conn.sid    = self.sid    or conn.sid;
		conn.stanza = self.stanza or conn.stanza;
		conn.block  = self.block  or conn.block;
		conn:initiate(self.peer, self.sid, self.stanza);
		self.conn = conn;
	end
	function ibb:generate_initiate()
		print("ibb:generate_initiate() as ".. self.role);
		local sid = new_id();
		self.sid = sid;
		self.stanza = 'iq';
		self.block = 2048;
		local transport = verse.stanza("transport", { xmlns = xmlns_jingle_ibb,
			sid = self.sid, stanza = self.stanza, ["block-size"] = self.block });
		return transport;
	end
	function ibb:generate_accept(initiate_transport)
		print("ibb:generate_accept() as ".. self.role);
		local attr = initiate_transport.attr;
		self.sid    = attr.sid    or self.sid;
		self.stanza = attr.stanza or self.stanza;
		self.block  = attr["block-size"] or self.block;
		self:_setup();
		return initiate_transport;
	end
	function ibb:connect(callback)
		if not self.conn then
			self:_setup();
		end
		local conn = self.conn;
		print("ibb:connect() as ".. self.role);
		if self.role == "initiator" then
			conn:open(function(ok, ...)
				assert(ok, table.concat({...}, ", "));
				callback(conn);
			end);
		else
			callback(conn);
		end
	end
	function ibb:info_received(jingle_tag)
		print("ibb:info_received()");
		-- TODO, what exactly?
	end
	function ibb:disconnect()
		if self.conn then
			self.conn:close()
		end
	end
	function ibb:handle_accepted(jingle_tag) end

	local ibb_mt = { __index = ibb };
	stream:hook("jingle/transport/"..xmlns_jingle_ibb, function (jingle)
		return setmetatable({
			role = jingle.role,
			peer = jingle.peer,
			stream = jingle.stream,
			jingle = jingle,
		}, ibb_mt);
	end);
end
