/*
 * GO interface to libheif
 *
 * MIT License
 *
 * Copyright (c) 2018 Dirk Farin <dirk.farin@gmail.com>
 * Copyright (c) 2018 Joachim Bauch <bauch@struktur.de>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package heif

/*
#cgo pkg-config: libheif
#include <stdlib.h>
#include <string.h> // We use 'memcpy'
#include <libheif/heif.h>
*/
import "C"

import (
	"fmt"
	"image"
	"image/color"
	"io"
	"io/ioutil"
	"runtime"
	"unsafe"
)

func GetVersion() string {
	return C.GoString(C.heif_get_version())
}

type Compression C.heif_compression_format

const (
	CompressionUndefined    = C.heif_compression_undefined
	CompressionHEVC         = C.heif_compression_HEVC
	CompressionAV1          = C.heif_compression_AV1
	CompressionAVC          = C.heif_compression_AVC
	CompressionJPEG         = C.heif_compression_JPEG
	CompressionJPEG2000     = C.heif_compression_JPEG2000
	CompressionVVC          = C.heif_compression_VVC
	CompressionEVC          = C.heif_compression_EVC
	CompressionUncompressed = C.heif_compression_uncompressed
	CompressionMask         = C.heif_compression_mask
	CompressionHTJ2K        = C.heif_compression_HTJ2K
)

type Chroma C.heif_chroma

const (
	ChromaUndefined              = C.heif_chroma_undefined
	Chroma420                    = C.heif_chroma_420
	Chroma422                    = C.heif_chroma_422
	Chroma444                    = C.heif_chroma_444
	ChromaInterleavedRGB         = C.heif_chroma_interleaved_RGB
	ChromaInterleavedRGBA        = C.heif_chroma_interleaved_RGBA
	ChromaInterleavedRRGGBBAA_BE = C.heif_chroma_interleaved_RRGGBBAA_BE
	ChromaInterleavedRRGGBBAA_LE = C.heif_chroma_interleaved_RRGGBBAA_LE
	ChromaInterleavedRRGGBB_BE   = C.heif_chroma_interleaved_RRGGBB_BE
	ChromaInterleavedRRGGBB_LE   = C.heif_chroma_interleaved_RRGGBB_LE
	ChromaPlanar                 = C.heif_chroma_planar

	ChromaMonochrome       = C.heif_chroma_monochrome
	ChromaInterleaved24Bit = C.heif_chroma_interleaved_24bit
	ChromaInterleaved32Bit = C.heif_chroma_interleaved_32bit
)

type ChromaDownsamplingAlgorithm C.heif_chroma_downsampling_algorithm

const (
	ChromaDownsamplingAverage         = C.heif_chroma_downsampling_average
	ChromaDownsamplingNearestNeighbor = C.heif_chroma_downsampling_nearest_neighbor
	ChromaDownsamplingSharpYUV        = C.heif_chroma_downsampling_sharp_yuv
)

type ChromaUpsamplingAlgorithm C.heif_chroma_upsampling_algorithm

const (
	ChromaUpsamplingNearestNeighbor = C.heif_chroma_upsampling_nearest_neighbor
	ChromaUpsamplingBilinear        = C.heif_chroma_upsampling_bilinear
)

type Colorspace C.heif_colorspace

const (
	ColorspaceUndefined   = C.heif_colorspace_undefined
	ColorspaceYCbCr       = C.heif_colorspace_YCbCr
	ColorspaceRGB         = C.heif_colorspace_RGB
	ColorspaceCustom      = C.heif_colorspace_custom
	ColorspaceMonochrome  = C.heif_colorspace_monochrome
	ColorspaceFilterArray = C.heif_colorspace_filter_array
	ColorspaceNonvisual   = C.heif_colorspace_nonvisual
)

type Channel C.heif_channel

const (
	ChannelY           = C.heif_channel_Y
	ChannelCb          = C.heif_channel_Cb
	ChannelCr          = C.heif_channel_Cr
	ChannelR           = C.heif_channel_R
	ChannelG           = C.heif_channel_G
	ChannelB           = C.heif_channel_B
	ChannelAlpha       = C.heif_channel_Alpha
	ChannelInterleaved = C.heif_channel_interleaved
	ChannelFilterArray = C.heif_channel_filter_array
	ChannelDepth       = C.heif_channel_depth
	ChannelDisparity   = C.heif_channel_disparity
	ChannelUnknown     = C.heif_channel_unknown
)

type ProgressStep C.heif_progress_step

const (
	ProgressStepTotal    = C.heif_progress_step_total
	ProgressStepLoadTile = C.heif_progress_step_load_tile
)

type LosslessMode int

const (
	LosslessModeDisabled LosslessMode = iota
	LosslessModeEnabled
)

type LoggingLevel int

const (
	LoggingLevelNone LoggingLevel = iota
	LoggingLevelBasic
	LoggingLevelAdvanced
	LoggingLevelFull
)

// --- HeifError

type ErrorCode C.heif_error_code

const (
	ErrorOK = C.heif_error_Ok

	// Input file does not exist.
	ErrorInputDoesNotExist = C.heif_error_Input_does_not_exist

	// Error in input file. Corrupted or invalid content.
	errorInvalidInput = C.heif_error_Invalid_input

	// Input file type is not supported.
	ErrorUnsupportedFiletype = C.heif_error_Unsupported_filetype

	// Image requires an unsupported decoder feature.
	ErrorUnsupportedFeature = C.heif_error_Unsupported_feature

	// Library API has been used in an invalid way.
	ErrorUsage = C.heif_error_Usage_error

	// Could not allocate enough memory.
	ErrorMemoryAllocation = C.heif_error_Memory_allocation_error

	// The decoder plugin generated an error
	ErrorDecoderPlugin = C.heif_error_Decoder_plugin_error

	// The decoder plugin generated an error
	ErrorEncoderPlugin = C.heif_error_Encoder_plugin_error

	// Error during encoding or when writing to the output
	ErrorEncoding = C.heif_error_Encoding_error

	ErrorEndOfSequence = C.heif_error_End_of_sequence

	// Application has asked for a color profile type that does not exist
	ErrorColorProfileDoesNotExist = C.heif_error_Color_profile_does_not_exist

	ErrorPluginLoadingError = C.heif_error_Plugin_loading_error

	ErrorCanceled = C.heif_error_Canceled
)

type ErrorSubcode C.heif_suberror_code

