-- lx-bundle:true

-- This file is part of Lx, created by Liqube <www.liqube.com>
-- Copyright Liqube Audio. All rights reserved.
-- Lx Interim License. See LICENSE for details.

--- Macintosh ICNS metadata and frame loader
-- @module plugins.icopreview.icns

local icns = {}

local PNG_SIGNATURE = "\137PNG\r\n\26\n"
local JP2_SIGNATURE = "\0\0\0\12jP  \r\n\135\n"
local MAX_ENTRY_COUNT = 256
local MAX_ELEMENT_COUNT = 512
local MAX_PAYLOAD_WORK = 64 * 1024 * 1024
local MAX_IMAGE_DIMENSION = 4096
local MAX_IMAGE_PIXELS = 16 * 1024 * 1024

local RGB24_HEADER_SIZES = { is32 = 0, il32 = 0, ih32 = 0, it32 = 4 }
local ALPHA_MASK_TYPES = {
	["16x16"] = "s8mk",
	["32x32"] = "l8mk",
	["48x48"] = "h8mk",
	["128x128"] = "t8mk",
}
local PALETTE_4 = {
	{ 0xFF, 0xFF, 0xFF }, { 0xFC, 0xF3, 0x05 }, { 0xFF, 0x64, 0x02 }, { 0xDD, 0x08, 0x06 },
	{ 0xF2, 0x08, 0x84 }, { 0x46, 0x00, 0xA5 }, { 0x00, 0x00, 0xD4 }, { 0x02, 0xAB, 0xEA },
	{ 0x1F, 0xB7, 0x14 }, { 0x00, 0x64, 0x11 }, { 0x56, 0x2C, 0x05 }, { 0x90, 0x71, 0x3A },
	{ 0xC0, 0xC0, 0xC0 }, { 0x80, 0x80, 0x80 }, { 0x40, 0x40, 0x40 }, { 0x00, 0x00, 0x00 },
}
local PALETTE_8_CUBE = { 0xFF, 0xCC, 0x99, 0x66, 0x33, 0x00 }
local PALETTE_8_EXTRA = { 0xEE, 0xDD, 0xBB, 0xAA, 0x88, 0x77, 0x55, 0x44, 0x22, 0x11 }

