/* SPDX-FileCopyrightText: 2022 Blender Authors
 *
 * SPDX-License-Identifier: GPL-2.0-or-later */

/** \file
 * \ingroup bke
 */

#pragma once

#include "BLI_array.hh"
#include "BLI_math_vector.hh"
#include "BLI_rect.h"
#include "BLI_vector.hh"

#include "DNA_image_types.h"

#include "BKE_image.hh"
#include "BKE_image_wrappers.hh"
#include "BKE_paint_bvh.hh"

#include "IMB_imbuf_types.hh"

namespace blender::bke::pbvh::pixels {

struct UVPrimitivePaintInput {
  /** Corresponding index into triangles */
  int tri_index;
  /**
   * Delta barycentric coordinates between 2 neighboring UVs in the U direction.
   *
   * Only the first two coordinates are stored. The third should be recalculated
   */
  float2 delta_barycentric_coord_u;

  /**
   * Initially only the vert indices are known.
   *
   * delta_barycentric_coord_u is initialized in a later stage as it requires image tile
   * dimensions.
   */
  UVPrimitivePaintInput(int tri_index)
      : tri_index(tri_index), delta_barycentric_coord_u(0.0f, 0.0f)
  {
  }
};

/**
 * Encode sequential pixels to reduce memory footprint.
 */
struct PackedPixelRow {
  /** Barycentric coordinate of the first pixel. */
  float2 start_barycentric_coord;
  /** Image coordinate starting of the first pixel. */
  ushort2 start_image_coordinate;
  /** Number of sequential pixels encoded in this package. */
  ushort num_pixels;
  /** Reference to the pbvh triangle index. */
  ushort uv_primitive_index;
};

/**
 * Node pixel data containing the pixels for a single UDIM tile.
 */
struct UDIMTilePixels {
  /** UDIM Tile number. */
  short tile_number;

  struct {
    bool dirty : 1;
  } flags;

  /* Dirty region of the tile in image space. */
  rcti dirty_region;

  Vector<PackedPixelRow> pixel_rows;

  UDIMTilePixels()
  {
    flags.dirty = false;
    BLI_rcti_init_minmax(&dirty_region);
  }

  void mark_dirty(const PackedPixelRow &pixel_row)
  {
    int2 start_image_coord(pixel_row.start_image_coordinate.x, pixel_row.start_image_coordinate.y);
    BLI_rcti_do_minmax_v(&dirty_region, start_image_coord);
    BLI_rcti_do_minmax_v(&dirty_region, start_image_coord + int2(pixel_row.num_pixels + 1, 0));
    flags.dirty = true;
  }

  void clear_dirty()
  {
    BLI_rcti_init_minmax(&dirty_region);
    flags.dirty = false;
  }
};

struct UDIMTileUndo {
  short tile_number;
  rcti region;

  UDIMTileUndo(short tile_number, rcti &region) : tile_number(tile_number), region(region) {}
};

/**
 * Contains triangle/pixel data used during texture painting.
 */
struct NodeData {
  struct {
    bool dirty : 1;
  } flags;

  Vector<UDIMTilePixels> tiles;
  Vector<UDIMTileUndo> undo_regions;
  Vector<UVPrimitivePaintInput> uv_primitives;

  NodeData()
  {
    flags.dirty = false;
  }

  UDIMTilePixels *find_tile_data(const image::ImageTileWrapper &image_tile)
  {
    for (UDIMTilePixels &tile : tiles) {
      if (tile.tile_number == image_tile.get_tile_number()) {
        return &tile;
      }
    }
    return nullptr;
  }

  void rebuild_undo_regions()
  {
    undo_regions.clear();
    for (UDIMTilePixels &tile : tiles) {
      if (tile.pixel_rows.is_empty()) {
        continue;
      }

      rcti region;
      BLI_rcti_init_minmax(&region);
      for (PackedPixelRow &pixel_row : tile.pixel_rows) {
        BLI_rcti_do_minmax_v(
            &region, int2(pixel_row.start_image_coordinate.x, pixel_row.start_image_coordinate.y));
        BLI_rcti_do_minmax_v(&region,
                             int2(pixel_row.start_image_coordinate.x + pixel_row.num_pixels + 1,
                                  pixel_row.start_image_coordinate.y + 1));
      }
      undo_regions.append(UDIMTileUndo(tile.tile_number, region));
    }
  }

  void mark_region(Image &image, const image::ImageTileWrapper &image_tile, ImBuf &image_buffer)
  {
    UDIMTilePixels *tile = find_tile_data(image_tile);
    if (tile && tile->flags.dirty) {
      if (image_buffer.planes == 8) {
        image_buffer.planes = 32;
        BKE_image_partial_update_mark_full_update(&image);
      }
      else {
        BKE_image_partial_update_mark_region(
            &image, image_tile.image_tile, &image_buffer, &tile->dirty_region);
      }
      tile->clear_dirty();
    }
  }

  void collect_dirty_tiles(Vector<image::TileNumber> &r_dirty_tiles)
  {
    for (UDIMTilePixels &tile : tiles) {
      if (tile.flags.dirty) {
        r_dirty_tiles.append_non_duplicates(tile.tile_number);
      }
    }
  }

  void clear_data()
  {
    tiles.clear();
    uv_primitives.clear();
  }

  static void free_func(void *instance)
  {
    NodeData *node_data = static_cast<NodeData *>(instance);
    MEM_delete(node_data);
  }
};

/* -------------------------------------------------------------------- */
/** \name Fix non-manifold edge bleeding.
 * \{ */

struct DeltaCopyPixelCommand {
  char2 delta_source_1;
  char2 delta_source_2;
  uint8_t mix_factor;