const (
	// no further information available
	SuberrorUnspecified = C.heif_suberror_Unspecified

	// --- Invalid_input ---

	// End of data reached unexpectedly.
	SuberrorEndOfData = C.heif_suberror_End_of_data

	// Size of box (defined in header) is wrong
	SuberrorInvalidBoxSize = C.heif_suberror_Invalid_box_size

	// Mandatory 'ftyp' box is missing
	SuberrorNoFtypBox = C.heif_suberror_No_ftyp_box

	SuberrorNoIdatBox = C.heif_suberror_No_idat_box

	SuberrorNoMetaBox = C.heif_suberror_No_meta_box

	SuberrorNoMoovBox = C.heif_suberror_No_moov_box

	SuberrorNoHdlrBox = C.heif_suberror_No_hdlr_box

	SuberrorNoHvcCBox = C.heif_suberror_No_hvcC_box

	SuberrorNoPitmBox = C.heif_suberror_No_pitm_box

	SuberrorNoIpcoBox = C.heif_suberror_No_ipco_box

	SuberrorNoIpmaBox = C.heif_suberror_No_ipma_box

	SuberrorNoIlocBox = C.heif_suberror_No_iloc_box

	SuberrorNoIinfBox = C.heif_suberror_No_iinf_box

	SuberrorNoIprpBox = C.heif_suberror_No_iprp_box

	SuberrorNoIrefBox = C.heif_suberror_No_iref_box

	SuberrorNoPictHandler = C.heif_suberror_No_pict_handler

	// An item property referenced in the 'ipma' box is not existing in the 'ipco' container.
	SuberrorIpmaBoxReferencesNonexistingProperty = C.heif_suberror_Ipma_box_references_nonexisting_property

	// No properties have been assigned to an item.
	SuberrorNoPropertiesAssignedToItem = C.heif_suberror_No_properties_assigned_to_item

	// Image has no (compressed) data
	SuberrorNoItemData = C.heif_suberror_No_item_data

	// Invalid specification of image grid (tiled image)
	SuberrorInvalidGridData = C.heif_suberror_Invalid_grid_data

	// Tile-images in a grid image are missing
	SuberrorMissingGridImages = C.heif_suberror_Missing_grid_images

	// The colr (NCLX) box and the codec bitstream VUI/color signalling disagree.
	SuberrorNCLXColrVUIMismatch = C.heif_suberror_NCLX_colr_VUI_mismatch

	SuberrorNoAV1CBox = C.heif_suberror_No_av1C_box

	SuberrorNoAVCCBox = C.heif_suberror_No_avcC_box

	SuberrorInvalidCleanAperture = C.heif_suberror_Invalid_clean_aperture

	// Invalid specification of overlay image
	SuberrorInvalidOverlayData = C.heif_suberror_Invalid_overlay_data

	// Overlay image completely outside of visible canvas area
	SuberrorOverlayImageOutsideOfCanvas = C.heif_suberror_Overlay_image_outside_of_canvas

	SuberrorPluginIsNotLoaded = C.heif_suberror_Plugin_is_not_loaded

	SuberrorPluginLoadingError = C.heif_suberror_Plugin_loading_error

	SuberrorAuxiliaryImageTypeUnspecified = C.heif_suberror_Auxiliary_image_type_unspecified

	SuberrorCannotReadPluginDirectory = C.heif_suberror_Cannot_read_plugin_directory

	SuberrorNoMatchingDecoderInstalled = C.heif_suberror_No_matching_decoder_installed

	SuberrorNoOrInvalidPrimaryItem = C.heif_suberror_No_or_invalid_primary_item

	SuberrorNoInfeBox = C.heif_suberror_No_infe_box

	SuberrorUnknownColorProfileType = C.heif_suberror_Unknown_color_profile_type

	SuberrorWrongTileImageChromaFormat = C.heif_suberror_Wrong_tile_image_chroma_format

	SuberrorInvalidFractionalNumber = C.heif_suberror_Invalid_fractional_number

	SuberrorInvalidImageSize = C.heif_suberror_Invalid_image_size

	SuberrorCameraIntrinsicMatrixUndefined = C.heif_suberror_Camera_intrinsic_matrix_undefined

	SuberrorCameraExtrinsicMatrixUndefined = C.heif_suberror_Camera_extrinsic_matrix_undefined

	SuberrorDecompressionInvalidData = C.heif_suberror_Decompression_invalid_data

	// --- Memory_allocation_error ---

	// A security limit preventing unreasonable memory allocations was exceeded by the input file.
	// Please check whether the file is valid. If it is, contact us so that we could increase the
	// security limits further.
	SuberrorSecurityLimitExceeded = C.heif_suberror_Security_limit_exceeded

	CompressionInitialisationError = C.heif_suberror_Compression_initialisation_error

	// --- Usage_error ---

	// An item ID was used that is not present in the file.
	SuberrorNonexistingItemReferenced = C.heif_suberror_Nonexisting_item_referenced // also used for Invalid_input

	// An API argument was given a NULL pointer, which is not allowed for that function.
	SuberrorNullPointerArgument = C.heif_suberror_Null_pointer_argument

	// Image channel referenced that does not exist in the image
	SuberrorNonexistingImageChannelReferenced = C.heif_suberror_Nonexisting_image_channel_referenced

	// The version of the passed plugin is not supported.
	SuberrorUnsupportedPluginVersion = C.heif_suberror_Unsupported_plugin_version

	// The version of the passed writer is not supported.
	SuberrorUnsupportedWriterVersion = C.heif_suberror_Unsupported_writer_version

	// The given (encoder) parameter name does not exist.
	SuberrorUnsupportedParameter = C.heif_suberror_Unsupported_parameter

	// The value for the given parameter is not in the valid range.
	SuberrorInvalidParameterValue = C.heif_suberror_Invalid_parameter_value

	SuberrorInvalidProperty = C.heif_suberror_Invalid_property

	SuberrorItemReferenceCycle = C.heif_suberror_Item_reference_cycle

	SuberrorInvalidPixiBox = C.heif_suberror_Invalid_pixi_box

	SuberrorInvalidRegionData = C.heif_suberror_Invalid_region_data

	SuberrorNoIspeProperty = C.heif_suberror_No_ispe_property

	SuberrorWrongTileImagePixelDepth = C.heif_suberror_Wrong_tile_image_pixel_depth

	SuberrorUnknownNCLXColorPrimaries = C.heif_suberror_Unknown_NCLX_color_primaries

	SuberrorUnknownNCLXTransferCharacteristics = C.heif_suberror_Unknown_NCLX_transfer_characteristics

	SuberrorUnknownNCLXMatrixCoefficients = C.heif_suberror_Unknown_NCLX_matrix_coefficients

	SuberrorInvalidJ2KCodestream = C.heif_suberror_Invalid_J2K_codestream

	SuberrorNoVcCBox = C.heif_suberror_No_vvcC_box

	SuberrorNoIcbrBox = C.heif_suberror_No_icbr_box

	SuberrorInvalidMiniBox = C.heif_suberror_Invalid_mini_box

	// --- Unsupported_feature ---

	// Image was coded with an unsupported compression method.
	SuberrorUnsupportedCodec = C.heif_suberror_Unsupported_codec

	// Image is specified in an unknown way, e.g. as tiled grid image (which is supported)
	SuberrorUnsupportedImageType = C.heif_suberror_Unsupported_image_type

	SuberrorUnsupportedDataVersion = C.heif_suberror_Unsupported_data_version

	SuberrorUnsupportedGenericCompressionMethod = C.heif_suberror_Unsupported_generic_compression_method

	SuberrorUnsupportedEssentialProperty = C.heif_suberror_Unsupported_essential_property

	// The conversion of the source image to the requested chroma / colorspace is not supported.
	SuberrorUnsupportedColorConversion = C.heif_suberror_Unsupported_color_conversion

	SuberrorUnsupportedItemConstructionMethod = C.heif_suberror_Unsupported_item_construction_method

	SuberrorUnsupportedHeaderCompressionMethod = C.heif_suberror_Unsupported_header_compression_method

	SubErrorUnsupportedTrackType = C.heif_suberror_Unsupported_track_type

	// --- Encoder_plugin_error ---

	SuberrorUnsupportedBitDepth = C.heif_suberror_Unsupported_bit_depth

	// --- Encoding_error ---

	SuberrorCannotWriteOutputData = C.heif_suberror_Cannot_write_output_data

	SuberrorEncoderInitialization = C.heif_suberror_Encoder_initialization

	SuberrorEncoderEncoding = C.heif_suberror_Encoder_encoding

	SuberrorEncoderCleanup = C.heif_suberror_Encoder_cleanup

	SuberrorTooManyRegions = C.heif_suberror_Too_many_regions
)

