----------------------------------------------------------------------------------
--
-- LibCompress.lua
--
-- Authors: jjsheets and Galmok of European Stormrage (Horde)
-- Email : sheets.jeff@gmail.com and galmok@gmail.com
-- Licence: GPL version 2 (General Public License)
----------------------------------------------------------------------------------


local MAJOR, MINOR = "LibCompress", 3

local LibCompress,oldminor = LibStub:NewLibrary(MAJOR, MINOR)

if not LibCompress then return end

-- list of codecs in this file:
-- \000 - Never used
-- \001 - Uncompressed
-- \002 - LZW
-- \003 - Huffman


-- local is faster than global
local table_insert = table.insert
local table_remove = table.remove
local table_concat = table.concat
local string_char = string.char
local string_byte = string.byte
local string_len = string.len
local string_sub = string.sub
local unpack = unpack
local pairs = pairs
local math_modf = math.modf
local bit_band = bit.band
local bit_bor = bit.bor
local bit_lshift = bit.lshift
local bit_rshift = bit.rshift
local tostring = tostring
local next = next
local strlen = strlen
local strsplit = string.split
local strfind = string.find
local strgfind = string.gfind
local tonumber = tonumber

--------------------------------------------------------------------------------
-- Cleanup

local tables = {} -- tables that may be cleaned have to be kept here
local tables_to_clean = {} -- list of tables by name (string) that may be reset to {} after a timeout
local timeout = -1

-- tables that may be erased
local function cleanup()
	for k,v in pairs(tables_to_clean) do
		tables[k]={}
		tables_to_clean[k]=nil
	end
end

local function onUpdate(frame, elapsed)
	timeout = timeout - arg1
	if timeout < 0 then
		this:Hide()
		cleanup()
	end
end

LibCompress.frame = LibCompress.frame or CreateFrame("frame", nil, UIParent) -- reuse the old frame
LibCompress.frame:SetScript("OnUpdate", onUpdate)
LibCompress.frame:Hide()

local function setCleanupTables(s1, s2)
	timeout = 15 -- empty tables after 15 seconds
	if not LibCompress.frame:IsShown() then
		LibCompress.frame:Show()
	end

	if tables_to_clean[s1] then
		tables_to_clean[s1] = true
	end
	if tables_to_clean[s2] then
		tables_to_clean[s2] = true
	end
end


--------------------------------------------------------------------------------
-- LZW codec
-- implemented by sheets.jeff@gmail.com

-- encode is used to uniquely encode a number into a sequence of bytes that can be decoded using decode()
-- the bytes returned by this do not contain "\000"
local bytes = {}
local function encode(x)
	local math_modf = math.modf
	for k = 1, getn(bytes) do bytes[k] = nil end
	local xmod
	x, xmod = math_modf(x/255.0)
	xmod = xmod * 255
	bytes[getn(bytes) + 1] = xmod
	while x > 0 do
		x, xmod = math_modf(x/255.0)
		xmod = xmod * 255.0
		bytes[getn(bytes) + 1] = xmod
	end
	if getn(bytes) == 1 and bytes[1] > 0 and bytes[1] < 250 then
		return string_char(bytes[1])
	else
		for i = 1, getn(bytes) do bytes[i] = bytes[i] + 1 end
		return string_char(256 - getn(bytes), unpack(bytes))
	end
end

--decode converts a unique character sequence into its equivalent number, from ss, beginning at the ith char.
-- returns the decoded number and the count of characters used in the decode process.
local function decode(ss,i)
	i = i or 1
	local a = string_byte(ss,i,i)
	if a > 249 then
		local r = 0
		a = 256 - a
		for n = i+a, i+1, -1 do
			r = r * 255 + string_byte(ss,n,n) - 1
		end
		return r, a + 1
	else
		return a, 1
	end
end