  DeltaCopyPixelCommand(char2 delta_source_1, char2 delta_source_2, uint8_t mix_factor)
      : delta_source_1(delta_source_1), delta_source_2(delta_source_2), mix_factor(mix_factor)
  {
  }
};

struct CopyPixelGroup {
  int2 start_destination;
  int2 start_source_1;
  int64_t start_delta_index;
  int num_deltas;
};

/** Pixel copy command to mix 2 source pixels and write to a destination pixel. */
struct CopyPixelCommand {
  /** Pixel coordinate to write to. */
  int2 destination;
  /** Pixel coordinate to read first source from. */
  int2 source_1;
  /** Pixel coordinate to read second source from. */
  int2 source_2;
  /** Factor to mix between first and second source. */
  float mix_factor;

  CopyPixelCommand() = default;
  CopyPixelCommand(const CopyPixelGroup &group)
      : destination(group.start_destination),
        source_1(group.start_source_1),
        source_2(),
        mix_factor(0.0f)
  {
  }

  template<typename T>
  void mix_source_and_write_destination(image::ImageBufferAccessor<T> &tile_buffer) const
  {
    float4 source_color_1 = tile_buffer.read_pixel(source_1);
    float4 source_color_2 = tile_buffer.read_pixel(source_2);
    float4 destination_color = source_color_1 * (1.0f - mix_factor) + source_color_2 * mix_factor;
    tile_buffer.write_pixel(destination, destination_color);
  }

  void apply(const DeltaCopyPixelCommand &item)
  {
    destination.x += 1;
    source_1 += int2(item.delta_source_1);
    source_2 = source_1 + int2(item.delta_source_2);
    mix_factor = float(item.mix_factor) / 255.0f;
  }

  DeltaCopyPixelCommand encode_delta(const CopyPixelCommand &next_command) const
  {
    return DeltaCopyPixelCommand(char2(next_command.source_1 - source_1),
                                 char2(next_command.source_2 - next_command.source_1),
                                 uint8_t(next_command.mix_factor * 255));
  }

  bool can_be_extended(const CopyPixelCommand &command) const
  {
    /* Can only extend sequential pixels. */
    if (destination.x != command.destination.x - 1 || destination.y != command.destination.y) {
      return false;
    }

    /* Can only extend when the delta between with the previous source fits in a single byte. */
    int2 delta_source_1 = source_1 - command.source_1;
    if (max_ii(UNPACK2(blender::math::abs(delta_source_1))) > 127) {
      return false;
    }
    return true;
  }
};

struct CopyPixelTile {
  image::TileNumber tile_number;
  Vector<CopyPixelGroup> groups;
  Vector<DeltaCopyPixelCommand> command_deltas;

  CopyPixelTile(image::TileNumber tile_number) : tile_number(tile_number) {}

  void copy_pixels(ImBuf &tile_buffer, IndexRange group_range) const
  {
    if (tile_buffer.float_buffer.data) {
      image::ImageBufferAccessor<float4> accessor(tile_buffer);
      copy_pixels<float4>(accessor, group_range);
    }
    else {
      image::ImageBufferAccessor<int> accessor(tile_buffer);
      copy_pixels<int>(accessor, group_range);
    }
  }

  void print_compression_rate()
  {
    int decoded_size = command_deltas.size() * sizeof(CopyPixelCommand);
    int encoded_size = groups.size() * sizeof(CopyPixelGroup) +
                       command_deltas.size() * sizeof(DeltaCopyPixelCommand);
    printf("Tile %d compression rate: %d->%d = %d%%\n",
           tile_number,
           decoded_size,
           encoded_size,
           int(100.0 * float(encoded_size) / float(decoded_size)));
  }

 private:
  template<typename T>
  void copy_pixels(image::ImageBufferAccessor<T> &image_buffer, IndexRange group_range) const
  {
    for (const int64_t group_index : group_range) {
      const CopyPixelGroup &group = groups[group_index];
      CopyPixelCommand copy_command(group);
      for (const DeltaCopyPixelCommand &item : Span<const DeltaCopyPixelCommand>(
               &command_deltas[group.start_delta_index], group.num_deltas))
      {
        copy_command.apply(item);
        copy_command.mix_source_and_write_destination<T>(image_buffer);
      }
    }
  }
};

struct CopyPixelTiles {
  Vector<CopyPixelTile> tiles;

  std::optional<std::reference_wrapper<CopyPixelTile>> find_tile(image::TileNumber tile_number)
  {
    for (CopyPixelTile &tile : tiles) {
      if (tile.tile_number == tile_number) {
        return tile;
      }
    }
    return std::nullopt;
  }

  void clear()
  {
    tiles.clear();
  }
};

/** \} */

/**
 * Storage for texture painting on blender::bke::pbvh::Tree level.
 */
struct PBVHData {
  /* Per UVPRimitive contains the paint data. */
  Array<int3> vert_tris;

  /** Per ImageTile the pixels to copy to fix non-manifold bleeding. */
  CopyPixelTiles tiles_copy_pixels;

  void clear_data()
  {
    this->vert_tris = {};
  }
};

NodeData &node_data_get(blender::bke::pbvh::Node &node);
void mark_image_dirty(blender::bke::pbvh::Node &node, Image &image, ImageUser &image_user);
PBVHData &data_get(blender::bke::pbvh::Tree &pbvh);
void collect_dirty_tiles(blender::bke::pbvh::Node &node, Vector<image::TileNumber> &r_dirty_tiles);

void copy_pixels(blender::bke::pbvh::Tree &pbvh,
                 Image &image,
                 ImageUser &image_user,
                 image::TileNumber tile_number);

}  // namespace blender::bke::pbvh::pixels