type HeifError struct {
	Code    ErrorCode
	Subcode ErrorSubcode
	Message string
}

func (e *HeifError) Error() string {
	return e.Message
}

func convertHeifError(cerror C.struct_heif_error) error {
	if cerror.code == ErrorOK {
		return nil
	}

	return &HeifError{
		Code:    ErrorCode(cerror.code),
		Subcode: ErrorSubcode(cerror.subcode),
		Message: C.GoString(cerror.message),
	}
}

func convertItemIDs(ids []C.heif_item_id, count int) []int {
	result := make([]int, count)
	for i := 0; i < count; i++ {
		result[i] = int(ids[i])
	}
	return result
}

// --- Context

type Context struct {
	context *C.struct_heif_context
}

func NewContext() (*Context, error) {
	ctx := &Context{
		context: C.heif_context_alloc(),
	}
	if ctx.context == nil {
		return nil, fmt.Errorf("Could not allocate context")
	}

	runtime.SetFinalizer(ctx, freeHeifContext)
	return ctx, nil
}

func freeHeifContext(c *Context) {
	C.heif_context_free(c.context)
	c.context = nil
}

func (c *Context) ReadFromFile(filename string) error {
	c_filename := C.CString(filename)
	defer C.free(unsafe.Pointer(c_filename))

	err := C.heif_context_read_from_file(c.context, c_filename, nil)
	runtime.KeepAlive(c)
	return convertHeifError(err)
}

func (c *Context) ReadFromMemory(data []byte) error {
	// TODO: Use reader API internally.
	err := C.heif_context_read_from_memory(c.context, unsafe.Pointer(&data[0]), C.size_t(len(data)), nil)
	runtime.KeepAlive(c)
	return convertHeifError(err)
}

type Encoder struct {
	encoder *C.struct_heif_encoder
	id      string
	name    string
}

func (e *Encoder) ID() string {
	return e.id
}

func (e *Encoder) Name() string {
	return e.name
}

func (e *Encoder) SetQuality(q int) error {
	err := C.heif_encoder_set_lossy_quality(e.encoder, C.int(q))
	runtime.KeepAlive(e)
	return convertHeifError(err)
}

func (e *Encoder) SetLossless(l LosslessMode) error {
	err := C.heif_encoder_set_lossless(e.encoder, C.int(l))
	runtime.KeepAlive(e)
	return convertHeifError(err)
}

func (e *Encoder) SetLoggingLevel(l LoggingLevel) error {
	err := C.heif_encoder_set_logging_level(e.encoder, C.int(l))
	runtime.KeepAlive(e)
	return convertHeifError(err)
}

func freeHeifEncoder(enc *Encoder) {
	C.heif_encoder_release(enc.encoder)
	enc.encoder = nil
}

func (c *Context) convertEncoderDescriptor(d *C.struct_heif_encoder_descriptor) (*Encoder, error) {
	cid := C.heif_encoder_descriptor_get_id_name(d)
	cname := C.heif_encoder_descriptor_get_name(d)
	enc := &Encoder{
		id:   C.GoString(cid),
		name: C.GoString(cname),
	}
	err := C.heif_context_get_encoder(c.context, d, &enc.encoder)
	runtime.KeepAlive(c)
	if err := convertHeifError(err); err != nil {
		return nil, err
	}
	runtime.SetFinalizer(enc, freeHeifEncoder)
	return enc, nil
}

func (c *Context) NewEncoder(compression Compression) (*Encoder, error) {
	const max = 1
	descriptors := make([]*C.struct_heif_encoder_descriptor, max)
	num := int(C.heif_context_get_encoder_descriptors(c.context, C.heif_compression_format(compression), nil, &descriptors[0], C.int(max)))
	runtime.KeepAlive(c)
	if num == 0 {
		return nil, fmt.Errorf("no encoder for compression %v", compression)
	}
	return c.convertEncoderDescriptor(descriptors[0])
}

func (c *Context) WriteToFile(filename string) error {
	err := C.heif_context_write_to_file(c.context, C.CString(filename))
	runtime.KeepAlive(c)
	return convertHeifError(err)
}

func (c *Context) GetNumberOfTopLevelImages() int {
	i := int(C.heif_context_get_number_of_top_level_images(c.context))
	runtime.KeepAlive(c)
	return i
}

func (c *Context) IsTopLevelImageID(ID int) bool {
	ok := C.heif_context_is_top_level_image_ID(c.context, C.heif_item_id(ID)) != 0
	runtime.KeepAlive(c)
	return ok
}

func (c *Context) GetListOfTopLevelImageIDs() []int {
	num := int(C.heif_context_get_number_of_top_level_images(c.context))
	runtime.KeepAlive(c)
	if num == 0 {
		return []int{}
	}

	origIDs := make([]C.heif_item_id, num)
	C.heif_context_get_list_of_top_level_image_IDs(c.context, &origIDs[0], C.int(num))
	runtime.KeepAlive(c)
	return convertItemIDs(origIDs, num)
}

func (c *Context) GetPrimaryImageID() (int, error) {
	var id C.heif_item_id
	err := C.heif_context_get_primary_image_ID(c.context, &id)
	runtime.KeepAlive(c)
	if err := convertHeifError(err); err != nil {
		return 0, err
	}
	return int(id), nil
}