local IMAGE_TYPES = {
	ICON = { width = 32, height = 32, bits_per_pixel = 1, format = "1-BIT" },
	["ICN#"] = { width = 32, height = 32, bits_per_pixel = 1, format = "1-BIT" },
	["icm#"] = { width = 16, height = 12, bits_per_pixel = 1, format = "1-BIT" },
	icm4 = { width = 16, height = 12, bits_per_pixel = 4, format = "4-BIT", mask_type = "icm#" },
	icm8 = { width = 16, height = 12, bits_per_pixel = 8, format = "8-BIT", mask_type = "icm#" },
	["ics#"] = { width = 16, height = 16, bits_per_pixel = 1, format = "1-BIT" },
	ics4 = { width = 16, height = 16, bits_per_pixel = 4, format = "4-BIT", mask_type = "ics#" },
	ics8 = { width = 16, height = 16, bits_per_pixel = 8, format = "8-BIT", mask_type = "ics#" },
	is32 = { width = 16, height = 16, bits_per_pixel = 24, format = "RGB24", mask_type = "s8mk" },
	icl4 = { width = 32, height = 32, bits_per_pixel = 4, format = "4-BIT", mask_type = "ICN#" },
	icl8 = { width = 32, height = 32, bits_per_pixel = 8, format = "8-BIT", mask_type = "ICN#" },
	il32 = { width = 32, height = 32, bits_per_pixel = 24, format = "RGB24", mask_type = "l8mk" },
	["ich#"] = { width = 48, height = 48, bits_per_pixel = 1, format = "1-BIT" },
	ich4 = { width = 48, height = 48, bits_per_pixel = 4, format = "4-BIT", mask_type = "ich#" },
	ich8 = { width = 48, height = 48, bits_per_pixel = 8, format = "8-BIT", mask_type = "ich#" },
	ih32 = { width = 48, height = 48, bits_per_pixel = 24, format = "RGB24", mask_type = "h8mk" },
	it32 = { width = 128, height = 128, bits_per_pixel = 24, format = "RGB24", mask_type = "t8mk" },
	icp4 = { width = 16, height = 16, bits_per_pixel = 32, format = "32-BIT" },
	ic11 = { width = 32, height = 32, bits_per_pixel = 32, format = "32-BIT", density = 2 },
	icp5 = { width = 32, height = 32, bits_per_pixel = 32, format = "32-BIT" },
	ic12 = { width = 64, height = 64, bits_per_pixel = 32, format = "32-BIT", density = 2 },
	icp6 = { width = 64, height = 64, bits_per_pixel = 32, format = "32-BIT" },
	ic07 = { width = 128, height = 128, bits_per_pixel = 32, format = "32-BIT" },
	ic13 = { width = 256, height = 256, bits_per_pixel = 32, format = "32-BIT", density = 2 },
	ic08 = { width = 256, height = 256, bits_per_pixel = 32, format = "32-BIT" },
	ic14 = { width = 512, height = 512, bits_per_pixel = 32, format = "32-BIT", density = 2 },
	ic09 = { width = 512, height = 512, bits_per_pixel = 32, format = "32-BIT" },
	ic10 = { width = 1024, height = 1024, bits_per_pixel = 32, format = "32-BIT", density = 2 },
	ic04 = { width = 16, height = 16, bits_per_pixel = 32, format = "ARGB" },
	ic05 = { width = 32, height = 32, bits_per_pixel = 32, format = "ARGB" },
	ic06 = { width = 64, height = 64, bits_per_pixel = 32, format = "ARGB" },
	icsb = { width = 18, height = 18, bits_per_pixel = 32, format = "ARGB" },
	icsB = { width = 36, height = 36, bits_per_pixel = 32, format = "32-BIT", density = 2 },
	sb24 = { width = 24, height = 24, bits_per_pixel = 32, format = "32-BIT" },
	SB24 = { width = 48, height = 48, bits_per_pixel = 32, format = "32-BIT", density = 2 },
}


--- Validate dimensions before they reach preview layout or native decoding
-- @param width number decoded width
-- @param height number decoded height
-- @param format string image format name
-- @return boolean valid
-- @return string|nil error_message
local function validate_dimensions(width, height, format)
	if width < 1 or height < 1 then
		return false, format .. " dimensions must be positive"
	end
	if width > MAX_IMAGE_DIMENSION or height > MAX_IMAGE_DIMENSION
		or width > MAX_IMAGE_PIXELS // height
	then
		return false, string.format(
			"%s dimensions exceed the preview limit: %dx%d",
			format,
			width,
			height
		)
	end

	return true
end

--- Reject a byte range that extends beyond image-set data
-- @param data string complete ICNS data
-- @param offset number zero-based byte offset
-- @param size number byte count
-- @param description string range name for errors
local function check_range(data, offset, size, description)
	if offset < 0 or size < 0 or offset + size > #data then
		error(string.format(
			"%s is outside the file: offset=%d size=%d file_size=%d",
			description,
			offset,
			size,
			#data
		), 3)
	end
end

--- Read one big-endian unsigned 32-bit value
-- @param data string source bytes
-- @param offset number zero-based byte offset
-- @return number value
local function u32be(data, offset)
	check_range(data, offset, 4, "big-endian uint32")

	return string.unpack(">I4", data, offset + 1)
end

--- Read one byte
-- @param data string source bytes
-- @param offset number zero-based byte offset
-- @return number value
local function byte(data, offset)
	check_range(data, offset, 1, "byte")

	return data:byte(offset + 1)
end