--
-- Added by Shino
--
function LibCompress:TableToString(t)
	if t == nil then return nil end
	if type(t)=="string" then return t end
	local cache={}
	local function sub_mod(t)
		if (type(t)=="table") then
			for pos,val in pairs(t) do
				if (type(val)=="table") then
					table_insert(cache, pos..">".."{")
					sub_mod(val)
					table_insert(cache, "}")
				elseif (type(val)=="string") then
					table_insert(cache, pos..'>"'..val..'"')
				else
					table_insert(cache, pos..">"..tostring(val))
				end
			end
		else
			table_insert(cache, tostring(t))
		end
	end
	sub_mod(t)
	return table_concat(cache, ",") -- ? maybe use a different delimiter
end

-- Adding a costum string.split function cause this one has a problem with too big tables
-- Credits: https://gist.github.com/jaredallard/ddb152179831dd23b230
function LibCompress:StringSplit(delimiter, str)
	local result = { }
	local from  = 1
	local delim_from, delim_to = strfind( str, delimiter, from  )
	while delim_from do
		table_insert( result, strsub( str, from , delim_from-1 ) )
		from  = delim_to + 1
		delim_from, delim_to = strfind( str, delimiter, from  )
	end
	table.insert( result, strsub( str, from  ) )
	return result
end

function LibCompress:StringToTable(t)
	if t=="" or t==nil then return nil end
	if type(t)=="table" then return t end
	local table = self:StringSplit(",",t)
	local function fillSubTable(start)
		local subT = {}
		local cont = true
		local po = nil
		for k, v in pairs(table) do
			if k>start then
				cont = true
				po = nil
				for pos in strgfind(v, "(.+)>{") do -- opening sub table
					po = tonumber(pos)
					if po then
						subT[po], start = fillSubTable(k)
					else
						subT[pos], start = fillSubTable(k)
					end
				end
				if k>start then -- cause it could have changed if a new table was opened
					for pos, val in strgfind(v, '(.+)%>"(.+)"') do
						po = tonumber(pos)
						if po then
							subT[po] = ""..val
						else
							subT[pos] = ""..val
						end
						cont = false
					end
					if cont then
						for pos, val in strgfind(v, "(.+)%>(.+)") do -- Do floats work too?
							po = tonumber(pos)
							if val == "false" then
								if po then
									subT[po] = false
								else
									subT[pos] = false
								end
							elseif val == "true" then
								if po then
									subT[po] = true
								else
									subT[pos] = true
								end
							else
								if po then
									subT[po] = tonumber(val)
								else
									subT[pos] = tonumber(val)
								end
							end
						end
						if v=="}" then -- closing sub table
							return subT,k -- Returning the end
						end
					end
				end
			end
		end
		return subT, 0
	end
	return fillSubTable(0)
end

-- Shino end --

-- Compresses the given uncompressed string.
-- Unless the uncompressed string starts with "\002", this is guaranteed to return a string equal to or smaller than
-- the passed string.
-- the returned string will only contain "\000" characters in rare circumstances, and will contain none if the
-- source string has none.
local dict = {}
function LibCompress:CompressLZW(uncompressed)
	if type(uncompressed) == "string" then
		local dict_size = 256
		for k, _ in pairs(dict) do
			dict[k] = nil
		end
		local result = {"\002"}
		local w = ''
		local ressize = 1
		for i = 0, 255 do
			dict[string_char(i)] = i
		end
		for i = 1, strlen(uncompressed) do
			local c = string_sub(uncompressed, i,i)
			local wc = w..c
			if dict[wc] then
				w = wc
			else
				dict[wc] = dict_size
				dict_size = dict_size +1
				local r = encode(dict[w])
				ressize = ressize + strlen(r)
				result[getn(result) + 1] = r
				w = c
			end
		end
		if w then
			local r = encode(dict[w])
			ressize = ressize + strlen(r)
			result[getn(result) + 1] = r
		end
		if (strlen(uncompressed)+1) > ressize then
			return table_concat(result, "")
		else
			return "\002"..uncompressed
		end
	else
		return nil, "Can only compress strings"
	end
end