// --- ImageHandle

type ImageHandle struct {
	handle *C.struct_heif_image_handle
}

func freeHeifImageHandle(c *ImageHandle) {
	C.heif_image_handle_release(c.handle)
	c.handle = nil
}

func (c *Context) GetPrimaryImageHandle() (*ImageHandle, error) {
	var handle ImageHandle
	err := C.heif_context_get_primary_image_handle(c.context, &handle.handle)
	runtime.KeepAlive(c)
	if err := convertHeifError(err); err != nil {
		return nil, err
	}
	runtime.SetFinalizer(&handle, freeHeifImageHandle)
	return &handle, convertHeifError(err)
}

func (c *Context) GetImageHandle(id int) (*ImageHandle, error) {
	var handle ImageHandle
	err := C.heif_context_get_image_handle(c.context, C.heif_item_id(id), &handle.handle)
	runtime.KeepAlive(c)
	if err := convertHeifError(err); err != nil {
		return nil, err
	}
	runtime.SetFinalizer(&handle, freeHeifImageHandle)
	return &handle, nil
}

func (h *ImageHandle) IsPrimaryImage() bool {
	ok := C.heif_image_handle_is_primary_image(h.handle) != 0
	runtime.KeepAlive(h)
	return ok
}

func (h *ImageHandle) GetWidth() int {
	i := int(C.heif_image_handle_get_width(h.handle))
	runtime.KeepAlive(h)
	return i
}

func (h *ImageHandle) GetHeight() int {
	i := int(C.heif_image_handle_get_height(h.handle))
	runtime.KeepAlive(h)
	return i
}

func (h *ImageHandle) HasAlphaChannel() bool {
	ok := C.heif_image_handle_has_alpha_channel(h.handle) != 0
	runtime.KeepAlive(h)
	return ok
}

func (h *ImageHandle) HasDepthImage() bool {
	ok := C.heif_image_handle_has_depth_image(h.handle) != 0
	runtime.KeepAlive(h)
	return ok
}

func (h *ImageHandle) GetNumberOfDepthImages() int {
	i := int(C.heif_image_handle_get_number_of_depth_images(h.handle))
	runtime.KeepAlive(h)
	return i
}

func (h *ImageHandle) GetListOfDepthImageIDs() []int {
	num := int(C.heif_image_handle_get_number_of_depth_images(h.handle))
	runtime.KeepAlive(h)
	if num == 0 {
		return []int{}
	}

	origIDs := make([]C.heif_item_id, num)
	C.heif_image_handle_get_list_of_depth_image_IDs(h.handle, &origIDs[0], C.int(num))
	runtime.KeepAlive(h)
	return convertItemIDs(origIDs, num)
}

func (h *ImageHandle) GetDepthImageHandle(depth_image_id int) (*ImageHandle, error) {
	var handle ImageHandle
	err := C.heif_image_handle_get_depth_image_handle(h.handle, C.heif_item_id(depth_image_id), &handle.handle)
	runtime.KeepAlive(h)
	if err := convertHeifError(err); err != nil {
		return nil, err
	}

	runtime.SetFinalizer(&handle, freeHeifImageHandle)
	return &handle, nil
}

func (h *ImageHandle) GetNumberOfThumbnails() int {
	i := int(C.heif_image_handle_get_number_of_thumbnails(h.handle))
	runtime.KeepAlive(h)
	return i
}

func (h *ImageHandle) GetListOfThumbnailIDs() []int {
	num := int(C.heif_image_handle_get_number_of_thumbnails(h.handle))
	runtime.KeepAlive(h)
	if num == 0 {
		return []int{}
	}

	origIDs := make([]C.heif_item_id, num)
	C.heif_image_handle_get_list_of_thumbnail_IDs(h.handle, &origIDs[0], C.int(num))
	runtime.KeepAlive(h)
	return convertItemIDs(origIDs, num)
}

func (h *ImageHandle) GetThumbnail(thumbnail_id int) (*ImageHandle, error) {
	var handle ImageHandle
	err := C.heif_image_handle_get_thumbnail(h.handle, C.heif_item_id(thumbnail_id), &handle.handle)
	runtime.KeepAlive(h)
	runtime.SetFinalizer(&handle, freeHeifImageHandle)
	return &handle, convertHeifError(err)
}

// TODO: EXIF metadata

// --- Image

type DecodingOptions struct {
	options *C.struct_heif_decoding_options
}

func NewDecodingOptions() (*DecodingOptions, error) {
	options := &DecodingOptions{
		options: C.heif_decoding_options_alloc(),
	}
	if options.options == nil {
		return nil, fmt.Errorf("Could not allocate decoding options")
	}

	runtime.SetFinalizer(options, freeHeifDecodingOptions)
	return options, nil
}

func freeHeifDecodingOptions(options *DecodingOptions) {
	C.heif_decoding_options_free(options.options)
	options.options = nil
}

type Image struct {
	image *C.struct_heif_image
}

func NewImage(width, height int, colorspace Colorspace, chroma Chroma) (*Image, error) {
	var image Image
	err := C.heif_image_create(C.int(width), C.int(height), C.heif_colorspace(colorspace), C.heif_chroma(chroma), &image.image)
	if err := convertHeifError(err); err != nil {
		return nil, err
	}
	runtime.SetFinalizer(&image, freeHeifImage)
	return &image, nil
}

func freeHeifImage(image *Image) {
	C.heif_image_release(image.image)
	image.image = nil
}

func (h *ImageHandle) DecodeImage(colorspace Colorspace, chroma Chroma, options *DecodingOptions) (*Image, error) {
	var image Image

	var opt *C.struct_heif_decoding_options
	if options != nil {
		opt = options.options
	}

	err := C.heif_decode_image(h.handle, &image.image, C.heif_colorspace(colorspace), C.heif_chroma(chroma), opt)
	runtime.KeepAlive(h)
	if err := convertHeifError(err); err != nil {
		return nil, err
	}

	runtime.SetFinalizer(&image, freeHeifImage)
	return &image, nil
}

func (img *Image) GetColorspace() Colorspace {
	cs := Colorspace(C.heif_image_get_colorspace(img.image))
	runtime.KeepAlive(img)
	return cs
}

func (img *Image) GetChromaFormat() Chroma {
	c := Chroma(C.heif_image_get_chroma_format(img.image))
	runtime.KeepAlive(img)
	return c
}

func (img *Image) GetWidth(channel Channel) int {
	i := int(C.heif_image_get_width(img.image, C.heif_channel(channel)))
	runtime.KeepAlive(img)
	return i
}

func (img *Image) GetHeight(channel Channel) int {
	i := int(C.heif_image_get_height(img.image, C.heif_channel(channel)))
	runtime.KeepAlive(img)
	return i
}