--- Read dimensions and color information from a PNG element
-- @param payload string complete PNG payload
-- @return table|nil metadata
-- @return string|nil error_message
local function parse_png_metadata(payload)
	if #payload < 33 or payload:sub(1, 8) ~= PNG_SIGNATURE then
		return nil, "invalid or truncated PNG payload"
	end
	if u32be(payload, 8) ~= 13 or payload:sub(13, 16) ~= "IHDR" then
		return nil, "PNG does not begin with a normal IHDR chunk"
	end

	local width = u32be(payload, 16)
	local height = u32be(payload, 20)
	local valid, dimension_error = validate_dimensions(width, height, "PNG")
	if not valid then
		return nil, dimension_error
	end

	return {
		format = "png",
		width = width,
		height = height,
		bit_depth = byte(payload, 24),
		color_type = byte(payload, 25),
	}
end

--- Return effective total bits per pixel for one PNG element
-- @param image table parsed PNG metadata
-- @param fallback number ICNS element bit depth
-- @return number bits_per_pixel
local function get_png_bits_per_pixel(image, fallback)
	local channel_counts = { [0] = 1, [2] = 3, [3] = 1, [4] = 2, [6] = 4 }
	local channel_count = channel_counts[image.color_type]

	return channel_count and image.bit_depth * channel_count or fallback
end

