/*
 * HEIF codec.
 * Copyright (c) 2024 Dirk Farin <dirk.farin@gmail.com>
 *
 * This file is part of libheif.
 *
 * libheif is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * libheif is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with libheif.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "tiled.h"
#include "context.h"
#include "file.h"
#include <algorithm>
#include <limits>
#include "security_limits.h"
#include "codecs/hevc_dec.h"
#if WITH_UNCOMPRESSED_CODEC
#include "codecs/uncompressed/unc_boxes.h"
#endif
#include "api_structs.h"


static uint64_t readvec(const std::vector<uint8_t>& data, size_t& ptr, int len)
{
  uint64_t val = 0;
  while (len--) {
    val <<= 8;
    val |= data[ptr++];
  }

  return val;
}


Result<uint64_t> number_of_tiles(const heif_tiled_image_parameters& params, const heif_security_limits* limits)
{
  uint64_t nTiles = nTiles_h(params) * static_cast<uint64_t>(nTiles_v(params));

  // Enforce the limit before the extra-dimensions loop so it is checked
  // even when number_of_extra_dimensions == 0.
  if (limits && limits->max_number_of_tiles && nTiles > limits->max_number_of_tiles) {
    return Error{
      heif_error_Unsupported_filetype,
      heif_suberror_Security_limit_exceeded,
      "Number of tiles exceeds security limit"
    };
  }

  for (int i = 0; i < params.number_of_extra_dimensions; i++) {
    // We only support up to 8 extra dimensions
    if (i == 8) {
      break;
    }

    if (params.extra_dimensions[i] == 0) {
      return Error{
        heif_error_Unsupported_filetype,
        heif_suberror_Unspecified,
        "Zero extra dimension size."
      };
    }

    if (nTiles > UINT64_MAX / params.extra_dimensions[i]) {
      return Error{
        heif_error_Unsupported_filetype,
        heif_suberror_Unspecified,
        "Number of tiles exceeds uint64 maximum."
      };
    }

    nTiles *= params.extra_dimensions[i];

    if (limits && limits->max_number_of_tiles && nTiles > limits->max_number_of_tiles) {
      return Error{
        heif_error_Unsupported_filetype,
        heif_suberror_Security_limit_exceeded,
        "Number of tiles exceeds security limit"
      };
    }
  }

  return nTiles;
}


uint32_t nTiles_h(const heif_tiled_image_parameters& params)
{
  // 64-bit arithmetic prevents wrap-around when image_width + tile_width - 1
  // exceeds UINT32_MAX. The quotient is bounded by image_width, so the
  // narrowing cast is safe. Callers are responsible for ensuring tile_width > 0.
  return static_cast<uint32_t>(
      (static_cast<uint64_t>(params.image_width) + params.tile_width - 1) / params.tile_width);
}


uint32_t nTiles_v(const heif_tiled_image_parameters& params)
{
  return static_cast<uint32_t>(
      (static_cast<uint64_t>(params.image_height) + params.tile_height - 1) / params.tile_height);
}


void Box_tilC::init_heif_tiled_image_parameters(heif_tiled_image_parameters& params)
{
  params.version = 1;

  params.image_width = 0;
  params.image_height = 0;
  params.tile_width = 0;
  params.tile_height = 0;
  params.compression_format_fourcc = 0;
  params.offset_field_length = 40;
  params.size_field_length = 24;
  params.number_of_extra_dimensions = 0;

  for (uint32_t& dim : params.extra_dimensions) {
    dim = 0;
  }

  params.tiles_are_sequential = false;
}


void Box_tilC::derive_box_version()
{
  set_version(0);

  uint8_t flags = 0;

  switch (m_parameters.offset_field_length) {
    case 32:
      flags |= 0;
      break;
    case 40:
      flags |= 0x01;
      break;
    case 48:
      flags |= 0x02;
      break;
    case 64:
      flags |= 0x03;
      break;
    default:
      assert(false); // TODO: return error
  }

  switch (m_parameters.size_field_length) {
    case 0:
      flags |= 0;
      break;
    case 24:
      flags |= 0x04;
      break;
    case 32:
      flags |= 0x08;
      break;
    case 64:
      flags |= 0x0c;
      break;
    default:
      assert(false); // TODO: return error
  }

  if (m_parameters.tiles_are_sequential) {
    flags |= 0x10;
  }

  set_flags(flags);
}


Error Box_tilC::write(StreamWriter& writer) const
{
  assert(m_parameters.version == 1);

  size_t box_start = reserve_box_header_space(writer);

  if (m_parameters.number_of_extra_dimensions > 8) {
    assert(false); // currently not supported
  }

  writer.write32(m_parameters.tile_width);
  writer.write32(m_parameters.tile_height);
  writer.write32(m_parameters.compression_format_fourcc);

  writer.write8(m_parameters.number_of_extra_dimensions);

  for (int i = 0; i < m_parameters.number_of_extra_dimensions; i++) {
    writer.write32(m_parameters.extra_dimensions[i]);
  }

  auto& tile_properties = m_children;
  if (tile_properties.size() > 255) {
    return {heif_error_Encoding_error,
            heif_suberror_Unspecified,
            "Cannot write more than 255 tile properties in tilC header"};
  }

  writer.write8(static_cast<uint8_t>(tile_properties.size()));
  for (const auto& property : tile_properties) {
    property->write(writer);
  }

  prepend_header(writer, box_start);

  return Error::Ok;
}


std::string Box_tilC::dump(Indent& indent) const
{
  std::ostringstream sstr;

  sstr << BoxHeader::dump(indent);

  sstr << indent << "version: " << ((int) get_version()) << "\n"
       //<< indent << "image size: " << m_parameters.image_width << "x" << m_parameters.image_height << "\n"
       << indent << "tile size: " << m_parameters.tile_width << "x" << m_parameters.tile_height << "\n"
       << indent << "compression: " << fourcc_to_string(m_parameters.compression_format_fourcc) << "\n"
       << indent << "tiles are sequential: " << (m_parameters.tiles_are_sequential ? "yes" : "no") << "\n"
       << indent << "offset field length: " << ((int) m_parameters.offset_field_length) << " bits\n"
       << indent << "size field length: " << ((int) m_parameters.size_field_length) << " bits\n"
       << indent << "number of extra dimensions: " << ((int) m_parameters.number_of_extra_dimensions) << "\n";

  sstr << indent << "tile properties:\n"
       << dump_children(indent, true);

  return sstr.str();

}


Error Box_tilC::parse(BitstreamRange& range, const heif_security_limits* limits)
{
  parse_full_box_header(range);

  // Note: actually, we should allow 0 only, but there are a few images around that use version 1.
  if (get_version() > 1) {
    std::stringstream sstr;
    sstr << "'tili' image version " << ((int) get_version()) << " is not implemented yet";

    return {heif_error_Unsupported_feature,
            heif_suberror_Unsupported_data_version,
            sstr.str()};
  }

  m_parameters.version = get_version();

  uint32_t flags = get_flags();

  switch (flags & 0x03) {
    case 0:
      m_parameters.offset_field_length = 32;
      break;
    case 1:
      m_parameters.offset_field_length = 40;
      break;
    case 2:
      m_parameters.offset_field_length = 48;
      break;
    case 3:
      m_parameters.offset_field_length = 64;
      break;
  }

  switch (flags & 0x0c) {
    case 0x00:
      m_parameters.size_field_length = 0;
      break;
    case 0x04:
      m_parameters.size_field_length = 24;
      break;
    case 0x08:
      m_parameters.size_field_length = 32;
      break;
    case 0x0c:
      m_parameters.size_field_length = 64;
      break;
  }

  m_parameters.tiles_are_sequential = !!(flags & 0x10);


  m_parameters.tile_width = range.read32();
  m_parameters.tile_height = range.read32();
  m_parameters.compression_format_fourcc = range.read32();

  if (m_parameters.tile_width == 0 || m_parameters.tile_height == 0) {
    return {heif_error_Invalid_input,
            heif_suberror_Unspecified,
            "Tile with zero width or height."};
  }


  // --- extra dimensions

  m_parameters.number_of_extra_dimensions = range.read8();

  for (int i = 0; i < m_parameters.number_of_extra_dimensions; i++) {
    uint32_t size = range.read32();

    if (size == 0) {
      return {heif_error_Invalid_input,
              heif_suberror_Unspecified,
              "'tili' extra dimension may not be zero."};
    }

    if (i < 8) {
      m_parameters.extra_dimensions[i] = size;
    }
    else {
      // TODO: error: too many dimensions (not supported)
    }
  }

  // --- read tile properties

  // Check version for backwards compatibility with old format.
  // TODO: remove when spec is final and old test images have been converted
  if (get_version() == 0) {
    uint8_t num_properties = range.read8();

    Error error = read_children(range, num_properties, limits);
    if (error) {
      return error;
    }
  }

  return range.get_error();
}


Error TiledHeader::set_parameters(const heif_tiled_image_parameters& params)
{
  m_parameters = params;

  Result<uint64_t> num_tiles_result = number_of_tiles(params, heif_get_global_security_limits());
  if (auto err = num_tiles_result.error()) {
    return err;
  }

  m_offsets.resize(*num_tiles_result);

  for (auto& tile: m_offsets) {
    tile.offset = TILD_OFFSET_NOT_LOADED;
  }

  return Error::Ok;
}


Error TiledHeader::read_full_offset_table(const std::shared_ptr<HeifFile>& file, heif_item_id tild_id, const heif_security_limits* limits)
{
  Result<uint64_t> nTiles_result = number_of_tiles(m_parameters, limits);
  if (auto err = nTiles_result.error()) {
    return err;
  }

  return read_offset_table_range(file, tild_id, 0, *nTiles_result);
}


Error TiledHeader::read_offset_table_range(const std::shared_ptr<HeifFile>& file, heif_item_id tild_id,
                                           uint64_t start, uint64_t end)
{
  const Error eofError(heif_error_Invalid_input,
                       heif_suberror_Unspecified,
                       "Tili header data incomplete");

  std::vector<uint8_t> data;



  // --- load offsets

  size_t size_to_read = (end - start) * (m_parameters.offset_field_length + m_parameters.size_field_length) / 8;
  size_t start_offset = start * (m_parameters.offset_field_length + m_parameters.size_field_length) / 8;

  // TODO: when we request a file range from the stream reader, it may return a larger range.
  //       We should then also use this larger range to read more table entries.
  //       But this is not easy since our data may span several iloc extents and we have to map this back to item addresses.
  //       Maybe it is easier to just ignore the extra data and rely on the stream read to cache this data.

  Error err = file->append_data_from_iloc(tild_id, data, start_offset, size_to_read);
  if (err) {
    return err;
  }

  size_t idx = 0;
  for (uint64_t i = start; i < end; i++) {
    m_offsets[i].offset = readvec(data, idx, m_parameters.offset_field_length / 8);

    if (m_parameters.size_field_length) {
      assert(m_parameters.size_field_length <= 32);
      m_offsets[i].size = static_cast<uint32_t>(readvec(data, idx, m_parameters.size_field_length / 8));
    }

    // printf("[%zu] : offset/size: %zu %d\n", i, m_offsets[i].offset, m_offsets[i].size);
  }

  return Error::Ok;
}


size_t TiledHeader::get_header_size() const
{
  assert(m_header_size);
  return m_header_size;
}


uint32_t TiledHeader::get_offset_table_entry_size() const
{
  return (m_parameters.offset_field_length + m_parameters.size_field_length) / 8;
}


std::pair<uint32_t, uint32_t> TiledHeader::get_tile_offset_table_range_to_read(uint32_t idx, uint32_t nEntries) const
{
  // Defense in depth: callers are expected to validate idx, but if they don't,
  // returning an empty range prevents the subsequent read_offset_table_range
  // from writing past m_offsets.
  if (idx >= m_offsets.size()) {
    return {0, 0};
  }

  uint32_t start = idx;
  uint32_t end = idx+1;

  while (end < m_offsets.size() && end - idx < nEntries && m_offsets[end].offset == TILD_OFFSET_NOT_LOADED) {
    end++;
  }

  while (start > 0 && idx - start < nEntries && m_offsets[start-1].offset == TILD_OFFSET_NOT_LOADED) {
    start--;
  }

  // try to fill the smaller hole

  if (end - start > nEntries) {
    if (idx - start < end - idx) {
      end = start + nEntries;
    }
    else {
      start = end - nEntries;
    }
  }

  return {start, end};
}


Error TiledHeader::set_tild_tile_range(uint32_t tile_x, uint32_t tile_y, uint64_t offset, uint32_t size)
{
  // Offset and size are written into bit-fields of the configured widths;
  // silently truncating here produces files where the offset table points to
  // garbage. Reject the value so the caller knows to widen the fields.
  uint8_t off_bits = m_parameters.offset_field_length;
  uint8_t sz_bits  = m_parameters.size_field_length;

  if (off_bits < 64 && offset >> off_bits) {
    std::stringstream sstr;
    sstr << "Tile offset " << offset << " does not fit in the configured "
         << static_cast<int>(off_bits) << "-bit offset field. Use a wider "
            "offset_field_length (40/48/64) when encoding the tili image.";
    return {heif_error_Encoding_error, heif_suberror_Unspecified, sstr.str()};
  }

  if (sz_bits != 0 && sz_bits < 32 && size >> sz_bits) {
    std::stringstream sstr;
    sstr << "Tile size " << size << " does not fit in the configured "
         << static_cast<int>(sz_bits) << "-bit size field.";
    return {heif_error_Encoding_error, heif_suberror_Unspecified, sstr.str()};
  }

  uint64_t idx = uint64_t{tile_y} * nTiles_h(m_parameters) + tile_x;
  m_offsets[idx].offset = offset;
  m_offsets[idx].size = size;
  return Error::Ok;
}


template<typename I>
void writevec(uint8_t* data, size_t& idx, I value, int len)
{
  for (int i = 0; i < len; i++) {
    data[idx + i] = static_cast<uint8_t>((value >> (len - 1 - i) * 8) & 0xFF);
  }

  idx += len;
}


Result<std::vector<uint8_t>> TiledHeader::write_offset_table()
{
  Result<uint64_t> nTiles_result = number_of_tiles(m_parameters, nullptr);
  if (auto err = nTiles_result.error()) {
    return err;
  }


  int offset_entry_size = (m_parameters.offset_field_length + m_parameters.size_field_length) / 8;
  uint64_t size = *nTiles_result * offset_entry_size;

  std::vector<uint8_t> data;
  data.resize(size);

  size_t idx = 0;

  uint8_t off_bits = m_parameters.offset_field_length;
  uint8_t sz_bits  = m_parameters.size_field_length;

  for (const auto& offset: m_offsets) {
    if (off_bits < 64 && offset.offset >> off_bits) {
      std::stringstream sstr;
      sstr << "Tile offset " << offset.offset << " does not fit in the "
              "configured " << static_cast<int>(off_bits) << "-bit offset field.";
      return Error{heif_error_Encoding_error, heif_suberror_Unspecified, sstr.str()};
    }
    if (sz_bits != 0 && sz_bits < 32 && offset.size >> sz_bits) {
      std::stringstream sstr;
      sstr << "Tile size " << offset.size << " does not fit in the "
              "configured " << static_cast<int>(sz_bits) << "-bit size field.";
      return Error{heif_error_Encoding_error, heif_suberror_Unspecified, sstr.str()};
    }

    writevec(data.data(), idx, offset.offset, m_parameters.offset_field_length / 8);

    if (m_parameters.size_field_length != 0) {
      writevec(data.data(), idx, offset.size, m_parameters.size_field_length / 8);
    }
  }

  assert(idx == data.size());

  m_header_size = data.size();

  return data;
}


std::string TiledHeader::dump() const
{
  std::stringstream sstr;

  sstr << "offsets: ";

  // TODO

  for (const auto& offset: m_offsets) {
    sstr << offset.offset << ", size: " << offset.size << "\n";
  }

  return sstr.str();
}


ImageItem_Tiled::ImageItem_Tiled(HeifContext* ctx)
        : ImageItem(ctx)
{
  m_tile_encoding_options = heif_encoding_options_alloc();
}


ImageItem_Tiled::ImageItem_Tiled(HeifContext* ctx, heif_item_id id)
        : ImageItem(ctx, id)
{
  m_tile_encoding_options = heif_encoding_options_alloc();
}


ImageItem_Tiled::~ImageItem_Tiled()
{
  heif_encoding_options_free(m_tile_encoding_options);
}


heif_compression_format ImageItem_Tiled::get_compression_format() const
{
  return compression_format_from_fourcc_infe_type(m_tild_header.get_parameters().compression_format_fourcc);
}


Error ImageItem_Tiled::initialize_decoder()
{
  auto heif_file = get_context()->get_heif_file();

  auto tilC_box = get_property<Box_tilC>();
  if (!tilC_box) {
    return {heif_error_Invalid_input,
            heif_suberror_Unspecified,
            "Tiled image without 'tilC' property box."};
  }

  auto ispe_box = get_property<Box_ispe>();
  if (!ispe_box) {
    return {heif_error_Invalid_input,
            heif_suberror_Unspecified,
            "Tiled image without 'ispe' property box."};
  }

  heif_tiled_image_parameters parameters = tilC_box->get_parameters();
  parameters.image_width = ispe_box->get_width();
  parameters.image_height = ispe_box->get_height();

  if (parameters.image_width == 0 || parameters.image_height == 0) {
    return {heif_error_Invalid_input,
            heif_suberror_Unspecified,
            "'tili' image with zero width or height."};
  }

  if (Error err = m_tild_header.set_parameters(parameters)) {
    return err;
  }


  // --- create a dummy image item for decoding tiles

  heif_compression_format format = compression_format_from_fourcc_infe_type(m_tild_header.get_parameters().compression_format_fourcc);
  m_tile_item = ImageItem::alloc_for_compression_format(get_context(), format);

  // For backwards compatibility: copy over properties from `tili` item.
  // TODO: remove when spec is final and old test images have been converted
  if (tilC_box->get_version() == 1) {
    auto propertiesResult = get_properties();
    if (!propertiesResult) {
      return propertiesResult.error();
    }

    // Filter out per-tile boxes incompatible with tili's shared template
    auto props = *propertiesResult;
#if WITH_UNCOMPRESSED_CODEC
    for (const auto& box : props) {
      if (box->get_short_type() == fourcc("icef")) {
        auto icef = std::dynamic_pointer_cast<Box_icef>(box);
        if (icef && icef->get_units().size() > 1) {
          return {heif_error_Invalid_input,
                  heif_suberror_Unspecified,
                  "icef box with multiple units is incompatible with tili shared tile template."};
        }
      }
    }
    std::erase_if(props, [](const std::shared_ptr<Box>& box) {
      uint32_t type = box->get_short_type();
      return type == fourcc("icef") || type == fourcc("sbpm") || type == fourcc("snuc");
    });
#endif

    m_tile_item->set_properties(props);
  }
  else {
    // --- This is the new method

    // Synthesize an ispe box if there was none in the file

    auto tile_properties = tilC_box->get_all_child_boxes();

    // Filter out per-tile boxes incompatible with tili's shared template
#if WITH_UNCOMPRESSED_CODEC
    for (const auto& box : tile_properties) {
      if (box->get_short_type() == fourcc("icef")) {
        auto icef = std::dynamic_pointer_cast<Box_icef>(box);
        if (icef && icef->get_units().size() > 1) {
          return {heif_error_Invalid_input,
                  heif_suberror_Unspecified,
                  "icef box with multiple units is incompatible with tili shared tile template."};
        }
      }
    }
    std::erase_if(tile_properties, [](const std::shared_ptr<Box>& box) {
      uint32_t type = box->get_short_type();
      return type == fourcc("icef") || type == fourcc("sbpm") || type == fourcc("snuc");
    });
#endif

    bool have_ispe = false;
    for (const auto& property : tile_properties) {
      if (property->get_short_type() == fourcc("ispe")) {
        have_ispe = true;
        break;
      }
    }

    if (!have_ispe) {
      auto ispe = std::make_shared<Box_ispe>();
      ispe->set_size(parameters.tile_width, parameters.tile_height);
      tile_properties.emplace_back(std::move(ispe));
    }

    m_tile_item->set_properties(tile_properties);
  }

  m_tile_decoder = Decoder::alloc_for_infe_type(m_tile_item.get());
  if (!m_tile_decoder) {
    return {heif_error_Unsupported_feature,
            heif_suberror_Unsupported_codec,
            "'tili' image with unsupported compression format."};
  }

  if (m_preload_offset_table) {
    if (Error err = m_tild_header.read_full_offset_table(heif_file, get_id(), get_context()->get_security_limits())) {
      return err;
    }
  }


  return Error::Ok;
}


void ImageItem_Tiled::populate_component_descriptions()
{
  // Idempotent: skip if already populated.
  if (!get_component_descriptions().empty()) {
    return;
  }

  // initialize_decoder() must have run; m_tile_item carries the per-tile
  // properties (cmpd/uncC for unci tiles, codec config for visual tiles)
  // and its own populate has filled tile-sized component descriptions.
  if (!m_tile_item) {
    return;
  }

  uint32_t tile_w = m_tild_header.get_parameters().tile_width;
  uint32_t tile_h = m_tild_header.get_parameters().tile_height;
  populate_descriptions_from_child(*m_tile_item, tile_w, tile_h);
}


Result<std::shared_ptr<ImageItem_Tiled>>
ImageItem_Tiled::add_new_tiled_item(HeifContext* ctx, const heif_tiled_image_parameters* parameters,
                                    const heif_encoder* encoder,
                                    const heif_encoding_options* encoding_options)
{
  Result<uint64_t> num_tiles_result = number_of_tiles(*parameters, ctx->get_security_limits());
  if (auto err = num_tiles_result.error()) {
    return err;
  }

  // Create 'tili' Item

  auto file = ctx->get_heif_file();

  auto tild_id_result = ctx->get_heif_file()->add_new_image(fourcc("tili"));
  if (!tild_id_result) {
    return tild_id_result.error();
  }
  heif_item_id tild_id = *tild_id_result;
  auto tild_image = std::make_shared<ImageItem_Tiled>(ctx, tild_id);
  tild_image->set_resolution(parameters->image_width, parameters->image_height);
  ctx->insert_image_item(tild_id, tild_image);

  if (encoding_options) {
    // encoding options for the tiles, but do not apply transformative properties
    heif_encoding_options_copy(tild_image->m_tile_encoding_options, encoding_options);
    tild_image->m_tile_encoding_options->image_orientation = heif_orientation_normal;

    // orientation of the main image
    tild_image->m_image_orientation = encoding_options->image_orientation;
  }

  // Create tilC box

  auto tilC_box = std::make_shared<Box_tilC>();
  tilC_box->set_parameters(*parameters);
  tilC_box->set_compression_format(encoder->plugin->compression_format);
  tild_image->add_property(tilC_box, true);

  // Create header + offset table

  TiledHeader tild_header;
  tild_header.set_parameters(*parameters);
  tild_header.set_compression_format(encoder->plugin->compression_format);

  Result<std::vector<uint8_t>> header_data_result = tild_header.write_offset_table();
  if (auto err = header_data_result.error()) {
    return err;
  }

  const int construction_method = 0; // 0=mdat 1=idat
  file->append_iloc_data(tild_id, *header_data_result, construction_method);


  if (parameters->image_width > 0xFFFFFFFF || parameters->image_height > 0xFFFFFFFF) {
    return {Error(heif_error_Usage_error, heif_suberror_Invalid_image_size,
                  "'ispe' only supports image sized up to 4294967295 pixels per dimension")};
  }

  // Add ISPE property
  auto ispe = std::make_shared<Box_ispe>();
  ispe->set_size(static_cast<uint32_t>(parameters->image_width),
                 static_cast<uint32_t>(parameters->image_height));
  tild_image->add_property(ispe, true);

#if 0
  // TODO

  // Add PIXI property (copy from first tile)
  auto pixi = m_heif_file->get_property<Box_pixi>(tile_ids[0]);
  m_heif_file->add_property(grid_id, pixi, true);
#endif

  tild_image->set_tild_header(tild_header);
  tild_image->set_next_tild_position(header_data_result->size());

  // Set Brands
  //m_heif_file->set_brand(encoder->plugin->compression_format,
  //                       out_grid_image->is_miaf_compatible());

  return {tild_image};
}


Error ImageItem_Tiled::add_image_tile(uint32_t tile_x, uint32_t tile_y,
                                     const std::shared_ptr<HeifPixelImage>& image,
                                     heif_encoder* encoder)
{
  auto item = ImageItem::alloc_for_compression_format(get_context(), encoder->plugin->compression_format);

  Result<std::shared_ptr<HeifPixelImage>> colorConversionResult;
  colorConversionResult = item->get_encoder()->convert_colorspace_for_encoding(image, encoder,
                                                                               m_tile_encoding_options->output_nclx_profile,
                                                                               &m_tile_encoding_options->color_conversion_options,
                                                                               get_context()->get_security_limits());
  if (!colorConversionResult) {
    return colorConversionResult.error();
  }

  std::shared_ptr<HeifPixelImage> colorConvertedImage = *colorConversionResult;

  Result<Encoder::CodedImageData> encodeResult = item->encode_to_bitstream_and_boxes(colorConvertedImage, encoder, *m_tile_encoding_options, heif_image_input_class_normal);

  if (!encodeResult) {
    return encodeResult.error();
  }

  const int construction_method = 0; // 0=mdat 1=idat
  get_file()->append_iloc_data(get_id(), encodeResult->bitstream, construction_method);

  auto& header = m_tild_header;

  if (image->get_width() != header.get_parameters().tile_width ||
      image->get_height() != header.get_parameters().tile_height) {
    return {heif_error_Usage_error,
            heif_suberror_Unspecified,
            "Tile image size does not match the specified tile size."};
  }

  uint64_t offset = get_next_tild_position();
  size_t dataSize = encodeResult->bitstream.size();
  if (dataSize > 0xFFFFFFFF) {
    return {heif_error_Encoding_error, heif_suberror_Unspecified, "Compressed tile size exceeds maximum tile size."};
  }
  if (Error err = header.set_tild_tile_range(tile_x, tile_y, offset, static_cast<uint32_t>(dataSize))) {
    return err;
  }
  set_next_tild_position(offset + encodeResult->bitstream.size());

  auto tilC = get_property<Box_tilC>();
  assert(tilC);

  std::vector<std::shared_ptr<Box>>& tile_properties = tilC->get_tile_properties();

  for (auto& propertyBox : encodeResult->properties) {

    // we do not have to save ispe boxes in the tile properties as this is automatically synthesized

    if (propertyBox->get_short_type() == fourcc("ispe")) {
      continue;
    }

#if WITH_UNCOMPRESSED_CODEC
    // icef/sbpm/snuc contain per-tile data incompatible with tili's shared tile template
    uint32_t ptype = propertyBox->get_short_type();

    if (ptype == fourcc("icef")) {
      auto icef = std::dynamic_pointer_cast<Box_icef>(propertyBox);
      if (icef && icef->get_units().size() > 1) {
        return {heif_error_Usage_error,
                heif_suberror_Unspecified,
                "icef box with multiple units is incompatible with tili shared tile template."};
      }
      // Single-unit icef can be safely skipped
      continue;
    }

    if (ptype == fourcc("sbpm") || ptype == fourcc("snuc")) {
      return {heif_error_Usage_error,
              heif_suberror_Unspecified,
              "Cannot store per-tile property (" + fourcc_to_string(ptype) + ") in tili shared tile template."};
    }
#endif

    // skip properties that exist already

    bool exists = std::any_of(tile_properties.begin(),
                              tile_properties.end(),
                              [&propertyBox](const std::shared_ptr<Box>& p) { return p->get_short_type() == propertyBox->get_short_type();});
    if (exists) {
      continue;
    }

    tile_properties.emplace_back(propertyBox);

    // some tile properties are also added to the tili image

    switch (propertyBox->get_short_type()) {
      case fourcc("pixi"):
        get_file()->add_property(get_id(), propertyBox, propertyBox->is_essential());
        break;
    }

    get_file()->add_orientation_properties(get_id(), m_image_orientation);
  }

  //get_file()->set_brand(encoder->plugin->compression_format,
  //                      true); // TODO: out_grid_image->is_miaf_compatible());

  return Error::Ok;
}


Error ImageItem_Tiled::process_before_write()
{
  // overwrite offsets

  const int construction_method = 0; // 0=mdat 1=idat

  Result<std::vector<uint8_t>> header_data_result = m_tild_header.write_offset_table();
  if (auto err = header_data_result.error()) {
    return err;
  }

  get_file()->replace_iloc_data(get_id(), 0, *header_data_result, construction_method);
  return {};
}


Result<std::shared_ptr<HeifPixelImage>>
ImageItem_Tiled::decode_compressed_image(const heif_decoding_options& options,
                                         bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0,
                                         std::set<heif_item_id> processed_ids) const
{
  if (decode_tile_only) {
    return decode_grid_tile(options, tile_x0, tile_y0);
  }
  else {
    return Error{heif_error_Unsupported_feature, heif_suberror_Unspecified,
                 "'tili' images can only be accessed per tile"};
  }
}


Error ImageItem_Tiled::append_compressed_tile_data(std::vector<uint8_t>& data, uint32_t tx, uint32_t ty) const
{
  uint64_t idx64 = static_cast<uint64_t>(ty) * nTiles_h(m_tild_header.get_parameters()) + tx;
  if (idx64 >= m_tild_header.get_num_tiles()) {
    return Error{heif_error_Invalid_input,
                 heif_suberror_Unspecified,
                 "Tile index out of range."};
  }
  auto idx = static_cast<uint32_t>(idx64);

  if (!m_tild_header.is_tile_offset_known(idx)) {
    Error err = const_cast<ImageItem_Tiled*>(this)->load_tile_offset_entry(idx);
    if (err) {
      return err;
    }
  }

  uint64_t offset = m_tild_header.get_tile_offset(idx);
  uint64_t size = m_tild_header.get_tile_size(idx);

  Error err = get_file()->append_data_from_iloc(get_id(), data, offset, size);
  if (err.error_code) {
    return err;
  }

  return Error::Ok;
}


Result<DataExtent>
ImageItem_Tiled::get_compressed_data_for_tile(uint32_t tx, uint32_t ty) const
{
  // --- get compressed data

  Error err = m_tile_item->initialize_decoder();
  if (err) {
    return err;
  }

  Result<std::vector<uint8_t>> dataResult = m_tile_item->read_bitstream_configuration_data();
  if (!dataResult) {
    return dataResult.error();
  }

  std::vector<uint8_t> data = std::move(*dataResult);
  err = append_compressed_tile_data(data, tx, ty);
  if (err) {
    return err;
  }

  // --- decode

  DataExtent extent;
  extent.m_raw = std::move(data);

  return std::move(extent);
}


Result<std::shared_ptr<HeifPixelImage>>
ImageItem_Tiled::decode_grid_tile(const heif_decoding_options& options, uint32_t tx, uint32_t ty) const
{
  Result<DataExtent> extentResult = get_compressed_data_for_tile(tx, ty);
  if (!extentResult) {
    return extentResult.error();
  }

  m_tile_decoder->set_data_extent(std::move(*extentResult));

  uint32_t tw = 0, th = 0;
  get_tile_size(tw, th);
  heif_security_limits tightened = tighten_image_size_limit_for_ispe(
      get_context()->get_security_limits(), tw, th,
      max_coding_unit_size_for_codec(m_tile_decoder->get_compression_format()));

  return m_tile_decoder->decode_single_frame_from_compressed_data(options, &tightened);
}


Error ImageItem_Tiled::load_tile_offset_entry(uint32_t idx)
{
  uint32_t nEntries = mReadChunkSize_bytes / m_tild_header.get_offset_table_entry_size();
  std::pair<uint32_t, uint32_t> range = m_tild_header.get_tile_offset_table_range_to_read(idx, nEntries);

  return m_tild_header.read_offset_table_range(get_file(), get_id(), range.first, range.second);
}


heif_image_tiling ImageItem_Tiled::get_heif_image_tiling() const
{
  heif_image_tiling tiling{};

  tiling.num_columns = nTiles_h(m_tild_header.get_parameters());
  tiling.num_rows = nTiles_v(m_tild_header.get_parameters());

  tiling.tile_width = m_tild_header.get_parameters().tile_width;
  tiling.tile_height = m_tild_header.get_parameters().tile_height;

  tiling.image_width = m_tild_header.get_parameters().image_width;
  tiling.image_height = m_tild_header.get_parameters().image_height;
  tiling.number_of_extra_dimensions = m_tild_header.get_parameters().number_of_extra_dimensions;
  for (int i = 0; i < std::min(tiling.number_of_extra_dimensions, uint8_t(8)); i++) {
    tiling.extra_dimension_size[i] = m_tild_header.get_parameters().extra_dimensions[i];
  }

  return tiling;
}


void ImageItem_Tiled::get_tile_size(uint32_t& w, uint32_t& h) const
{
  w = m_tild_header.get_parameters().tile_width;
  h = m_tild_header.get_parameters().tile_height;
}


Error ImageItem_Tiled::get_coded_image_colorspace(heif_colorspace* out_colorspace, heif_chroma* out_chroma) const
{
  uint32_t tx=0, ty=0; // TODO: find a tile that is defined.

  Result<DataExtent> extentResult = get_compressed_data_for_tile(tx, ty);
  if (!extentResult) {
    return extentResult.error();
  }

  m_tile_decoder->set_data_extent(std::move(*extentResult));

  Error err = m_tile_decoder->get_coded_image_colorspace(out_colorspace, out_chroma);
  if (err) {
    return err;
  }

  postprocess_coded_image_colorspace(out_colorspace, out_chroma);

  return Error::Ok;
}


int ImageItem_Tiled::get_luma_bits_per_pixel() const
{
  DataExtent any_tile_extent;
  append_compressed_tile_data(any_tile_extent.m_raw, 0,0); // TODO: use tile that is already loaded
  m_tile_decoder->set_data_extent(std::move(any_tile_extent));

  return m_tile_decoder->get_luma_bits_per_pixel();
}

int ImageItem_Tiled::get_chroma_bits_per_pixel() const
{
  DataExtent any_tile_extent;
  append_compressed_tile_data(any_tile_extent.m_raw, 0,0); // TODO: use tile that is already loaded
  m_tile_decoder->set_data_extent(std::move(any_tile_extent));

  return m_tile_decoder->get_chroma_bits_per_pixel();
}

heif_brand2 ImageItem_Tiled::get_compatible_brand() const
{
  return 0;

  // TODO: it is not clear to me what brand to use here.

  /*
  switch (m_tild_header.get_parameters().compression_format_fourcc) {
    case heif_compression_HEVC:
      return heif_brand2_heic;
  }
   */
}