func (img *Image) GetBitsPerPixel(channel Channel) int {
	i := int(C.heif_image_get_bits_per_pixel(img.image, C.heif_channel(channel)))
	runtime.KeepAlive(img)
	return i
}

func (img *Image) GetBitsPerPixelRange(channel Channel) int {
	i := int(C.heif_image_get_bits_per_pixel_range(img.image, C.heif_channel(channel)))
	runtime.KeepAlive(img)
	return i
}

func (img *Image) GetImage() (image.Image, error) {
	var i image.Image
	cf := img.GetChromaFormat()
	switch cs := img.GetColorspace(); cs {
	case ColorspaceYCbCr:
		var subsample image.YCbCrSubsampleRatio
		switch cf {
		case Chroma420:
			subsample = image.YCbCrSubsampleRatio420
		case Chroma422:
			subsample = image.YCbCrSubsampleRatio422
		case Chroma444:
			subsample = image.YCbCrSubsampleRatio444
		default:
			return nil, fmt.Errorf("Unsupported YCbCr chroma format: %v", cf)
		}
		y, err := img.GetPlane(ChannelY)
		if err != nil {
			return nil, err
		}
		cb, err := img.GetPlane(ChannelCb)
		if err != nil {
			return nil, err
		}
		cr, err := img.GetPlane(ChannelCr)
		if err != nil {
			return nil, err
		}
		i = &image.YCbCr{
			Y:              y.Plane,
			Cb:             cb.Plane,
			Cr:             cr.Plane,
			YStride:        y.Stride,
			CStride:        cb.Stride,
			SubsampleRatio: subsample,
			Rect: image.Rectangle{
				Min: image.Point{
					X: 0,
					Y: 0,
				},
				Max: image.Point{
					X: img.GetWidth(ChannelY),
					Y: img.GetHeight(ChannelY),
				},
			},
		}
	case ColorspaceRGB:
		switch cf {
		case Chroma444:
			r, err := img.GetPlane(ChannelR)
			if err != nil {
				return nil, err
			}
			g, err := img.GetPlane(ChannelG)
			if err != nil {
				return nil, err
			}
			b, err := img.GetPlane(ChannelB)
			if err != nil {
				return nil, err
			}
			width := img.GetWidth(ChannelR)
			height := img.GetHeight(ChannelR)
			read_pos_r := 0
			read_pos_g := 0
			read_pos_b := 0
			write_pos := 0
			var rgba []byte
			var stride int
			if bpp := img.GetBitsPerPixelRange(ChannelR); bpp > 8 {
				// NOTE: We only support the same bits per pixel on all components.
				stride = width * 8
				rgba = make([]byte, height*stride)
				stride_add_r := r.Stride - width*2
				stride_add_g := g.Stride - width*2
				stride_add_b := b.Stride - width*2
				if bpp == 16 {
					for y := 0; y < height; y++ {
						for x := 0; x < width; x++ {
							rgba[write_pos] = r.Plane[read_pos_r]
							rgba[write_pos+1] = r.Plane[read_pos_r+1]
							rgba[write_pos+2] = g.Plane[read_pos_g]
							rgba[write_pos+3] = g.Plane[read_pos_g+1]
							rgba[write_pos+4] = b.Plane[read_pos_b]
							rgba[write_pos+5] = b.Plane[read_pos_b+1]
							rgba[write_pos+6] = 0xff
							rgba[write_pos+7] = 0xff
							read_pos_r += 2
							read_pos_g += 2
							read_pos_b += 2
							write_pos += 8
						}
						read_pos_r += stride_add_r
						read_pos_g += stride_add_g
						read_pos_b += stride_add_b
					}
				} else {
					for y := 0; y < height; y++ {
						for x := 0; x < width; x++ {
							r_value := (int16(r.Plane[read_pos_r+1]) << 8) | int16(r.Plane[read_pos_r])
							r_value = (r_value << (16 - uint(bpp))) | (r_value >> (2*uint(bpp) - 16))
							rgba[write_pos] = byte(r_value >> 8)
							rgba[write_pos+1] = byte(r_value & 0xff)
							g_value := (int16(g.Plane[read_pos_g+1]) << 8) | int16(g.Plane[read_pos_g])
							g_value = (g_value << (16 - uint(bpp))) | (g_value >> (2*uint(bpp) - 16))
							rgba[write_pos+2] = byte(g_value >> 8)
							rgba[write_pos+3] = byte(g_value & 0xff)
							b_value := (int16(b.Plane[read_pos_b+1]) << 8) | int16(b.Plane[read_pos_b])
							b_value = (b_value << (16 - uint(bpp))) | (b_value >> (2*uint(bpp) - 16))
							rgba[write_pos+4] = byte(b_value >> 8)
							rgba[write_pos+5] = byte(b_value & 0xff)
							rgba[write_pos+6] = 0xff
							rgba[write_pos+7] = 0xff
							read_pos_r += 2
							read_pos_g += 2
							read_pos_b += 2
							write_pos += 8
						}
						read_pos_r += stride_add_r
						read_pos_g += stride_add_g
						read_pos_b += stride_add_b
					}
				}

				i = &image.RGBA64{
					Pix:    rgba,
					Stride: stride,
					Rect: image.Rectangle{
						Min: image.Point{
							X: 0,
							Y: 0,
						},
						Max: image.Point{
							X: width,
							Y: height,
						},
					},
				}
			} else {
				stride = width * 4
				rgba = make([]byte, height*stride)
				stride_add_r := r.Stride - width
				stride_add_g := g.Stride - width
				stride_add_b := b.Stride - width
				for y := 0; y < height; y++ {
					for x := 0; x < width; x++ {
						rgba[write_pos] = r.Plane[read_pos_r]
						rgba[write_pos+1] = g.Plane[read_pos_g]
						rgba[write_pos+2] = b.Plane[read_pos_b]
						rgba[write_pos+3] = 0xff
						read_pos_r++
						read_pos_g++
						read_pos_b++
						write_pos += 4
					}
					read_pos_r += stride_add_r
					read_pos_g += stride_add_g
					read_pos_b += stride_add_b
				}

				i = &image.RGBA{
					Pix:    rgba,
					Stride: stride,
					Rect: image.Rectangle{
						Min: image.Point{
							X: 0,
							Y: 0,
						},
						Max: image.Point{
							X: width,
							Y: height,
						},
					},
				}
			}
		case ChromaInterleavedRGB:
			rgb, err := img.GetPlane(ChannelInterleaved)
			if err != nil {
				return nil, err
			}
			width := img.GetWidth(ChannelInterleaved)
			height := img.GetHeight(ChannelInterleaved)
			rgba := make([]byte, width*height*4)
			read_pos := 0
			write_pos := 0
			stride_add := rgb.Stride - width*3
			for y := 0; y < height; y++ {
				for x := 0; x < width; x++ {
					rgba[write_pos] = rgb.Plane[read_pos]
					rgba[write_pos+1] = rgb.Plane[read_pos+1]
					rgba[write_pos+2] = rgb.Plane[read_pos+2]
					rgba[write_pos+3] = 0xff
					read_pos += 3
					write_pos += 4
				}
				read_pos += stride_add
			}
			i = &image.RGBA{
				Pix:    rgba,
				Stride: width * 4,
				Rect: image.Rectangle{
					Min: image.Point{
						X: 0,
						Y: 0,
					},
					Max: image.Point{
						X: width,
						Y: height,
					},
				},
			}
		case ChromaInterleavedRGBA:
			rgba, err := img.GetPlane(ChannelInterleaved)
			if err != nil {
				return nil, err
			}
			i = &image.RGBA{
				Pix:    rgba.Plane,
				Stride: rgba.Stride,
				Rect: image.Rectangle{
					Min: image.Point{
						X: 0,
						Y: 0,
					},
					Max: image.Point{
						X: img.GetWidth(ChannelInterleaved),
						Y: img.GetHeight(ChannelInterleaved),
					},
				},
			}
		case ChromaInterleavedRRGGBB_BE:
			rgb, err := img.GetPlane(ChannelInterleaved)
			if err != nil {
				return nil, err
			}
			width := img.GetWidth(ChannelInterleaved)
			height := img.GetHeight(ChannelInterleaved)
			rgba := make([]byte, width*height*8)
			read_pos := 0
			write_pos := 0
			stride_add := rgb.Stride - width*6
			if bpp := img.GetBitsPerPixelRange(ChannelInterleaved); bpp != 16 {
				for y := 0; y < height; y++ {
					for x := 0; x < width; x++ {
						r_value := (int16(rgb.Plane[read_pos]) << 8) | int16(rgb.Plane[read_pos+1])
						r_value = (r_value << (16 - uint(bpp))) | (r_value >> (2*uint(bpp) - 16))
						rgba[write_pos] = byte(r_value >> 8)
						rgba[write_pos+1] = byte(r_value & 0xff)
						g_value := (int16(rgb.Plane[read_pos+2]) << 8) | int16(rgb.Plane[read_pos+3])
						g_value = (g_value << (16 - uint(bpp))) | (g_value >> (2*uint(bpp) - 16))
						rgba[write_pos+2] = byte(g_value >> 8)
						rgba[write_pos+3] = byte(g_value & 0xff)
						b_value := (int16(rgb.Plane[read_pos+4]) << 8) | int16(rgb.Plane[read_pos+5])
						b_value = (b_value << (16 - uint(bpp))) | (b_value >> (2*uint(bpp) - 16))
						rgba[write_pos+4] = byte(b_value >> 8)
						rgba[write_pos+5] = byte(b_value & 0xff)
						rgba[write_pos+6] = 0xff
						rgba[write_pos+7] = 0xff
						read_pos += 6
						write_pos += 8
					}
					read_pos += stride_add
				}
			} else {
				for y := 0; y < height; y++ {
					for x := 0; x < width; x++ {
						rgba[write_pos] = rgb.Plane[read_pos]
						rgba[write_pos+1] = rgb.Plane[read_pos+1]
						rgba[write_pos+2] = rgb.Plane[read_pos+2]
						rgba[write_pos+3] = rgb.Plane[read_pos+3]
						rgba[write_pos+4] = rgb.Plane[read_pos+4]
						rgba[write_pos+5] = rgb.Plane[read_pos+5]
						rgba[write_pos+6] = 0xff
						rgba[write_pos+7] = 0xff
						read_pos += 6
						write_pos += 8
					}
					read_pos += stride_add
				}
			}
			i = &image.RGBA64{
				Pix:    rgba,
				Stride: width * 4,
				Rect: image.Rectangle{
					Min: image.Point{
						X: 0,
						Y: 0,
					},
					Max: image.Point{
						X: width,
						Y: height,
					},
				},
			}
		case ChromaInterleavedRRGGBBAA_BE:
			rgba, err := img.GetPlane(ChannelInterleaved)
			if err != nil {
				return nil, err
			}
			width := img.GetWidth(ChannelInterleaved)
			height := img.GetHeight(ChannelInterleaved)
			var plane []byte
			if bpp := img.GetBitsPerPixelRange(ChannelInterleaved); bpp != 16 {
				read_pos := 0
				write_pos := 0
				stride_add := rgba.Stride - width*8
				plane = make([]byte, width*height*8)
				for y := 0; y < height; y++ {
					for x := 0; x < width; x++ {
						r_value := (int16(rgba.Plane[read_pos]) << 8) | int16(rgba.Plane[read_pos+1])
						r_value = (r_value << (16 - uint(bpp))) | (r_value >> (2*uint(bpp) - 16))
						plane[write_pos] = byte(r_value >> 8)
						plane[write_pos+1] = byte(r_value & 0xff)
						g_value := (int16(rgba.Plane[read_pos+2]) << 8) | int16(rgba.Plane[read_pos+3])
						g_value = (g_value << (16 - uint(bpp))) | (g_value >> (2*uint(bpp) - 16))
						plane[write_pos+2] = byte(g_value >> 8)
						plane[write_pos+3] = byte(g_value & 0xff)
						b_value := (int16(rgba.Plane[read_pos+4]) << 8) | int16(rgba.Plane[read_pos+5])
						b_value = (b_value << (16 - uint(bpp))) | (b_value >> (2*uint(bpp) - 16))
						plane[write_pos+4] = byte(b_value >> 8)
						plane[write_pos+5] = byte(b_value & 0xff)
						a_value := (int16(rgba.Plane[read_pos+6]) << 8) | int16(rgba.Plane[read_pos+7])
						a_value = (a_value << (16 - uint(bpp))) | (a_value >> (2*uint(bpp) - 16))
						plane[write_pos+6] = byte(a_value >> 8)
						plane[write_pos+7] = byte(a_value & 0xff)
						read_pos += 8
						write_pos += 8
					}
					read_pos += stride_add
				}
			} else {
				plane = rgba.Plane
			}
			i = &image.RGBA64{
				Pix:    plane,
				Stride: rgba.Stride,
				Rect: image.Rectangle{
					Min: image.Point{
						X: 0,
						Y: 0,
					},
					Max: image.Point{
						X: width,
						Y: height,
					},
				},
			}
		default:
			return nil, fmt.Errorf("Unsupported RGB chroma format: %v", cf)
		}
	default:
		return nil, fmt.Errorf("Unsupported colorspace: %v", cs)
	}

	return i, nil
}

