/*
 * HEIF codec.
 * Copyright (c) 2023 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 "region.h"
#include "error.h"
#include "file.h"
#include "box.h"
#include "libheif/heif_regions.h"
#include <algorithm>
#include <utility>


Error RegionItem::parse(const std::vector<uint8_t>& data)
{
  if (data.size() < 8) {
    return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data,
                 "Less than 8 bytes of data");
  }

  uint8_t version = data[0];
  (void) version; // version is unused

  uint8_t flags = data[1];
  int field_size = ((flags & 1) ? 32 : 16);

  unsigned int dataOffset;
  if (field_size == 32) {
    if (data.size() < 12) {
      return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data,
                   "Region data incomplete");
    }
    reference_width =
        ((data[2] << 24) | (data[3] << 16) | (data[4] << 8) | (data[5]));

    reference_height =
        ((data[6] << 24) | (data[7] << 16) | (data[8] << 8) | (data[9]));
    dataOffset = 10;
  }
  else {
    reference_width = ((data[2] << 8) | (data[3]));
    reference_height = ((data[4] << 8) | (data[5]));
    dataOffset = 6;
  }

  uint8_t region_count = data[dataOffset];
  dataOffset += 1;
  for (int i = 0; i < region_count; i++) {
    if (data.size() <= dataOffset) {
      return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data,
                   "Region data incomplete");
    }

    uint8_t geometry_type = data[dataOffset];
    dataOffset += 1;

    std::shared_ptr<RegionGeometry> region;

    if (geometry_type == heif_region_type_point) {
      region = std::make_shared<RegionGeometry_Point>();
    }
    else if (geometry_type == heif_region_type_rectangle) {
      region = std::make_shared<RegionGeometry_Rectangle>();
    }
    else if (geometry_type == heif_region_type_ellipse) {
      region = std::make_shared<RegionGeometry_Ellipse>();
    }
    else if (geometry_type == heif_region_type_polygon) {
      auto polygon = std::make_shared<RegionGeometry_Polygon>();
      polygon->closed = true;
      region = polygon;
    }
    else if (geometry_type == heif_region_type_referenced_mask) {
      region = std::make_shared<RegionGeometry_ReferencedMask>();
    }
    else if (geometry_type == heif_region_type_inline_mask) {
      region = std::make_shared<RegionGeometry_InlineMask>();
    }
    else if (geometry_type == heif_region_type_polyline) {
      auto polygon = std::make_shared<RegionGeometry_Polygon>();
      polygon->closed = false;
      region = polygon;
    }
    else {
      //     // TODO: this isn't going to work - we can only exit here.
      //   std::cout << "ignoring unsupported region geometry type: "
      //             << (int)geometry_type << std::endl;

      continue;
    }

    Error error = region->parse(data, field_size, &dataOffset);
    if (error) {
      return error;
    }

    mRegions.push_back(region);
  }
  return Error::Ok;
}

Error RegionItem::encode(std::vector<uint8_t>& result) const
{
  StreamWriter writer;

  writer.write8(0);

  // --- compute required field size

  int field_size_bytes = 2;

  if (reference_width <= 0xFFFF &&
      reference_height <= 0xFFFF) {
    field_size_bytes = 4;
  }

  if (field_size_bytes != 4) {
    for (auto& region : mRegions) {
      if (region->encode_needs_32bit()) {
        field_size_bytes = 4;
        break;
      }
    }
  }

  // --- write flags

  uint8_t flags = 0;

  if (field_size_bytes == 4) {
    flags |= 1;
  }

  writer.write8(flags);

  // --- write reference size

  writer.write(field_size_bytes, reference_width);
  writer.write(field_size_bytes, reference_height);

  // --- write regions

  if (mRegions.size() >= 256) {
    return Error(heif_error_Encoding_error, heif_suberror_Too_many_regions);
  }

  writer.write8((uint8_t) mRegions.size());

  for (auto& region : mRegions) {
    region->encode(writer, field_size_bytes);
  }

  result = writer.get_data();

  return Error::Ok;
}


uint32_t RegionGeometry::parse_unsigned(const std::vector<uint8_t>& data,
                                        int field_size,
                                        unsigned int* dataOffset)
{
  uint32_t x;
  if (field_size == 32) {
    x = ((data[*dataOffset] << 24) | (data[*dataOffset + 1] << 16) |
         (data[*dataOffset + 2] << 8) | (data[*dataOffset + 3]));
    *dataOffset = *dataOffset + 4;
  }
  else {
    x = ((data[*dataOffset] << 8) | (data[*dataOffset + 1]));
    *dataOffset = *dataOffset + 2;
  }
  return x;
}

int32_t RegionGeometry::parse_signed(const std::vector<uint8_t>& data,
                                     int field_size,
                                     unsigned int* dataOffset)
{
  if (field_size == 32) {
    return (int32_t)parse_unsigned(data, field_size, dataOffset);
  } else {
    return (int16_t)parse_unsigned(data, field_size, dataOffset);
  }
}

Error RegionGeometry_Point::parse(const std::vector<uint8_t>& data,
                                  int field_size,
                                  unsigned int* dataOffset)
{
  unsigned int bytesRequired = (field_size / 8) * 2;
  if (data.size() - *dataOffset < bytesRequired) {
    return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data,
                 "Insufficient data remaining for point region");
  }
  x = parse_signed(data, field_size, dataOffset);
  y = parse_signed(data, field_size, dataOffset);

  return Error::Ok;
}


static bool exceeds_s16(int32_t v)
{
  return (v > 32767 || v < -32768);
}

static bool exceeds_u16(uint32_t v)
{
  return v > 0xFFFF;
}


bool RegionGeometry_Point::encode_needs_32bit() const
{
  return exceeds_s16(x) || exceeds_s16(y);
}


void RegionGeometry_Point::encode(StreamWriter& writer, int field_size_bytes) const
{
  writer.write8(heif_region_type_point);
  writer.write(field_size_bytes, x);
  writer.write(field_size_bytes, y);
}


Error RegionGeometry_Rectangle::parse(const std::vector<uint8_t>& data,
                                      int field_size,
                                      unsigned int* dataOffset)
{
  unsigned int bytesRequired = (field_size / 8) * 4;
  if (data.size() - *dataOffset < bytesRequired) {
    return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data,
                 "Insufficient data remaining for rectangle region");
  }
  x = parse_signed(data, field_size, dataOffset);
  y = parse_signed(data, field_size, dataOffset);
  width = parse_unsigned(data, field_size, dataOffset);
  height = parse_unsigned(data, field_size, dataOffset);
  return Error::Ok;
}


bool RegionGeometry_Rectangle::encode_needs_32bit() const
{
  return exceeds_s16(x) || exceeds_s16(y) || exceeds_u16(width) || exceeds_u16(height);
}


void RegionGeometry_Rectangle::encode(StreamWriter& writer, int field_size_bytes) const
{
  writer.write8(heif_region_type_rectangle);
  writer.write(field_size_bytes, x);
  writer.write(field_size_bytes, y);
  writer.write(field_size_bytes, width);
  writer.write(field_size_bytes, height);
}

Error RegionGeometry_Ellipse::parse(const std::vector<uint8_t>& data,
                                    int field_size,
                                    unsigned int* dataOffset)
{
  unsigned int bytesRequired = (field_size / 8) * 4;
  if (data.size() - *dataOffset < bytesRequired) {
    return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data,
                 "Insufficient data remaining for ellipse region");
  }
  x = parse_signed(data, field_size, dataOffset);
  y = parse_signed(data, field_size, dataOffset);
  radius_x = parse_unsigned(data, field_size, dataOffset);
  radius_y = parse_unsigned(data, field_size, dataOffset);
  return Error::Ok;
}

bool RegionGeometry_Ellipse::encode_needs_32bit() const
{
  return exceeds_s16(x) || exceeds_s16(y) || exceeds_u16(radius_x) || exceeds_u16(radius_y);
}


void RegionGeometry_Ellipse::encode(StreamWriter& writer, int field_size_bytes) const
{
  writer.write8(heif_region_type_ellipse);
  writer.write(field_size_bytes, x);
  writer.write(field_size_bytes, y);
  writer.write(field_size_bytes, radius_x);
  writer.write(field_size_bytes, radius_y);
}



Error RegionGeometry_Polygon::parse(const std::vector<uint8_t>& data,
                                    int field_size,
                                    unsigned int* dataOffset)
{
  uint32_t bytesRequired1 = (field_size / 8) * 1;
  if (data.size() - *dataOffset < bytesRequired1) {
    return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data,
                 "Insufficient data remaining for polygon");
  }

  // Note: we need to do the calculation in uint64_t because numPoints may be any 32-bit number
  // and it is multiplied by (at most) 8.

  uint32_t numPoints = parse_unsigned(data, field_size, dataOffset);
  uint64_t bytesRequired2 = (field_size / 8) * uint64_t(numPoints) * 2;
  if (data.size() - *dataOffset < bytesRequired2) {
    return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data,
                 "Insufficient data remaining for polygon");
  }

  for (uint32_t i = 0; i < numPoints; i++) {
    Point p;
    p.x = parse_signed(data, field_size, dataOffset);
    p.y = parse_signed(data, field_size, dataOffset);
    points.push_back(p);
  }

  return Error::Ok;
}


Error RegionGeometry_ReferencedMask::parse(const std::vector<uint8_t>& data,
                                          int field_size,
                                          unsigned int* dataOffset)
{
  unsigned int bytesRequired = (field_size / 8) * 4;
  if (data.size() - *dataOffset < bytesRequired) {
    return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data,
                 "Insufficient data remaining for referenced mask region");
  }
  x = parse_signed(data, field_size, dataOffset);
  y = parse_signed(data, field_size, dataOffset);
  width = parse_unsigned(data, field_size, dataOffset);
  height = parse_unsigned(data, field_size, dataOffset);
  return Error::Ok;
}


void RegionGeometry_ReferencedMask::encode(StreamWriter& writer, int field_size_bytes) const
{
  writer.write8(heif_region_type_referenced_mask);
  writer.write(field_size_bytes, x);
  writer.write(field_size_bytes, y);
  writer.write(field_size_bytes, width);
  writer.write(field_size_bytes, height);
}

bool RegionGeometry_Polygon::encode_needs_32bit() const
{
  if (exceeds_u16((uint32_t)points.size())) {
    return true;
  }

  for (auto& p : points) {
    if (exceeds_s16(p.x) || exceeds_s16(p.y)) {
      return true;
    }
  }

  return false;
}


void RegionGeometry_Polygon::encode(StreamWriter& writer, int field_size_bytes) const
{
  writer.write8(closed ? heif_region_type_polygon : heif_region_type_polyline);

  writer.write(field_size_bytes, points.size());

  for (auto& p : points) {
    writer.write(field_size_bytes, p.x);
    writer.write(field_size_bytes, p.y);
  }
}


Error RegionGeometry_InlineMask::parse(const std::vector<uint8_t>& data,
                                       int field_size,
                                       unsigned int* dataOffset)
{
  unsigned int bytesRequired = (field_size / 8) * 4 + 1;
  if (data.size() - *dataOffset < bytesRequired) {
    return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data,
                 "Insufficient data remaining for inline mask region");
  }
  x = parse_signed(data, field_size, dataOffset);
  y = parse_signed(data, field_size, dataOffset);
  width = parse_unsigned(data, field_size, dataOffset);
  height = parse_unsigned(data, field_size, dataOffset);
  uint8_t mask_coding_method = data[*dataOffset];
  *dataOffset = *dataOffset + 1;
  if (mask_coding_method != 0) {
    return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data,
                 "Deflate compressed inline mask is not yet supported");
  }
  unsigned int additionalBytesRequired = width * height / 8;
  if (data.size() - *dataOffset < additionalBytesRequired) {
        return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data,
                 "Insufficient data remaining for inline mask region data[]");
  }
  mask_data.resize(additionalBytesRequired);
  std::copy(data.begin() + *dataOffset, data.begin() + *dataOffset + additionalBytesRequired, mask_data.begin());
  return Error::Ok;
}


void RegionGeometry_InlineMask::encode(StreamWriter& writer, int field_size_bytes) const
{
  writer.write8(heif_region_type_inline_mask);
  writer.write(field_size_bytes, x);
  writer.write(field_size_bytes, y);
  writer.write(field_size_bytes, width);
  writer.write(field_size_bytes, height);
  writer.write8(0); // coding method
  // if using some other coding method, there are parameters to write out here.
  writer.write(mask_data);
}


RegionCoordinateTransform RegionCoordinateTransform::create(std::shared_ptr<HeifFile> file,
                                                            heif_item_id item_id,
                                                            int reference_width, int reference_height)
{
  std::vector<std::shared_ptr<Box>> properties;

  Error err = file->get_properties(item_id, properties);
  if (err) {
    return {};
  }

  uint32_t image_width = 0, image_height = 0;

  for (auto& property : properties) {
    if (auto ispe = std::dynamic_pointer_cast<Box_ispe>(property)) {
      image_width = ispe->get_width();
      image_height = ispe->get_height();
      break;
    }
  }

  if (image_width == 0 || image_height == 0) {
    return {};
  }

  RegionCoordinateTransform transform;
  transform.a = image_width / (double) reference_width;
  transform.d = image_height / (double) reference_height;

  for (auto& property : properties) {
    switch (property->get_short_type()) {
      case fourcc("imir"): {
        auto imir = std::dynamic_pointer_cast<Box_imir>(property);
        if (imir->get_mirror_direction() == heif_transform_mirror_direction_horizontal) {
          transform.a = -transform.a;
          transform.b = -transform.b;
          transform.tx = image_width - 1 - transform.tx;
        }
        else {
          transform.c = -transform.c;
          transform.d = -transform.d;
          transform.ty = image_height - 1 - transform.ty;
        }
        break;
      }
      case fourcc("irot"): {
        auto irot = std::dynamic_pointer_cast<Box_irot>(property);
        RegionCoordinateTransform tmp;
        switch (irot->get_rotation_ccw()) {
          case 90:
            tmp.a = transform.c;
            tmp.b = transform.d;
            tmp.c = -transform.a;
            tmp.d = -transform.b;
            tmp.tx = transform.ty;
            tmp.ty = -transform.tx + image_width - 1;
            transform = tmp;
            std::swap(image_width, image_height);
            break;
          case 180:
            transform.a = -transform.a;
            transform.b = -transform.b;
            transform.tx = image_width - 1 - transform.tx;
            transform.c = -transform.c;
            transform.d = -transform.d;
            transform.ty = image_height - 1 - transform.ty;
            break;
          case 270:
            tmp.a = -transform.c;
            tmp.b = -transform.d;
            tmp.c = transform.a;
            tmp.d = transform.b;
            tmp.tx = -transform.ty + image_height - 1;
            tmp.ty = transform.tx;
            transform = tmp;
            std::swap(image_width, image_height);
            break;
          default:
            break;
        }
        break;
      }
      case fourcc("clap"): {
        auto clap = std::dynamic_pointer_cast<Box_clap>(property);
        int left = clap->left_rounded(image_width);
        int top = clap->top_rounded(image_height);
        transform.tx -= left;
        transform.ty -= top;
        image_width = clap->get_width_rounded();
        image_height = clap->get_height_rounded();
        break;
      }
      default:
        break;
    }
  }

  return transform;
}


RegionCoordinateTransform::Point RegionCoordinateTransform::transform_point(Point p)
{
  Point newp;
  newp.x = p.x * a + p.x * b + tx;
  newp.y = p.x * c + p.y * d + ty;
  return newp;
}


RegionCoordinateTransform::Extent RegionCoordinateTransform::transform_extent(Extent e)
{
  Extent newe;
  newe.x = e.x * a + e.y * b;
  newe.y = e.x * c + e.y * d;
  return newe;
}



