-----------------------------------------------------------------------------
-- GZip filter for WSAPI
--
-- Author: Ignacio Burgueño
-- Copyright (c) 2009 inConcert
--
-----------------------------------------------------------------------------

local zlib = require "zlib"
local common = require"wsapi.common"
local coroutine = coroutine
local string = string
local table = table
local type = type
local tonumber = tonumber
local assert = assert

local _M = {}

-- Content-Length must be greater than this value to apply the compression
local MINIMUM_CONTENT_SIZE = 512

local m_allowed_types = {
	["text/css"] = true,
	["text/plain"] = true,
	["text/html"] = true,
	["application/xhtml+xml"] = true,
	["application/x-javascript"] = true,
	["application/xml"] = true,
	["image/svg+xml"] = true,
	["text/sgml"] = true,
	["application/json"] = true,
	["text/javascript"] = true,
}

--
-- Allows a given mime type to be encoded
function _M.AllowType(mimeType)
	assert(type(mimeType) == "string")
	m_allowed_types[mimeType] = true
end

--
-- Disallows a given mime type to be encoded
function _M.DisallowType(mimeType)
	assert(type(mimeType) == "string")
	m_allowed_types[mimeType] = nil
end

--
-- Sets the minimum content size a response needs to be compressed
function _M.SetMinimumSize(newSize)
	assert(type(newSize) == "number")
	MINIMUM_CONTENT_SIZE = newSize
end

-- helper function: tests for the presence of a header, removing it if found
local function test_and_clear(headers, header)
	local h = headers[header]
	if h then
		headers[header] = nil
	end
	return h
end

-- 
-- Returns a functions to be used as a WSAPI filter. It will compress the output given the following (in order):
--  The compression was not forcibly disabled
--  The browser supports 'gzip' compression
--  The output size is greater that MINIMUM_CONTENT_SIZE bytes or the compression is not forced
--  The Content-Type is one of the allowed types or the compression is not forced
--   Note: If the browser does not support compression the 'force' header is ignored.
function _M.makeGzipper(appToFilter)
	return function(wsapi_env)
		local status, headers, iterator
		if type(appToFilter) == "table" and type(appToFilter["run"]) == "function" then
			status, headers, iterator = appToFilter.run(wsapi_env)
		else
			status, headers, iterator = appToFilter(wsapi_env)
		end
		
		local disableCompression = test_and_clear(headers, "x-inConcert-Compression-Disable")
		if disableCompression or not string.find(wsapi_env["HTTP_ACCEPT_ENCODING"], "gzip") then
			return status, headers, iterator
		end
		
		local contentLength = headers["Content-Length"]
		
		local forceCompress = test_and_clear(headers, "x-inConcert-Compression-Force")
		if not forceCompress then
			local contentType = string.match(headers["Content-Type"] or "", "([^;]*)")	-- get only the mime-type (text/html; charset=utf-8)
			if not contentType or not m_allowed_types[contentType] or tonumber(contentLength) <= MINIMUM_CONTENT_SIZE then
				-- the content type is marked as not compressable
				return status, headers, iterator
			end
		end
		
		headers["x-inConcert-Old-Content-Length"] = contentLength
		
		local buffer = {}
		local stream = {
			write = function(self, data)
				table.insert(buffer, data)
				return true
			end
		}
		common.send_content(stream, iterator, "write")
		
		--zlib.compress(string buffer [, int level] [, int method] [, int windowBits] [, int memLevel] [, int strategy])
		-- esa magia salió de http://lua-users.org/lists/lua-l/2005-03/msg00221.html
		local gzippedData = zlib.compress(table.concat(buffer), 9, nil, 15 + 16)
		headers["Content-Encoding"] = "gzip"
		headers["Content-Length"] = #gzippedData
		headers["Vary"] = "Accept-Encoding"
		
		return status, headers, coroutine.wrap(function() 
				coroutine.yield(gzippedData)
			end)
	end
end

return _M