type ImageAccess struct {
	Plane    []byte
	planePtr unsafe.Pointer
	Stride   int
	height   int

	image *Image // need this reference to make sure the image is not GC'ed while we access it
}

func (i *ImageAccess) setData(data []byte, stride int) {
	// Handle common case directly
	if stride == i.Stride {
		dstP := uintptr(i.planePtr)
		srcP := uintptr(unsafe.Pointer(&data[0]))
		C.memcpy(unsafe.Pointer(dstP), unsafe.Pointer(srcP), C.size_t(i.height*stride))
	} else {
		for y := 0; y < i.height; y++ {
			dstP := uintptr(i.planePtr) + uintptr(y*i.Stride)
			srcP := uintptr(unsafe.Pointer(&data[0])) + uintptr(y*stride)
			C.memcpy(unsafe.Pointer(dstP), unsafe.Pointer(srcP), C.size_t(stride))
		}
	}
	i.Plane = C.GoBytes(i.planePtr, C.int(i.height*i.Stride))
}

// C.GoBytes takes a C.int length, so it can copy at most this many bytes.
const maxGoBytesLen = int64(^uint32(0) >> 1) // math.MaxInt32

func (img *Image) GetPlane(channel Channel) (*ImageAccess, error) {
	height := C.heif_image_get_height(img.image, C.heif_channel(channel))
	runtime.KeepAlive(img)
	if height == -1 {
		return nil, fmt.Errorf("No such channel %v", channel)
	}

	// Use the size_t-stride variant. The deprecated heif_image_get_plane() returns
	// an int stride; multiplying that by the height overflows int32 for large
	// multi-byte images, which then makes C.GoBytes() panic on a negative length.
	var stride C.size_t
	plane := C.heif_image_get_plane2(img.image, C.heif_channel(channel), &stride)
	runtime.KeepAlive(img)
	if plane == nil {
		return nil, fmt.Errorf("No such channel %v", channel)
	}

	ptr := unsafe.Pointer(plane)

	// Compute the plane size in 64-bit arithmetic and reject planes larger than
	// C.GoBytes() can represent instead of overflowing into a negative length.
	size := int64(stride) * int64(height)
	if size < 0 || size > maxGoBytesLen {
		return nil, fmt.Errorf("Image plane too large: %d bytes", size)
	}

	access := &ImageAccess{
		Plane:    C.GoBytes(ptr, C.int(size)),
		planePtr: ptr,
		Stride:   int(stride),
		height:   int(height),
		image:    img,
	}
	return access, nil
}

