
--BMP file load/save.
--Written by Cosmin Apreutesei. Public Domain.

if not ... then require'bmp_demo'; return end

local ffi = require'ffi'
local bit = require'bit'
local bitmap = require'bitmap'
local glue = require'glue'
local shr, shl, bor, band, bnot =
	bit.rshift, bit.lshift, bit.bor, bit.band, bit.bnot

local M = {}

--BITMAPFILEHEADER
local file_header = ffi.typeof[[struct __attribute__((__packed__)) {
	char     magic[2]; // 'BM'
	uint32_t size;
	uint16_t reserved1;
	uint16_t reserved2;
	uint32_t image_offset;
	uint32_t header_size;
}]]

--BITMAPCOREHEADER, Windows 2.0 or later
local core_header = ffi.typeof[[struct __attribute__((__packed__)) {
	// BITMAPCOREHEADER
	uint16_t w;
	uint16_t h;
	uint16_t planes;       // 1
	uint16_t bpp;          // 1, 4, 8, 24
}]]

--BITMAPINFOHEADER, Windows NT, 3.1x or later
local info_header = ffi.typeof[[struct __attribute__((__packed__)) {
	int32_t  w;
	int32_t  h;
	uint16_t planes;       // 1
	uint16_t bpp;          // 0, 1, 4, 8, 16, 24, 32; 64 (GDI+)
	uint32_t compression;  // 0-6
	uint32_t image_size;   // 0 for BI_RGB
	uint32_t dpi_v;
	uint32_t dpi_h;
	uint32_t palette_colors; // 0 = 2^n
	uint32_t palette_colors_important; // ignored
}]]

--BITMAPV2INFOHEADER, undocumented, Adobe Photoshop
local v2_header = ffi.typeof([[struct __attribute__((__packed__)) {
	$;
	uint32_t mask_r;
	uint32_t mask_g;
	uint32_t mask_b;
}]], info_header)

--BITMAPV3INFOHEADER, undocumented, Adobe Photoshop
local v3_header = ffi.typeof([[struct __attribute__((__packed__)) {
	$;
	uint32_t mask_a;
}]], v2_header)

--BITMAPV4HEADER, Windows NT 4.0, 95 or later
local v4_header = ffi.typeof([[struct __attribute__((__packed__)) {
	$;
	uint32_t cs_type;
	struct { int32_t rx, ry, rz, gx, gy, gz, bx, by, bz; } endpoints;
	uint32_t gamma_r;
	uint32_t gamma_g;
	uint32_t gamma_b;
}]], v3_header)

--BITMAPV5HEADER, Windows NT 5.0, 98 or later
local v5_header = ffi.typeof([[struct __attribute__((__packed__)) {
	$;
	uint32_t intent;
	uint32_t profile_data;
	uint32_t profile_size;
	uint32_t reserved;
}]], v4_header)

local rgb_triple = ffi.typeof[[struct __attribute__((__packed__)) {
	uint8_t b;
	uint8_t g;
	uint8_t r;
}]]

local rgb_quad = ffi.typeof([[struct __attribute__((__packed__)) {
	$;
	uint8_t a;
}]], rgb_triple)

local compressions = {[0] = 'rgb', 'rle8', 'rle4', 'bitfields',
	'jpeg', 'png', 'alphabitfields'}

local valid_bpps = {
	rgb = glue.index{1, 2, 4, 8, 16, 24, 32, 64},
	rle4 = glue.index{4},
	rle8 = glue.index{8},
	bitfields = glue.index{16, 32},
	alphabitfields = glue.index{16, 32},
	jpeg = glue.index{0},
	png = glue.index{0},
}