--- Return encoded image format visible from one element payload
-- @param payload string element data
-- @param type_info table ICNS type metadata
-- @return string format
local function get_payload_format(payload, type_info)
	if payload:sub(1, 8) == PNG_SIGNATURE then
		return "PNG"
	end
	if payload:sub(1, #JP2_SIGNATURE) == JP2_SIGNATURE
		or payload:sub(1, 4) == "\255\79\255\81"
	then
		return "JP2"
	end
	if payload:sub(1, 4) == "ARGB" then
		return "ARGB"
	end

	return type_info.format
end

--- Load one encoded image through existing image loader
-- @param data string encoded image data
-- @return userdata|nil image
-- @return string|nil error_message
local function load_encoded_image(data)
	local loaded, image, load_error = pcall(canvas.load_image_data, data)

	if not loaded then
		return nil, tostring(image)
	end

	return image, load_error
end

--- Return one color from classic 256-color ICNS palette
-- First 215 entries are a 6x6x6 RGB cube without black, followed by
-- ten red, green, blue and gray ramps, then black
-- @param index number zero-based palette index
-- @return number red
-- @return number green
-- @return number blue
local function get_palette_8_color(index)
	if index < 215 then
		local red_index = index // 36
		local green_index = (index % 36) // 6
		local blue_index = index % 6

		return PALETTE_8_CUBE[red_index + 1],
			PALETTE_8_CUBE[green_index + 1],
			PALETTE_8_CUBE[blue_index + 1]
	end
	if index < 225 then
		return PALETTE_8_EXTRA[index - 214], 0, 0
	end
	if index < 235 then
		return 0, PALETTE_8_EXTRA[index - 224], 0
	end
	if index < 245 then
		return 0, 0, PALETTE_8_EXTRA[index - 234]
	end
	if index < 255 then
		local value = PALETTE_8_EXTRA[index - 244]
		return value, value, value
	end

	return 0, 0, 0
end

--- Decode an ICNS mask element to one alpha byte per pixel
-- One-bit mask resources may contain image bits followed by mask bits
-- @param element table mask element
-- @param width number image width
-- @param height number image height
-- @return string|nil alpha
-- @return string|nil error_message
local function decode_mask(element, width, height)
	local pixel_count = width * height
	local payload = element.payload

	if element.type_code:sub(2) == "8mk" then
		if #payload ~= pixel_count then
			return nil, string.format(
				"%s mask has invalid size: expected %d bytes, have %d",
				element.type_code,
				pixel_count,
				#payload
			)
		end

		return payload
	end

	local data_size = (pixel_count + 7) // 8
	local mask_offset = #payload == data_size * 2 and data_size or 0
	if #payload ~= data_size and #payload ~= data_size * 2 then
		return nil, string.format(
			"%s mask has invalid size: expected %d or %d bytes, have %d",
			element.type_code,
			data_size,
			data_size * 2,
			#payload
		)
	end

	local alpha = {}
	for pixel_index = 0, pixel_count - 1 do
		local packed = byte(payload, mask_offset + pixel_index // 8)
		local bit_index = 7 - pixel_index % 8
		alpha[#alpha + 1] = string.char((packed & (1 << bit_index)) ~= 0 and 255 or 0)
	end

	return table.concat(alpha)
end

--- Decode one classic 1-, 4- or 8-bit ICNS image to RGBA channels
-- @param entry table parsed ICNS entry
-- @return string|nil red
-- @return string|nil green
-- @return string|nil blue
-- @return string|nil alpha
-- @return string|nil error_message
local function decode_indexed(entry)
	local pixel_count = entry.width * entry.height
	local expected_size = (pixel_count * entry.bits_per_pixel + 7) // 8
	local payload = entry.payload
	local image_payload_size = expected_size

	if entry.bits_per_pixel == 1 and #payload == expected_size * 2 then
		image_payload_size = expected_size
	elseif #payload ~= expected_size then
		return nil, nil, nil, nil, string.format(
			"%s image data has invalid size: expected %d bytes, have %d",
			entry.type_code,
			expected_size,
			#payload
		)
	end

	local red = {}
	local green = {}
	local blue = {}

	for pixel_index = 0, pixel_count - 1 do
		local r
		local g
		local b

		if entry.bits_per_pixel == 8 then
			r, g, b = get_palette_8_color(byte(payload, pixel_index))
		elseif entry.bits_per_pixel == 4 then
			local packed = byte(payload, pixel_index // 2)
			local palette_index = pixel_index % 2 == 0 and packed >> 4 or packed & 0x0F
			local color = PALETTE_4[palette_index + 1]
			r, g, b = color[1], color[2], color[3]
		else
			local packed = byte(payload, pixel_index // 8)
			local bit_index = 7 - pixel_index % 8
			local value = (packed & (1 << bit_index)) ~= 0 and 0 or 255
			r, g, b = value, value, value
		end

		red[#red + 1] = string.char(r)
		green[#green + 1] = string.char(g)
		blue[#blue + 1] = string.char(b)
	end

	local alpha = string.rep("\255", pixel_count)
	if entry.mask_element then
		local mask_alpha, mask_error = decode_mask(entry.mask_element, entry.width, entry.height)
		if not mask_alpha then
			return nil, nil, nil, nil, mask_error
		end
		alpha = mask_alpha
	elseif entry.bits_per_pixel == 1 and #payload == image_payload_size * 2 then
		local mask_alpha, mask_error = decode_mask(
			{ type_code = entry.type_code, payload = payload },
			entry.width,
			entry.height
		)
		if not mask_alpha then
			return nil, nil, nil, nil, mask_error
		end
		alpha = mask_alpha
	end

	return table.concat(red), table.concat(green), table.concat(blue), alpha
end

--- Decode one PackBits-like ICNS color channel
-- @param payload string RGB element payload
-- @param offset number zero-based compressed-data offset
-- @param pixel_count number decoded channel byte count
-- @param type_code string ICNS element type for errors
-- @return string|nil channel
-- @return number|nil next_offset
-- @return string|nil error_message
local function decode_rle_channel(payload, offset, pixel_count, type_code)
	local chunks = {}
	local decoded_size = 0

	while decoded_size < pixel_count do
		if offset >= #payload then
			return nil, nil, string.format("truncated %s compressed color data", type_code)
		end

		local control = byte(payload, offset)
		offset = offset + 1
		local block_size

		if control >= 128 then
			block_size = control - 125
			if offset >= #payload then
				return nil, nil, string.format("truncated %s compressed color run", type_code)
			end
			if block_size > pixel_count - decoded_size then
				return nil, nil, string.format("%s compressed color run exceeds its channel", type_code)
			end

			chunks[#chunks + 1] = string.rep(string.char(byte(payload, offset)), block_size)
			offset = offset + 1
		else
			block_size = control + 1
			if block_size > pixel_count - decoded_size then
				return nil, nil, string.format("%s compressed color block exceeds its channel", type_code)
			end
			if offset + block_size > #payload then
				return nil, nil, string.format("truncated %s compressed color block", type_code)
			end

			chunks[#chunks + 1] = payload:sub(offset + 1, offset + block_size)
			offset = offset + block_size
		end

		decoded_size = decoded_size + block_size
	end

	return table.concat(chunks), offset
end

--- Decode one classic 24-bit ICNS element into separate RGB channels
-- @param entry table parsed ICNS entry
-- @return string|nil red
-- @return string|nil green
-- @return string|nil blue
-- @return string|nil alpha
-- @return string|nil error_message
local function decode_rgb24(entry)
	local header_size = RGB24_HEADER_SIZES[entry.type_code]
	local pixel_count = entry.width * entry.height
	local payload = entry.payload

	if header_size == nil then
		return nil, nil, nil, nil, "not a classic 24-bit ICNS element"
	end
	if header_size > 0 then
		if #payload < header_size or payload:sub(1, header_size) ~= string.rep("\0", header_size) then
			return nil, nil, nil, nil, string.format("invalid %s color-data prefix", entry.type_code)
		end
	end

	local data_size = #payload - header_size
	local red
	local green
	local blue

	if data_size == pixel_count * 3 then
		local red_parts = {}
		local green_parts = {}
		local blue_parts = {}

		for pixel_index = 0, pixel_count - 1 do
			local pixel_offset = header_size + pixel_index * 3
			red_parts[#red_parts + 1] = string.char(byte(payload, pixel_offset))
			green_parts[#green_parts + 1] = string.char(byte(payload, pixel_offset + 1))
			blue_parts[#blue_parts + 1] = string.char(byte(payload, pixel_offset + 2))
		end

		red = table.concat(red_parts)
		green = table.concat(green_parts)
		blue = table.concat(blue_parts)
	else
		local offset = header_size
		local decode_error

		red, offset, decode_error = decode_rle_channel(payload, offset, pixel_count, entry.type_code)
		if not red then
			return nil, nil, nil, nil, decode_error
		end
		green, offset, decode_error = decode_rle_channel(payload, offset, pixel_count, entry.type_code)
		if not green then
			return nil, nil, nil, nil, decode_error
		end
		blue, offset, decode_error = decode_rle_channel(payload, offset, pixel_count, entry.type_code)
		if not blue then
			return nil, nil, nil, nil, decode_error
		end
		if offset ~= #payload then
			return nil, nil, nil, nil, string.format("%s has trailing compressed color data", entry.type_code)
		end
	end

	local alpha
	if entry.mask_element then
		if #entry.mask_element.payload ~= pixel_count then
			return nil, nil, nil, nil, string.format(
				"%s mask has invalid size: expected %d bytes, have %d",
				entry.type_code,
				pixel_count,
				#entry.mask_element.payload
			)
		end

		alpha = entry.mask_element.payload
	else
		alpha = string.rep("\255", pixel_count)
	end

	return red, green, blue, alpha
end

--- Encode decoded RGB channels as a one-frame 32-bit ICO
-- Existing canvas loader already handles this representation for ICO previews
-- @param entry table parsed ICNS entry
-- @param red string red channel
-- @param green string green channel
-- @param blue string blue channel
-- @param alpha string alpha channel
-- @return string data
local function encode_rgba_as_ico(entry, red, green, blue, alpha)
	local width = entry.width
	local height = entry.height
	local color_rows = {}
	local mask_rows = {}
	local mask_stride = ((width + 31) // 32) * 4

	for y = height - 1, 0, -1 do
		local color_parts = {}

		for x = 0, width - 1 do
			local pixel_index = y * width + x + 1
			color_parts[#color_parts + 1] = string.char(
				blue:byte(pixel_index),
				green:byte(pixel_index),
				red:byte(pixel_index),
				alpha:byte(pixel_index)
			)
		end

		color_rows[#color_rows + 1] = table.concat(color_parts)

		local mask_bytes = {}
		for byte_index = 0, mask_stride - 1 do
			local mask_value = 0

			for bit_index = 0, 7 do
				local x = byte_index * 8 + bit_index
				if x < width then
					local pixel_index = y * width + x + 1
					if alpha:byte(pixel_index) == 0 then
						mask_value = mask_value | (0x80 >> bit_index)
					end
				end
			end

			mask_bytes[#mask_bytes + 1] = string.char(mask_value)
		end

		mask_rows[#mask_rows + 1] = table.concat(mask_bytes)
	end

	local color_data = table.concat(color_rows)
	local mask_data = table.concat(mask_rows)
	local dib = string.pack(
		"<I4i4i4I2I2I4I4i4i4I4I4",
		40, width, height * 2, 1, 32, 0,
		#color_data + #mask_data, 0, 0, 0, 0
	) .. color_data .. mask_data

	return string.pack(
		"<I2I2I2BBBBI2I2I4I4",
		0, 1, 1,
		width, height, 0, 0,
		1, 32, #dib, 22
	) .. dib
end

--- Return one element header plus its payload
-- @param element table parsed ICNS element
-- @return string data
local function encode_element(element)
	return element.type_code .. string.pack(">I4", #element.payload + 8) .. element.payload
end

--- Parse one complete ICNS file
-- @param data string complete ICNS data
-- @return table image_set
function icns.parse(data)
	assert(type(data) == "string", "ICNS data must be a string")
	check_range(data, 0, 8, "ICNS header")

	if data:sub(1, 4) ~= "icns" then
		error("invalid ICNS signature")
	end

	local total_size = u32be(data, 4)
	if total_size ~= #data then
		error(string.format(
			"ICNS size field does not match file size: header=%d file_size=%d",
			total_size,
			#data
		))
	end

	local elements = {}
	local elements_by_type = {}
	local copied_payload_size = 0
	local offset = 8

	while offset < total_size do
		if #elements >= MAX_ELEMENT_COUNT then
			error(string.format("ICNS contains too many elements (maximum %d)", MAX_ELEMENT_COUNT))
		end

		check_range(data, offset, 8, string.format("ICNS element header %d", #elements))

		local type_code = data:sub(offset + 1, offset + 4)
		local element_size = u32be(data, offset + 4)
		if element_size < 8 then
			error(string.format("ICNS element %d has invalid size: %d", #elements, element_size))
		end

		check_range(data, offset, element_size, string.format("ICNS element %d", #elements))
		local payload_size = element_size - 8
		if payload_size > MAX_PAYLOAD_WORK - copied_payload_size then
			error(string.format("ICNS element payloads exceed the %d MiB work limit", MAX_PAYLOAD_WORK // 1024 // 1024))
		end
		copied_payload_size = copied_payload_size + payload_size

		local element = {
			index = #elements,
			type_code = type_code,
			payload = data:sub(offset + 9, offset + element_size),
		}
		elements[#elements + 1] = element
		elements_by_type[type_code] = elements_by_type[type_code] or element
		offset = offset + element_size
	end

	if offset ~= total_size then
		error("ICNS element table does not end at the declared file size")
	end

	local image_set = {
		type = "icns",
		count = 0,
		element_count = #elements,
		file_size = #data,
		entries = {},
	}
	local payload_work = 0

	for element_index = 1, #elements do
		local element = elements[element_index]
		local type_info = IMAGE_TYPES[element.type_code]

		if type_info then
			if #image_set.entries >= MAX_ENTRY_COUNT then
				error(string.format("ICNS contains too many images (maximum %d)", MAX_ENTRY_COUNT))
			end

			local valid, dimension_error = validate_dimensions(type_info.width, type_info.height, "ICNS")
			if not valid then
				error(dimension_error)
			end

			local mask_element = type_info.mask_type and elements_by_type[type_info.mask_type] or nil
			if not mask_element and type_info.bits_per_pixel < 24 then
				local mask_type = ALPHA_MASK_TYPES[string.format("%dx%d", type_info.width, type_info.height)]
				mask_element = mask_type and elements_by_type[mask_type] or nil
			end
			local work_size = #element.payload + (mask_element and #mask_element.payload or 0)
			if work_size > MAX_PAYLOAD_WORK - payload_work then
				error(string.format("ICNS image payloads exceed the %d MiB work limit", MAX_PAYLOAD_WORK // 1024 // 1024))
			end
			payload_work = payload_work + work_size

			local entry = {
				index = #image_set.entries,
				element_index = element.index,
				type_code = element.type_code,
				width = type_info.width,
				height = type_info.height,
				bits_per_pixel = type_info.bits_per_pixel,
				density = type_info.density or 1,
				format = get_payload_format(element.payload, type_info),
				payload = element.payload,
				mask_element = mask_element,
				image = {
					width = type_info.width,
					height = type_info.height,
				},
			}

			if element.payload:sub(1, 8) == PNG_SIGNATURE then
				local image, metadata_error = parse_png_metadata(element.payload)
				entry.image = image
				entry.metadata_error = metadata_error

				if image then
					entry.width = image.width
					entry.height = image.height
					entry.bits_per_pixel = get_png_bits_per_pixel(image, entry.bits_per_pixel)

					if image.width ~= type_info.width or image.height ~= type_info.height then
						entry.metadata_error = string.format(
							"PNG dimensions do not match %s: %dx%d instead of %dx%d",
							element.type_code,
							image.width,
							image.height,
							type_info.width,
							type_info.height
						)
					end
				end
			end

			image_set.entries[#image_set.entries + 1] = entry
		end
	end

	image_set.count = #image_set.entries

	if image_set.count == 0 then
		error("ICNS contains no supported images")
	end

	return image_set
end

--- Read and parse one ICNS file
-- @param path string ICNS path
-- @return table|nil image_set
-- @return string|nil error_message
function icns.read_file(path)
	local handle, open_error = io.open(path, "rb")

	if not handle then
		return nil, open_error
	end

	local data = handle:read("*a")
	handle:close()

	local loaded, image_set = pcall(icns.parse, data)

	return loaded and image_set or nil, loaded and nil or tostring(image_set)
end

--- Decode one ICNS entry through existing image loader
-- PNG elements can be loaded directly; classic RGB elements are converted to a one-frame ICO
-- Other element encodings keep native ICNS fallback for loaders that support them
-- @param entry table parsed ICNS entry
-- @return userdata|nil image
-- @return string|nil error_message
function icns.load_entry_image(entry)
	if entry.payload:sub(1, 8) == PNG_SIGNATURE then
		return load_encoded_image(entry.payload)
	end

	if RGB24_HEADER_SIZES[entry.type_code] ~= nil then
		local red, green, blue, alpha, decode_error = decode_rgb24(entry)

		if not red then
			return nil, decode_error
		end

		return load_encoded_image(encode_rgba_as_ico(entry, red, green, blue, alpha))
	end

	if entry.bits_per_pixel == 1 or entry.bits_per_pixel == 4 or entry.bits_per_pixel == 8 then
		local red, green, blue, alpha, decode_error = decode_indexed(entry)

		if not red then
			return nil, decode_error
		end

		return load_encoded_image(encode_rgba_as_ico(entry, red, green, blue, alpha))
	end

	if entry.format == "JP2" then
		local image, load_error = load_encoded_image(entry.payload)

		if image then
			return image
		end

		-- Some loaders recognize JPEG 2000 only while it is wrapped in an ICNS container
		entry.direct_load_error = load_error
	end

	local elements = {
		encode_element({ type_code = entry.type_code, payload = entry.payload }),
	}

	if entry.mask_element then
		elements[#elements + 1] = encode_element(entry.mask_element)
	end

	local payload = table.concat(elements)
	local single_image = "icns" .. string.pack(">I4", #payload + 8) .. payload

	return load_encoded_image(single_image)
end

return icns