func (img *Image) NewPlane(channel Channel, width, height, depth int) (*ImageAccess, error) {
	err := C.heif_image_add_plane(img.image, C.heif_channel(channel), C.int(width), C.int(height), C.int(depth))
	runtime.KeepAlive(img)
	if err := convertHeifError(err); err != nil {
		return nil, err
	}
	return img.GetPlane(channel)
}

func (img *Image) ScaleImage(width int, height int) (*Image, error) {
	var scaled_image Image
	err := C.heif_image_scale_image(img.image, &scaled_image.image, C.int(width), C.int(height), nil)
	runtime.KeepAlive(img)
	if err := convertHeifError(err); err != nil {
		return nil, err
	}

	runtime.SetFinalizer(&scaled_image, freeHeifImage)
	return &scaled_image, nil
}

// --- High-level encoding API.

type EncodingOptions struct {
	options *C.struct_heif_encoding_options
}

func NewEncodingOptions() (*EncodingOptions, error) {
	options := &EncodingOptions{
		options: C.heif_encoding_options_alloc(),
	}
	if options.options == nil {
		return nil, fmt.Errorf("Could not allocate encoding options")
	}

	runtime.SetFinalizer(options, freeHeifEncodingOptions)
	return options, nil
}

func freeHeifEncodingOptions(options *EncodingOptions) {
	C.heif_encoding_options_free(options.options)
	options.options = nil
}

func imageFromRGBA(i *image.RGBA) (*Image, error) {
	min := i.Bounds().Min
	max := i.Bounds().Max
	w := max.X - min.X
	h := max.Y - min.Y

	out, err := NewImage(w, h, ColorspaceRGB, ChromaInterleavedRGBA)
	if err != nil {
		return nil, fmt.Errorf("failed to create image: %v", err)
	}

	p, err := out.NewPlane(ChannelInterleaved, w, h, 8)
	if err != nil {
		return nil, fmt.Errorf("failed to add plane: %v", err)
	}
	p.setData([]byte(i.Pix), w*4)

	return out, nil
}

func imageFromRGBA64(i *image.RGBA64) (*Image, error) {
	min := i.Bounds().Min
	max := i.Bounds().Max
	w := max.X - min.X
	h := max.Y - min.Y

	out, err := NewImage(w, h, ColorspaceRGB, ChromaInterleavedRRGGBBAA_BE)
	if err != nil {
		return nil, fmt.Errorf("failed to create image: %v", err)
	}

	p, err := out.NewPlane(ChannelInterleaved, w, h, 10)
	if err != nil {
		return nil, fmt.Errorf("failed to add plane: %v", err)
	}

	pix := make([]byte, w*h*8)
	read_pos := 0
	write_pos := 0
	for y := 0; y < h; y++ {
		for x := 0; x < w; x++ {
			r := (uint16(i.Pix[read_pos]) << 8) | uint16(i.Pix[read_pos+1])
			r = r >> 6
			pix[write_pos] = byte(r >> 8)
			pix[write_pos+1] = byte(r & 0xff)
			read_pos += 2
			g := (uint16(i.Pix[read_pos]) << 8) | uint16(i.Pix[read_pos+1])
			g = g >> 6
			pix[write_pos+2] = byte(g >> 8)
			pix[write_pos+3] = byte(g & 0xff)
			read_pos += 2
			b := (uint16(i.Pix[read_pos]) << 8) | uint16(i.Pix[read_pos+1])
			b = b >> 6
			pix[write_pos+4] = byte(b >> 8)
			pix[write_pos+5] = byte(b & 0xff)
			read_pos += 2
			a := (uint16(i.Pix[read_pos]) << 8) | uint16(i.Pix[read_pos+1])
			a = a >> 6
			pix[write_pos+6] = byte(a >> 8)
			pix[write_pos+7] = byte(a & 0xff)
			pix[write_pos+6] = byte(a >> 8)
			pix[write_pos+7] = byte(a & 0xff)
			read_pos += 2
			write_pos += 8
		}
	}
	p.setData(pix, w*8)

	return out, nil
}