M.open = glue.protect(function(read_bytes)

	--wrap the reader so we can count the bytes read
	local bytes_read = 0
	local function read(buf, sz)
		if sz == 0 then return buf end
		local sz = sz or ffi.sizeof(buf)
		assert(read_bytes(buf, sz) == sz, 'eof')
		bytes_read = bytes_read + sz
		return buf
	end

	--load the file header and validate it
	local fh = read(file_header())
	assert(ffi.string(fh.magic, 2) == 'BM')

	--load the DIB header
	local z = fh.header_size - 4
	local core --the ancient core header is more restricted
	local alpha_mask = true --bitfields can contain a mask for alpha or not
	local quad_pal = true --palette entries are quads except for core header
	local ext_bitmasks = false
	local h
	if z == ffi.sizeof(core_header) then
		core = true
		quad_pal = false
		h = read(core_header())
	elseif z == ffi.sizeof(info_header) then
		alpha_mask = false --...unless comp == 'alphabitfields', see below
		ext_bitmasks = true --bitfield masks are right after the header
		h = read(info_header())
	elseif z == ffi.sizeof(v2_header) then
		alpha_mask = false
		h = read(v2_header())
	elseif z == ffi.sizeof(v3_header) then
		h = read(v3_header())
	elseif z == ffi.sizeof(v4_header) then
		h = read(v4_header())
	elseif z == ffi.sizeof(v5_header) then
		h = read(v5_header())
	elseif z == 64 + 4 then
		error'OS22XBITMAPHEADER is not supported'
	else
		error('invalid info header size '..(z+4))
	end

	--validate it and extract info from it
	assert(h.planes == 1, 'invalid number of planes')
	local comp = core and 0 or h.compression
	local comp = assert(compressions[comp], 'invalid compression type')
	alpha_mask = alpha_mask or comp == 'alphabitfields' --Windows CE
	local bpp = h.bpp
	assert(valid_bpps[comp][bpp], 'invalid bpp')
	local rle = comp:find'^rle'
	local bitfields = comp:find'bitfields$'
	local palettized = bpp >=1 and bpp <= 8
	local width = h.w
	local height = math.abs(h.h)
	local bottom_up = h.h > 0
	assert(width >= 1, 'invalid width')
	assert(height >= 1, 'invalid height')

	--load the channel masks for bitfield bitmaps
	local bitmasks, has_alpha
	if bitfields then
		bitmasks = ffi.new('uint32_t[?]', 4)
		local masks_size = (alpha_mask and 4 or 3) * 4
		if ext_bitmasks then
			read(bitmasks, masks_size)
		else
			local masks_ptr = ffi.cast('uint8_t*', h) + ffi.offsetof(h, 'mask_r')
			ffi.copy(bitmasks, masks_ptr, masks_size)
		end
		has_alpha = bitmasks[3] > 0
	end

	--make a one-time palette loader and indexer
	local load_pal
	local pal_size = fh.image_offset - bytes_read
	assert(pal_size >= 0, 'invalid image offset')
	local function noop() end
	local function skip_pal()
		read(nil, pal_size) --null-read to pixel data
		load_pal = noop
	end
	load_pal = skip_pal
	local pal_count = 0
	local pal
	if palettized then
		local pal_entry_ct = quad_pal and rgb_quad or rgb_triple
		local pal_ct = ffi.typeof('$[?]', pal_entry_ct)
		pal_count = math.floor(pal_size / ffi.sizeof(pal_entry_ct))
		pal_count = math.min(pal_count, 2^bpp)
		if pal_count > 0 then
			function load_pal()
				pal = read(pal_ct(pal_count))
				read(nil, pal_size - ffi.sizeof(pal)) --null-read to pixel data
				load_pal = noop
			end
		end
	end
	local function pal_entry(i)
		load_pal()
		assert(i < pal_count, 'palette index out of range')
		return pal[i].r, pal[i].g, pal[i].b, 0xff
	end

	--make a row loader iterator and a bitmap loader
	local row_iterator, load_rows
	local function init_load()

		assert(not row_iterator, 'already loaded')

		if comp == 'jpeg' then
			error'jpeg not supported'
		elseif comp == 'png' then
			error'png not supported'
		end

		--decide on the row bitmap format and if needed make a pixel converter
		local format, convert_pixel, dst_colorspace
		if bitfields then --packed, standard or custom format

			--compute the shift distance and the number of bits for each mask
			local function mask_shr_bits(mask)
				if mask == 0 then
					return 0, 0
				end
				local shift = 0
				while band(mask, 1) == 0 do --lowest bit not reached yet
					mask = shr(mask, 1)
					shift = shift + 1
				end
				local bits = 0
				while mask > 0 do --highest bit not cleared yet
					mask = shr(mask, 1)
					bits = bits + 1
				end
				return shift, bits
			end

			--build a standard format name based on the bitfield masks
			local t = {} --{shr1, ...}
			local tc = {} --{shr -> color}
			local tb = {} --{shr -> bits}
			for ci, color in ipairs{'r', 'g', 'b', 'a'} do
				local shr, bits = mask_shr_bits(bitmasks[ci-1])
				if bits > 0 then
					t[#t+1] = shr
					tc[shr] = color
					tb[shr] = bits
				end
			end
			--NOTE: 16bit rgb bitmap formats have their color letters named
			--in little-endian order, so we need to reverse the color order.
			table.sort(t, bpp == 16 and function(a, b) return a > b end or nil)
			local tc2, tb2 = {}, {}
			for i,shr in ipairs(t) do
				tc2[i] = tc[shr]
				tb2[i] = tb[shr]
			end
			format = table.concat(tc2)..table.concat(tb2)
			format = format:gsub('([^%d])8?888$', '%18')

			--make a custom pixel converter if the bitfields do not represent
			--a standard format implemented in the `bitmap` module.
			if not bitmap.formats[format] then
				format = 'raw'..bpp
				dst_colorspace = 'rgba8'
				local r_and = bitmasks[0]
				local r_shr = mask_shr_bits(r_and)
				local g_and = bitmasks[1]
				local g_shr = mask_shr_bits(g_and)
				local b_and = bitmasks[2]
				local b_shr = mask_shr_bits(b_and)
				local a_and = bitmasks[3]
				local a_shr = mask_shr_bits(a_and)
				function convert_pixel(x)
					return
						shr(band(x, r_and), r_shr),
						shr(band(x, g_and), g_shr),
						shr(band(x, b_and), b_shr),
						has_alpha and shr(band(x, a_and), a_shr) or 0xff
				end
			end

		elseif bpp <= 8 then --palettized, using custom converter

			format = 'g'..bpp --using gray<1,2,4,8> as the base format
			dst_colorspace = 'rgba8'
			if bpp == 1 then
				function convert_pixel(g8)
					return pal_entry(shr(g8, 7))
				end
			elseif bpp == 2 then
				function convert_pixel(g8)
					return pal_entry(shr(g8, 6))
				end
			elseif bpp == 4 then
				function convert_pixel(g8)
					return pal_entry(shr(g8, 4))
				end
			elseif bpp == 8 then
				convert_pixel = pal_entry
			else
				assert(false)
			end

		else --packed, standard format

			local formats = {
				[16] = 'rgb0555',
				[24] = 'bgr8',
				[32] = 'bgrx8',
				[64] = 'bgrx16',
			}
			format = assert(formats[bpp])

		end

		--make a row reader: either a RLE decoder or a straight buffer reader
		local function row_reader(row_bmp)
			if rle then

				local read_pixels, fill_pixels

				local rle_buf = ffi.new'uint8_t[2]'
				local p = ffi.cast('uint8_t*', row_bmp.data)

				if bpp == 8 then --RLE8

					function read_pixels(i, n)
						read(p + i, n)
						--read the word-align padding
						local n2 = band(n + 1, bnot(1)) - n
						if n2 > 0 then
							read(nil, n2)
						end
					end

					function fill_pixels(i, n, v)
						ffi.fill(p + i, n, v)
					end

				elseif bpp == 4 then --RLE4

					local function shift_back(i, n) --shift data back one nibble
						local i0 = math.floor(i)
						if i0 == i then return end --no need for shifting
						p[i0] = bor(band(p[i0], 0xf0), shr(p[i0+1], 4)) --stitch the first nibble
						for i = math.ceil(i), i0 + n do
							p[i] = bor(shl(p[i], 4), shr(p[i+1], 4))
						end
					end

					function read_pixels(i, n)
						local i = i * 0.5
						local n = math.ceil(n * 0.5)
						read(p + math.ceil(i), n)
						shift_back(i, n)
						--read the word-align padding
						local n2 = band(n + 1, bnot(1)) - n
						if n2 > 0 then
							read(nil, n2)
						end
					end

					function fill_pixels(i, n, v)
						local i = i * 0.5
						local n = math.ceil(n * 0.5)
						ffi.fill(p + math.ceil(i), n, v)
						shift_back(i, n)
					end

				else
					assert(false)
				end

				local j = 0
				return function()
					local i = 0
					while true do
						read(rle_buf, 2)
						local n = rle_buf[0]
						local k = rle_buf[1]
						if n == 0 then --escape
							if k == 0 then --eol
								assert(i == width, 'RLE EOL too soon')
								j = j + 1
								break
							elseif k == 1 then --eof
								assert(j == height-1, 'RLE EOF too soon')
								break
							elseif k == 2 then --delta
								read(rle_buf, 2)
								local x = rle_buf[0]
								local y = rle_buf[1]
								--we can't use a row-by-row loader with this code
								error'RLE delta not supported'
							else --absolute mode: k = number of pixels to read
								assert(i + k <= width, 'RLE overflow')
								read_pixels(i, k)
								i = i + k
							end
						else --repeat: n = number of pixels to repeat, k = color
							assert(i + n <= width, 'RLE overflow')
							fill_pixels(i, n, k)
							i = i + n
						end
					end
				end

			else
				return function()
					read(row_bmp.data, row_bmp.stride)
				end
			end
		end

		function row_iterator(arg, ...)

			local dst_bmp
			if type(arg) == 'table' and arg.data then --arg is a bitmap
				dst_bmp = arg
			else --arg is a format name or specifier
				dst_bmp = bitmap.new(width, 1, arg, ...)
			end

			--load row function: convert or direct copy
			local load_row
			local stride = bitmap.aligned_stride(bitmap.min_stride(format, width))
			if convert_pixel                 --needs pixel conversion
				or dst_bmp.format ~= format   --needs pixel conversion
				or dst_bmp.w < width          --needs clipping
				or dst_bmp.stride < stride    --can't copy whole stride
			then
				local row_bmp = bitmap.new(width, 1, format, false, true)
				local read_row = row_reader(row_bmp)
				function load_row()
					read_row()
					bitmap.paint(dst_bmp, row_bmp, 0, 0,
						convert_pixel, nil, dst_colorspace)
				end
			else --load row into dst_bmp directly
				load_row = row_reader(dst_bmp)
			end

			load_pal()

			--unprotected row iterator
			local j = bottom_up and height or -1
			local j1 = bottom_up and -1 or height
			local step = bottom_up and -1 or 1
			return function()
				j = j + step
				if j == j1 then return end
				load_row()
				return j, dst_bmp
			end
		end

		function load_rows(arg, ...)

			local dst_bmp, dst_x, dst_y
			if type(arg) == 'table' and arg.data then
				dst_bmp, dst_x, dst_y = arg, ...
			else
				dst_bmp = bitmap.new(width, height, arg, ...)
			end
			local dst_x = dst_x or 0
			local dst_y = dst_y or 0

			local row_bmp = bitmap.new(width, 1, format, false, true)
			local read_row = row_reader(row_bmp)

			local function load_row(j)
				read_row()
				bitmap.paint(dst_bmp, row_bmp, dst_x, dst_y + j,
					convert_pixel, nil, dst_colorspace)
			end

			load_pal()

			local j0 = bottom_up and height-1 or 0
			local j1 = bottom_up and 0 or height-1
			local step = bottom_up and -1 or 1
			for j = j0, j1, step do
				load_row(j)
			end

			return dst_bmp
		end

	end

	local function bool(x)
		return x and true or false
	end

	--gather everything in a bmp object
	local bmp = {}
	--dimensions and color depth
	bmp.w = width
	bmp.h = height
	bmp.bpp = bpp
	--encoding info
	bmp.bottom_up = bottom_up
	bmp.compression = comp
	bmp.transparent = bool(has_alpha)
	bmp.palettized = bool(palettized)
	bmp.bitmasks = bitmasks --uint32_t[4] or nil
	bmp.rle = bool(rle)
	--low-level info
	bmp.file_header = fh
	bmp.header = h
	--palette
	bmp.pal_count = pal_count
	function bmp:load_pal()
		local ok, err = pcall(load_pal)
		if ok then
			self.pal = pal
			return true
		else
			return nil, err
		end
	end
	function bmp:pal_entry(i)
		return pal_entry(i)
	end
	--loading
	bmp.rows = function(self, ...)
		init_load()
		return row_iterator(...)
	end
	bmp.load = glue.protect(function(self, ...)
		init_load()
		return load_rows(...)
	end)

	return bmp
end)

M.save = glue.protect(function(bmp, write)
	local fh = file_header()
	local h = info_header()
	local image_size = h.w * h.h * 4
	local masks =
		'\x00\x00\xff\x00'..  --R
		'\x00\xff\x00\x00'..  --G
		'\xff\x00\x00\x00'..  --B
		'\x00\x00\x00\xff'    --A
	ffi.copy(fh.magic, 'BM', 2)
	fh.image_offset = ffi.sizeof(fh) + ffi.sizeof(h) + #masks
	fh.size = fh.image_offset + image_size
	fh.header_size = ffi.sizeof(h) + 4
	h.w = bmp.w
	h.h = bmp.h
	h.planes = 1
	h.bpp = 32
	h.compression = 3 --bitfields so we can have alpha
	h.image_size = image_size
	write(fh, ffi.sizeof(fh))
	write(h, ffi.sizeof(h))
	write(masks, #masks)
	--save progressively line-by-line using a 1-row bitmap
	local row_bmp = bitmap.new(bmp.w, 1, 'bgra8')
	for j=bmp.h-1,0,-1 do
		local src_row_bmp = bitmap.sub(bmp, 0, j, bmp.w, 1)
		bitmap.paint(row_bmp, src_row_bmp)
		write(row_bmp.data, row_bmp.stride)
	end
end)


return M