-- if the passed string is a compressed string, this will decompress it and return the decompressed string.
-- Otherwise it return an error message
-- compressed strings are marked by beginning with "\002"
function LibCompress:DecompressLZW(compressed)
	if type(compressed) == "string" then
		if string_sub(compressed,1,1) ~= "\002" then
			return nil, "Can only decompress LZW compressed data ("..tostring(string_sub(compressed,1,1))..")"
		end
		compressed = string_sub(compressed,2)-- ?
		local dict_size = 256
		for k, _ in pairs(dict) do
			dict[k] = nil
		end
		for i = 0, 255 do
			dict[i] = string_char(i)
		end
		local result = {}
		local t = 1
		local delta, k
		k, delta = decode(compressed,t)
		t = t + delta
		result[getn(result)+1] = dict[k]
		local w = dict[k]
		local entry
		while t <= strlen(compressed) do
			k, delta = decode(compressed,t)
			t = t + delta
			entry = dict[k] or (w..string_sub(w,1,1))
			result[getn(result)+1] = entry
			dict[dict_size] = w..string_sub(entry,1,1)
			dict_size = dict_size + 1
			w = entry
		end
		return table_concat(result)
	else
		return nil, "Can only uncompress strings"
	end
end


--------------------------------------------------------------------------------
-- Huffman codec
-- implemented by Galmok of European Stormrage (Horde), galmok@gmail.com

local function addCode(tree, bcode,len)
	if tree then
		tree.bcode = bcode;
		tree.blength = len;
		if tree.c1 then
			addCode(tree.c1, bit_bor(bcode, bit_lshift(1,len)), len+1)
		end
		if tree.c2 then
			addCode(tree.c2, bcode, len+1)
		end
	end
end

local function escape_code(code, len)
	local escaped_code = 0;
	local b;
	local l = 0;
	for i = len-1, 0,- 1 do
		b = bit_band( code, bit_lshift(1,i))==0 and 0 or 1
		escaped_code = bit_lshift(escaped_code,1+b) + b
		l = l + b;
	end
	return escaped_code, len+l
end

tables.Huffman_compressed = {}
tables.Huffman_large_compressed = {}

local compressed_size = 0
local remainder;
local remainder_length;
local function addBits(tbl, code, len)
	remainder = remainder + bit_lshift(code, remainder_length)
	remainder_length = len + remainder_length
	if remainder_length > 32 then
		return true -- Bits lost due to too long code-words.
	end
	while remainder_length>=8 do
		compressed_size = compressed_size + 1
		tbl[compressed_size] = string_char(bit_band(remainder, 255))
		remainder = bit_rshift(remainder, 8)
		remainder_length = remainder_length -8
	end
end