func imageFromGray(i *image.Gray) (*Image, error) {
	min := i.Bounds().Min
	max := i.Bounds().Max
	w := max.X - min.X
	h := max.Y - min.Y

	out, err := NewImage(w, h, ColorspaceYCbCr, ChromaMonochrome)
	if err != nil {
		return nil, fmt.Errorf("failed to create image: %v", err)
	}

	const depth = 8
	pY, err := out.NewPlane(ChannelY, w, h, depth)
	if err != nil {
		return nil, fmt.Errorf("failed to add Y plane: %v", err)
	}
	pY.setData([]byte(i.Pix), i.Stride)

	return out, nil
}

func imageFromYCbCr(i *image.YCbCr) (*Image, error) {
	min := i.Bounds().Min
	max := i.Bounds().Max
	w := max.X - min.X
	h := max.Y - min.Y

	var cm Chroma
	switch sr := i.SubsampleRatio; sr {
	case image.YCbCrSubsampleRatio420:
		cm = Chroma420
	case image.YCbCrSubsampleRatio422:
		cm = Chroma422
	case image.YCbCrSubsampleRatio444:
		cm = Chroma444
	default:
		return nil, fmt.Errorf("unsupported subsample ratio: %s", sr.String())
	}

	out, err := NewImage(w, h, ColorspaceYCbCr, cm)
	if err != nil {
		return nil, fmt.Errorf("failed to create image: %v", err)
	}

	const depth = 8
	pY, err := out.NewPlane(ChannelY, w, h, depth)
	if err != nil {
		return nil, fmt.Errorf("failed to add Y plane: %v", err)
	}
	pY.setData([]byte(i.Y), i.YStride)

	// TODO: Might need to be updated for other SubsampleRatio values.
	var cw, ch int
	switch cm {
	case Chroma420:
		cw, ch = (w+1)/2, (h+1)/2
	case Chroma444:
		cw, ch = w, h
	case Chroma422:
		cw, ch = (w+1)/2, h
	default:
		return nil, fmt.Errorf("cm not support: %v", cm)
	}
	pCb, err := out.NewPlane(ChannelCb, cw, ch, depth)
	if err != nil {
		return nil, fmt.Errorf("failed to add Cb plane: %v", err)
	}
	pCb.setData([]byte(i.Cb), i.CStride)
	pCr, err := out.NewPlane(ChannelCr, cw, ch, depth)
	if err != nil {
		return nil, fmt.Errorf("failed to add Cr plane: %v", err)
	}
	pCr.setData([]byte(i.Cr), i.CStride)

	return out, nil
}

func EncodeFromImage(img image.Image, compression Compression, quality int, lossless LosslessMode, logging LoggingLevel) (*Context, error) {
	var out *Image

	switch i := img.(type) {
	default:
		return nil, fmt.Errorf("unsupported image type: %T", i)
	case *image.RGBA:
		tmp, err := imageFromRGBA(i)
		if err != nil {
			return nil, fmt.Errorf("failed to create image: %v", err)
		}
		out = tmp
	case *image.RGBA64:
		tmp, err := imageFromRGBA64(i)
		if err != nil {
			return nil, fmt.Errorf("failed to create image: %v", err)
		}
		out = tmp
	case *image.Gray:
		tmp, err := imageFromGray(i)
		if err != nil {
			return nil, fmt.Errorf("failed to create image: %v", err)
		}
		out = tmp
	case *image.YCbCr:
		tmp, err := imageFromYCbCr(i)
		if err != nil {
			return nil, fmt.Errorf("failed to create image: %v", err)
		}
		out = tmp
	}

	ctx, err := NewContext()
	if err != nil {
		return nil, fmt.Errorf("failed to create HEIF context: %v", err)
	}

	enc, err := ctx.NewEncoder(compression)
	if err != nil {
		return nil, fmt.Errorf("failed to create encoder: %v", err)
	}

	if err := enc.SetQuality(quality); err != nil {
		return nil, fmt.Errorf("failed to set quality: %v", err)
	}
	if err := enc.SetLossless(lossless); err != nil {
		return nil, fmt.Errorf("failed to set lossless mode: %v", err)
	}
	if err := enc.SetLoggingLevel(logging); err != nil {
		return nil, fmt.Errorf("failed to set logging level: %v", err)
	}

	encOpts, err := NewEncodingOptions()
	if err != nil {
		return nil, fmt.Errorf("failed to get encoding options: %v", err)
	}

	var handle ImageHandle
	err2 := C.heif_context_encode_image(ctx.context, out.image, enc.encoder, encOpts.options, &handle.handle)
	runtime.KeepAlive(ctx)
	runtime.KeepAlive(out)
	runtime.KeepAlive(enc)
	runtime.KeepAlive(encOpts)
	if err := convertHeifError(err2); err != nil {
		return nil, fmt.Errorf("failed to encode image: %v", err)
	}
	runtime.SetFinalizer(&handle, freeHeifImageHandle)

	return ctx, nil
}

// --- High-level decoding API, always decodes primary image (if present).

func decodePrimaryImageFromReader(r io.Reader) (*ImageHandle, error) {
	ctx, err := NewContext()
	if err != nil {
		return nil, err
	}

	data, err := ioutil.ReadAll(r)
	if err != nil {
		return nil, err
	}

	if err := ctx.ReadFromMemory(data); err != nil {
		return nil, err
	}

	handle, err := ctx.GetPrimaryImageHandle()
	if err != nil {
		return nil, err
	}

	return handle, nil
}

func decodeImage(r io.Reader) (image.Image, error) {
	handle, err := decodePrimaryImageFromReader(r)
	if err != nil {
		return nil, err
	}

	img, err := handle.DecodeImage(ColorspaceUndefined, ChromaUndefined, nil)
	if err != nil {
		return nil, err
	}

	return img.GetImage()
}

func decodeConfig(r io.Reader) (image.Config, error) {
	var config image.Config
	handle, err := decodePrimaryImageFromReader(r)
	if err != nil {
		return config, err
	}

	config = image.Config{
		ColorModel: color.YCbCrModel,
		Width:      handle.GetWidth(),
		Height:     handle.GetHeight(),
	}
	return config, nil
}

func init() {
	image.RegisterFormat("heif", "????ftypheic", decodeImage, decodeConfig)
	image.RegisterFormat("heif", "????ftypheim", decodeImage, decodeConfig)
	image.RegisterFormat("heif", "????ftypheis", decodeImage, decodeConfig)
	image.RegisterFormat("heif", "????ftypheix", decodeImage, decodeConfig)
	image.RegisterFormat("heif", "????ftyphevc", decodeImage, decodeConfig)
	image.RegisterFormat("heif", "????ftyphevm", decodeImage, decodeConfig)
	image.RegisterFormat("heif", "????ftyphevs", decodeImage, decodeConfig)
	image.RegisterFormat("heif", "????ftypmif1", decodeImage, decodeConfig)
	image.RegisterFormat("avif", "????ftypavif", decodeImage, decodeConfig)
	image.RegisterFormat("avif", "????ftypavis", decodeImage, decodeConfig)
}
