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

/** \file
 * \ingroup bke
 *
 * To reduce the overhead of image processing this file contains a mechanism to detect areas of the
 * image that are changed. These areas are organized in chunks. Changes that happen over time are
 * organized in changesets.
 *
 * A common use case is to update #GPUTexture for drawing where only that part is uploaded that
 * only changed.
 */

#pragma once

#include "DNA_vec_types.h"

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

#include "DNA_image_types.h"

extern "C" {
struct PartialUpdateUser;
}

namespace blender::bke::image::partial_update {

/* --- image_partial_update.cc --- */
/** Image partial updates. */

/**
 * \brief Result codes of #BKE_image_partial_update_collect_changes.
 */
enum class ePartialUpdateCollectResult {
  /** \brief Unable to construct partial updates. Caller should perform a full update. */
  FullUpdateNeeded,

  /** \brief No changes detected since the last time requested. */
  NoChangesDetected,

  /** \brief Changes detected since the last time requested. */
  PartialChangesDetected,
};

/**
 * \brief A region to update.
 *
 * Data is organized in tiles. These tiles are in texel space (1 unit is a single texel). When
 * tiles are requested they are merged with neighboring tiles.
 */
struct PartialUpdateRegion {
  /** \brief region of the image that has been updated. Region can be bigger than actual changes.
   */
  rcti region;

  /**
   * \brief Tile number (UDIM) that this region belongs to.
   */
  TileNumber tile_number;
};

/**
 * \brief Return codes of #BKE_image_partial_update_get_next_change.
 */
enum class ePartialUpdateIterResult {
  /** \brief no tiles left when iterating over tiles. */
  Finished = 0,

  /** \brief a chunk was available and has been loaded. */
  ChangeAvailable = 1,
};

/**
 * \brief collect the partial update since the last request.
 *
 * Invoke #BKE_image_partial_update_get_next_change to iterate over the collected tiles.
 *
 * \returns ePartialUpdateCollectResult::FullUpdateNeeded: called should not use partial updates
 * but recalculate the full image. This result can be expected when called for the first time for a
 * user and when it isn't possible to reconstruct the changes as the internal state doesn't have
 * enough data stored. ePartialUpdateCollectResult::NoChangesDetected: The have been no changes
 * detected since last invoke for the same user.
 * ePartialUpdateCollectResult::PartialChangesDetected: Parts of the image has been updated since
 * last invoke for the same user. The changes can be read by using
 * #BKE_image_partial_update_get_next_change.
 */
ePartialUpdateCollectResult BKE_image_partial_update_collect_changes(Image *image,
                                                                     PartialUpdateUser *user);

ePartialUpdateIterResult BKE_image_partial_update_get_next_change(PartialUpdateUser *user,
                                                                  PartialUpdateRegion *r_region);

/** \brief Abstract class to load tile data when using the PartialUpdateChecker. */
class AbstractTileData {
 protected:
  virtual ~AbstractTileData() = default;

 public:
  /**
   * \brief Load the data for the given tile_number.
   *
   * Invoked when changes are on a different tile compared to the previous tile..
   */
  virtual void init_data(TileNumber tile_number) = 0;
  /**
   * \brief Unload the data that has been loaded.
   *
   * Invoked when changes are on a different tile compared to the previous tile or when finished
   * iterating over the changes.
   */
  virtual void free_data() = 0;
};

/**
 * \brief Class to not load any tile specific data when iterating over changes.
 */
class NoTileData : AbstractTileData {
 public:
  NoTileData(Image * /*image*/, ImageUser * /*image_user*/) {}

  void init_data(TileNumber /*new_tile_number*/) override {}

  void free_data() override {}
};

/**
 * \brief Load the ImageTile and ImBuf associated with the partial change.
 */
class ImageTileData : AbstractTileData {
 public:
  /**
   * \brief Not owned Image that is being iterated over.
   */
  Image *image;