-- word size for this huffman algorithm is 8 bits (1 byte). This means the best compression is representing 1 byte with 1 bit, i.e. compress to 0.125 of original size.
function LibCompress:CompressHuffman(uncompressed)
	if not type(uncompressed)=="string" then
		return nil, "Can only compress strings"
	end

	-- make histogram
	local hist = {}
	local n = 0
	-- dont have to use all datat to make the histogram
	local uncompressed_size = string_len(uncompressed)
	local c;
	for i = 1, uncompressed_size do
		c = string_byte(uncompressed, i)
		hist[c] = (hist[c] or 0) + 1
	end

	--Start with as many leaves as there are symbols.
	local leafs = {}
	local leaf;
	local symbols = {}
	for symbol, weight in pairs(hist) do
		leaf = { symbol=string_char(symbol), weight=weight };
		symbols[symbol] = leaf;
		table_insert(leafs, leaf)
	end
	--Enqueue all leaf nodes into the first queue (by probability in increasing order so that the least likely item is in the head of the queue).
	sort(leafs, function(a,b) if a.weight<b.weight then return true elseif a.weight>b.weight then return false else return nil end end)

	local nLeafs = getn(leafs)

	-- create tree
	local huff = {}
	--While there is more than one node in the queues:
	local l,h, li, hi, leaf1, leaf2
	local newNode;
	while (getn(leafs)+getn(huff) > 1) do
		-- Dequeue the two nodes with the lowest weight.
		-- Dequeue first
		if not next(huff) then
			li, leaf1 = next(leafs)
			table_remove(leafs, li)
		elseif not next(leafs) then
			hi, leaf1 = next(huff)
			table_remove(huff, hi)
		else
			li, l = next(leafs);
			hi, h = next(huff);
			if l.weight<=h.weight then
				leaf1 = l;
				table_remove(leafs, li)
			else
				leaf1 = h;
				table_remove(huff, hi)
			end
		end
		-- Dequeue second
		if not next(huff) then
			li, leaf2 = next(leafs)
			table_remove(leafs, li)
		elseif not next(leafs) then
			hi, leaf2 = next(huff)
			table_remove(huff, hi)
		else
			li, l = next(leafs);
			hi, h = next(huff);
			if l.weight<=h.weight then
				leaf2 = l;
				table_remove(leafs, li)
			else
				leaf2 = h;
				table_remove(huff, hi)
			end
		end

		--Create a new internal node, with the two just-removed nodes as children (either node can be either child) and the sum of their weights as the new weight.
		newNode = { c1 = leaf1, c2 = leaf2, weight = leaf1.weight+leaf2.weight }
		table_insert(huff,newNode)
	end
	if getn(leafs)>0 then
		li, l = next(leafs)
		table_insert(huff, l)
		table_remove(leafs, li)
	end
	huff = huff[1];

	-- assign codes to each symbol
	-- c1 = "0", c2 = "1"
	-- As a common convention, bit '0' represents following the left child and bit '1' represents following the right child.
	-- c1 = left, c2 = right

	addCode(huff,0,0);
	if huff then
		huff.bcode = 0
		huff.blength = 1
	end

	-- READING
	-- bitfield = 0
	-- bitfield_len = 0
	-- read byte1
	-- bitfield = bitfield + bit_lshift(byte1, bitfield_len)
	-- bitfield_len = bitfield_len + 8
	-- read byte2
	-- bitfield = bitfield + bit_lshift(byte2, bitfield_len)
	-- bitfield_len = bitfield_len + 8
	-- (use 5 bits)
	--	word = bit_band( bitfield, bit_lshift(1,5)-1)
	--	bitfield = bit_rshift( bitfield, 5)
	--	bitfield_len = bitfield_len - 5
	-- read byte3
	-- bitfield = bitfield + bit_lshift(byte3, bitfield_len)
	-- bitfield_len = bitfield_len + 8

	-- WRITING
	remainder = 0;
	remainder_length = 0;

	local compressed = tables.Huffman_compressed
	--compressed_size = 0

	-- first byte is version info. 0 = uncompressed, 1 = 8-bit word huffman compressed
	compressed[1] = "\003"

	-- Header: byte 0=#leafs, byte 1-3=size of uncompressed data
	-- max 2^24 bytes
	local l = string_len(uncompressed)
	compressed[2] = string_char(bit_band(nLeafs-1, 255))			-- number of leafs
	compressed[3] = string_char(bit_band(l, 255))			-- bit 0-7
	compressed[4] = string_char(bit_band(bit_rshift(l, 8), 255))	-- bit 8-15
	compressed[5] = string_char(bit_band(bit_rshift(l, 16), 255))	-- bit 16-23
	compressed_size = 5

	-- create symbol/code map
	for symbol, leaf in pairs(symbols) do
		addBits(compressed, symbol, 8);
		if addBits(compressed, escape_code(leaf.bcode, leaf.blength)) then
			-- code word too long. Needs new revision to be able to handle more than 32 bits
			return string_char(0)..uncompressed
		end
		addBits(compressed, 3, 2);
	end

	-- create huffman code
	local large_compressed = tables.Huffman_large_compressed
	local large_compressed_size = 0
	local ulimit
	for i = 1, l, 200 do
		ulimit = l<(i+199) and l or (i+199)
		for sub_i = i, ulimit do
			c = string_byte(uncompressed, sub_i)
			addBits(compressed, symbols[c].bcode, symbols[c].blength)
		end
		large_compressed_size = large_compressed_size + 1
		large_compressed[large_compressed_size] = table_concat(compressed, "", 1, compressed_size)
		compressed_size = 0
	end

	-- add remainding bits (if any)
	if remainder_length>0 then
		large_compressed_size = large_compressed_size + 1
		large_compressed[large_compressed_size] = string_char(remainder)
	end
	local compressed_string = table_concat(large_compressed, "", 1, large_compressed_size)

	-- is compression worth it? If not, return uncompressed data.
	if (strlen(uncompressed)+1) <= strlen(compressed_string) then
		return "\001"..uncompressed
	end

	setCleanupTables("Huffman_compressed", "Huffman_large_compressed")
	return compressed_string
