This module provides common functions for HTTP streams
no matter the underlying protocol version.

This is usually an internal module, and should be used by adding the
`methods` exposed to your own HTTP stream objects.
]]

local cqueues = require "cqueues"
local monotime = cqueues.monotime
local new_headers = require "http.headers".new

local CHUNK_SIZE = 2^20 -- write in 1MB chunks

local stream_methods = {}

function stream_methods:checktls()
	return self.connection:checktls()
end

function stream_methods:localname()
	return self.connection:localname()
end

function stream_methods:peername()
	return self.connection:peername()
end

-- 100-Continue response
local continue_headers = new_headers()
continue_headers:append(":status", "100")
function stream_methods:write_continue(timeout)
	return self:write_headers(continue_headers, false, timeout)
end

-- need helper to discard 'last' argument
-- (which would otherwise end up going in 'timeout')
local function each_chunk_helper(self)
	return self:get_next_chunk()
end
function stream_methods:each_chunk()
	return each_chunk_helper, self
end

function stream_methods:get_body_as_string(timeout)
	local deadline = timeout and (monotime()+timeout)
	local body, i = {}, 0
	while true do
		local chunk, err, errno = self:get_next_chunk(timeout)
		if chunk == nil then
			if err == nil then
				break
			else
				return nil, err, errno
			end
		end
		i = i + 1
		body[i] = chunk
		timeout = deadline and (deadline-monotime())
	end
	return table.concat(body, "", 1, i)
end

function stream_methods:get_body_chars(n, timeout)
	local deadline = timeout and (monotime()+timeout)
	local body, i, len = {}, 0, 0
	while len < n do
		local chunk, err, errno = self:get_next_chunk(timeout)
		if chunk == nil then
			if err == nil then
				break
			else
				return nil, err, errno
			end
		end
		i = i + 1
		body[i] = chunk
		len = len + #chunk
		timeout = deadline and (deadline-monotime())
	end
	if i == 0 then
		return nil
	end
	local r = table.concat(body, "", 1, i)
	if n < len then
		self:unget(r:sub(n+1, -1))
		r = r:sub(1, n)
	end
	return r
end

function stream_methods:get_body_until(pattern, plain, include_pattern, timeout)
	local deadline = timeout and (monotime()+timeout)
	local body
	while true do
		local chunk, err, errno = self:get_next_chunk(timeout)
		if chunk == nil then
			if err == nil then
				return body, err
			else
				return nil, err, errno
			end
		end
		if body then
			body = body .. chunk
		else
			body = chunk
		end
		local s, e = body:find(pattern, 1, plain)
		if s then
			if e < #body then
				self:unget(body:sub(e+1, -1))
			end
			if include_pattern then
				return body:sub(1, e)
			else
				return body:sub(1, s-1)
			end
		end
		timeout = deadline and (deadline-monotime())
	end
end

function stream_methods:save_body_to_file(file, timeout)
	local deadline = timeout and (monotime()+timeout)
	while true do
		local chunk, err, errno = self:get_next_chunk(timeout)
		if chunk == nil then
			if err == nil then
				break
			else
				return nil, err, errno
			end
		end
		assert(file:write(chunk))
		timeout = deadline and (deadline-monotime())
	end
	return true
end

function stream_methods:get_body_as_file(timeout)
	local file = assert(io.tmpfile())
	local ok, err, errno = self:save_body_to_file(file, timeout)
	if not ok then
		return nil, err, errno
	end
	assert(file:seek("set"))
	return file
end

function stream_methods:write_body_from_string(str, timeout)
	return self:write_chunk(str, true, timeout)
end

function stream_methods:write_body_from_file(file, timeout)
	local deadline = timeout and (monotime()+timeout)
	-- Can't use :lines here as in Lua 5.1 it doesn't take a parameter
	while true do
		local chunk, err = file:read(CHUNK_SIZE)
		if chunk == nil then
			if err then
				error(err)
			end
			break
		end
		local ok, err2, errno2 = self:write_chunk(chunk, false, deadline and (deadline-monotime()))
		if not ok then
			return nil, err2, errno2
		end
	end
	return self:write_chunk("", true, deadline and (deadline-monotime()))
end

return {
	methods = stream_methods;
}