  /**
   * \brief Local copy of the image user.
   *
   * The local copy is required so we don't change the image user of the caller.
   * We need to change it in order to request data for a specific tile.
   */
  ImageUser image_user = {nullptr};

  /**
   * \brief ImageTile associated with the loaded tile.
   * Data is not owned by this instance but by the `image`.
   */
  ImageTile *tile = nullptr;

  /**
   * \brief ImBuf of the loaded tile.
   *
   * Can be nullptr when the file doesn't exist or when the tile hasn't been initialized.
   */
  ImBuf *tile_buffer = nullptr;
  void *tile_buffer_lock = nullptr;

  ImageTileData(Image *image, ImageUser *image_user) : image(image)
  {
    if (image_user != nullptr) {
      this->image_user = *image_user;
    }
    else {
      /* When no image user is given the lastframe of the image should be used. This reflect the
       * same logic when using a stencil image in the clone tool. */
      this->image_user.framenr = image->lastframe;
    }
  }

  void init_data(TileNumber new_tile_number) override
  {
    image_user.tile = new_tile_number;
    tile = BKE_image_get_tile(image, new_tile_number);
    tile_buffer = BKE_image_acquire_ibuf(image, &image_user, &tile_buffer_lock);
  }

  void free_data() override
  {
    BKE_image_release_ibuf(image, tile_buffer, tile_buffer_lock);
    tile = nullptr;
    tile_buffer = nullptr;
    tile_buffer_lock = nullptr;
  }
};

template<typename TileData = NoTileData> struct PartialUpdateChecker {

  /**
   * \brief Not owned Image that is being iterated over.
   */
  Image *image;
  ImageUser *image_user;

  /**
   * \brief the collected changes are stored inside the PartialUpdateUser.
   */
  PartialUpdateUser *user;

  struct CollectResult {
    PartialUpdateChecker<TileData> *checker;

    /**
     * \brief Tile specific data.
     */
    TileData tile_data;
    PartialUpdateRegion changed_region;
    ePartialUpdateCollectResult result_code;

   private:
    TileNumber last_tile_number = 0;

   public:
    CollectResult(PartialUpdateChecker<TileData> *checker, ePartialUpdateCollectResult result_code)
        : checker(checker),
          tile_data(checker->image, checker->image_user),
          result_code(result_code)
    {
    }

    ePartialUpdateCollectResult get_result_code() const
    {
      return result_code;
    }

    /**
     * \brief Load the next changed region.
     *
     * This member function can only be called when partial changes are detected.
     * (`get_result_code()` returns `ePartialUpdateCollectResult::PartialChangesDetected`).
     *
     * When changes for another tile than the previous tile is loaded the #tile_data will be
     * updated.
     */
    ePartialUpdateIterResult get_next_change()
    {
      BLI_assert(result_code == ePartialUpdateCollectResult::PartialChangesDetected);
      ePartialUpdateIterResult result = BKE_image_partial_update_get_next_change(checker->user,
                                                                                 &changed_region);
      switch (result) {
        case ePartialUpdateIterResult::Finished:
          tile_data.free_data();
          return result;

        case ePartialUpdateIterResult::ChangeAvailable:
          if (last_tile_number == changed_region.tile_number) {
            return result;
          }
          tile_data.free_data();
          tile_data.init_data(changed_region.tile_number);
          last_tile_number = changed_region.tile_number;
          return result;

        default:
          BLI_assert_unreachable();
          return result;
      }
    }
  };

 public:
  PartialUpdateChecker(Image *image, ImageUser *image_user, PartialUpdateUser *user)
      : image(image), image_user(image_user), user(user)
  {
  }

  /**
   * \brief Check for new changes since the last time this method was invoked for this #user.
   */
  CollectResult collect_changes()
  {
    ePartialUpdateCollectResult collect_result = BKE_image_partial_update_collect_changes(image,
                                                                                          user);
    return CollectResult(this, collect_result);
  }
};

}  // namespace blender::bke::image::partial_update