end

-- lookuptable (cached between calls)
local lshiftMask = {}
setmetatable(lshiftMask, {
	__index = function (t, k)
		local v = bit_lshift(1, k)
		rawset(t, k, v)
		return v
	end
})

-- lookuptable (cached between calls)
local lshiftMinusOneMask = {}
setmetatable(lshiftMinusOneMask, {
	__index = function (t, k)
		local v = bit_lshift(1, k)-1
		rawset(t, k, v)
		return v
	end
})

local function getCode(bitfield, field_len)
	if field_len>=2 then
		local b;
		local p = 0;
		for i = 0, field_len-1 do
			b = bit_band(bitfield, lshiftMask[i])
			if not (p==0) and not (b == 0) then
				-- found 2 bits set right after each other (stop bits)
				return bit_band( bitfield, lshiftMinusOneMask[i-1]), i-1,
					bit_rshift(bitfield, i+1), field_len-i-1
			end
			p = b
		end
	end
	return nil
end

local function unescape_code(code, code_len)
	local unescaped_code=0;
	local b;
	local l = 0;
	local i = 0
	while i < code_len do
		b = bit_band( code, lshiftMask[i])
		if not (b==0) then
			unescaped_code = bit_bor(unescaped_code, lshiftMask[l])
			i = i + 1
		end
		i = i + 1
		l = l + 1
	end
	return unescaped_code, l
end

tables.Huffman_uncompressed = {}
tables.Huffman_large_uncompressed = {} -- will always be as big as the larges string ever decompressed. Bad, but clearing i every timetakes precious time.

function LibCompress:DecompressHuffman(compressed)
	if type(compressed)~="string" or compressed == nil then
		if type(compressed)=="table" then
			return compressed
		end
		return nil
	end

	local compressed_size = strlen(compressed)
	--decode header
	local info_byte = string_byte(compressed)
	-- is data compressed
	if info_byte==1 then
		return string_sub(compressed,2) --return uncompressed data
	end
	if not (info_byte==3) then
		return nil, "Can only decompress Huffman compressed data ("..tostring(info_byte)..")"
	end

	local num_symbols = string_byte(string_sub(compressed, 2, 2)) + 1
	local c0 = string_byte(string_sub(compressed, 3, 3))
	local c1 = string_byte(string_sub(compressed, 4, 4))
	local c2 = string_byte(string_sub(compressed, 5, 5))
	local orig_size = c2*65536 + c1*256 + c0
	if orig_size==0 then
		return "";
	end

	-- decode code->symbal map
	local bitfield = 0;
	local bitfield_len = 0;
	local map = {} -- only table not reused in Huffman decode.
	setmetatable(map, {
		__index = function (t, k)
			local v = {}
			rawset(t, k, v)
			return v
		end
	})

	local i = 6; -- byte 1-5 are header bytes
	local c, cl;
	local minCodeLen = 1000;
	local maxCodeLen = 0;
	local symbol, code, code_len, _bitfield, _bitfield_len;
	local n = 0;
	local state = 0; -- 0 = get symbol (8 bits),  1 = get code (varying bits, ends with 2 bits set)
	while n<num_symbols do
		if i>compressed_size then
			return nil, "Cannot decode map"
		end

		c = string_byte(compressed, i)
		bitfield = bit_bor(bitfield, bit_lshift(c, bitfield_len))
		bitfield_len = bitfield_len + 8

		if state == 0 then
			symbol = bit_band(bitfield, 255)
			bitfield = bit_rshift(bitfield, 8)
			bitfield_len = bitfield_len -8
			state = 1 -- search for code now
		else
			code, code_len, _bitfield, _bitfield_len = getCode(bitfield, bitfield_len)
			if code then
				bitfield, bitfield_len = _bitfield, _bitfield_len
				c, cl = unescape_code(code, code_len)
				map[cl][c]=string_char(symbol)
				minCodeLen = cl<minCodeLen and cl or minCodeLen
				maxCodeLen = cl>maxCodeLen and cl or maxCodeLen
				--print("symbol: "..string_char(symbol).."  code: "..tobinary(c, cl))
				n = n + 1
				state = 0 -- search for next symbol (if any)
			end
		end
		i=i+1
	end

	-- dont create new subtables for entries not in the map. Waste of space.
	-- But do return an empty table to prevent runtime errors. (instead of returning nil)
	local mt = {}
	setmetatable(map, {
		__index = function (t, k)
			return mt
		end
	})

	local uncompressed = tables.Huffman_uncompressed
	local large_uncompressed = tables.Huffman_large_uncompressed
	local uncompressed_size = 0
	local large_uncompressed_size = 0
	local test_code
	local test_code_len = minCodeLen;
	local symbol;
	local dec_size = 0;
	compressed_size = compressed_size + 1
	local temp_limit = 200; -- first limit of uncompressed data. large_uncompressed will hold strings of length 200
	while true do
		if test_code_len<=bitfield_len then
			test_code=bit_band( bitfield, lshiftMinusOneMask[test_code_len])
			symbol = map[test_code_len][test_code]
			if symbol then
				uncompressed_size = uncompressed_size + 1
				uncompressed[uncompressed_size]=symbol
				dec_size = dec_size + 1

				if dec_size>=orig_size then -- checked here for speed reasons
					break;
				end
				-- process compressed bytes in smaller chunks
				large_uncompressed_size = large_uncompressed_size + 1
				large_uncompressed[large_uncompressed_size] = table_concat(uncompressed, "", 1, uncompressed_size)
				uncompressed_size = 0
				temp_limit = temp_limit + 200 -- repeated chunk size is 200 uncompressed bytes
				temp_limit = temp_limit > orig_size and orig_size or temp_limit

				bitfield = bit_rshift(bitfield, test_code_len)
				bitfield_len = bitfield_len - test_code_len
				test_code_len = minCodeLen
			else
				test_code_len = test_code_len + 1
				if test_code_len>maxCodeLen then
					return nil, "Decompression error at "..tostring(i).."/"..tostring(strlen(compressed))
				end
			end
		else
			c = string_byte(compressed, i)
			bitfield = bitfield + bit_lshift(c or 0, bitfield_len)
			bitfield_len = bitfield_len + 8
			i = i + 1
			if i > temp_limit then
				if i > compressed_size then
					break;
				end
			end
		end
	end

	setCleanupTables("Huffman_uncompressed", "Huffman_large_uncompressed")
	return table_concat(large_uncompressed, "", 1, large_uncompressed_size)..table_concat(uncompressed, "", 1, uncompressed_size)
end

--------------------------------------------------------------------------------
-- Generic codec interface

function LibCompress:DecompressUncompressed(data)
	if type(data)~="string" then
		return nil, "Can only handle strings"
	end
	if string.byte(data) ~= 1 then
		return nil, "Can only handle uncompressed data"
	end
	return string_sub(data, 2)
end

local compression_methods = {
	[2] = LibCompress.CompressLZW,
	[3] = LibCompress.CompressHuffman
}

local decompression_methods = {
	[1] = LibCompress.DecompressUncompressed,
	[2] = LibCompress.DecompressLZW,
	[3] = LibCompress.DecompressHuffman
}

-- try all compression codecs and return best result
function LibCompress:Compress(data)
	local method = next(compression_methods)
	local result = compression_methods[method](self, data)
	local nTable = {}
	local rTable = {}
	local n
	method = next(compression_methods, method)
	while method do
		n = compression_methods[method](self, data)
		nTable = {}
		for i = 1, strlen(n) do
			nTable[i] = string.sub(n, i, i)
		end
		for i = 1, strlen(result) do
			rTable[i] = string.sub(result, i, i)
		end
		if getn(nTable) < getn(rTable) then
			result = nTable
		end
		method = next(compression_methods, method)
	end
	return result
end

function LibCompress:Decompress(data)
	local header_info = string.byte(data)
	if decompression_methods[header_info] then
		return decompression_methods[header_info](self, data)
	else
		return nil, "Unknown compression method ("..tostring(header_info)..")"
	end
end